1

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 :

  • the first emitter starts
  • after 500ms, the second emitter starts, and then,
    • stop the execution of the first emitter,
    • throw the error message
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
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • 1
    If a uniform is not used in your shader program the variable is optimized out by the compiler and does not become an active program resource. Hence you cannot set the value of this variable. Which variable is causing the error? – Rabbid76 Dec 30 '21 at 18:07

1 Answers1

2

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.

LJᛃ
  • 7,655
  • 2
  • 24
  • 35
  • Tanks ! I have updated the codepen and moved the useProgram() in the render function. I moved all assignation value to uniforms in the render too. Now, the first error message has disapeared but a second happened : **drawArraysInstanced: Vertex attrib array 0 is enabled but has no buffer bound.** And as you can notice, the second emitter switch off the first emitter at the moment it starts. I suspect the attributes buffer but... – Stéphane M. Dec 30 '21 at 23:23
  • I suspect the attributes buffer to be confused between the two programs. Do you have an idea on how to execute this two emitters together ? – Stéphane M. Dec 30 '21 at 23:30
  • You also need to bind the buffer and set the attribute pointer every time you render, textures, buffers, attributes and programs are all global states. There can only be a single active shader program, one active texture per unit, one active buffer per type. – LJᛃ Dec 31 '21 at 11:14
  • Thank you very muche. It woks fine. i moved all the buffer links and feed in the render part. An other problem was the line **this.ctx.clear(this.ctx.COLOR_BUFFER_BIT);**, I've commented it. Just above, you've said _You're (unnecessarily) creating two shader programs_ What could be a better way ? https://codepen.io/stephanemill/pen/KKXQJqG – Stéphane M. Dec 31 '21 at 12:25
  • 1
    It's the same shader program(code wise), just create it once and use it for both/all instances. – LJᛃ Jan 05 '22 at 12:42
  • Thanx ! I did it. – Stéphane M. Jan 08 '22 at 18:41