onBeforeCompile madness

Hi,

I have been wokring on this for a day and it is driving me insane.
It’s in the middle of a big project, and it’s very hard to share my code, or to isolate the issue, so before I try to make a project just for this, I would appreciate if you could just read a bit about it, and see some out of context code and tell me if it rings any bell?
In advance thanks.

So basically I have to transform a flash animation, into a three js animation.
Not too complexe for the most, I can replace most vectorial sprites with bitmaps, but of course I need a bit more.
For example: an outline out of the selected sprite, and a transition between two textures for appearances.
So obviously I turned to shaders and added some code.

But I need to add code from different places in my code.
So the default on onBeforeCompile mechanic is not enough.

So I created this class ( I use typescript ):

import { MeshBasicMaterial, MeshStandardMaterialParameters, Renderer, Shader, Texture, } from "three";
import {generateUID} from "./utils";

export type Instruction = { instruction:"uniform", id:string, type:"float", value:number }
                        | { instruction:"uniform",id:string, type:"vec2", value:[number, number] }
                        | { instruction:"uniform",id:string, type:"vec3", value:[number, number, number] }
                        | { instruction:"uniform",id:string, type:"vec3", value:[number, number, number, number] }
                        | { instruction:"uniform",id:string, type:"sampler2D", value:Texture }
                        | { instruction:"inject", search:string, injection:string }
                        | { instruction:"replace", search:string, replacement:string };

export class InstructableMaterial extends MeshBasicMaterial {

    private readonly instructions:Instruction[] = new Array<Instruction>();
    public set nextInstruction( instruction:Instruction ) { this.instructions.push( instruction ); }

    private _shader:Shader;
    public get shader():Shader { return this._shader; }

    constructor( parameters:MeshStandardMaterialParameters ) {

        super( parameters );

        this.onBeforeCompile = ( shader: Shader, renderer: Renderer ) => {

            this._shader = shader;

            //@ts-ignore because apparently typescript doesn't expose this property
            this._shader.name = this.name + "-shader";

            if ( this.instructions.length == 0 ) return;

            let fragmentShader:string = shader.fragmentShader.slice();

            for ( let instruction of this.instructions ) {

                switch( instruction.instruction ) {

                    case "uniform":
                        fragmentShader = `uniform ${instruction.type} ${instruction.id};\n` + fragmentShader;
                        shader.uniforms[ instruction.id ] = { value: instruction.value };
                        break;

                    case "replace":
                        if ( fragmentShader.indexOf( instruction.search ) == -1 ) console.warn( "could not find: " + instruction.search );
                        fragmentShader = fragmentShader.replace( instruction.search, instruction.replacement );
                        break;

                    case "inject":
                        if ( fragmentShader.indexOf( instruction.search ) == -1 ) console.warn( "could not find: " + instruction.search );
                        fragmentShader = fragmentShader.replace( instruction.search, instruction.search + "\n" + instruction.injection );
                        break;
                }
            }

            shader.fragmentShader = fragmentShader;
            
            console.log( this.name + " was compiled" );
        }
    }
}

Then to use it as such:

let material:InstructableMaterial = (<InstructableMaterial>mesh.material);

material.nextInstruction = { instruction:"uniform", id:"selection", type: "float", value:0 };

material.nextInstruction = { instruction: "inject", search: "#include <map_fragment>", injection: `
		vec4 pixel = texture2D( map, vUv );
		
		if ( selection != 0. && pixel.a != 1. ) {
			
			vec3 red = vec3( 1., .0, .0 );
			
			if ( pixel.a != 0. )
				diffuseColor.rgb = mix( red, diffuseColor.rgb, diffuseColor.a );
			else
				diffuseColor.rgb = red;
			
			float u; 
			float v;
			
			const float total = 32.;
			float longDist = .07 + ( 1. - selection ) * 0.18;
			float shortDist = .03 - ( 1. - selection ) * 0.03;
			
			float a1 = 0.;
			for ( float i = 0.; i <= total; i++ ) {
				
				u = vUv.x + cos( 2. * PI * i / total ) * longDist;// * selection;
				v = vUv.y + sin( 2. * PI * i / total ) * longDist;// * selection;
				
				a1 = max( a1, texture2D( map, vec2( u, v ) ).a );
			}
			
			float a2 = 0.;
			for ( float i = 0.; i <= total; i++ ) {
				
				u = vUv.x + cos( 2. * PI * i / total ) * shortDist;// * selection;
				v = vUv.y + sin( 2. * PI * i / total ) * shortDist;// * selection;
				
				a2 = max( a2, texture2D( map, vec2( u, v ) ).a );
			}
			
			diffuseColor.a = max( pixel.a, selection * ( a1 - a2 ) * ( a1 - a2 ) );
		}
	`
};

I load GLTF animations that I have exported from 3DS Max with Babylon.js exporter.
And when I parse them I just replace the material with one I instantiate.

And sometimes is works… but it is so inconsistant!
Sometimes, and although I keep a reference to the shader and can change the uniforms values, nothing is displayed.
Sometimes the sprite is affected, but also are meshes that should not be start showing oulines…

Overall the feeling I get… is that the shaders are sometimes not attributed properly.
And the issue seams to change depending on what GLSL animation I load first…
I over-checked my code all day, I don’t think it is on my side (as in: something not related to three.js).

So what do you think?
Is there a problem with the way I override the mechanic?
Or is there a possible issue in the background?
Do you see an alternative?

Thank you for any hint you can give me, because I’m kinda stuck on this…

PS: I use r106. I tried updating but it didn’t solve the issue and introduced another one.

You can’t have any logic in onbeforecompile.

Thanks a lot… but I am not sure what that might include.
Or more exactly what that might not include…

What can I do exactly in the function?

Really nothing else other than replacing keys with values. To me it seems like a very weird, verbose l, async way to basically write JSON. There has been a PR though aiming to address some of these issues. Will try to find it.

I see…
Thanks then.
I’ll try to rework all my project with ShaderMaterials.

You can try this little plugin, it works out of box without core modifations, you can assign your callbacks multiple times without overriding each other.

2 Likes