How to access raw shader information?

Hi, I’m toying around with Babylon. I’m interested in writing shaders where I take the raw GLSL created by babylon, changing it automatically by a compiler, and putting it back into a babylonjs shader. I want to do this to plug a custom graph editor built on top of a custom compiler, into a raw babylonjs shader.

I’ve been poking around the context at runtime. My first question is: What’s the best way to get the raw GLSL from a babylon shader? I see material._effect._fragmentSourceCode - is that a reliable place? Does the scene need to render first for this to be populated?

Second: if I take the above fragment and vertex source from a babylon PBR shader, set that to BABYLON.Effect.ShadersStore['customVertexShader'] =... and BABYLON.Effect.ShadersStore['customFragmentShader'] =... and create a new shader like:

shaderMaterial = new BABYLON.ShaderMaterial(
        'shader',
        scene,
        {
          vertex: 'custom',
          fragment: 'custom',
        },
        {
          attributes: ['position', 'normal', 'uv'],
          uniforms: [
            'world',
            'worldView',
            'worldViewProjection',
            'view',
            'projection',
            'Scene',
          ],
        }
      );

I get nothing rendered, and from WebGL, the warning/error

[.WebGL-0x7f86a6808200]RENDER WARNING: there is no texture bound to the unit 0
babylon:1
WebGL: too many errors, no more errors will be reported to the console for this context.

Is this because I didn’t include enough uniforms and attributes in the shader definition?

Welcome aboard!

Using ShaderMaterial won’t work because you would need to bind all uniforms/samplers required by the shader as well as handling light binding yourself. See #include directives in shader code output from Node Material Editor - #2 by Evgeni_Popov for more context. This thread is about using the code from a node material, but the problem is the same.

What you can do instead is override the material.customShaderNameResolve function to modify the shader sent to the gpu. See for eg:

2 Likes

@Evgeni_Popov customShaderNameResolve doesn’t appear to be a method of PBR materials?

      shaderMaterial = new BABYLON.PBRSpecularGlossinessMaterial('pbr', scene);
      shaderMaterial._saveCustomShaderNameResolve =
        shaderMaterial.customShaderNameResolve.bind(shaderMaterial);

Results in

TypeError: Cannot read properties of undefined (reading ‘bind’)

The property does exist but it can be undefined (if nobody has registered a custom shader name resolve callback yet), so you should handle this case in your code.

I have some code like this:

const shaderMaterial = new BABYLON.PBRMaterial('pbr', scene);
shaderMaterial.customShaderNameResolve = (
  shaderName, uniforms, uniformBuffers, samplers, defines, attributes, options
) => {
  if (options) {
    options.processFinalCode = (type, code) => {
      if (type === 'vertex') {
        return myCustomVertex;
      }
      return myCustomFragment;
    };
  }
  return shaderName;
};
mesh.material = shaderMaterial;

The first time my scene renders, the entire above block executes, and processFinalCode is called, and the custom shader source code is executed.

Later, I update my custom source code, and re-run the entire block of code above. As in, I create a new PBRMaterial, and reassign it to the mesh.

The second time this happens, customShaderNameResolve runs, but the processFinalCode callback is never called (verified by logging). Am I missing something to make processFinalCode run again?

If I re-create the babylon engine+scene entirely, then processFinalCode runs again.

processFinalCode won’t be reexecuted if the effect is not recompiled and can be found in the cache. To force a recompilation, you can change the value of a define with a value not used yet (in the defines property passed to customShaderNameResolve).

id() generates a new unique number every time it’s called. This doesn’t appear to bust the cache in the sense I see the same behavior. Is this what you’re suggesting?

shaderMaterial.customShaderNameResolve = (
    shaderName,
    uniforms,
    uniformBuffers,
    samplers,
    defines,
    attributes,
    options
  ) => {
    if (Array.isArray(defines)) {
      defines.push('' + id());
    } else {
      defines['' + id()] = id();
    }

Yes, but you should give a name to the define. For eg:

    if (Array.isArray(defines)) {
      defines.push('MYDUMMY' + id());
    } else {
      defines['MYDUMMY' + id()] = id();
    }

That doesn’t appear to have the desired effect of cache busting. This line does appear to work:

defines.AMBIENTDIRECTUV = 0.0000001 * Math.random();

Is this because I’m using a PBRMaterial, so I can only mutate known defines?

Indeed, only defines known by PBRMaterial are taken into account…

So, another way would be to call material.markAsDirty(BABYLON.Constants.MATERIAL_AllDirtyFlag) when you want to regenerate the effect and have processFinalCode called.

Hello @andyray just checking in, was your question answered?

Yes! I was able to use my experimental tool to take a full shader written for Three.js, update its AST to use Babylon uniforms instead, and then parse the Babylon PBRMaterial megashader and replace Babylon’s normal map uniform with the output of the previous shader!

1 Like

@Evgeni_Popov What is customShaderNameResolve supposed to return? See shaderName in customShaderNameResolve ignores the shader's name? · Issue #12154 · BabylonJS/Babylon.js · GitHub

If I return a different name than shaderName then my processFinalCode callback isn’t it. I don’t get why this callback is supposed to return anything if the name is hard coded and can’t be changed?

It seems like you set customShaderNameResolve on the instance, with an additional config object named csnrOptions

.Babylon.js/pbrBaseMaterial.ts at 0b3e860dd88bf14644680a02ef7bd9045b17ea42 · BabylonJS/Babylon.js · GitHub

So if customShaderNameResolve is present on the instance, it should override the hard coded name. Thats what it looks like to me at least, but idk.

shaderName is the name to lookup the shader code in the shader store. The PBR code is in ShaderStore.ShadersStore["pbrVertexShader"] and ShaderStore.ShadersStore["pbrPixelShader"] so the default name is “pbr” (VertexShader and PixelShader are automatically added for the lookups).

If in your customShaderNameResolve callback you put your code in your own ShadersStore entries, you should return the name corresponding to these entries. That’s what the CustomMaterial / PBRCustomMaterial is doing:

2 Likes

material.markAsDirty(BABYLON.Constants.MATERIAL_AllDirtyFlag) has no effect. Neither does material.markDirty(). Only the hack of modifying a defines like defines.AMBIENTDIRECTUV = Math.random() works. Are these dirty methods just buggy?

My bad, markAsDirty will only force the system to run the engine.createEffect method, but if the effect is found in the cache, it won’t be recreated.

Using a material plugin would be the cleanest way to handle custom code injection, as you can inject your own defines and they will be correctly taken into account when checking for effect recompilation. Is this an option for you?

A brute force approach would be to clean the cache of all effects: engine.releaseEffects();. This means that all effects will be recreated the first time they are needed…