UNPKG

blaze-2d

Version:

A fast and simple WebGL 2 2D game engine written in TypeScript

502 lines (450 loc) 15.7 kB
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