Fyrestar/THREE.extendMaterial

This is awesome, but does it not work with standard materials?

hayden2114 opened this issue · 13 comments

I stumbled upon your code here and thought my life was saved, but it appears it doesn't include capabilities to extend a standard material? If so, is it a three.js limitation?

I've been struggling trying to load in a draco gltf to replace it's meshStandardMaterial with a ShaderMaterial that copies the frag shader, vertex shader, and uniforms from shaderLib["standard"]. I get an error saying cannot read property toArray of undefined when mesh in injected into scene with ShaderMaterial. When I copy lambert the error changes to an undefined property direction. Copying the basic material works correctly.

I'm relatively new to three.js and very new to shaders so any guidance would be really appreciated. My end goal is to extend the standard shader to be able to lerp between two textures.

Oh wow, on second look there is MeshStandardMaterial! Let me give this a try

So I got your example version of this to work on my wheels gltf scene using the patchShader like so:

const customShader = new THREE.MeshStandardMaterial({
          onBeforeCompile: function(shader) {
              THREE.patchShader(shader, {
      
                  header: 'uniform vec3 tint;',
      
                  fragment: {
                      '#include <fog_fragment>': 'gl_FragColor.rgb *= tint;'
                  },
      
                  vertex: {
                      'project_vertex': {
                          '@vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );':
                              'vec4 mvPosition = modelViewMatrix * vec4( transformed * 0.25, 1.0 );'
                      }
                  },
      
                  uniforms: {
                    tint: new THREE.Color('orange')
                  }
      
              });
      
          }
      });

 wheels.children[0].material = customShader;

However, this same implementation with the extend function doesn't work and throws the following errors:

index.js:1375 THREE.ShaderMaterial.extend: no mapping for material class "Gr" found
aframe-v1.0.4.min.js:95 Uncaught TypeError: Cannot set property 'value' of undefined

Note: The same errors are thrown if I replace wheels.children[0].material with THREE.MeshStandardMaterial

const customShader = THREE.ShaderMaterial.extend(wheels.children[0].material, {
    
          header: 'uniform vec3 tint;',

          fragment: {
              '#include <fog_fragment>': 'gl_FragColor.rgb *= tint;'
          },

          vertex: {
              'project_vertex': {
                  '@vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );':
                      'vec4 mvPosition = modelViewMatrix * vec4( transformed * 0.25, 1.0 );'
              }
          },

          uniforms: {
            tint: new THREE.Color('orange')
          }

      });

What's the best way to go about extending the wheels.children[0].material?

This seems so be caused by the minified THREE. I've updated the code now which avoids this issue and added other new features and improvements. See the readme for updates but your code should work now, you can pass a material class constructor as well as a instance of a built-in material and shader material. If there are still issues let me know.

Okay it's working now! At least there are no errors anymore.

My new issue is that the values from the material I'm passing in (the wheel's gltf material) are not being cloned and passed onto customShader. An example is the metalness value, which is 0 on the wheels material but when I extend it the metalness value gets set as 0.5, the THREE.js default.


 const customShader = THREE.extendMaterial(wheels.children[0].material, {
        // Will be prepended to vertex and fragment code
        header: 'uniform sampler2D mapTwo; \n uniform float fac; \n',


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


        fragment: {
          'map_fragment': {
            "@#include <map_fragment>" : '#ifdef USE_MAP vec4 texelColor = texture2D( map, vUv ); vec4 texelColorTwo = texture2D( mapTwo, vUv ); texelColor = mapTexelToLinear( mix( texelColor, texelColorTwo, fac)); diffuseColor *= texelColor; #endif \n'
          }
        },


        // Properties to apply to the new THREE.ShaderMaterial
        material: {
          defines: {
            STANDARD: '',
            USE_MAP: true,
            USE_UV: true,
          },
          extensions: {
            derivatives: true,
          },
          lights: true
        },


        // Uniforms (will be applied to existing or added) as value or uniform object
        uniforms: {
          map: {
            value: null,
          },
          mapTwo: {
              value: null,
          },
          fac: {
              value: 0.0
          },
        }
      });

Figured out I can pass in uniform values (metalness: 0) that match the wheels material but is there a better way to clone the uniform's values?

I'll check out later, i'm also currently making another adaption handling all cases of uniform inheritation/override and creation from in-built materials more specific, especially of when to clone and when to share.

In your case it should already assign these values to the uniforms, i'll check it later ^^

Edit: fixed that case of zero value properties, that should work now. Btw, i think since your intent is to rather just add a minor change to existing materials onBeforeCompile might be a more suiting solution.

