Working with shader chunks

This comes from a discord discussion where a user wanted to convert a custom shader to use shader chunks. Since a lot useful and undocumented details where shared, I’m compiling a summary of the discussion. At the end the user was kind enough to share the final project.

how hard is it to transfer this shader tutorial Custom Shaders | Learn PlayCanvas to this way of writing shaders? Warp a Sprite with GLSL | Learn PlayCanvas

Not that hard, from what I see you don’t need to override any vertex shader chunk, and you can add the pixel shader in the diffusePS shader chunk. That would be a good place for that. You will need to pass some custom uniforms to the material for the heightMap, and another one for the time. For the uDiffuseMap uniform you can just use the regular material diffuse texture.

heightmap would be parallaxPS ?

Ah no, the name may be the same, but it’s not for the same use. The parallax effect is used to give a depth/3D feeling to flat surfaces.

or everything can be done in diffusePS

Yes, everything can be done in diffuse.

how I can pass heighMap into diffusePS, i thought it only takes one texture which is assigned to material in inspector?

In the same way you pass parameters in the custom shaders tutorial:

this.material.setParameter('uHeightMap', heightTexture);

And in your shader code in the diffusePS chunk make sure to define that uniform:

uniform sampler2D uHeightMap;

Same with the rest of the parameters required.

so basically diffuse map will be the one which is attached in editor and heightmap can be done by just setting it in parameter. am I correct?

image

Yes, you got it! Just a note, if you name it texture_heightMap then it may conflict with the parallax map, if your material uses one eventually. I would name it using a different schema e.g. uHeightMap. To make sure it doesn’t override any other uniform set by the engine.

Oh, so “texture_” is kinda keyword in this case. that makes a sense

Yes, it’s just part of the name, nothing required by GLSL.

i have another question! if we don’t put \n in our strings that means that the whole shader will be written in single line???

Yes, that’s correct. Alternatively you can use this ES6 syntax with template literals to have multiline string support:

const shader = `
// multiple lines supported
// line 1
// line 2

i guess if everything will be written in a single line that will be much faster?:smiley:

Ah I don’t think that would make much of a difference for the compiler. But for you the developer it will make debugging the shader much much harder. Since any errors coming up will always point to the same line.

we also cant use if-else in shaders right? (if we choose this way)

You can, it’s part of the GLSL specification.

how do i debug shader?

Usually when there is an error you will get an exception showing what kind of error and on what line it happened. You can’t add console logs or debug numbers directly. The shader isn’t a program running once for a certain input, it executes millions of times per frame, running for each vertex and for each pixel in parallel. So that wouldn’t make sense here. We debug shaders most of the time visually, by the end result. You can also use SpectreJS to get the full shader and inspect the input data and output buffer. Though in some cases that’s too high level and it can’t be of direct help.

btw what does $ mean in this context? or it’s GLSL relevant thing

That is PlayCanvas, it finds those variables and replaces them internally.

will it work like this?

image

  1. Remove all // that’s meant for comments only.

  2. You can’t just copy the custom shader and call it diffuseEffect. You need to find and override the engine diffusePS method. Here is the original:
    https://github.com/playcanvas/engine/blob/main/src/graphics/program-lib/chunks/standard/frag/diffuse.js

  3. Example code overriding diffuse, observe how I copy/pasted the original shader code for this chunk from github and I’m making changes:

this.material.chunks.diffusePS = `
#ifdef MAPCOLOR
uniform vec3 material_diffuse;
#endif

#ifdef MAPTEXTURE
uniform sampler2D texture_diffuseMap;
#endif

void getAlbedo() {
    dAlbedo = vec3(1.0);

#ifdef MAPCOLOR
    dAlbedo *= material_diffuse.rgb;
#endif

#ifdef MAPTEXTURE
    vec3 albedoBase = gammaCorrectInput(texture2DBias(texture_diffuseMap, $UV, textureBias).$CH);
    dAlbedo *= addAlbedoDetail(albedoBase);
#endif

#ifdef MAPVERTEX
    dAlbedo *= gammaCorrectInput(saturate(vVertexColor.$VC));
#endif

    // make albedo fully RED
    dAlbedo = vec3(1.0, 0.0, 0.0);
}
`;
this.material.update();

Now you could simplify things a bit and remove everything inside getAlbedo(){}. It depends on what kind of features you expect your shader to support.

The only thing you need to remember is instead of setting the final color to this variable like in your custom shader:

gl_FragColor = color;

Now you are expected to set it to:

dAlbedo = color;

Final project with shader: PlayCanvas | HTML5 Game Engine

11 Likes