Chainable onBeforeCompile + Uniforms access (per Mesh)

This little code will patch all material classes to have a chainable onBeforeCompile pattern, so when you assign a callback to onBeforeCompile it will be pushed into a plugins array property on the material instead overriding it. Assign null to reset or use removePlugin to remove a specific. Also addPlugin (same as assining) and hasPlugin is available.

It doesn’t break the API and works without core modification, so WebGLPrograms.js will get a combined string of all functions.

You can also set a priority on the patch function, for example for a patch patching another patch that should be called before, but especially on the code itself, if you use #include .. as entry point to insert the patch code order might matter. If you set DecalPatch.priority = 1; to 0 you’ll see the TintBluePatch will be applied in the shader after DecalPatch just like the order you assigned it and turn it fully blue, with higher priority a callback dominates it’s descendants. Of course this logic depends on how code is inserted, such as before a standard line or after.

So you can do the following without overriding the other:

material.onBeforeCompile = function( shader ) { ... };
material.onBeforeCompile = function( shader ) { ... };

or

material.onBeforeCompile = [
    function( shader ) { ... },
    function( shader ) { ... }
];

Below you find a example how you can make a more advanced plugin.

Code (latest): https://github.com/Fyrestar/MaterialPlugin

Basic example:
https://codepen.io/Fyrestar/pen/BXYGgN

Simple waving grass plugin:
https://codepen.io/Fyrestar/pen/PMyZpR

Material Callbacks

When extending shaders with onBeforeCompile it only gives you acces to the uniforms once. This allows you to not only use uniforms per material but also per object, like in this example:
https://codepen.io/Fyrestar/pen/ymjqMm

So you can extend materials in a actual plugin pattern and pass properties of meshes or materials to the shader.

Where you store per-mesh properties is up to you, you can either use the mesh directly like in the example (might be less optimal), store it in it’s userData object, or extend the mesh class (optimal).

Call THREE.MaterialCallback.use(); to apply it to the prototype of Mesh and SkinnedMesh, if you want it manually or need onBeforeRender yourself, just call within your callback:

THREE.MaterialCallback.call( this, renderer, scene, ... )

7 Likes

Beautiful! I kept wanting to look into overriding toString on that function, but i guess it wasn’t as trivial as that, you still wanted to do some tracking.

+1 for another thing that doesnt take 6+ years and thousands of lines of code!

4 Likes

Made some updates

  • Cleanup, only at toString the instance needs to get in it’s scope
  • You can assign a array with callbacks to assign multiple at once
  • Gives error if anything but null, function or a array with such is assigned
  • Fixed order, so like in the example DecalPatch and TintBluePatch are assigned in the order so TintBluePatch makes it fully blue by the entry point logic, so a higher priority to DecalPatch lets it dominate it’s descendants
1 Like

Another downside of onBeforeCompile is other than ShaderMaterial you can only access it’s uniforms when compiling. I’ve extended it now with material callbacks at onBeforeRender.

See first post for more details, here is an example, notice all 3 spheres use the same material:
https://codepen.io/Fyrestar/pen/ymjqMm

You can define a patch like the following, notice that the first object to be rendererd will be passed at compile, here you copy it’s properties already, otherwise one object would be the default value for the first frame. You could also store uniform values per material of course.

const myPatch = {
      compile: function( shader, object ) {
      
          THREE.patchShader(shader, {
            
            uniforms: {
              tintColor: new THREE.Color().copy( object.tint )
            },
            
            header: 'uniform vec3 tintColor;',
			
            fragment: {
                '#include <fog_fragment>': 'gl_FragColor.rgb *= tintColor;'
            }

          });

      },
      render: function( object, uniforms, context ) {
        
        context.set( uniforms.tintColor, object.tint );
        
      }
};

myMaterial.onBeforeCompile = myPatch;

Here an example creating a RoundedBoxMesh class based on Cheap round-edged box (vertex shader)

https://codepen.io/Fyrestar/pen/XvYWXW

2 Likes

Made another update

  • patches array renamed to plugins
  • Added addPlugin, removePlugin and hasPlugin to material prototypes
  • Ensures plugins/callbacks aren’t assigned multiple times
  • A frame callback can be defined, called once per frame, like in the following example to update a global time variable
  • If a requires array with plugins/callbacks is defined, it will add these and make sure the they added only once. This is usefull for shared ones like in this example, separating the noise code.

