Creating custom shaders by modifying existing shaders

Hello!

I’ve just done a lot of reading, and it seems some people are suggesting chunk replacement as a way of modifying existing shaders (rather than having to re-implement shadows, etc). Is this a fairly sane approach as a solution? At the moment I’m considering starting with this and then moving toward exporting the resultant shader as dedicated fragment and vertex shaders for futureproofing.

1 Like

Hi @hearsepileup,

I’d say that for most model/material based shaders overriding/extending shader chunks is the way to go.

You will get a great boilerplate of uniforms to use (uv coordinates, model matrix, view/world position etc), and as you said support for lighting/shadows etc.

I don’t see these two ways of developing shaders (custom shader vs modifying shader chunks) as much different, since even with the shader chunk approach you have great control over the resulting shader. You can quickly modify the base chunks and get a very custom shader.

2 Likes

Great to hear it! Yeah I’m doing a couple of interating things like screenspace shading and an attempt at a Return of the Obra Dinn style dither (for a couple of minor effects). It’d be nice to just lay these on top of the groundwork already here rather than trying to re-implement. (I’m still a GLSL n00b, so really it would be trying and failing to implement hahaha).

I’m going to play around and see if I can find the most apropriate chunks to modify. Thanks for your help!

2 Likes

Where would be best to look for reference for these? I know they’re semi undocumented, but I’m having trouble getting access to these variables. What I’m really after is screen space uvs, though I can see if I set pixel snap I can get the screen size, so at worst I can just do gl_FragCoord.xy/uScreenSize.xy.

I’d say start your study on the chunks repo here, especially the base/start VS/PS programs:

At any point in the console you can see the final compiled shader of any material using the following notation:

console.log(material.shader.definitions);

Take note though, that is for model/material based shaders. If you are writing a post process effect there you are authoring a custom shader from scratch and you will be using the default WebGL attributes.

The following site is a great learning resource to get comfortable with how WebGL works:

4 Likes

Thanks for all your help - I did it!

Here’s a sample of the important code for anyone else reading this:

// Grab all materials in project
var assets = this.app.assets.filter(function (asset) {
    return asset.type === 'material';
});

// Grab custom fragment shader functions - these just make sure I can grab only the bits I need for each shader
var luma = this.app.assets.find("getluma.fs").resource;
var dither = this.app.assets.find("dither8x8.fs").resource;

// We are replacing the default diffuse light function in order to insert the new shader functions. This shader fragment is literally a copy paste of the code at https://github.com/playcanvas/engine/blob/137f6cb19fdb01caf2f936029bd76ae2fe0ce2a9/src/graphics/program-lib/chunks/lightDiffuseLambert.frag
var lightDifuse = this.app.assets.find("lightdiffuse.fs").resource;

// Concat all the functions together
var functions = luma + '\n' + dither + '\n' + lightDifuse + '\n';

// Grab the custom shader manipulation
var setDither = '\n' + this.app.assets.find("distancedither.fs").resource + '\n';

// Copy the very end of the default shader implementation (combineColour and getEmission) to correctly set your fragment colour, then drop in the custom function. As you can see I set a 'scale' float; this is because I'm doing some pixellation in the dither function, but I wanted to keep the scale easily manipulatable.
var endPS = 'gl_FragColor.rgb = combineColor(); \n\
        gl_FragColor.rgb += getEmission(); \n\
        float scale = 2.0; \n' + 
        setDither;

// Loop over all materials
assets.forEach( asset => {
    const material = asset.resource;
    // Replace the diffuse lambert pixel shader with a new one + our custom functions
    material.chunks.lightDiffuseLambertPS = functions;
    // Ensure that the screen dimensions are available globally (I only need this because of my pixellation shader)
    material.pixelSnap = true;
    // Add your function calls to set the fragment
    material.chunks.endPS = endPS;
});
2 Likes

Oh, and because I simply can’t help myself, here’s a preview of the results - really happy that I got it working!

4 Likes

Thanks for sharing @hearsepileup, that looks awesome!

1 Like

Hello,

is this still the best way to create a shader based on the default shader?

In my case I need to alpha mask a rotating model (sphere) in global space - meaning, the mask does not rotate with the model. The goal is to see inside the sphere but only where it is facing the camera. Doing this in a shader is trivial but I’m not sure what the best approach is to not lose the default shader features.

Any hints appreciated!

yeah I’d capture the shader without this feature using Spector JS.
Then inspect the code in order to figure out what needs to change.
When find that code in chunks engine/src/scene/shader-lib/chunks at main · playcanvas/engine · GitHub, copy those into your script, customize them and specify them on the material as an override.

1 Like

Good, I’ll try that. Thank you!

Sharing the result:

/*
    Spherical cutout at world position.
*/
var CutoutShader = pc.createScript('cutoutShader');
CutoutShader.attributes.add('materialAssets', {type: 'asset', assetType: 'material', array: true});
CutoutShader.attributes.add('cutoutPosition', {type: 'vec3' });
CutoutShader.attributes.add('radius', {type: 'number', min: 0.01, max: 15});
CutoutShader.attributes.add('feather', {type: 'number', min: 0.01, max: 5});

CutoutShader.prototype.initialize = function() {

    const opacityChunk = "uniform float material_opacity;\
        uniform vec3 uCutoutPosition;\
        uniform float uRadius;\
        uniform float uFeather;\
        \
        void getOpacity() {\
            dAlpha = smoothstep(uRadius, uRadius + uFeather, length(vPositionW-uCutoutPosition));\
            dAlpha *= material_opacity;\
        }";

    const renders = this.entity.findComponents('render');

    const modifyAndApplyMaterial = function(sourceMaterial)
    {
        const material = sourceMaterial.resource.clone();
        material.chunks.APIVersion = pc.CHUNKAPI_1_57;
        material.chunks.opacityPS = opacityChunk;
        material.update();

        // replace the model material that we are overriding        
        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 === sourceMaterial.resource) {
                    meshInstances[j].material = material;
                }
            }
        }  
        return material;
    };

    this.materials = [];
    this.materialAssets.forEach(sourceMaterial => {
        this.materials.push(modifyAndApplyMaterial(sourceMaterial));
    });    

    this.on('destroy', () => {
        this.materials.forEach(material => {
            material.destroy();
        });
        this.materials = [];
    });
};

CutoutShader.prototype.update = function(dt) {
    var pos = new Float32Array([this.cutoutPosition.x, this.cutoutPosition.y, this.cutoutPosition.z]);
    this.materials.forEach(material => {
        material.setParameter('uCutoutPosition', pos);
        material.setParameter('uRadius', this.radius);
        material.setParameter('uFeather', this.feather);
    });
};

Example:
image

5 Likes