Stéphane M.
Stéphane M.

Reputation: 55

Problem with webGL uniforms for particle emitter : "uniform1f: location not for current program"

I have created a particle emitter class with WebGL/JS and it works fine with one instanciation. The problem is when I try to create 2 instances of this object. I get an error message from the GPU : WebGL: INVALID_OPERATION: uniform1f: location not for current program

So, I know that it is a problem with the shader program link : One program per uniform location. But after many searches on the subject, I can't get a solution. I'm not enough familiar with the concept of uniform location and attributes buffer to understand the bug.

Here is the code of my class. The two instanciation are at the beginning of the code. https://codepen.io/stephanemill/pen/KKXQJqG

What you are seeing :

window.addEventListener('load',()=>{
    
    new Particle({      
            quantity: 2000,         
            timeFactor: 100,            
            lifetime: 10,           
            fadeOut: .2,            
            size: 2,
            x: 50,          
            y: 70
    });
    
    setTimeout(()=>{            
            
        new Particle({          
                quantity: 2000,             
                timeFactor: 100,                
                lifetime: 5,                
                fadeOut: .2,                
                size: 2,                            
                x: 50,              
                y: 50,              
        });

    },500)
});

    
class Particle {

    static id = 0;
    
    static canvas = document.querySelector("#canvas-webgl");
    
    constructor(settings){
                
        settings = Object.assign({
            
            // Nombre de particules
            quantity: 200,
            
            // Taille des particules
            size: 2,        

            color: {
                r: {
                    min: 255,
                    max: 255,
                },
                g: {
                    min: 255,
                    max: 255,
                },
                b: {
                    min: 0,
                    max: 0,
                },
            },
        
            timeFactor: 1,
            
            x: 50,
            y: 50,
            
            // Durée de vie
            lifetime: 2,

            // Durée du fadeout         
            fadeOut: 1,     
            
            speed: { 
                min: 0, 
                max: 5,
                spread: 0,              
                sigma: .2,  
            },
            
            angle: {
                min: 0,
                max: 90,
                spread: 0,              
                sigma: .2,              
            },          
            
            gravity: 0,
            
            texture: null,
                        
        }, settings);
        
        Object.assign(this, settings);
        
        this.canvas     = this.constructor.canvas;
        
        this.ctx        = this.canvas.getContext("webgl");
        
        this.width      = this.canvas.width;
        
        this.height     = this.canvas.height;
        
        this.ratio      = this.width / this.height;
        
        this.startTime  = Date.now();
        
        if(!this.ctx) {
            alert("WebGL not supported!");
        }       
        
        
        // Séquence de démarrage du flux
        // -------------------------------      
        this.initProgram();
        
        this.initAttributes();
        
        this.initUniforms();
        
        this.initBlending();
        
        // this.initHUD();
        
        this.autokill();
        
        this.render();
        
        this.constructor.id++;
        
        return this;
    }
    
    initProgram(){  
        this.codeVertexShader   = this.getShader('explosion');
        this.codeFragmentShader = this.getFragmentShader();
        
        // Création du vertex shader
        this.vertexShader = this.ctx.createShader(this.ctx.VERTEX_SHADER);
        this.ctx.shaderSource(this.vertexShader, this.codeVertexShader);     
        this.ctx.compileShader(this.vertexShader);

        // Création du fragment shader
        this.fragmentShader = this.ctx.createShader(this.ctx.FRAGMENT_SHADER);
        this.ctx.shaderSource(this.fragmentShader, this.codeFragmentShader);
        this.ctx.compileShader(this.fragmentShader);

        // Création du programme
        this.program = this.ctx.createProgram();
        this.ctx.attachShader(this.program, this.vertexShader);
        this.ctx.attachShader(this.program, this.fragmentShader);
        this.ctx.linkProgram(this.program);
        this.ctx.useProgram(this.program);  
    }

