Function to extend Materials

This is a little helper function to create a THREE.ShaderMaterial extending a standard material. The second parameter is optional. Instead the constructor of a standard material, you can also pass a instance of a shader material and extend it the same way.

Usage

To extend materials:
THREE.extendMaterial( constructor|material|shaderMaterial [, options])

For patching in onBeforeCompile:
THREE.patchShader( object [, options])

const myMaterial = THREE.extendMaterial(THREE.MeshPhongMaterial, {

	// Will be prepended to vertex and fragment code
	header: 'varying vec3 vEye;',

	// Will be prepended to vertex code
	headerVertex: '',

	// Will be prepended to fragment code
	headerFragment: '',

	// If desired, the material class to create can be defined such as RawShaderMaterial or ShaderMaterial, by
	// default in order to seamlessly work with in-built features the CustomMaterial class provided by this
	// plugin is used which is a slightly extended ShaderMaterial.
	// class: THREE.ShaderMaterial,

	// Insert code lines by hinting at a existing
	vertex: {

		// Inserts the line after #include <fog_vertex>

		'#include <fog_vertex>': 'vEye = normalize(cameraPosition - w.xyz);',

		// Replaces a line (@ prefix) inside of the project_vertex include

		'project_vertex': {
			'@vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );': 'vec4 mvPosition = modelViewMatrix * vec4( transformed * 0.5, 1.0 );'
		}
	},
	fragment: {
		'#include <envmap_fragment>': 'diffuseColor.rgb += pow(dot(vNormal, vEye), 3.0);'
	},


	// Properties to apply to the new THREE.ShaderMaterial
	material: {
		skinning: true
	},


	// Uniforms (will be applied to existing or added) as value or uniform object
	uniforms: {

		// Use a value directly, uniform object will be created for or ..
		diffuse: new THREE.Color(0xffffff),

		// ... provide the uniform object, by declaring a shared: true property and such you can ensure
		// the object will be shared across materials rather than cloned.
		emissive: {
			shared: true, // This uniform can be shared across all materials it gets assigned to, sharing the value
			mixed : true, // When creating a material with/from a template this will be passed through
			linked: true, // To share them when used as template but not when extending them further, this ensures you donā€™t have
						  // to sync. uniforms from your original material with the depth material for shadows for example (see Demo)
			value: new THREE.Color('pink')
		}
	}

});

Templates

To inherit code patches of an existing patched material. For example creating a shadow material from your custom modified material, patches you made will be inherited included uniforms declared as mixed (described above).

myMaterial.customDepthMaterial = THREE.extendMaterial( THREE.MeshDepthMaterial, {
	
	template: myMaterial
	
});

Also notice the plugin makes customDepthMaterial and customDistanceMaterial available per material instead having to assign it to every mesh.

Code

14 Likes

Iā€™ve updated the code to automatically set the constants (such as USE_MAP) when the corresponding maps are set as uniform.

Also here is an example extending the THREE.MeshPhongMaterial with a fresnel effect:
https://codepen.io/Fyrestar/pen/RzVLYd

6 Likes

I updated the code, you can now replace lines too instead only inserting after them by prepending a @ symbol, you can also insert/replace lines inside of includes. Here how it both works basically.

THREE.ShaderMaterial.extend(THREE.MeshPhongMaterial, {

    vertex: {
    
        // Replaces a line inside of the project_vertex include
    
        'project_vertex': {
            '@vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );': 'vec4 mvPosition = modelViewMatrix * vec4( transformed * 0.5, 1.0 );'
        }
    }
});
3 Likes

Nice! Iā€™ve been using a similar pattern but instead pass the ShaderLib.* object into it and return an object with the vertexShader, fragmentShader, defines, and uniforms fields modified so they can recursively extended ā€“ sort of like shader mixins:

const checkboardShader = checkerboardMixin(ShaderLib.standard);
const topographicLineShader = topolineMixin(ShaderLib.standard);
const checkboardTopoShader = checkerboardMixin(topolineMixin(ShaderLib.standard));

// ...

const material = new ShaderMaterial(checkboardTopoShader);

The application order of shader mixins can be a bit confusing but Iā€™ve found it to be pretty flexible so far. You may be able to achieve something similar by allowing mixins to ā€œbuildā€ the extension object you pass in.

Nice work!

1 Like

Thanks :+1: i updated it now so you can also pass a ShaderMaterial too instead the constructor of a standard material and extend them the same way.

Edit: Iā€™ve also added THREE.patchShader now, it basically deals with plain objects only and can be used for example at onBeforeCompile. It only deals with fragmentShader, vertexShader and uniforms, but with the same patching pattern, recursive includes etc.

In the pen above the outer ring is a material extending MeshPhongMaterial, the second is extending the extended material, and the third is patching with onBeforeCompile.

1 Like

Little update, with a question mark prefix lines can be prepended. If the alphaTest property is set and above 0 in uniforms the constant will be set.

@Fyrestar What license is your code here shared under? I.e. can it be used in a commercial project?

MIT yes, i updated the link.

3 Likes

@Fyrestar
Thank you for this cool solution!

There is an error when I am trying to extend MeshMatcapMaterial
As you can see here - https://codepen.io/bjrockrider/pen/RwrGrzJ
Am I doing something wrong is this material not applicable for your extension function?

Thanks!

1 Like

This kind of material doesnā€™t support lights, so setting for the material lights to false fixes it. https://codepen.io/Fyrestar/pen/NWxRyMq