A major difference is that with onBeforeCompile you can't have individual additional uniforms you could access, i've made a plugin for this too actually, so you can have per-material or per-mesh uniforms.

Another reason i suggest this is that the concept of ShaderMaterial.extend is slightly different assuming you intent to use actual uniforms based materials, the other plugin i linked is focusing on extending in-built materials staying in-built rather than converting to ShaderMaterial.

Thank you for the quick responses and fixing that 0 issue! I took what you said into consideration and have found a simple solution using onBeforeCompile.

var wheelsMat = wheels.children[0].material;

wheelsMat.userData.map_from = { value: wheelsMat.map };
wheelsMat.userData.fac = { value: 0.0 };

wheelsMat.onBeforeCompile = (shader) => {

  THREE.patchShader(shader, {

    // adds to top of frag and vert shader
    header: `uniform sampler2D map_from; \n uniform float fac; \n`,

    // replaces map_fragment with replacement
    fragment: {
      'map_fragment' : {
        replacement:
          `
          #ifdef USE_MAP
              vec4 tCol = texture2D( map, vUv );
              vec4 tColFrom = texture2D( map_from, vUv );

              tCol = sRGBToLinear( 
                mix( tCol, tColFrom, fac)
              );

              diffuseColor *= tCol;
          #endif
          `
      }
    },

    // define uniforms that reference userData values
    uniforms: {
      map_from: wheelsMat.userData.map_from,
      fac: wheelsMat.userData.fac,
    }
  });
}

wheelsMat.userData.lerp = true; // used to check if this mat has lerp abilities

It's great because it adds onto the original material from the gltf and then during runtime I can update wheelsMat.userData values and influence the uniforms I added.

Also, I wanted to replace all of the map_fragment chunk and didn't see an easy way to do it with the syntax you supported (please correct me if I'm wrong here) so I swapped out one line of code in ShaderMaterialExtend.js. If there's a way to do it with your build I'd prefer that, so please let me know.

// chunk = chunk.replace( '#include <' + name + '>', applyPatches( THREE.ShaderChunk[ name ], value ) );
chunk = chunk.replace( '#include <' + name + '>', value.replacement );

On another note, I did try to use onBeforeCompile with your MaterialPlugins.js but I was unable to succeed with the patch. I don't need to take this route but thought I'd let you know for feedback purposes.

Anytime I tried to use a patch (like https://codepen.io/Fyrestar/pen/ymjqMm) and assign it in the onBeforeCompile array I got the following error:

image

I don't have that code anymore or I'd share it but I do remember trying to pass in empty objects into the patch and it still threw the same error.

It's great because it adds onto the original material from the gltf and then during runtime I can update wheelsMat.userData values and influence the uniforms I added.

Notice that these uniforms are only global and not individually when only using onBeforeCompile (can't be different for multiple meshes), but if you don't need that kind of control onBeforeCompile would be enough.

Regarding the error: which version of THREE did you use? i updated the code yesterday to fix compatibility with latest THREE versions.

I did notice that wheelsMat.uniforms was undefined, but could you elaborate on what you mean by global and the implications of that? I have implemented this same onBeforeCompile code to a few other mesh and their mats and updating userData.map_from is working as expected - It appears each mesh/material is able to have different userData.map_from values. Do you mean every mesh/material will now have the map_from and fac values in userData? Also, am I able to access the global store of uniforms after onBeforeCompile?

I'm using the three.js version that is shipped with aframe. It appears to be 0.111.6.
image

Do you mean every mesh/material will now have the map_from and fac values in userData?

Yes it will only uses those uniforms you provided at the onBeforeCompile callback, this is why i made the plugin in order to access and change the uniform values before every render call, with the freedom of where/how you want to store the parameters for them.

I'm not sure about your case, but assuming your 4 wheels would all fade from one texture to another this will be fine, if you want to change the texture of each wheel individually this wouldn't work anymore, they all will use the uniforms of the first that got compiled/rendered.

Okay this is making more sense now.

I'm not sure about your case, but assuming your 4 wheels would all fade from one texture to another this will be fine, if you want to change the texture of each wheel individually this wouldn't work anymore

Is that the case because those wheels would share the same material? Because I have other mesh in the scene that use a separate material (using same map_from and fac logic though) and I'm able to lerp those textures independently of the wheels.

Also, any update on that three.js error?

It means you could attempt to clone the wheel materials to change each individually as you would expect with regular materials, though the uniforms are only passed once, so they will either all share one or only the first will have them.

I'll check regarding the error, it's a bit confusing with THREE as there was a kinda double structure with shader, program program etc.

Okay I understand, thanks again.

I should be updating three relatively soon so if that fixes it I’ll comment again