How to change custom material properties

Introduction

This example shows how to apply material properties on certain meshes within the structure of the displayed 3d model.
In detail we'll be showing how to change the albedoTexture of PBRMaterials loaded from external image textures via URL.

There are 3 major tasks in order to make it work:

Parameter declaration in Specification

At first the custom parameter has to be defined in the Specification, as already described on the Parameters page.
In our example we have a custom parameter called BinTexture, which is defined on the variant level.
This one should only affect the Bin element, therefore it is forwarded to this element via "string substitution".

variants: {
//...
Bin: {
parameterDeclaration: {
// define type of custom parameter
BinTexture: { type: 'string' },
},
parameters: {
// parameter on variant level
BinTexture: '',
// parameter forwarded to element via "string substitution"
['Bin60.BinTexture']: '${BinTexture}',
},
},
//...
},

Parameter observer

In the next step we have to implement the parameter observer, which gets called anytime the corresponding parameter changes.
The parameter observer provides the code which actually applies new parameter values.
Eg. by setting the given value as a materials albedoTexture etc.
We append the parameter observer to the Bin element (with addParameterObserver), since we only want to change the texture of this particular element.

//...
const variant = await viewer.variantInstances.get('BinInstance');
const element = await variant.getElement('Bin60');
element.addParameterObserver('BinTexture', onBinTextureChanged);

async function onBinTextureChanged(element, oldValue, textureUrl) {
// For detailed implementation check out the `Full code example` chapter
}
//...

Inside the observer callback it is important to target the correct nodes of the element.
Here are some considerations:

  • All nodes vs specific nodes
    • use for (const node of element.nodes) to target all nodes within the element
    • use getNode or getMesh for specific nodes
  • Additional filtering for capabilities
    • some parameters (like textures) can only be applied to Meshes but not to TransformNodes
    • some parameters (like albedoTextures) can only be applied to specific material types, eg. PBRMaterials
  • Consider child nodes
    • use node.getChildTransformNodes recursively to apply the parameter to the child nodes as well

In many cases it's required to call the parameter observer once with a default value after the bootstrapping is finished.
In order to achieve this it's recommended to call commitParameter immediatly after the parameter observer has been declared.

Parameter commitment

The parameter value can be changed at runtime by using the commitParameter function:

//...
const variant = await viewer.variantInstances.get('BinInstance');
const texture = 'https://some.new/texture.png';
variant.commitParameter('BinTexture', texture);
//...

Full code example

import { Mesh, PBRMaterial, Texture } from '@combeenation/3d-viewer';

/**
* @return {StructureJson}
*/
export function createSpec() {
return {
scene: { /* ... */ },
setup: {
instances: [
{
name: 'BinInstance',
variant: 'Bin',
},
],
},
variants: {
Bin: {
glTF: './assets/produkter-bin.glb',
parameterDeclaration: {
// define type of custom parameter
BinTexture: { type: 'string' },
},
parameters: {
// parameter on variant level
BinTexture: '',
// parameter forwarded to element via "string substitution"
['Bin60.BinTexture']: '${BinTexture}',
},
elements: {
Bin60: ['__root__.bin.20_30_60'],
Cap: ['__root__.top.20_30_bo'],
},
},
},
};
}

/**
* Called right after the viewer has been bootstrapped.
* Place for project specific code.
*
* @param {Viewer} viewer
*/
export function afterBootstrap(viewer) {
// ...
// create the listener for the "custom parameter"
createBinTextureObserver(viewer);
}

/**
* Function for adjusting the bin texture parameter.
* Can be assigned as callback for a component listener, HTML event,...
*
* @param {Viewer} viewer
* @param {string} texture
*/
async function updateBinTexture(viewer, texture) {
const variant = await viewer.variantInstances.get('BinInstance');
variant.commitParameter('BinTexture', texture);
}

/**
* Defines the callback, which is executed after the bin texture parameter has been changed
*
* @param {Viewer} viewer
*/
async function createBinTextureObserver(viewer) {
// append the texture changed callback for the desired element
const variant = await viewer.variantInstances.get('BinInstance');
const element = await variant.getElement('Bin60');
element.addParameterObserver('BinTexture', onBinTextureChanged);

// apply the default value of the parameter
// this has to be done AFTER the parameter observer has been added
await updateBinTexture('BinTexture', '../textures/wood1.jpg');
}

/**
* Callback for bin texture has changed
*
* @param {VariantElement} element
* @param {string} oldValue
* @param {string} textureUrl
*/
async function onBinTextureChanged(element, oldValue, textureUrl) {
// apply the texture for all nodes inside the bin element
for (const node of element.nodes) {
await applyBinTexture(node, textureUrl);
}
}

/**
* Help function for applying the (albedo) texture for a node
*
* @param {TransformNode} node
* @param {string} textureUrl
* @param {Texture} [texture]
*/
async function applyBinTexture(node, textureUrl, texture) {
// first check if this is a mesh with a PBR material, otherwise the material can't be applied
if (node instanceof Mesh && node.material instanceof PBRMaterial) {
const scene = node.getScene();
// only load the texture if it's not already available
texture = texture ?? (await loadTexture(textureUrl, scene));
node.material.albedoTexture = texture;

// adjust metalness and roughness as well, since the texture represents wood
node.material.metallic = 0;
node.material.roughness = 0.5;
}

// Do this recursively for all nodes inside the element.
// Heavily depends on the concrete use case if this is needed or not.
// Some properties only need to be applied to the "top level nodes".
const childNodes = node.getChildTransformNodes(true);
for (const childNode of childNodes) {
await applyBinTexture(childNode, textureUrl, texture);
}
}

/**
* Help function for loading and creating a texture from a desired URL
*
* @param {string} textureUrl
* @param {Scene} scene
*
* @return {Promise<Texture>}
*/
async function loadTexture(textureUrl, scene) {
return new Promise(resolve => {
let albedoTexture = new Texture(textureUrl, scene);
albedoTexture.uScale = 5;
albedoTexture.vScale = 5;
albedoTexture.onLoadObservable.add(() => {
resolve(albedoTexture);
});
});
}

Generated using TypeDoc