24 May 2021

Material swatches in Forge Viewer

When developing a configurator-like application using Forge Viewer, perhaps you would like to allow your users to apply different types of materials to individual elements of their models. If these materials can be defined using standard three.js classes such as THREE.MeshBasicMaterial or THREE.MeshPhongMaterial, it's easy - just assign the material to fragments as needed:

function setMaterial(viewer, dbids, material) {
  const model = viewer.model;
  model.unconsolidate(); // If the model is consolidated, material changes won't have any effect
  const tree = model.getInstanceTree();
  const frags = model.getFragmentList();
  const dbids = viewer.getSelection();
  for (const dbid of dbids) {
    tree.enumNodeFragments(dbid, (fragid) => {
      frags.setMaterial(fragid, material);
    });
  }
}

But what if you'd like to reuse some of the official materials available in the different desktop products? Creating those by hand would be difficult. For example, the beautiful PBR (physically-based rendering) materials in Fusion 360 require dozens of different material parameters and textures to be set and tweaked to achieve the desired look-and-feel:

Forge 360 Materials

If you'd like to use these materials in your own designs, here's a little trick you could try:

Create a special "material swatch" design in the desktop product you're interested in, with different materials applied to simple geometries such as spheres, and give the individual objects names that can help you identify the specific materials. Then, load the swatch design into Forge Viewer as a hidden model, and use the viewer's API to apply materials from the swatch model to any other elements in the scene.

Here's a basic viewer extension that encapsulates this functionality:

class MaterialSwatchExtension extends Autodesk.Viewing.Extension {
    constructor(viewer, options) {
        super(viewer, options);
        this._swatch = null;
        this._urn = 'dXJ...'; // Use a URN of your own existing swatch model.
    }

    load() {
        return true;
    }

    unload() {
        return true;
    }

    /**
     * Lists all available material presets.
     * @async
     * @returns {Map} Mapping of preset names to instances of THREE.Material.
     */
    async getPresets() {
        if (!this._swatch) {
            this._swatch = await this._loadSwatchModel(this._urn);
        }
        const presets = new Map();
        const tree = this._swatch.getInstanceTree();
        const frags = this._swatch.getFragmentList();
        tree.enumNodeChildren(tree.getRootId(), function (dbid) {
            if (tree.getChildCount(dbid) === 0) {
                const name = tree.getNodeName(dbid);
                tree.enumNodeFragments(dbid, function (fragid) {
                    if (!presets.has(name)) {
                        presets.set(name, frags.getMaterial(fragid));
                    }
                }, true);
            }
        }, true);
        return presets;
    }

    /**
     * Applies material preset to specific object.
     * @async
     * @param {string} name Material preset name.
     * @param {Autodesk.Viewing.Model} targetModel Model that contains the object to be modified.
     * @param {number} targetObjectId DbID of the object to be modified.
     */
    async applyPreset(name, targetModel, targetObjectId) {
        const presets = await this.getPresets();
        if (!presets.has(name)) {
            console.error('Material swatch not found', name);
            return;
        }
        const material = presets.get(name);
        const tree = targetModel.getInstanceTree();
        const frags = targetModel.getFragmentList();
        tree.enumNodeFragments(targetObjectId, function (fragid) {
            frags.setMaterial(fragid, material);
        }, true);
        targetModel.unconsolidate();
    }

    async _loadSwatchModel(urn) {
        const viewer = this.viewer;
        return new Promise(function (resolve, reject) {
            function onSuccess(doc) {
                const viewable = doc.getRoot().getDefaultGeometry();
                viewer.addEventListener(Autodesk.Viewing.TEXTURES_LOADED_EVENT, function (ev) {
                    if (ev.model._isSwatch) {
                        resolve(ev.model);
                    }
                });
                viewer.loadDocumentNode(doc, viewable, {
                    preserveView: true,
                    keepCurrentModels: true,
                    loadAsHidden: true // <-- I see what you did there <_<
                }).then(model => model._isSwatch = true);
            }
            function onError(code, msg) {
                reject(msg);
            };
            Autodesk.Viewing.Document.load('urn:' + urn, onSuccess, onError);
        });
    }
}

Autodesk.Viewing.theExtensionManager.registerExtension('MaterialSwatchExtension', MaterialSwatchExtension);

With the extension registered, you can simply query the material presets available in your hard-coded swatch model using the getPresets method, and apply any preset to other models and elements using the applyPreset method.

The viewer extension listed above (together with another extension that shows all the presets in a simple UI) is available in the experiment/material-swatch branch of the https://github.com/petrbroz/forge-basic-app sample application. Feel free to try it yourself, but don't forget to replace the hard-coded URN with your own.

Disclaimer: geometry compatibility

It's important to note that certain materials in Forge Viewer require additional data to be present on the geometry to work properly. For example, the procedural wood material presets in Fusion 360 require that the geometry include uvw data for properly orienting and positioning the wood grain within its volume:

Geometry with UVW data

Forge handles this requirement by selectively generating the uvw data just for the objects that have the procedural wood applied to them. However, if you try to apply the procedural wood material to another geometry that didn't have the uvw channel generated for it, the material won't look good (it will most likely just be a boring solid color). Generating the uvw data yourself is a more difficult task, unfortunately. You would have to either generate the uvw channel manually during runtime, or apply one of the procedural wood presets to all the objects in your design before sending it to Forge to make sure that the Model Derivative service generates this data for you.

@Michael Beale has an example of that, here: https://gist.github.com/wallabyway/27137986f4a8164af67b591eb8c31b74

 

Related Article