Could need some advice on adapting the SHADER BURN example

Hi,
I’m trying to adapt the SHADER BURN example for our needs.
We want our ship model to appear using this effect.
But when applying the demo code to our model, the effect does work, but the final model looks like this:

while the original model looked like this:

I think there are two problems:
1 - the textures look wrong
2 - lights are not calculated

The problem with the ligths not being calculated is probably because se have a shader that does not do that (the same shader as in the example). Right? So we have to combine the original shader with the simplistic one of the example? Where can we find the code of the original shaders?

But why are the textures wrong? All we did is replacing the statue model with our model. Is it because our model is more complex?

  1. It looks like the ‘burn’ texture is being rendered instead.
  2. The shader burn example is using a basic material that doesn’t have lighting. Here’s a thread about a similar topic: How can I make the custom shader stop? - #12 by yaustar

Having an example project either from the Editor or Codepen would really help.

You can find all the shader code used in the Standard Material chunks here: engine/src/scene/shader-lib/chunks at b152f733922fc56852fa2b3b3ad629cf4d1ef619 · playcanvas/engine · GitHub

In that thread you are referring to a project that uses the standard material. I think that is exactly what I need. But the project uses a simple custom shader. I think it first did use the standard material and then you changed it back later to using a modified custom shader.

Is there still a copy of the project using a shader with the standard material?

In the thread, I converted a Basic Material shader to use the chunks in a Standard Material.

I don’t understand?

There’s a project in that thread using a shader with Standard material.

Then I think I do not understand the code yet. Sorry for the confusion.
You use this code to change the materials:

Gem.prototype.initialize = function() {
    const material = this.materialAsset.resource.clone();
    material.chunks.APIVersion = pc.CHUNKAPI_1_56;
    material.chunks.diffusePS = this.diffuseChunk.resource;
    material.chunks.opacityPS = this.opacityChunk.resource;
    material.setParameter('uHeightMap', this.heightMapAsset.resource);

    material.update();

    // Replace the model material that we are overriding
    const renders = this.entity.findComponents('render');
    for (let i = 0; i < renders.length; ++i) {
        const meshInstances = renders[i].meshInstances;
        for (let j = 0; j < meshInstances.length; j++) {
            if (meshInstances[j].material === this.materialAsset.resource) {
                meshInstances[j].material = material;
            }
        }
    }  

    this.material = material;

    this.on('destroy', () => {
        this.material.destroy();
        this.material = null;
    });
};

Both the diffuseChunk and opacityChunk are very simple pieces of code. So I thought this can never be the full blown physical based rendering. So I guess it is not clear to me how this chunk mechanism works.

In my case the ship model uses physical based rendering including clearcoat. So probably quite a complex shader.
How does that work in the PlayCanvas architecture? Will the final shader be a collection of merged chunks?
And how can I then add a few lines to a chunk? I guess I need to add some lines like:

float height = texture2D(uHeightMap, vUv0).r;
if (height < uTime) discard;

(I do not need the edge effect)
But how can I add this, and where should I add this?
I tried to find more information about the shader chunk architecture. But can not find much. Is it still in the making? Or is it now an official part of PlayCanvas?

The Standard Material builds the final shader out of ‘chunks’ which generally represent different parts to material rendering logic. For example opacityPS for the opacity channel and properties:

When the shader is built and compiled, it uses as little or as many chunks as it needs based on the material data.

We have ‘default’ shaders for each of these chunks but they can be overridden (as you’ve seen above) to use custom logic instead of the default for that particular chunk of logic.

The shader burn example is very close to the dissolve effect in the thread.

I would start off by doing the opacity logic first (so overriding the opacity chunk).

You can also see the full shader code that is built at runtime via Spector.js by finding the draw call and looking at the fragment shader

Will there be a shader generated for each material in the model?
If so, how can I know which opacity chunk was used by which material.
When I just override all materials with a new opacity chunck, the render result may changed in a way I do not want to.

In general yes, but if two materials show the same combination of chunks then instead of compiling a new shader, it uses one the one that has been generated before for performance reasons.

You are looking at it from the wrong direction. You want to find the materials that you want to apply your own shader chunks to.

Can you share an Editor project (you can pretty easily create an ‘engine only’ example in the editor) or in CodePen and it be much easier to help when there is something to refer to.

