Material: Added onBeforeCompile()

Over the years, a common feature request has been to be able to modify the built-in materials. Today I realised that it could be implemented in a similar way to Object3D‘s onBeforeRender().

e55898c

WebGLPrograms adds onBeforeCompile.toString() to the program hash so this shouldn’t affect other built-in materials used in the scene.

Here’s an example of the feature in action:

http://rawgit.com/mrdoob/three.js/dev/examples/webgl_materials_modified.html

material.onBeforeCompile = function ( shader ) {

	// console.log( shader )

	shader.uniforms.time = { value: 0 };

	shader.vertexShader = 'uniform float time;\n' + shader.vertexShader;
	shader.vertexShader = shader.vertexShader.replace(
		'#include <begin_vertex>',
		'vec3 transformed = vec3( position.x + sin( time + position.y ) / 2.0, position.y, position.z );'
	);

	materialShader = shader;

};

Not only we can mess with the shader code, but we can add custom uniforms.

if ( materialShader ) {

	materialShader.uniforms.time.value = performance.now() / 1000;

}

Is it too hacky?

/cc @WestLangley @bhouston @tschw

Author: Fantashit

5 thoughts on “Material: Added onBeforeCompile()

  1. It does seem a bit hacky to require custom string manipulation when WebGLProgram.js already does a lot of processing. There is a related use case, which is to add new #includes rather than overriding existing ones. You can do this today by modifying THREE.ShaderLib at runtime, but it feels equally dirty.

    Perhaps a nicer solution for both is to allow you to provide a custom ShaderLib for a material, that would override/augment the built-in chunks without manual string manipulation and without permanently clobbering them. That doesn’t preclude an onBeforeCompile hook for other uses, but it seems more in line with what your example is trying to do.

    That said, shader composition based on raw includes is always going to be very fragile from one version to the next. If you don’t mind including all the skeleton code (like meshphong_vert.glsl), you can already extend the built-in materials with something like this:

    export class MyPhongMaterial extends ShaderMaterial {
      constructor({ color, radius, map, normalMap, emissiveMap }) {
        super();
        this.vertexShader = "...."
        this.fragmentShader = "....";
        this.uniforms = UniformsUtils.clone(ShaderLib.phong.uniforms);
    
        this.isMeshPhongMaterial = true;
        this.lights = true;
       
        this.uniforms.time = { value: 1 };
        //...
      }
    }

    In all cases, you have to rely on the internal organization of built-in shaders not to change too much from one release to the next. Each include is already a function in disguise, with an ill-specified signature. I suspect it is going to be much cleaner and maintainable on both sides to provide function-level rather than include-level overrides.

  2. I’m happy to see that this features is finally coming to three.js :). Yes I also created a yet another material modifier PR (#7581) It was so old that the code is not relevant anymore, but is basically injecting hooks like @mrdoob proposes and then just replace them by your own code.
    I like the idea of predefined hooks as it’s easy to understand what you’re modifying as I believe most of the people that want to use this feature want to modify “slightly” the default material instead of completely rewrite it.

    I like the simplicity of this PR but if we’re going for a more structured/clean way to do things I’d prefer to have already a dictionary of hooks to replace with your code and a simpler way to add uniforms or custom code than just replacing strings.

  3. Reading #7581 again… On top of e55898c we can add the %% HOOKS %% and then create a class like this:

    THREE.ExtendedMaterial = function ( material, hooks ) {
    
        material.onBeforeCompile = function ( shader ) {
            var vertexShader = shader.vertexShader;
            var fragmentShader = parameters.fragmentShader;
            for ( var name in hooks ) {
               vertexShader = vertexShader.replace( '%%' + name + '%%', hooks[ name ] );
               fragmentShader = fragmentShader.replace( '%%' + name + '%%', hooks[ name ] );
            }
            shader.vertexShader = vertexShader;
            shader.fragmentShader = fragmentShader;
        };
    
        return material;
    
    };

    Then we can do this:

    var material = new THREE.ExtendedMaterial(
        new THREE.MeshBasicMaterial(),
        { vertex: 'transformed.x += sin( position.y ) / 2.0;' }
    );
  4. @mrdoob I think it’s difficult (or impossible) to serialize Materials hacked with .onBeforeCompile(). Do you think users should use ShaderMaterial if they want full functional(render, copy, clone, serialize, and so on) modified built-in materials?

    I didn’t expect (considered) the the use of onBeforeCompile() to be serialised…

Comments are closed.