Iā€™ve made some changes i didnā€™t updated in the repo yet, that includes detecting lights support, using objects with value as well as just the value for the uniforms and inheritance which makes it much easier to create different material versions such as a depth material inheriting the custom vertex transforms.

3 Likes

@Fyrestar
Thank you very much for the answer.
One more question. How to pass to the extended material some values on initialization?
For example in case of MeshMatcapMaterial I would like to pass some texture in ā€˜matcapā€™ property and set ā€˜sideā€™ to DoubleSide.
Is it possible?

1 Like

Like i did to disable lights, anything in material is assigend to the new material:

const myMaterial = THREE.ShaderMaterial.extend( THREE.MeshMatcapMaterial, {

    material: {
        lights: false,
        side: THREE.DoubleSide
    }

});
2 Likes

I tried to do so

var material = THREE.ShaderMaterial.extend(THREE.MeshMatcapMaterial, {
  material: {
    lights: false,
    side: THREE.DoubleSide,
    color: 0xcc4444
  },

});

You can have a look here:

But it has no effect.

I see this version also doesnā€™t map color yet, you can set the uniform though:

2 Likes

@Fyrestar
Thank you for response once again.
If you would like to update some uniforms values in run time how would you go about it if using your plugin?
For example, if I were to use ShaderMaterial I would pass some Uniforms object there and then would be able to modify any of the properties that I passed. Of course they should be defined in Shaders.
Is something like this possible?

Of course, the material created with ShaderMaterial.extend is always a ShaderMaterial with uniforms.
Regarding the passed uniforms: like i said in the updated version you can also pass uniform objects instead the values if needed. Such as instead

const myMaterial = THREE.ShaderMaterial.extend( THREE.MeshStandardMaterial, {

    uniforms: {
        map: myTexture
    }

});

The uniform wrapper object

const myMaterial = THREE.ShaderMaterial.extend( THREE.MeshStandardMaterial, {

    uniforms: {
        map: {
        	value: myTexture
        }
    }

});

Iā€™ll update the repo next days.

1 Like

Almost forgot ^^

Updated the plugin now, couple changes and fixes:

  • Alias THREE.extendMaterial as shorter alternative to THREE.ShaderMaterial.extend
  • Fixed compatibility with minified THREE bundles
  • Inheritation (applying of previous code patches and inheriting properties)
  • Uniforms can be given now as wrapper-object or their value
  • Sharing of uniform wrapper by defining a shared {shared: true, value: .. } boolean in a uniform object
  • vertexHeader, fragmentHeader, vertexEnd, fragmentEnd added wich will only add the given string to the corresponding part while the ā€œEndā€ ones will add the string at the end of the main function
  • Constants (defines) which are a boolean false will get removed as they are mainly used with #ifdef in THREE what causes a false positive for this condition

It also has a inheritation feature now which comes quite handy when applying previous features again. For example creating a MeshStandardMaterial based one with a patch for some vertex distortions, and extending that again for another mesh with another transformation change, in order to have these changes in shadows you need a custom depth material which also applies these transformations, instead keeping track of all hardcoded you can also do the following which creates a depth material with all previous patches.

const depthMaterial = THREE.extendMaterial( THREE.MeshDepthMaterial, {
	
	extends: aExtendedMaterial
	
});

Iā€™ll add some further improvements such as only using the used (at least defined with null) and necessary uniforms rather than just all. I already implemented it but need to make sure there is nothing missed out for the necesary (like lights). Also more detailed control over inheritation such as skipping uniforms that arenā€™t relevant for the new base (like normal map for a depth material).

2 Likes

So here are a lot more important changes. I optimized the concept for shared and mixed uniforms, as with different sources for uniforms and possibly wanted references there are many scenarios to consider.

Demo: Creating new materials based on MeshStandardMaterial and custom depth material for shadows

All needed to create a depth material for shadows with the custom transforms:

myMaterial.customDepthMaterial = THREE.extendMaterial( THREE.MeshDepthMaterial, {

	template: myMaterial

} );
  • Big reduction of uniforms to defined and necessary ones
  • class property in options to use another material class than ShaderMaterial (e.g CustomMaterial)
  • mixed property in uniforms to pass them from templates
  • linked property in uniforms to share them when used as template but not when extending them, this ensures you donā€™t have to sync. uniforms from your original material with the depth material for shadows for example
  • customDepthMaterial and customDistanceMaterial can be defined on materials now instead only on the mesh, cloning a material also automatically clones these if defined and links uniforms to the new material (see Demo)
  • Cloning materials will respect shared uniforms
  • Templates (see Demo)
  • Extending a instance of a built-in material is improved to inherit the properties
  • New THREE.CustomMaterial based on ShaderMaterial for extending in-built materials, it ensures better compatibility with the in-built features, for example when using envMap (which requires THREE to setup constants internally)

After trying to use it (and actually managing using it! :smiley: ), the only thing I could ask for is a bit more docs :ā€™)

Right now itā€™s kinda mandatory to go through source to find out stuff like aliases.

Also - is there a reason why API is not consistent with ShaderMaterial (ie. vertexShader = vertex) ?

1 Like

Yeah iā€™ll probably make some doc soon, at least the cheat-sheet kinda example should show all options then.

The vertexShader/fragmentShader are string properties on those while here an object is required describing the patching, the object passed in extendMaterial also isnā€™t the object that applies directly to the new material, only the ā€œmaterialā€ property if defined is applied directly.