Ok, as promised here are the results of my investigations regarding PBR materials, just in case someone with a similar problem stumbles over this thread:
- Not only PBR materials are adaptable, but also StandardMaterial can be adapted.
I am concentrating on the latter here.
- There is CustomMaterial, a subclass of StandardMaterial.
- Advantage over MaterialPlugin: At least for my use case CustomMaterials feel much more light-weight.
(I don’t want to tweak all materials but only need an additional feature for a particular material.)
- Disadvantage: I needed to install another package (@babylonjs/materials)
- Disadvantage: I hardly found any documentation. I searched for playground examples and RTFS.
- And I actually figured out how to subclass StandardMaterial directly for my needs.
- Disadvantage: Even more RTFS (and I might be relying on implementation details
that might change in future BabylonJS releases).
- Disadvantage: Code is a bit longer than the CustomMaterial-based solution.
- Advantage: No need for the extra package @babylonjs/materials
I used a simpler example than in my earlier posts. Assume you have a mesh that roughly resembles the unit sphere. The material gets a uniform parameter “bulge”. When it is 0, vertices are at the mesh positions. When it is 1, vertices are normalized and thus on the unit sphere. Values in between are used for interpolation.
Here the derivation from CustomMaterials:
import {CustomMaterial} from "@babylonjs/materials";
...
class SphereMaterial extends CustomMaterial {
constructor(name: string, scene: Scene) {
super(name, scene);
this.AddUniform("bulge", "float", 0.9)
.Vertex_Before_PositionUpdated(`
vec3 normalizedPos = normalize(position);
positionUpdated = mix(position, normalizedPos, bulge);
`)
.Vertex_Before_NormalUpdated(`
normalUpdated = mix(normal, normalizedPos, bulge);
`);
}
set bulge(value: number) {
// No idea why we have to do this incantation of "onBindObservable".
// But I found this in a playground example and it seems to work.
this.onBindObservable.add(() => {
this.getEffect().setFloat("bulge", value);
});
}
}
And here an implementation directly derived from StandardMaterial:
const sphereMaterialShaderName = "SphereMaterial";
Effect.ShadersStore[sphereMaterialShaderName + "VertexShader"] =
Effect.ShadersStore["defaultVertexShader"]
.replace("#define CUSTOM_VERTEX_DEFINITIONS", `
uniform float bulge;
$&`)
.replace("#define CUSTOM_VERTEX_UPDATE_POSITION", `
vec3 normalizedPos = normalize(position);
positionUpdated = mix(position, normalizedPos, bulge);
$&`)
.replace("#define CUSTOM_VERTEX_UPDATE_NORMAL", `
normalUpdated = mix(normal, normalizedPos, bulge);
$&`);
Effect.ShadersStore[sphereMaterialShaderName + "PixelShader"] =
Effect.ShadersStore["defaultPixelShader"];
class SphereMaterial extends StandardMaterial {
constructor(name: string, scene: Scene) {
super(name, scene);
this.customShaderNameResolve = (_shaderName: string, uniforms: string[]): string => {
uniforms.push("bulge");
return sphereMaterialShaderName;
}
}
set bulge(value: number) {
// No idea why we have to do this incantation of "onBindObservable".
// But I found this in a playground example and it seems to work.
this.onBindObservable.add(() => {
this.getEffect().setFloat("bulge", value);
});
}
}
Both of these work. But here are nevertheless some questions to the experts:
@sebavan, was there a reason you did not mention CustomMaterial or did you just not think of it? And is the usage of this.customShaderNameResolve in the second example considered accessing an implementation detail? (After all, “customShaderNameResolve” does not start with an underscore!)