Reputation: 55
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
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