proton-engine
Version:
Proton is a simple and powerful javascript particle animation engine.
325 lines (263 loc) • 10.7 kB
JavaScript
import Mat3 from "../math/Mat3";
import BaseRenderer from "./BaseRenderer";
import Util from "../utils/Util";
import ImgUtil from "../utils/ImgUtil";
import MStack from "../utils/MStack";
import DomUtil from "../utils/DomUtil";
import WebGLUtil from "../utils/WebGLUtil";
import MathUtil from "../math/MathUtil";
/**
* Represents a WebGL-based renderer for particle systems.
* @extends BaseRenderer
*/
export default class WebGLRenderer extends BaseRenderer {
/**
* Creates a new WebGLRenderer instance.
* @param {HTMLCanvasElement} element - The canvas element to render to.
*/
constructor(element) {
super(element);
this.gl = this.element.getContext("experimental-webgl", { antialias: true, stencil: false, depth: false });
if (!this.gl) alert("Sorry your browser do not suppest WebGL!");
this.initVar();
this.setMaxRadius();
this.initShaders();
this.initBuffers();
this.gl.blendEquation(this.gl.FUNC_ADD);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
this.gl.enable(this.gl.BLEND);
this.addImg2Body = this.addImg2Body.bind(this);
this.name = "WebGLRenderer";
}
init(proton) {
super.init(proton);
this.resize(this.element.width, this.element.height);
}
resize(width, height) {
this.umat[4] = -2;
this.umat[7] = 1;
this.smat[0] = 1 / width;
this.smat[4] = 1 / height;
this.mstack.set(this.umat, 0);
this.mstack.set(this.smat, 1);
this.gl.viewport(0, 0, width, height);
this.element.width = width;
this.element.height = height;
}
setMaxRadius(radius) {
this.circleCanvasURL = this.createCircle(radius);
}
getVertexShader() {
const vsSource = [
"uniform vec2 viewport;",
"attribute vec2 aVertexPosition;",
"attribute vec2 aTextureCoord;",
"uniform mat3 tMat;",
"varying vec2 vTextureCoord;",
"varying float alpha;",
"void main() {",
"vec3 v = tMat * vec3(aVertexPosition, 1.0);",
"gl_Position = vec4(v.x, v.y, 0, 1);",
"vTextureCoord = aTextureCoord;",
"alpha = tMat[0][2];",
"}"
].join("\n");
return vsSource;
}
getFragmentShader() {
const fsSource = [
"precision mediump float;",
"varying vec2 vTextureCoord;",
"varying float alpha;",
"uniform sampler2D uSampler;",
"uniform vec4 color;",
"uniform bool useTexture;",
"uniform vec3 uColor;",
"void main() {",
"vec4 textureColor = texture2D(uSampler, vTextureCoord);",
"gl_FragColor = textureColor * vec4(uColor, 1.0);",
"gl_FragColor.w *= alpha;",
"}"
].join("\n");
return fsSource;
}
initVar() {
this.mstack = new MStack();
this.umat = Mat3.create([2, 0, 1, 0, -2, 0, -1, 1, 1]);
this.smat = Mat3.create([1 / 100, 0, 1, 0, 1 / 100, 0, 0, 0, 1]);
this.texturebuffers = {};
}
blendEquation(A) {
this.gl.blendEquation(this.gl[A]);
}
blendFunc(A, B) {
this.gl.blendFunc(this.gl[A], this.gl[B]);
}
getShader(gl, str, fs) {
const shader = fs ? gl.createShader(gl.FRAGMENT_SHADER) : gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(shader, str);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
initShaders() {
const fragmentShader = this.getShader(this.gl, this.getFragmentShader(), true);
const vertexShader = this.getShader(this.gl, this.getVertexShader(), false);
this.sprogram = this.gl.createProgram();
this.gl.attachShader(this.sprogram, vertexShader);
this.gl.attachShader(this.sprogram, fragmentShader);
this.gl.linkProgram(this.sprogram);
if (!this.gl.getProgramParameter(this.sprogram, this.gl.LINK_STATUS)) alert("Could not initialise shaders");
this.gl.useProgram(this.sprogram);
this.sprogram.vpa = this.gl.getAttribLocation(this.sprogram, "aVertexPosition");
this.sprogram.tca = this.gl.getAttribLocation(this.sprogram, "aTextureCoord");
this.gl.enableVertexAttribArray(this.sprogram.tca);
this.gl.enableVertexAttribArray(this.sprogram.vpa);
this.sprogram.tMatUniform = this.gl.getUniformLocation(this.sprogram, "tMat");
this.sprogram.samplerUniform = this.gl.getUniformLocation(this.sprogram, "uSampler");
this.sprogram.useTex = this.gl.getUniformLocation(this.sprogram, "useTexture");
this.sprogram.color = this.gl.getUniformLocation(this.sprogram, "uColor");
this.gl.uniform1i(this.sprogram.useTex, 1);
}
initBuffers() {
const vs = [0, 3, 1, 0, 2, 3];
let idx;
this.unitIBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.unitIBuffer);
this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vs), this.gl.STATIC_DRAW);
let i;
let ids = [];
for (i = 0; i < 100; i++) ids.push(i);
idx = new Uint16Array(ids);
this.unitI33 = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.unitI33);
this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, idx, this.gl.STATIC_DRAW);
ids = [];
for (i = 0; i < 100; i++) ids.push(i, i + 1, i + 2);
idx = new Uint16Array(ids);
this.stripBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.stripBuffer);
this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, idx, this.gl.STATIC_DRAW);
}
createCircle(raidus) {
this.circleCanvasRadius = WebGLUtil.nhpot(Util.initValue(raidus, 32));
const canvas = DomUtil.createCanvas("circle_canvas", this.circleCanvasRadius * 2, this.circleCanvasRadius * 2);
const context = canvas.getContext("2d");
context.beginPath();
context.arc(this.circleCanvasRadius, this.circleCanvasRadius, this.circleCanvasRadius, 0, Math.PI * 2, true);
context.closePath();
context.fillStyle = "#FFF";
context.fill();
return canvas.toDataURL();
}
drawImg2Canvas(particle) {
const _w = particle.body.width;
const _h = particle.body.height;
const _width = WebGLUtil.nhpot(particle.body.width);
const _height = WebGLUtil.nhpot(particle.body.height);
const _scaleX = particle.body.width / _width;
const _scaleY = particle.body.height / _height;
if (!this.texturebuffers[particle.data.src])
this.texturebuffers[particle.data.src] = [
this.gl.createTexture(),
this.gl.createBuffer(),
this.gl.createBuffer()
];
particle.data.texture = this.texturebuffers[particle.data.src][0];
particle.data.vcBuffer = this.texturebuffers[particle.data.src][1];
particle.data.tcBuffer = this.texturebuffers[particle.data.src][2];
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, particle.data.tcBuffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
new Float32Array([0.0, 0.0, _scaleX, 0.0, 0.0, _scaleY, _scaleY, _scaleY]),
this.gl.STATIC_DRAW
);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, particle.data.vcBuffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
new Float32Array([0.0, 0.0, _w, 0.0, 0.0, _h, _w, _h]),
this.gl.STATIC_DRAW
);
const context = particle.data.canvas.getContext("2d");
const data = context.getImageData(0, 0, _width, _height);
this.gl.bindTexture(this.gl.TEXTURE_2D, particle.data.texture);
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, data);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR_MIPMAP_NEAREST);
this.gl.generateMipmap(this.gl.TEXTURE_2D);
particle.data.textureLoaded = true;
particle.data.textureWidth = _w;
particle.data.textureHeight = _h;
}
onProtonUpdate() {
// this.gl.clearColor(0, 0, 0, 1);
// this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
}
onParticleCreated(particle) {
particle.data.textureLoaded = false;
particle.data.tmat = Mat3.create();
particle.data.tmat[8] = 1;
particle.data.imat = Mat3.create();
particle.data.imat[8] = 1;
if (particle.body) {
ImgUtil.getImgFromCache(particle.body, this.addImg2Body, particle);
} else {
ImgUtil.getImgFromCache(this.circleCanvasURL, this.addImg2Body, particle);
particle.data.oldScale = particle.radius / this.circleCanvasRadius;
}
}
// private
addImg2Body(img, particle) {
if (particle.dead) return;
particle.body = img;
particle.data.src = img.src;
particle.data.canvas = ImgUtil.getCanvasFromCache(img);
particle.data.oldScale = 1;
this.drawImg2Canvas(particle);
}
onParticleUpdate(particle) {
if (particle.data.textureLoaded) {
this.updateMatrix(particle);
this.gl.uniform3f(this.sprogram.color, particle.rgb.r / 255, particle.rgb.g / 255, particle.rgb.b / 255);
this.gl.uniformMatrix3fv(this.sprogram.tMatUniform, false, this.mstack.top());
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, particle.data.vcBuffer);
this.gl.vertexAttribPointer(this.sprogram.vpa, 2, this.gl.FLOAT, false, 0, 0);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, particle.data.tcBuffer);
this.gl.vertexAttribPointer(this.sprogram.tca, 2, this.gl.FLOAT, false, 0, 0);
this.gl.bindTexture(this.gl.TEXTURE_2D, particle.data.texture);
this.gl.uniform1i(this.sprogram.samplerUniform, 0);
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.unitIBuffer);
this.gl.drawElements(this.gl.TRIANGLES, 6, this.gl.UNSIGNED_SHORT, 0);
this.mstack.pop();
}
}
onParticleDead(particle) {}
updateMatrix(particle) {
const moveOriginMatrix = WebGLUtil.makeTranslation(
-particle.data.textureWidth / 2,
-particle.data.textureHeight / 2
);
const translationMatrix = WebGLUtil.makeTranslation(particle.p.x, particle.p.y);
const angel = particle.rotation * MathUtil.PI_180;
const rotationMatrix = WebGLUtil.makeRotation(angel);
const scale = particle.scale * particle.data.oldScale;
const scaleMatrix = WebGLUtil.makeScale(scale, scale);
let matrix = WebGLUtil.matrixMultiply(moveOriginMatrix, scaleMatrix);
matrix = WebGLUtil.matrixMultiply(matrix, rotationMatrix);
matrix = WebGLUtil.matrixMultiply(matrix, translationMatrix);
Mat3.inverse(matrix, particle.data.imat);
matrix[2] = particle.alpha;
this.mstack.push(matrix);
}
destroy() {
super.destroy();
this.gl = null;
this.mstack = null;
this.umat = null;
this.smat = null;
this.texturebuffers = null;
}
}