How to add a custom uniform to the standard or PBR material shaders?

Is it possible to send custom uniform values to a Standard or PBR material shaders. I understand that I would probably have to monkey patch some private BJS code to accomplish that. I understand that I would probably have to send data via Uniform buffers in WebGL 2.

Here is my first attempt: https://www.babylonjs-playground.com/#RBVU6M#2 (This is just a minimal reproduction, not actual use-case)

I get this WebGL error when I click the ground to set custom uniform data:
babylon.js:16 WebGL: INVALID_OPERATION: uniform3f: location not for current program

I’m clearly not sending the data correctly.

From my experience on Stack Overflow I’m obligated to pre-empt some answers, and point out that the following questions aren’t the ones I’m asking:

Is it proper and prim to add a custom uniform to the standard or PBR material shaders?

Can I use the ShaderMaterial to completely rewrite the functionality of the PBR shader?

Can I change the color of a material based on where I am clicking on said material?

Do I have a good reason to add a custom uniform to the standard or PBR material shaders?

EDIT: I have been able to send the data sometimes if I click on the ground frantically. That’s not quite good enough though.

Welcome to the forum!

Regarding your PG, try this:

https://www.babylonjs-playground.com/#RBVU6M#3

You need to update the uniform at the right time: onBeforeDraw is the very last callback before calling OpenGL drawing, so it seems to fit the bill.

To modify existing material shaders without messing with the shader store, you can try CustomMaterial and PBRCustomMaterial from the material library.

Some docs here: Babylon.js/materialsLibrary/src/custom at master · BabylonJS/Babylon.js · GitHub

They seem not up to date (for eg SelectVersion does not exist), but it should get you started.

3 Likes

Thank you very much. This is fantastic.

Can you say more about this part:

You need to update the uniform at the right time: onBeforeDraw is the very last callback before calling OpenGL drawing, so it seems to fit the bill.

What prevents us from calling effect.setFloat/etc in a requestAnimationFrame? I’m trying this method (in rAF callback), like

rafCallback(time) => {
   const effect = mesh.material.getEffect();
   effect.setFloat('time', time);
}

But, to your point, this doesn’t seem to be setting the time variable in the shader. What’s the difference between onBeforeDraw vs rAF? Do the rAF values get overwritten at some later point?

Yes, the bindForSubMesh method, which is called as part of the normal rendering of a mesh, will set on the effect the values you defined in your material. So, if you want to overwrite this value, your code must run after bindForSubMesh has done its work. You can use mesh.onBeforeDraw for that, or material.onBindObservable.

For properties that are not standard material properties (like time in your example, which must be a custom uniform), they are not passed to the shader through a uniform buffer, so their values are not retained from one render to the other. That’s why you have to set the value on the effect each time, you can’t do it just one time even if the value does not change.