    initAttributes(){       
        /*
        --------------------------------------------
        Mise en mémoire des attributs
        --------------------------------------------
        */
        
        const arraySpeedAngle   = new Float32Array(this.quantity * 2);
        const arrayPosition     = new Float32Array(this.quantity * 2);
        const arrayGravity      = new Float32Array(this.quantity * 2);
        const arrayColor        = new Float32Array(this.quantity * 3);
        
        for(let i=0; i<arraySpeedAngle.length; i+=2){
            const angleMin      = Math.deg2rad(this.angle.min);
            const angleMax      = Math.deg2rad(this.angle.max);
            const angleSpread   = Math.deg2rad(this.angle.spread);  
            
            const angle = Math.gradient(angleMin, angleMax, angleSpread, this.angle.sigma);         
            const speed = Math.gradient(this.speed.min, this.speed.max, this.speed.spread, this.speed.sigma);   
            
            arraySpeedAngle[i] = speed;
            arraySpeedAngle[i+1] = angle;
        }       
        
        for(let i=0; i<arrayPosition.length; i+=2){         
            let x0 = (this.x * 2 / 100) - 1;
            let y0 = 1 - (this.y * 2 / 100);
            x0 *= this.ratio;
            
            arrayPosition[i] = x0;
            arrayPosition[i+1] = y0;
        }
        
        for(let i=0; i<arrayGravity.length; i++){
            arrayGravity[i] = this.gravity;
            // arrayGravity[i] = Math.random()*10;
        }
        
        for(let i=0; i<arrayColor.length; i+=3){            
            const r = Math.rand(this.color.r.min, this.color.r.max);
            const g = Math.rand(this.color.g.min, this.color.g.max);
            const b = Math.rand(this.color.b.min, this.color.b.max);
            arrayColor[i] = r/255;
            arrayColor[i+1] = g/255;
            arrayColor[i+2] = b/255;
        }
        
        // Création du tampon de données sur Vitesse et Angle
        this.bufferSpeedAngle = this.ctx.createBuffer();
        this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.bufferSpeedAngle);
        this.ctx.bufferData(this.ctx.ARRAY_BUFFER, arraySpeedAngle, this.ctx.STATIC_DRAW);  
        const refSpeedAngle = this.ctx.getAttribLocation(this.program, "speed_angle");
        this.ctx.vertexAttribPointer(refSpeedAngle, 2, this.ctx.FLOAT, false, 0, 0);    
        this.ctx.enableVertexAttribArray(refSpeedAngle);        
        
        
        // Création du tampon de données sur la Position
        this.bufferPosition = this.ctx.createBuffer();      
        this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.bufferPosition);
        this.ctx.bufferData(this.ctx.ARRAY_BUFFER, arrayPosition, this.ctx.STATIC_DRAW);
        const refPosition = this.ctx.getAttribLocation(this.program, "position");
        this.ctx.vertexAttribPointer(refPosition, 2, this.ctx.FLOAT, false, 0, 0);  
        this.ctx.enableVertexAttribArray(refPosition);  
        
        
        // Création du tampon de données sur la Gravité
        this.bufferGravity = this.ctx.createBuffer();       
        this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.bufferGravity);
        this.ctx.bufferData(this.ctx.ARRAY_BUFFER, arrayGravity, this.ctx.STATIC_DRAW);
        const refGravity = this.ctx.getAttribLocation(this.program, "gravity");
        this.ctx.vertexAttribPointer(refGravity, 2, this.ctx.FLOAT, false, 0, 0);   
        this.ctx.enableVertexAttribArray(refGravity);           
        
        
        // Création du tampon de données sur la Couleur
        this.bufferColor = this.ctx.createBuffer();     
        this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.bufferColor);
        this.ctx.bufferData(this.ctx.ARRAY_BUFFER, arrayColor, this.ctx.STATIC_DRAW);
        const refColor = this.ctx.getAttribLocation(this.program, "color");
        this.ctx.vertexAttribPointer(refColor, 3, this.ctx.FLOAT, false, 0, 0); 
        this.ctx.enableVertexAttribArray(refColor);     

        // console.log(refSpeedAngle)       
        // console.log(refPosition)     
        // console.log(refGravity)  
        // console.log(refColor)            
    }

    initUniforms(){                     
        /*
        --------------------------------------------
        Mise en mémoire des uniforms
        --------------------------------------------
        */  
        
        // Location of uniforms
        this.refTime = this.ctx.getUniformLocation(this.program, "time");
        this.refTimeFactor = this.ctx.getUniformLocation(this.program, "timeFactor");
        this.refLifetime = this.ctx.getUniformLocation(this.program, "lifetime");
        this.refFadeOut = this.ctx.getUniformLocation(this.program, "fadeOut");
        this.refRatio = this.ctx.getUniformLocation(this.program, "ratio");
        this.refSize = this.ctx.getUniformLocation(this.program, "size");
        
        
        // Assig values to uniforms
        this.ctx.uniform1f(this.refTimeFactor, this.timeFactor);
        this.ctx.uniform1f(this.refLifetime, this.lifetime);
        this.ctx.uniform1f(this.refFadeOut, this.fadeOut);
        this.ctx.uniform1f(this.refRatio, this.ratio);
        this.ctx.uniform1f(this.refSize, this.size);
        
        
        // Uniform des textures
        if(!this.texture)
            return;
        
        if(this.texture)
            this.texture = document.querySelector(this.texture);
        
        var texture = this.ctx.createTexture();
        this.ctx.bindTexture(this.ctx.TEXTURE_2D, texture);
        this.ctx.texImage2D(this.ctx.TEXTURE_2D, 0, this.ctx.RGBA, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, this.texture);
        this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_WRAP_S, this.ctx.CLAMP_TO_EDGE);
        this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_WRAP_T, this.ctx.CLAMP_TO_EDGE);
        this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_MIN_FILTER, this.ctx.NEAREST);
        this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_MAG_FILTER, this.ctx.NEAREST);
        this.ctx.activeTexture(this.ctx.TEXTURE0);
        this.refTexture = this.ctx.getUniformLocation(this.program, "texture");
        
        this.ctx.uniform1i(this.textureLocation, 0);
        
    }

    initBlending(){     
        /*
        --------------------------------------------
        Paramètres de gestion des couleurs
        --------------------------------------------
        */  
        this.ctx.enable(this.ctx.BLEND);        
        this.ctx.blendFunc(this.ctx.SRC_ALPHA,this.ctx.ONE_MINUS_SRC_ALPHA);
        this.ctx.disable(this.ctx.DEPTH_TEST);
        this.ctx.clearColor(0,0,0,0.0);
    }
    
    initHUD(){
        document.querySelector('#nbr-particles span').innerHTML = this.quantity;
    }

    render() {
        // Rendu du canvas webgl
        this.ctx.clear(this.ctx.COLOR_BUFFER_BIT);          
        let time = (Date.now() - this.startTime)/1000;
        this.ctx.uniform1f(this.refTime, time);     
        
        this.ctx.drawArrays(this.ctx.POINTS, 0, this.quantity);

        
        this.loopID = window.requestAnimationFrame(()=>{
            this.render();
        });
    }
    
    getShader(){
        
            
                
            return `
                precision mediump float;

                attribute   vec2    speed_angle;
                attribute   vec2    position;
                attribute   vec2    gravity;
                attribute   vec3    color;
                            
                uniform     float   time; 
                uniform     float   timeFactor; 
                uniform     float   ratio; 
                uniform     float   size; 

                varying lowp vec3 vColor;
            
                void main(void) {
                    gl_PointSize = size;
                    
                    vColor = color;
                    
                    

                    // float modTime    = mod(time*10., 1.);
                    float modTime   = time / timeFactor;
                    float speed     = speed_angle.x;
                    float angle     = speed_angle.y;
                    float G         = gravity.x;

                    // if(sin((speed*(time))) > .5)                     
                        // gl_PointSize  *= gl_PointSize * sin(speed);

                    float x0 = position.x;
                    float y0 = position.y;

                    // Equations horaires de la parabole
                    // ---------------------------------
                    float x = speed * cos(angle) * modTime + x0;
                    float y = -.5 * G * pow(modTime, 2.) + speed * sin(angle) * modTime + y0;

                    // Ratio canvas
                    // ---------------------------------
                    x /= ratio;
                    
                    // x = x + sin(y)/.2;

                    gl_Position=vec4(    x , y,      0.,1.);      
                }`;
                
            
            
    }
                        
    getFragmentShader(){
        
        return `
            precision mediump float; 
            
            varying lowp vec3 vColor;
            
            uniform float time;
            uniform float lifetime;
            uniform float fadeOut;
            uniform sampler2D texture;
            
            vec4 baseTexture;
            
            void main(void) {
                  // float modTime  = mod(T*10., 1.);
                
                vec3 fragmentColor = vColor;                
                
                // float x = gl_PointCoord.x  * sin(T*10.*V.x);
                // float y = gl_PointCoord.y  * sin(T*10.*V.y);
                
                // vec2 coords = vec2(x,y);
                
                // baseTexture = texture2D( texture,  coords);
                baseTexture = texture2D( texture,  gl_PointCoord);
                
                // fadeOut
                // ----------------------------------
                float opacity = 1.;
                if(time > lifetime)
                        opacity -= (time - lifetime) / fadeOut;
                
                // To the white
                // ----------------------------------
                float whiteDuration = .5;
                if(time < whiteDuration){
                    float deltaColorR = (1. - fragmentColor.x) * (whiteDuration - time);
                    float deltaColorG = (1. - fragmentColor.y) * (whiteDuration - time);
                    float deltaColorB = (1. - fragmentColor.z) * (whiteDuration - time);
                    
                    fragmentColor = vec3(fragmentColor.x + deltaColorR, fragmentColor.y + deltaColorG, fragmentColor.z + deltaColorB);
                }
                
                
                
                gl_FragColor = vec4(fragmentColor.x, fragmentColor.y, fragmentColor.z, opacity);
                
                
                // gl_FragColor = vec4(vColor.x, vColor.y, vColor.z, opacity);
                
                // gl_FragColor = vec4(.6, 1.0, 1.0, opacity);
                
                
                // gl_FragColor = vec4(1.0, 1.0, 1., 1.);
                // gl_FragColor = baseTexture * vec4(.7 * sin(V.x), .0, 1., 1.);
                // gl_FragColor = baseTexture;
                
                
                // rotation
                // https://www.py4u.net/discuss/95325
            }`;
    }
    
    autokill(){
        setTimeout(()=>{
            this.kill();
            // myParticle();
        }, (this.lifetime + this.fadeOut)*1000);
    }

    kill(){
        this.ctx.bindTexture(this.ctx.TEXTURE_2D, null);
        this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, null);
        this.ctx.bindBuffer(this.ctx.ELEMENT_ARRAY_BUFFER, null);
        this.ctx.bindRenderbuffer(this.ctx.RENDERBUFFER, null);
        this.ctx.bindFramebuffer(this.ctx.FRAMEBUFFER, null);
        this.ctx.deleteBuffer(this.bufferSpeedAngle);
        this.ctx.deleteBuffer(this.bufferPosition);
        this.ctx.deleteBuffer(this.bufferGravity);
        this.ctx.deleteProgram(this.program);
        this.ctx.deleteShader(this.fragmentShader);
        this.ctx.deleteShader(this.vertexShader);
        // https://stackoverflow.com/questions/23598471/how-do-i-clean-up-and-unload-a-webgl-canvas-context-from-gpu-after-use
        
        window.cancelAnimationFrame(this.loopID);
        
        this.ctx.clear(this.ctx.COLOR_BUFFER_BIT);
        
        // console.clear();
        // new Particle(1000);
    }

    
} // End of class

Upvotes: 1

Views: 365

Answers (1)

LJᛃ
LJᛃ

Reputation: 8143

You're (unnecessarily) creating two shader programs, one with each instantiation but you only call useProgram in your initProgram method, however calling useProgram sets a global state on the rendering context (which in both instances is the same as it's requested from the same canvas) causing the program to remain bound until changed. Thus when you render with your first emitter it tries to render with the shader program of the second emitter (once that one's created). You need to select the program you want to render with every time you're rendering, so call useProgram in your render method.

Upvotes: 2

Related Questions