RTS: Fog of War

Yes Yes Yes and Yes :wink:

I can confirm, it is working greatly now!

That seems I won’t be able to do this with shaders. Let’s see who else kick-in with their experience :slight_smile:

1 Like

After getting more into Shaders, I was able to solve it:

Fog of War Shader is for WebGL2 only.

Also I wanted to share my old Shader (only WebGL1) for i.e. RTS/RPG Entity Selection Circles:

1 Like

Just for anyone wanting to use a plugin instead I have made one here:

type RevealerData = {
    position:Vector2;
    range:float;
}

export class FogOfWarPluginMaterial extends MaterialPluginBase {
    static maxRevealers = 1000;
    static revealerData:RevealerData[] = []; 
    static maxRevealerRange = 0;

    static resetRevealers() {
        this.revealerData = [];
        this.maxRevealerRange = 0;
    }

    static addRevealer(newRevealer:Vector2, range:number) {
        if(this.revealerData.length > this.maxRevealers) {
            console.error("Too many revealers added!");
        }
        if(range > this.maxRevealerRange) {
            this.maxRevealerRange = range;
        }
        this.revealerData.push({range:range,position:newRevealer});
    }

    static getRevealerData():{revealerPositions:number[],revealerRanges:number[]} {
        this.revealerData.sort((a, b) => a.position.x - b.position.x);
        const revealerPositions:number[] = new Array(FogOfWarPluginMaterial.maxRevealers*2).fill(0);;
        const revealerRanges:number[] = new Array(FogOfWarPluginMaterial.maxRevealers).fill(0);;
        for(var i = 0; i < this.revealerData.length;i++){
            const data = this.revealerData[i];
            const revealerId = i*2;
            revealerPositions[revealerId] = data.position.x;
            revealerPositions[revealerId + 1] = data.position.y;
            revealerRanges[i] = data.range;
        }
        return {
            revealerPositions:revealerPositions,
            revealerRanges:revealerRanges
        }
    }

    _isEnabled = false;

    constructor(material:Material) {
        // last parameter is a priority, which lets you define the order multiple plugins are run.
        super(material, "FogOfWar", 200, { "FogOfWar": false });
        this.isEnabled = true;
    }

    get isEnabled() {
        return this._isEnabled;
    }

    set isEnabled(enabled) {
        if (this._isEnabled === enabled) {
            return;
        }
        this._isEnabled = enabled;
        this.markAllDefinesAsDirty();
        this._enable(this._isEnabled);
    }
        
    prepareDefines(defines:any, scene:Scene, mesh:Mesh) {
        defines["FogOfWar"] = this._isEnabled;
    }

    getClassName() {
        return "FogOfWarPluginMaterial";
    }

    getUniforms() {
        return {
            "ubo": [
                { name: "revealers", size: 1, type: "float", arraySize: 2 * FogOfWarPluginMaterial.maxRevealers },
                { name: "ranges", size: 1, type: "float", arraySize: FogOfWarPluginMaterial.maxRevealers },
                { name: "maxRevealerRange", size: 1, type: "float" },
                { name: "revealersCount", size: 1, type: "highp int" }
            ],
            "fragment":
                `#ifdef FogOfWar
                    uniform float revealers[${FogOfWarPluginMaterial.maxRevealers}];
                    uniform float ranges[${FogOfWarPluginMaterial.maxRevealers}];
                    uniform highp int revealersCount;
                    uniform float maxRevealerRange;
                #endif`,
        };
    }

    bindForSubMesh(uniformBuffer:any, scene:any, engine:any, subMesh:any) {
        if (this._isEnabled) {
            const revealerData = FogOfWarPluginMaterial.getRevealerData();
            uniformBuffer.updateFloat("maxRevealerRange", FogOfWarPluginMaterial.maxRevealerRange);
            uniformBuffer.updateArray("revealers", revealerData.revealerPositions);
            uniformBuffer.updateArray("ranges", revealerData.revealerRanges);
            uniformBuffer.updateInt("revealersCount", FogOfWarPluginMaterial.revealerData.length);
        }
    }

    getCustomCode(shaderType:any) {
        if (shaderType === "vertex") {
            return {
                CUSTOM_VERTEX_DEFINITIONS: `
                    varying vec3 vWorldPos;
                `,
                CUSTOM_VERTEX_MAIN_END: `
                    vWorldPos = worldPos.xyz; 
                `
            }
        }

        if (shaderType === "fragment") {
            return {
                CUSTOM_FRAGMENT_DEFINITIONS: `
                    varying vec3 vWorldPos;
                `,
                CUSTOM_FRAGMENT_MAIN_END: `
                    float fogless = 0.4;
                    for(int i = 0; i < revealersCount; i++) {
                        int revealersId = i*2;

                        //Preprocess efficiency step
                        float xDist = vWorldPos.x - revealers[revealersId];

                        //Break if dist too great as positions ordered by X value
                        if(xDist < -maxRevealerRange) {
                            break;
                        }

                        //x dist too large?
                        float maxRange = ranges[i];
                        if(xDist > maxRange || xDist < -maxRange) {
                            continue;
                        }

                        float distance = length(vWorldPos.xz - vec2(revealers[revealersId],revealers[revealersId+1]));
                        float tmpFogless = (maxRange - distance) / ranges[i];
                        tmpFogless = clamp(tmpFogless, 0.0, 1.0);
                        if(tmpFogless > 0.1) {
                            tmpFogless = 1.0;
                        } else if(tmpFogless > 0.0) {
                            tmpFogless = tmpFogless/0.1;
                        }
                        if(tmpFogless > fogless) {
                            fogless = tmpFogless;
                        }
                    }
                    gl_FragColor.rgb *= vec3(fogless);
                `
            };
        }
        return null;
    }
}

4 Likes