Maybe this is over the limit off what I can understand (or learn). But I tried the following:

    const assets = {
        ship: new pc.Asset("ship", "container", {
            url: "/assets/models/ship/Ship_2K.glb",
        }),
        clouds: new pc.Asset("clouds", "texture", {
            url: "/assets/textures/clouds.jpg",
        }),
        opacityShader: new pc.Asset("opacityShader", "shader", {
            url: "/assets/shaders/opacity-fade.txt",
        }),
    };
    const assetListLoader = new pc.AssetListLoader(
        Object.values(assets),
        app.assets
    );
    assetListLoader.load((err, failed) => {
        if (err) {
            console.log(`assets failed to load: ${err}`);
        } else {
            console.log(`assets loaded`);

            const ship = assets.ship.resource.instantiateRenderEntity();
            app.root.addChild(ship);
            ship.setPosition(0, 5, 0);
            ship.rotate(0, 0, -22);
            ship.rotate(0, -45, 0);

            // create camera entity
            const camera = new pc.Entity("camera");
            camera.addComponent("camera", {
                clearColor: new pc.Color(0, 0, 0, 0),
            });
            app.root.addChild(camera);
            camera.setPosition(-3, 5, 10);

            // create directional light entity
            const dirLight = new pc.Entity("light");
            dirLight.addComponent("light", {
                //type: "omni",
                type: "directional",
                //color: new pc.Color(0.7, 0.9, 1),
                intensity: 5,
            });
            app.root.addChild(dirLight);
            dirLight.setEulerAngles(0, 0, 45);

            // prepare ship transistion
            const material = ship.model.material;
            material.chunks.APIVersion = pc.CHUNKAPI_1_56;
            material.chunks.opacityPS = assets.opacityShader.resource;
            material.setParameter("uHeightMap", assets.clouds.resource);
            material.update();

            let time = 0;
            app.on("update", function (dt) {
                time += 0.2 * dt;
                // reverse time
                let t = time % 2;
                if (t > 1) {
                    t = 1 - (t - 1);
                }
                // set time parameter for the shader
                material.setParameter("uTime", t);
                material.update();
            });

            app.start();
        }
    });

So I load the ship, cloud texture and shader code assets. This seems to work.
Then I create the ship, camera and a light. All seems to work ok too.
But then I want to override the opacity shader chunk for the ship. But that give the error that ship.model is undefined. While according the reference an entity should have a model object.

Or should it be more like this:

const renders = ship.findComponents("render");
for (let i = 0; i < renders.length; ++i) {
	const meshInstances = renders[i].meshInstances;
	for (let j = 0; j < meshInstances.length; j++) {
		const material = meshInstances[j].material;
		if (material) {
			material.chunks.APIVersion = pc.CHUNKAPI_1_56;
			material.chunks.opacityPS =
				assets.opacityShader.resource;
			material.setParameter(
				"uHeightMap",
				assets.clouds.resource
			);
			material.update();
		}
	}
}

Because my model has several submeshes and materials.

The last version of the code gives a shader compile error (which is a good sign that it tries to compile the correct shader :slight_smile: )

================================================================
Failed to compile fragment shader:

ERROR: 0:226: ‘vUv0’ : undeclared identifier
ERROR: 0:226: ‘texture’ : no matching overloaded function found
ERROR: 0:226: ‘r’ : field selection requires structure, vector, or interface block on left hand side

221:
222: void getOpacity() {
223: dAlpha = 1.0;
224:
225: float t = uTime;
226: float height = texture2D(uHeightMap, vUv0).r;
227: if (height < t) {
228: dAlpha = 0.0;
229: }
230: dAlpha *= material_opacity;
231: }

===============================================================

I copied the shader code from the example.

As a test I changed the shader to this code (I copied this from the opacity shader in the shader lib on Github mentioned above by @yaustar):

#ifdef MAPFLOAT
uniform float material_opacity;
#endif
void getOpacity() {
    dAlpha = 1.0;
    #ifdef MAPFLOAT
    dAlpha *= material_opacity;
    #endif
    #ifdef MAPTEXTURE
    dAlpha *= texture2DBias($SAMPLER, $UV, textureBias).$CH;
    #endif
    #ifdef MAPVERTEX
    dAlpha *= clamp(vVertexColor.$VC, 0.0, 1.0);
    #endif
}

No shader compiler error this time. But when setting dAlpha to 0.4 before returning the function doesn’t make a difference in rendered model. So I doubt if this shader is actually running.

You can capture a frame with JS Spector (I used Chome plugin) and inspect the shaders / uniforms.

Here’s an editor project that uses ‘engine-only’ code to demonstrate the effect: https://playcanvas.com/project/1031602/overview/f-shader-dissolve

The important part is here:

            const materials = asset.resource.materials;
            for (const materialAsset of materials) {
                /** @type {pc.StandardMaterial} */
                const material = materialAsset.resource;
                // Important to include the opacity chunk
                material.blendType = pc.BLEND_NORMAL;
                material.chunks.APIVersion = pc.CHUNKAPI_1_56;
                material.chunks.opacityPS = assets.opacityShader.resource;
                material.setParameter("uHeightMap", assets.clouds.resource);
                material.update();
            }

We have to make sure that the material includes the opacity chunk so that it compiles with our overridden shader code.

Hence changing the blend type from none to ‘normal’ which means the material has to include the opacity chunk.

We have a model with different sub objects. So is the double loop as shown below needed/correct?
(not yet addapted to change the blend type)

You can do it that way or just resource.materials like I did. You are changing the same materials either way.

I prefer to do it via my method as multiple render components and meshinstances can have the same material which means you are looping more than necessary

But what if our model has multiple materials?

Not sure what it changes? In my code, I’m applying the shader to all of them

I think I do not understand your code.
Do you assume I load a material using the asset loader? Because you use this line:

const materials = asset.resource.materials;

My models are loaded using the glTF files, which already include the materials.Like this:

const ship = assets.ship.resource.instantiateRenderEntity();

I do not see how your code combines with this.

No, this is the list of materials in the GLB container. engine/glb-container-resource.js at main · playcanvas/engine · GitHub

It’s not yet public API as it was subject to change when we first implemented this. But it hasn’t really changed since then

const ship = assets.ship.resource.instantiateRenderEntity();
const materials = assets.ship.resource.materials;