blaze-2d
Version:
A fast and simple WebGL 2 2D game engine written in TypeScript
502 lines (450 loc) • 15.7 kB
JavaScript
import { vec2 } from "gl-matrix";
import Rect from "../shapes/rect";
import { clear, createShaderProgram } from "../utils/gl";
import TextureLoader from "../texture/loader";
const vsRect = `#version 300 es
in vec2 a_Vertex;
in vec2 a_TexCoord;
in vec2 a_Uv;
uniform float u_ZIndex;
out vec2 texCoord;
out vec2 uv;
void main() {
texCoord = a_TexCoord;
uv = a_Uv;
gl_Position = vec4(a_Vertex.x, a_Vertex.y, u_ZIndex, 1.0);
}`;
const fsRect = `#version 300 es
precision mediump float;
in vec2 texCoord;
in vec2 uv;
uniform sampler2D u_Texture;
out vec4 outColor;
void main() {
outColor = texture(u_Texture, texCoord);
}`;
const vsCircle = `#version 300 es
in vec2 a_Vertex;
in vec2 a_TexCoord;
in vec2 a_Uv;
uniform float u_ZIndex;
out vec2 texCoord;
out vec2 uv;
void main() {
texCoord = a_TexCoord;
uv = a_Uv;
gl_Position = vec4(a_Vertex.x, a_Vertex.y, u_ZIndex, 1.0);
}`;
const fsCircle = `#version 300 es
precision mediump float;
in vec2 texCoord;
in vec2 uv;
uniform sampler2D u_Texture;
out vec4 outColor;
void main() {
// convert uv from 0 -> 1 to -1 -> 1
vec2 UV = uv * 2.0 - 1.0;
// no sqrt needed cause -1 to 1
float dist = UV.x * UV.x + UV.y * UV.y;
// discard pixels outside circle
if (dist > 1.0) {
discard;
}
// sample texture
outColor = texture(u_Texture, texCoord);
// anti-aliasing
// outColor.a = smoothstep(0.0, 0.008, dist);
}`;
const vsTriangle = `#version 300 es
in vec2 a_Vertex;
in vec2 a_TexCoord;
in vec2 a_Uv;
uniform float u_ZIndex;
out vec2 texCoord;
out vec2 uv;
void main() {
texCoord = a_TexCoord;
uv = a_Uv;
gl_Position = vec4(a_Vertex.x, a_Vertex.y, u_ZIndex, 1.0);
}`;
const fsTriangle = `#version 300 es
precision mediump float;
in vec2 texCoord;
in vec2 uv;
uniform sampler2D u_Texture;
out vec4 outColor;
void main() {
outColor = texture(u_Texture, texCoord);
}`;
const vsMetaball = `#version 300 es
in vec2 a_Vertex;
uniform float u_ZIndex;
uniform mediump float u_Radius;
out mediump float sqrRadius;
void main() {
sqrRadius = u_Radius * u_Radius;
gl_Position = vec4(a_Vertex.x, a_Vertex.y, u_ZIndex, 1.0);
}`;
const fsMetaball = `#version 300 es
precision mediump float;
uniform vec2 u_Metaballs[1000];
uniform int u_MetaballsCount;
uniform float u_Radius;
uniform float u_Threshold;
uniform vec4 u_Color;
uniform vec2 u_Resolution;
in float sqrRadius;
out vec4 outColor;
void main() {
float infl = 0.0; // total influence
for(int i = 0; i < u_MetaballsCount; i++) {
vec2 pos = u_Metaballs[i];
float currInfl = sqrRadius;
currInfl /= (pow(gl_FragCoord.x - pos.x, 2.0) + pow(u_Resolution.y - gl_FragCoord.y - pos.y, 2.0));
infl += currInfl;
}
// don't draw if influence < threshold
if(infl < u_Threshold)
discard;
outColor = u_Color;
// outColor.r = infl;
// outColor.a = 1.0;
}`;
import Blaze from "../blaze";
import Circle from "../shapes/circle";
import Triangle from "../shapes/triangle";
import Logger from "../logger";
/**
* Renders single instances of shapes at a time.
*/
export default class Renderer {
/**
* Sets up the renderer to be used for rendering.
*
* Creates the webgl2 rendering context for the canvas and clears the webgl buffer.
*
* @throws When browser does not support webgl 2.0
*
* @param canvas The canvas to grab webgl context from
* @param opts {@link WebGLContextAttributes} to pass to the `getContext` call
*/
static init(canvas, opts) {
const gl = canvas.getContext("webgl2", opts);
if (!gl)
throw Logger.error("Renderer", "Your browser does not support WebGL 2.0");
this.gl = gl;
this.resizeToCanvas();
// setup resize observer
const observer = new ResizeObserver((entries) => {
const rect = canvas.getBoundingClientRect();
const width = Math.ceil(rect.width);
const height = Math.ceil(rect.height);
if (canvas.width === width && canvas.height === height)
return;
// fixes infinite resize loop
// https://stackoverflow.com/questions/4288253/html5-canvas-100-width-height-of-viewport
if (canvas.style.display !== "block")
canvas.style.display = "block";
this.resizeToCanvas();
});
observer.observe(canvas);
// transparency
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.blendEquation(gl.FUNC_ADD);
// gl.enable(gl.DEPTH_TEST);
// gl.enable(gl.CULL_FACE);
// gl.depthFunc(gl.LEQUAL);
// gl.depthMask(true);
clear(gl);
this.initShaders(gl);
}
static initShaders(gl) {
// Rectangle shader
this.positionBuffer = gl.createBuffer();
this.indexBuffer = gl.createBuffer();
this.texBuffer = gl.createBuffer();
this.uvBuffer = gl.createBuffer();
this.rectProgram = createShaderProgram(gl, vsRect, fsRect);
this.rectProgramInfo = {
program: this.rectProgram,
attribLocations: {
vertex: gl.getAttribLocation(this.rectProgram, "a_Vertex"),
texCoord: gl.getAttribLocation(this.rectProgram, "a_TexCoord"),
uv: gl.getAttribLocation(this.rectProgram, "a_Uv"),
},
uniformLocations: {
zIndex: gl.getUniformLocation(this.rectProgram, "u_ZIndex"),
texture: gl.getUniformLocation(this.rectProgram, "u_Texture"),
},
};
// Circle shader
this.circleProgram = createShaderProgram(gl, vsCircle, fsCircle);
this.circleProgramInfo = {
program: this.circleProgram,
attribLocations: {
vertex: gl.getAttribLocation(this.circleProgram, "a_Vertex"),
texCoord: gl.getAttribLocation(this.circleProgram, "a_TexCoord"),
uv: gl.getAttribLocation(this.circleProgram, "a_Uv"),
},
uniformLocations: {
zIndex: gl.getUniformLocation(this.circleProgram, "u_ZIndex"),
texture: gl.getUniformLocation(this.circleProgram, "u_Texture"),
},
};
// triangle shader
this.triangleProgram = createShaderProgram(gl, vsTriangle, fsTriangle);
this.triangleProgramInfo = {
program: this.triangleProgram,
attribLocations: {
vertex: gl.getAttribLocation(this.triangleProgram, "a_Vertex"),
texCoord: gl.getAttribLocation(this.circleProgram, "a_TexCoord"),
uv: gl.getAttribLocation(this.triangleProgram, "a_Uv"),
},
uniformLocations: {
zIndex: gl.getUniformLocation(this.triangleProgram, "u_ZIndex"),
texture: gl.getUniformLocation(this.triangleProgram, "u_Texture"),
},
};
// metaball shader
this.metaballProgram = createShaderProgram(gl, vsMetaball, fsMetaball);
this.metaballProgramInfo = {
program: this.metaballProgram,
attribLocations: {
vertex: gl.getAttribLocation(this.metaballProgram, "a_Vertex"),
},
uniformLocations: {
zIndex: gl.getUniformLocation(this.metaballProgram, "u_ZIndex"),
resolution: gl.getUniformLocation(this.metaballProgram, "u_Resolution"),
color: gl.getUniformLocation(this.metaballProgram, "u_Color"),
radius: gl.getUniformLocation(this.metaballProgram, "u_Radius"),
threshold: gl.getUniformLocation(this.metaballProgram, "u_Threshold"),
metaballsCount: gl.getUniformLocation(this.metaballProgram, "u_MetaballsCount"),
metaballs: gl.getUniformLocation(this.metaballProgram, "u_Metaballs"),
},
};
}
/**
* Resizes the `width` and `height` of the canvas attached to `gl` to the canvas' `clientWidth` and `clientHeight` multiplied by the `resolutionScale` or 1.
*/
static resizeToCanvas() {
const gl = this.gl;
const canvas = gl.canvas;
const rect = canvas.getBoundingClientRect();
const width = Math.ceil(rect.width);
const height = Math.ceil(rect.height);
canvas.width = width * this.resolutionScale;
canvas.height = height * this.resolutionScale;
gl.viewport(0, 0, canvas.width, canvas.height);
}
static clear(clearColor) {
clear(this.gl, clearColor);
}
/**
* Renders all items currently in the render queue and clears the queue.
*
* Should be called at the end of each frame.
*
* If there is no camera specified in {@link Renderer} then nothing will be rendered.
*/
static flush(z) {
const min = this.queue.min || 0;
const max = this.queue.max || Blaze.getZLevels();
if (!this.camera || z < min || z > max || !this.queue[z])
return;
const queue = this.queue[z];
for (const item of queue) {
this.renderQueueItem(item, z);
}
delete this.queue[z];
}
/**
* Adds a shape to the render queue.
*
* @param shape The shape to queue
* @param position The x and y position to render the rectangle at
* @param rotation The rotation to apply to the shape
* @param zIndex The z position of the rendered rectangle
* @param scale The world to clip space scale value
*/
static queueShape(shape, position = vec2.create(), rotation = 0, zIndex = 0) {
const item = {
shape,
position,
rotation,
};
if (this.queue[zIndex])
this.queue[zIndex].push(item);
else
this.queue[zIndex] = [item];
if (zIndex >= (this.queue.max ? this.queue.max : 0))
this.queue.max = zIndex;
if (zIndex <= (this.queue.min ? this.queue.min : 0))
this.queue.min = zIndex;
}
/**
* Renders a shape from a {@link RenderQueueItem} object.
*
* @param item The {@link RenderQueueItem} to render
* @param zIndex The z index to render the shape at
*/
static renderQueueItem({ shape, position, rotation }, zIndex) {
const gl = this.gl;
let programInfo;
if (shape instanceof Rect) {
programInfo = this.rectProgramInfo;
}
else if (shape instanceof Circle) {
programInfo = this.circleProgramInfo;
}
else if (shape instanceof Triangle) {
programInfo = this.triangleProgramInfo;
}
// vertex positions
const vertices = shape.getVerticesClipSpace(position, this.scale, rotation, this.camera);
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.vertexAttribPointer(programInfo.attribLocations.vertex, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(programInfo.attribLocations.vertex);
// tex coords
const texCoords = shape.getUVCoords();
gl.bindBuffer(gl.ARRAY_BUFFER, this.texBuffer);
gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);
gl.vertexAttribPointer(programInfo.attribLocations.texCoord, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(programInfo.attribLocations.texCoord);
// uv coords
const uvs = shape.getUVCoords();
gl.bindBuffer(gl.ARRAY_BUFFER, this.uvBuffer);
gl.bufferData(gl.ARRAY_BUFFER, uvs, gl.STATIC_DRAW);
gl.vertexAttribPointer(programInfo.attribLocations.uv, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(programInfo.attribLocations.uv);
// indices
const indices = shape.getIndices();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
gl.useProgram(programInfo.program);
gl.uniform1f(programInfo.uniformLocations.zIndex, zIndex / Blaze.getZLevels());
// set active texture
if (shape.texture) {
TextureLoader.loadTexture(shape.texture);
const unit = shape.texture.getTextureUnit();
TextureLoader.updateLastUsed(unit);
gl.uniform1i(programInfo.uniformLocations.texture, unit - gl.TEXTURE0);
}
gl.drawElements(gl[this.mode], indices.length, gl.UNSIGNED_SHORT, 0);
}
/**
* Gets the renderer's webgl context.
*
* @returns The renderer's webgl context
*/
static getGL() {
return this.gl;
}
/**
* Sets the resolution scale to use when rendering.
*
* The width and height of the renderer canvas are set to `clientWidth * resolutionScale` and `clientHeight * resolutionScale` respectively.
*
* @param resolutionScale The new resolution scale to use
*/
static setResolutionScale(resolutionScale) {
if (resolutionScale <= 0)
return void Logger.error("Blaze: Resolution scale must be a number greater than 0.");
this.resolutionScale = resolutionScale;
this.resizeToCanvas();
}
/**
* Gets the renderer's current resolution scale.
*
* @returns The renderer's current resolution scale
*/
static getResolutionScale() {
return this.resolutionScale;
}
/**
* Sets the mode the renderer will use for drawing.
*
* @throws When the provided mode is not TRIANGLES, LINES or POINTS
*
* @param mode The mode to use
*/
static setMode(mode) {
const m = mode.toUpperCase();
if (m !== "TRIANGLES" && m !== "LINES" && m !== "POINTS")
return void Logger.error("Renderer", "Mode can only be TRIANGLES, LINES or POINTS.");
this.mode = m;
}
/**
* Gets the current webgl rendering mode being used by the renderer.
*
* @returns The rendering mode
*/
static getMode() {
return this.mode;
}
/**
* Sets the camera to use for rendering.
*
* @param camera The camera to use for rendering
*/
static useCamera(camera) {
this.camera = camera;
}
/**
* Gets the camera that is currently being used for rendering.
*
* @returns The camera that is currently being used for rendering
*/
static getCamera() {
return this.camera;
}
/**
* Set the scale that is applied to vertices to obtain the vertices in clip space.
*
* @param scale The scaling vector
*/
static setScale(scale) {
this.scale = scale;
}
/**
* Gets the scale that is applied to vertices to obtain the vertices in clip space.
*
* @returns The scaling vector
*/
static getScale() {
return this.scale;
}
/**
* Gets the render queue.
*
* @returns the render queue
*/
static getQueue() {
return this.queue;
}
/**
* Gets the maximum zIndex used by the queue.
*
* @returns The max zIndex of the queue
*/
static getQueueMax() {
return this.queue.max;
}
/**
* Gets the minimum zIndex used by the queue.
*
* @returns The min zIndex of the queue
*/
static getQueueMin() {
return this.queue.min;
}
}
Renderer.resolutionScale = 1;
Renderer.mode = "TRIANGLES";
/**
* The scale applied to shape vertices to obtain their clip space vertices.
*/
Renderer.scale = vec2.fromValues(1, 1);
Renderer.queue = {};
//# sourceMappingURL=renderer.js.map