Notes:

  • If you use post-processing or otherwise call renderer.render() more than once per frame set THREE.MaterialCallback.auto = false; and call THREE.MaterialCallback.frame(); whenever your actual loop starts.

Here is an example for a grass waving in wind plugin

It also doesn’t require you to modify the normals in advance. This is just a simple example, you could make it much more advanced, such as using some kind of global wind map, define a mass property per object for how much it gets influenced etc.

https://codepen.io/Fyrestar/pen/PMyZpR
GrassPlugin

Used the fiddle from Backface Directional Lighting as template.

The plugin:

const GrassPlugin = {
    time: 0,
    frame: function() {

        this.time += 0.025;

    },
    render: function( object, uniforms, context ) {

        context.set( uniforms.uTime, this.time );
        context.set( uniforms.uSize, object.waveLength );

    },
    compile: function( shader ) {

        shader.uniforms.uSize = {
            value: 0
        };
        shader.uniforms.uTime = {
            value: 0
        };

        shader.fragmentShader = shader.fragmentShader.replace(
            '#include <normal_fragment_begin>',
            'vec3 normal = normalize( vNormal );'
        );
        shader.vertexShader = shader.vertexShader.replace('#include <common>', `
                #include <common>
                
                #ifndef uTime
                uniform float uTime;
                #endif
                
                uniform float uSize;
                
                float rand(float n){return fract(sin(n) * 43758.5453123);}
                
                float noise(float p){
                float fl = floor(p);
                float fc = fract(p);
                return mix(rand(fl), rand(fl + 1.0), fc);
                }
                
                float noise(vec2 n) {
                const vec2 d = vec2(0.0, 1.0);
                vec2 b = floor(n), f = smoothstep(vec2(0.0), vec2(1.0), fract(n));
                return mix(mix(rand(b), rand(b + d.yx), f.x), mix(rand(b + d.xy), rand(b + d.yy), f.x), f.y);
                }

        `);

        shader.vertexShader = shader.vertexShader.replace('#include <beginnormal_vertex>', `
            #include <beginnormal_vertex>
            objectNormal = vec3(0.0, 1.0, 0.0);
        
        `);
        shader.vertexShader = shader.vertexShader.replace('#include <project_vertex>', `
            
            vec4 GRASS_world = modelMatrix * vec4( transformed, 1.0 );
            transformed.xz += uv.y * ( noise(GRASS_world.xz + uTime ) * uSize );
            
            #include <project_vertex>
        `);

    }
};
6 Likes

test is Perfect! i tested this with THREE.MeshStandardMaterial and get
ChainableOnBeforeCompile.js?8297:16 Uncaught TypeError: Cannot read property ‘setValue’ of undefined
at Object.set (ChainableOnBeforeCompile.js?8297:16)

the line is uniform.setValue(this.gl, value);
MeshPhongMaterial works fine

any idea?

Could you share a pen or fiddle to illustrate your issue? The grass example above uses MeshStandardMaterial. Seems like you passed a uniform that isn’t created.

ok thx, looks like there is something wrong in my code fiddle works fine
https://codepen.io/Fyrestar/pen/XvYWXW

what is the official way to do this with three? using the NodeMaterial should help here?

What do you want to do? The examples show actual usecases, NodeMaterial is a different thing.

like the example but with NodeMaterial

NodeMaterial isn’t part of the core and works different, it basically should be a regular material at the end though. Can you provide me a pen or fiddle to show whats the issue?

sorry there is no problem with this implementation,wrong thread to ask for a NodeMaterial version

looks like a good start on NodeMaterial is https://www.donmccurdy.com/2019/03/17/three-nodematerial-introduction/

Im not sure what your question is, this plugin isn’t related to NodeMaterial though.

the idea is to use NodeMaterial without this plugin to get the same Result

It isn’t related to the api level of NodeMaterial, it’s about patching on shader code level, not nodes. Also you can’t use per-mesh uniforms without the patches the plugin applies. If you have questions about the NodeMaterial please use the appropriate thread.

3 Likes