UNPKG

jsge

Version:

Javascript Game Engine

1,188 lines (1,054 loc) 99.2 kB
import { ERROR_CODES, CONST, DRAW_TYPE, WARNING_CODES } from "../../constants.js"; import { crossProduct } from "../../utils.js"; import { Exception, Warning } from "../Exception.js"; import { GameStageData } from "../GameStageData.js"; import { ImageTempStorage } from "../Temp/ImageTempStorage.js"; import { DrawTiledLayer } from "../2d/DrawTiledLayer.js"; import { DrawCircleObject } from "../2d/DrawCircleObject.js"; import { DrawConusObject } from "../2d/DrawConusObject.js"; import { DrawLineObject } from "../2d/DrawLineObject.js"; import { DrawPolygonObject } from "../2d/DrawPolygonObject.js"; import { DrawRectObject } from "../2d/DrawRectObject.js"; import { DrawTextObject } from "../2d/DrawTextObject.js"; import AssetsManager from "../../../modules/assetsm/src/AssetsManager.js"; import { utils } from "../../index.js"; import { TextAtlas } from "../Temp/TextAtlas.js"; export class WebGlEngine { /** * @type {WebGLRenderingContext} */ #gl; /** * @type {number} */ #MAX_TEXTURES; /** * @type {number} */ #MAX_TEXTURE_SIZE; /** * @type {boolean} */ #debug; /** * @type {Object} */ #gameOptions; /** * @type {AssetsManager} */ #loaderReference; /** * @type {WebGLBuffer | null} */ #positionBuffer; /** * @type {WebGLBuffer | null} */ #texCoordBuffer; /** * @type {Array<number> | null} */ #currentVertices = null; /** * @type {Array<number> | null} */ #currentTextures = null; /** * @type {Array<TextAtlas>} */ #textAtlases = []; /** * @type {number} */ #currentAtlasIndex = 0; /** * @type {Map<string, WebGLProgram>} */ #registeredWebGlPrograms = new Map(); /** * @type {Map<string, Object<string, WebGLUniformLocation | number>>} */ #webGlProgramsVarsLocations = new Map(); /** * @type {Map<string, {method: Function, webglProgramName: string}>} */ #registeredRenderObjects = new Map(); /** * @type {boolean} */ #loopDebug; constructor(context, gameOptions, iLoader) { if (!context || !(context instanceof WebGLRenderingContext)) { Exception(ERROR_CODES.UNEXPECTED_INPUT_PARAMS, " context parameter should be specified and equal to WebGLRenderingContext"); } this.#gl = context; this.#gameOptions = gameOptions; this.#loaderReference = iLoader; this.#debug = gameOptions.debug.checkWebGlErrors; this.#MAX_TEXTURES = context.getParameter(context.MAX_TEXTURE_IMAGE_UNITS); this.#MAX_TEXTURE_SIZE = context.getParameter(context.MAX_TEXTURE_SIZE); this.#positionBuffer = context.createBuffer(); this.#texCoordBuffer = context.createBuffer(); this.#textAtlases[0] = new TextAtlas(gameOptions.render.textAtlasMaxSize, this.#gl.createTexture(), 0); this._registerObjectRender(DrawTextObject.name, this._bindText, CONST.WEBGL.DRAW_PROGRAMS.IMAGES_M); this._registerObjectRender(DrawRectObject.name, this._bindPrimitives, CONST.WEBGL.DRAW_PROGRAMS.PRIMITIVES_M); this._registerObjectRender(DrawPolygonObject.name, this._bindPrimitives, CONST.WEBGL.DRAW_PROGRAMS.PRIMITIVES_M); this._registerObjectRender(DrawCircleObject.name, this._bindConus, CONST.WEBGL.DRAW_PROGRAMS.PRIMITIVES); this._registerObjectRender(DrawConusObject.name, this._bindConus, CONST.WEBGL.DRAW_PROGRAMS.PRIMITIVES); this._registerObjectRender(DrawLineObject.name, this._bindPrimitives, CONST.WEBGL.DRAW_PROGRAMS.PRIMITIVES_M); this._registerObjectRender(DrawTiledLayer.name, this._bindTileImages, CONST.WEBGL.DRAW_PROGRAMS.IMAGES_M); this._registerObjectRender(DRAW_TYPE.IMAGE, this._bindImage, CONST.WEBGL.DRAW_PROGRAMS.IMAGES_M); } getProgram(name) { return this.#registeredWebGlPrograms.get(name); } getProgramVarLocations(name) { return this.#webGlProgramsVarsLocations.get(name); } _fixCanvasSize(width, height) { this.#gl.viewport(0, 0, width, height); } _initiateJsRender = (stageData) => { return new Promise((resolve, reject) => { const tileLayers = stageData.getObjectsByInstance(DrawTiledLayer), [ settingsWorldWidth, settingsWorldHeight ] = stageData.worldDimensions; // count max possible boundaries sizes let maxBSize = 0, maxESize = 0, maxPSize = 0, maxWorldW = 0, maxWorldH = 0; tileLayers.forEach(tiledLayer => { const setBoundaries = tiledLayer.setBoundaries, layerData = tiledLayer.layerData, tilemap = tiledLayer.tilemap, tilesets = tiledLayer.tilesets, { tileheight:dtheight, tilewidth:dtwidth } = tilemap, tilewidth = dtwidth, tileheight = dtheight; for (let i = 0; i < tilesets.length; i++) { const layerCols = layerData.width, layerRows = layerData.height, worldW = tilewidth * layerCols, worldH = tileheight * layerRows; const polygonBondMax = layerData.polygonBoundariesLen, ellipseBondMax = layerData.ellipseBoundariesLen, pointBondMax = layerData.pointBoundariesLen; if (maxWorldW < worldW) { maxWorldW = worldW; } if (maxWorldH < worldH) { maxWorldH = worldH; } if (setBoundaries) { maxBSize += polygonBondMax; maxESize += ellipseBondMax; maxPSize += pointBondMax; // boundaries cleanups every draw cycles, we need to set world boundaries again } } }); if (maxWorldW !== 0 && maxWorldH !== 0 && (maxWorldW !== settingsWorldWidth || maxWorldH !== settingsWorldHeight)) { Warning(WARNING_CODES.UNEXPECTED_WORLD_SIZE, " World size from tilemap is different than settings one, fixing..."); stageData._setWorldDimensions(maxWorldW, maxWorldH); } if (this.#gameOptions.render.boundaries.mapBoundariesEnabled) { maxBSize+=16; //4 sides * 4 cords x1,y1,x2,y,2 } stageData._setMaxBoundariesSize(maxBSize, maxESize, maxPSize); stageData._initiateBoundariesData(); resolve(true); }); }; _initWebGlAttributes = () => { const gl = this.#gl; gl.enable(gl.BLEND); gl.enable(gl.STENCIL_TEST); gl.stencilFunc(gl.ALWAYS, 1, 0xFF); //if stencil test and depth test pass we replace the initial value gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); return Promise.resolve(); }; /** * * @returns {Promise<void>} */ _initiateWasm = (stageData) => { const url = this.#gameOptions.optimization === CONST.OPTIMIZATION.WEB_ASSEMBLY.NATIVE_WAT ? this.#gameOptions.optimizationWASMUrl : this.#gameOptions.optimizationAssemblyUrl; return new Promise((resolve, reject) => { this.layerData = new WebAssembly.Memory({ initial:1000 // 6.4MiB x 10 = 64MiB(~67,1Mb) }); this.layerDataFloat32 = new Float32Array(this.layerData.buffer); const importObject = { env: { memory: this.layerData, logi: console.log, logf: console.log } }; fetch(url) .then((response) => response.arrayBuffer()) .then((module) => WebAssembly.instantiate(module, importObject)) .then((obj) => { this.calculateBufferData = obj.instance.exports.calculateBufferData; resolve(); }); }); }; _initDrawCallsDebug(debugObjReference) { this.#loopDebug = debugObjReference; } /** * * @returns {void} */ _clearView() { const gl = this.#gl; this.#loopDebug.cleanupDebugInfo(); //cleanup buffer, is it required? //gl.bindBuffer(gl.ARRAY_BUFFER, null); gl.clearColor(0, 0, 0, 0);// shouldn't be gl.clearColor(0, 0, 0, 1); ? // Clear the color buffer with specified clear color gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); } /** * * @returns {Promise<any>} */ _render(verticesNumber, primitiveType, offset = 0) { this.#gl.drawArrays(primitiveType, offset, verticesNumber); // set blend to default return Promise.resolve(verticesNumber); } /** * * @returns {Promise<void>} */ _preRender() { return new Promise((resolve, reject) => { const gl = this.#gl; if (this.#debug) { const err = this.#debug ? gl.getError() : 0; if (err !== 0) { console.error(err); throw new Error("Error num: " + err); } } else { resolve(); } }); } /** * * @returns {Promise<void>} */ _postRender(inputData) { let verticesNumber = inputData; // A workaround for backward capability in 1.5.n if (Array.isArray(verticesNumber)) { const [vertices, primitiveType] = inputData; this.#gl.drawArrays(primitiveType, 0, vertices); verticesNumber = vertices; } return new Promise((resolve, reject) => { const gl = this.#gl; if (verticesNumber !== 0) { this.#loopDebug.incrementDrawCallsCounter(); this.#loopDebug.verticesDraw = verticesNumber; } gl.stencilFunc(gl.ALWAYS, 1, 0xFF); if (this.#gameOptions.debug.delayBetweenObjectRender) { setTimeout(() => { resolve(); }, 1000); } else { resolve(); } }); } /************************************* * Register and compile programs ************************************/ /** * * @param {string} programName * @param {string} vertexShader - raw vertex shader program * @param {string} fragmentShader - raw fragment shader program * @param {Array<string>} uVars - program uniform variables names * @param {Array<string>} aVars - program attribute variables names * @returns {Promise<void>} */ _registerAndCompileWebGlProgram(programName, vertexShader, fragmentShader, uVars, aVars) { const program = this.#compileWebGlProgram(vertexShader, fragmentShader), varsLocations = this.#getProgramVarsLocations(program, uVars, aVars); this.#registeredWebGlPrograms.set(programName, program); this.#webGlProgramsVarsLocations.set(programName, varsLocations); return Promise.resolve(); } /** * @returns {WebGLProgram} */ #compileWebGlProgram (vertexShader, fragmentShader) { const gl = this.#gl, program = gl.createProgram(); if (program) { const compVertexShader = this.#compileShader(gl, vertexShader, gl.VERTEX_SHADER); if (compVertexShader) { gl.attachShader(program, compVertexShader); } else { Exception(ERROR_CODES.WEBGL_ERROR, "#compileShader(vertexShaderSource) is null"); } const compFragmentShader = this.#compileShader(gl, fragmentShader, gl.FRAGMENT_SHADER); if (compFragmentShader) { gl.attachShader(program, compFragmentShader); } else { Exception(ERROR_CODES.WEBGL_ERROR, "#compileShader(fragmentShaderSource) is null"); } gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { const info = gl.getProgramInfoLog(program); Exception(ERROR_CODES.WEBGL_ERROR, `Could not compile WebGL program. \n\n${info}`); } } else { Exception(ERROR_CODES.WEBGL_ERROR, "gl.createProgram() is null"); } return program; } /** * * @param {WebGLProgram} program * @param {Array<string>} uVars - uniform variables * @param {Array<string>} aVars - attributes variables * @returns {Object<string, WebGLUniformLocation | number>} - uniform or attribute */ #getProgramVarsLocations(program, uVars, aVars) { const gl = this.#gl; let locations = {}; uVars.forEach(elementName => { locations[elementName] = gl.getUniformLocation(program, elementName); }); aVars.forEach(elementName => { locations[elementName] = gl.getAttribLocation(program, elementName); }); return locations; } #compileShader(gl, shaderSource, shaderType) { const shader = gl.createShader(shaderType); if (shader) { gl.shaderSource(shader, shaderSource); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { const info = gl.getShaderInfoLog(shader); Exception(ERROR_CODES.WEBGL_ERROR, "Couldn't compile webGl program. \n\n" + info); } } else { Exception(ERROR_CODES.WEBGL_ERROR, `gl.createShader(${shaderType}) is null`); } return shader; } /*------------------------------------ * End of Register and compile programs -------------------------------------*/ /********************************** * Predefined Drawing programs **********************************/ _bindPrimitives = (renderObject, gl, pageData, program, vars) => { const [ xOffset, yOffset ] = renderObject.isOffsetTurnedOff === true ? [0,0] : pageData.worldOffset, x = renderObject.x - xOffset, y = renderObject.y - yOffset, scale = renderObject.scale, blend = renderObject.blendFunc ? renderObject.blendFunc : [gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA], { u_resolution: resolutionUniformLocation, u_color: colorUniformLocation, a_position: positionAttributeLocation, u_fade_min: fadeMinLocation } = vars; let vertices = []; switch (renderObject.type) { case DRAW_TYPE.RECTANGLE: const x1 = 0, x2 = 0 + renderObject.width, y1 = 0, y2 = 0 + renderObject.height; vertices = [ x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]; break; case DRAW_TYPE.CIRCLE: const coords = renderObject.vertices; vertices = coords; break; case DRAW_TYPE.POLYGON: const triangles = this.#triangulatePolygon(renderObject.vertices); const len = triangles.length; vertices = triangles; if (len % 3 !== 0) { Warning(WARNING_CODES.POLYGON_VERTICES_NOT_CORRECT, `polygons ${renderObject.id}, vertices are not correct, skip drawing`); return Promise.reject(); } break; case DRAW_TYPE.LINE: const points = renderObject.vertices; // [[x0, y0], [x1, y1], ...] const thickness = renderObject.lineWidth; const halfThickness = thickness / 2; for (let i = 0; i < points.length - 1; i += 1) { const xStart = points[i][0]; const yStart = points[i][1]; const xEnd = points[i + 1][0]; const yEnd = points[i + 1][1]; // 1. Calculate direction vector of the segment const dx = xEnd - xStart; const dy = yEnd - yStart; const len = Math.sqrt(dx * dx + dy * dy); if (len === 0) continue; // Skip identical overlapping points // 2. Calculate the normalized perpendicular (normal) vector const nx = (-dy / len) * halfThickness; const ny = (dx / len) * halfThickness; // 3. Generate 4 corners of the quad (rectangle segment) const p0x = xStart + nx; const p0y = yStart + ny; const p1x = xStart - nx; const p1y = yStart - ny; const p2x = xEnd + nx; const p2y = yEnd + ny; const p3x = xEnd - nx; const p3y = yEnd - ny; // 4. Push 2 triangles (6 vertices total) to your array vertices.push( p0x, p0y, // Triangle 1 p1x, p1y, p2x, p2y, p2x, p2y, // Triangle 2 p1x, p1y, p3x, p3y ); } break; } const verticesMat = this.calculateTranslatedCoords(x, y, renderObject.rotation, scale, vertices); const nextObject = this.getNextRenderObject(renderObject, pageData); // 2. Is it have same texture and draw program? if (nextObject && this._canPrimitivesObjectsMerge(renderObject, nextObject)) { // if (this.#currentVertices === null) { this.#currentVertices = verticesMat; return Promise.resolve(0); } else { this.#currentVertices.push(...verticesMat); return Promise.resolve(0); } } else { gl.useProgram(program); // set the resolution gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height); gl.uniform1f(fadeMinLocation, 0); if (this.#currentVertices === null) { this.#currentVertices = verticesMat; } else { this.#currentVertices.push(...verticesMat); } gl.bindBuffer(gl.ARRAY_BUFFER, this.#positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.#currentVertices), gl.STATIC_DRAW); //Tell the attribute how to get data out of positionBuffer const size = 2, type = gl.FLOAT, // data is 32bit floats normalize = false, stride = 0, // move forward size * sizeof(type) each iteration to get next position offset = 0; // start of beginning of the buffer gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset); const colorArray = this.#rgbaToArray(renderObject.bgColor); gl.uniform4f(colorUniformLocation, colorArray[0]/255, colorArray[1]/255, colorArray[2]/255, colorArray[3]); if (blend) { gl.blendFunc(blend[0], blend[1]); } if (renderObject.isMaskAttached) { gl.stencilFunc(gl.EQUAL, renderObject._maskId, 0xFF); } else if (renderObject._isMask) { gl.stencilFunc(gl.ALWAYS, renderObject.id, 0xFF); } const verticesNumber = this.#currentVertices.length / 2; this.#currentVertices = null; return this._render(verticesNumber, gl.TRIANGLES); } }; _bindConus = (renderObject, gl, pageData, program, vars) => { const [ xOffset, yOffset ] = renderObject.isOffsetTurnedOff === true ? [0,0] : pageData.worldOffset, x = renderObject.x - xOffset, y = renderObject.y - yOffset, scale = renderObject.scale, rotation = renderObject.rotation, { u_translation: translationLocation, u_rotation: rotationRotation, u_scale: scaleLocation, u_resolution: resolutionUniformLocation, u_color: colorUniformLocation, a_position: positionAttributeLocation, u_fade_max: fadeMaxLocation, u_fade_min: fadeMinLocation } = vars, coords = renderObject.vertices, fillStyle = renderObject.bgColor, fade_min = renderObject.fade_min, fadeLen = renderObject.radius, blend = renderObject.blendFunc ? renderObject.blendFunc : [gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA]; let verticesNumber = 0; gl.useProgram(program); // set the resolution gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height); gl.uniform2f(translationLocation, x, y); gl.uniform2f(scaleLocation, scale[0], scale[1]); gl.uniform1f(rotationRotation, rotation); gl.uniform1f(fadeMinLocation, fade_min); gl.uniform1f(fadeMaxLocation, fadeLen); gl.bindBuffer(gl.ARRAY_BUFFER, this.#positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(coords), gl.STATIC_DRAW); gl.enableVertexAttribArray(positionAttributeLocation); //Tell the attribute how to get data out of positionBuffer const size = 2, type = gl.FLOAT, // data is 32bit floats normalize = false, stride = 0, // move forward size * sizeof(type) each iteration to get next position offset = 0; // start of beginning of the buffer gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset); verticesNumber += coords.length / 2; if (blend) { gl.blendFunc(blend[0], blend[1]); } const colorArray = this.#rgbaToArray(fillStyle); gl.uniform4f(colorUniformLocation, colorArray[0]/255, colorArray[1]/255, colorArray[2]/255, colorArray[3]); if (renderObject.isMaskAttached) { gl.stencilFunc(gl.EQUAL, renderObject._maskId, 0xFF); } else if (renderObject._isMask) { gl.stencilFunc(gl.ALWAYS, renderObject.id, 0xFF); } return this._render(verticesNumber, gl.TRIANGLE_FAN); }; /** * * @param {DrawTextObject} renderObject * @param {*} gl * @param {*} pageData * @param {*} program * @param {*} vars * @returns */ _bindText = (renderObject, gl, pageData, program, vars) => { const { u_resolution: resolutionUniformLocation, a_position: positionAttributeLocation, a_texCoord: texCoordLocation, u_image: u_imageLocation } = vars; const {width:boxWidth, height:boxHeight} = renderObject.boundariesBox, [ xOffset, yOffset ] = renderObject.isOffsetTurnedOff === true ? [0,0] : pageData.worldOffset, x = renderObject.x - xOffset, y = renderObject.y - yOffset - boxHeight, blend = renderObject.blendFunc ? renderObject.blendFunc : [gl.ONE, gl.ONE_MINUS_SRC_ALPHA], rotation = renderObject.rotation || 0, scale = renderObject.scale, vecX1 = 0, vecY1 = 0, vecX2 = vecX1 + boxWidth, vecY2 = vecY1 + boxHeight, verticesBufferData = [ vecX1, vecY1, vecX2, vecY1, vecX1, vecY2, vecX1, vecY2, vecX2, vecY1, vecX2, vecY2 ], atlasPosition = renderObject._atlasPos; // x, y, width, height let texturesCoords = [0, 0, 1, 1], currentAtlasIndex = typeof atlasPosition.atlasIndex !== "undefined" ? atlasPosition.atlasIndex : this.#currentAtlasIndex, currentAtlas = this.#textAtlases[currentAtlasIndex], atlasW = boxWidth, atlasH = boxHeight, atlasX, atlasY; if (atlasPosition.isMeasurementsSet === true) { const atlasPos = renderObject._atlasPos; // if we only change the texture - replace it with the new one if (renderObject._isTextureUpdated === true) { currentAtlas._updateTextImage(renderObject._textureCanvas, renderObject._atlasPos); renderObject._setTextureUpdated(); } atlasX = atlasPos.x; atlasY = atlasPos.y; texturesCoords = [ atlasX, atlasY, atlasW, atlasH ]; // calculate and set atlas pos } else { if (currentAtlas._isAddPossible(boxWidth, boxHeight)) { [atlasX, atlasY] = currentAtlas._addNewTextImage(renderObject._textureCanvas, boxWidth, boxHeight); } else { // is current atlas is full, // create a new one currentAtlas = new TextAtlas(this.#MAX_TEXTURE_SIZE, gl.createTexture(), currentAtlasIndex); [atlasX, atlasY] = currentAtlas._addNewTextImage(renderObject._textureCanvas, boxWidth, boxHeight); this.#currentAtlasIndex = currentAtlasIndex; this.#textAtlases[currentAtlasIndex] = currentAtlas; } // set new atlas pos atlasPosition._setMeasurements(currentAtlasIndex, atlasX, atlasY); texturesCoords = [ atlasX, atlasY, atlasW, atlasH ]; } const verticesMat = this.calculateTranslatedCoords(x, y, rotation, scale, verticesBufferData); const texturesBufferData = this.calculateTextureCoords(currentAtlas._atlasImage, texturesCoords[0], texturesCoords[1], texturesCoords[2], texturesCoords[3]); const nextObject = this.getNextRenderObject(renderObject, pageData); // 2. Is it have same texture and no texture reattach needed? if ((currentAtlas._isRecalculated === false) && nextObject && nextObject instanceof DrawTextObject && this._canTextObjectsMerge(renderObject, nextObject)) { if (this.#currentVertices === null) { this.#currentVertices = verticesMat; this.#currentTextures = texturesBufferData; return Promise.resolve(0); } else { this.#currentVertices.push(...verticesMat); this.#currentTextures.push(...texturesBufferData); return Promise.resolve(0); } } else { gl.useProgram(program); // set the resolution gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height); if (this.#currentVertices === null) { this.#currentVertices = verticesMat; this.#currentTextures = texturesBufferData; } else { this.#currentVertices.push(...verticesMat); this.#currentTextures.push(...texturesBufferData); } gl.bindBuffer(gl.ARRAY_BUFFER, this.#positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.#currentVertices), gl.STATIC_DRAW); gl.enableVertexAttribArray(positionAttributeLocation); //Tell the attribute how to get data out of positionBuffer const size = 2, type = gl.FLOAT, // data is 32bit floats normalize = false, stride = 0, // move forward size * sizeof(type) each iteration to get next position offset = 0; // start of beginning of the buffer gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset); //textures buffer gl.bindBuffer(gl.ARRAY_BUFFER, this.#texCoordBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.#currentTextures), gl.STATIC_DRAW); gl.enableVertexAttribArray(texCoordLocation); gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); const verticesNumber = this.#currentVertices.length / 2; gl.blendFunc(blend[0], blend[1]); if (currentAtlas._isRecalculated) { this.#updateWebGlTexture(gl, currentAtlas._texture, currentAtlas._atlasImage, currentAtlas._textureIndex); currentAtlas._isRecalculated = false; } else { this.#bindTexture(gl, currentAtlas._texture); } gl.uniform1i(u_imageLocation, currentAtlas._textureIndex); gl.depthMask(false); this.#currentVertices = null; this.#currentTextures = null; return this._render(verticesNumber, gl.TRIANGLES); } }; _bindImage = (renderObject, gl, pageData, program, vars) => { const [ xOffset, yOffset ] = renderObject.isOffsetTurnedOff === true ? [0,0] : pageData.worldOffset, x = renderObject.x - xOffset, y = renderObject.y - yOffset; if (renderObject.vertices && this.#gameOptions.debug.boundaries.drawObjectBoundaries) { pageData._enableDebugObjectBoundaries(); pageData._addImageDebugBoundaries(utils.calculateLinesVertices(x, y, renderObject.rotation, renderObject.vertices)); } const { u_resolution: resolutionUniformLocation, a_position: positionAttributeLocation, a_texCoord: texCoordLocation, u_image: u_imageLocation } = vars; if (!renderObject.image) { const image = this.#loaderReference.getImage(renderObject.key); if (!image) { Exception(ERROR_CODES.CANT_GET_THE_IMAGE, "iLoader can't get the image with key: " + renderObject.key); } else { renderObject.image = image; } } const atlasImage = renderObject.image, animationIndex = renderObject.imageIndex, shapeMaskId = renderObject._maskId, spacing = renderObject.spacing, margin = renderObject.margin, blend = renderObject.blendFunc ? renderObject.blendFunc : [gl.ONE, gl.ONE_MINUS_SRC_ALPHA], scale = renderObject.scale; let imageX = margin, imageY = margin, colNum = 0, rowNum = 0; if (animationIndex !== 0) { const imageColsNumber = (atlasImage.width + spacing - (2*margin)) / (renderObject.width + spacing); colNum = animationIndex % imageColsNumber; rowNum = Math.floor(animationIndex / imageColsNumber); imageX = colNum * renderObject.width + (colNum * spacing) + margin, imageY = rowNum * renderObject.height + (rowNum * spacing) + margin; } const vecX1 = 0 - renderObject.width / 2, vecY1 = 0 - renderObject.height / 2, vecX2 = vecX1 + renderObject.width, vecY2 = vecY1 + renderObject.height; const verticesD = [ vecX1, vecY1, vecX2, vecY1, vecX1, vecY2, vecX1, vecY2, vecX2, vecY1, vecX2, vecY2 ]; const verticesMat = this.calculateTranslatedCoords(x, y, renderObject.rotation, scale, verticesD), textures = this.calculateTextureCoords(atlasImage, imageX, imageY, renderObject.width, renderObject.height); // Determine could we merge next drawObject or not // 1. Find next object const nextObject = this.getNextRenderObject(renderObject, pageData); // 2. Is it have same texture and draw program? if (nextObject && this._canImageObjectsMerge(renderObject, nextObject)) { // if (this.#currentVertices === null) { this.#currentVertices = verticesMat; this.#currentTextures = textures; return Promise.resolve(0); } else { this.#currentVertices.push(...verticesMat); this.#currentTextures.push(...textures); return Promise.resolve(0); } } else { gl.useProgram(program); // set the resolution gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height); // bind data and call draw if (this.#currentVertices === null) { this.#currentVertices = verticesMat; this.#currentTextures = textures; } else { this.#currentVertices.push(...verticesMat); this.#currentTextures.push(...textures); } gl.bindBuffer(gl.ARRAY_BUFFER, this.#positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.#currentVertices), gl.STATIC_DRAW); const verticesNumber = this.#currentVertices.length / 2; gl.enableVertexAttribArray(positionAttributeLocation); //Tell the attribute how to get data out of positionBuffer const size = 2, type = gl.FLOAT, // data is 32bit floats normalize = false, stride = 0, // move forward size * sizeof(type) each iteration to get next position offset = 0; // start of beginning of the buffer gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset); //textures buffer gl.bindBuffer(gl.ARRAY_BUFFER, this.#texCoordBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.#currentTextures), gl.STATIC_DRAW); gl.enableVertexAttribArray(texCoordLocation); gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); let textureStorage = renderObject._textureStorage; if (!textureStorage) { textureStorage = new ImageTempStorage(gl.createTexture()); renderObject._textureStorage = textureStorage; } if (textureStorage._isTextureRecalculated === true) { this.#updateWebGlTexture(gl, textureStorage._texture, renderObject.image); textureStorage._isTextureRecalculated = false; } else { this.#bindTexture(gl, textureStorage._texture); } gl.uniform1i(u_imageLocation, textureStorage._textureIndex); // make image transparent parts transparent gl.blendFunc(blend[0], blend[1]); if (shapeMaskId) { gl.stencilFunc(gl.EQUAL, shapeMaskId, 0xFF); //gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); } this.#currentVertices = null; this.#currentTextures = null; return this._render(verticesNumber, gl.TRIANGLES); } }; /** * * @param {*} obj1 * @param {*} obj2 * @returns {boolean} */ _canImageObjectsMerge = (obj1, obj2) => { const registeredO1 = this.#registeredRenderObjects.get(obj1.constructor.name) || this.#registeredRenderObjects.get(obj1.type), registeredO2 = this.#registeredRenderObjects.get(obj2.constructor.name) || this.#registeredRenderObjects.get(obj2.type); if ((registeredO1.webglProgramName === registeredO2.webglProgramName) && (obj1.type === obj2.type) && (obj1.image === obj2.image) && (obj2.isRemoved === false)) { return true; } else { return false; } } /** * * @param {DrawTextObject} obj1 * @param {DrawTextObject} obj2 * @returns {boolean} */ _canTextObjectsMerge = (obj1, obj2) => { if ((obj2._atlasPos) && (obj1._atlasPos.atlasIndex === obj2._atlasPos.atlasIndex) && (obj2._atlasPos.isMeasurementsSet === true) && (obj2.isRemoved === false)) { return true; } else { return false; } } _canPrimitivesObjectsMerge = (obj1, obj2) => { const registeredO1 = this.#registeredRenderObjects.get(obj1.constructor.name) || this.#registeredRenderObjects.get(obj1.type), registeredO2 = this.#registeredRenderObjects.get(obj2.constructor.name) || this.#registeredRenderObjects.get(obj2.type); if ((registeredO1.webglProgramName === registeredO2.webglProgramName) // colors match && (obj1.bgColor === obj2.bgColor) && (obj2.isRemoved === false)) { return true; } else { return false; } } /** * * @param {*} obj1 * @param {*} obj2 * @returns {boolean} */ _canMergeNextTileObject = (obj1, obj2) => { if ((obj2 instanceof DrawTiledLayer) && (obj1.tilesetImages.length === 1) && (obj2.tilesetImages.length === 1) && (obj1.tilesetImages[0] === obj2.tilesetImages[0]) && (obj2.isRemoved === false)) { return true; } else { return false; } } _canTextBeMerged = (obj1, obj2) => { const registeredO1 = this.#registeredRenderObjects.get(obj1.constructor.name) || this.#registeredRenderObjects.get(obj1.type), registeredO2 = this.#registeredRenderObjects.get(obj2.constructor.name) || this.#registeredRenderObjects.get(obj2.type); if ((registeredO1.webglProgramName === registeredO2.webglProgramName) && (obj1.type === obj2.type) && (obj2.isRemoved === false)) { return true; } else { return false; } } _bindTileImages = async(renderLayer, gl, pageData, program, vars) => { const { u_translation: translationLocation, u_rotation: rotationRotation, u_scale: scaleLocation, u_resolution: resolutionUniformLocation, a_position: positionAttributeLocation, a_texCoord: texCoordLocation, u_image: u_imageLocation } = vars; gl.useProgram(program); /** * @type {Array<any> | null} */ let renderLayerData = null; switch (this.#gameOptions.optimization) { case CONST.OPTIMIZATION.NATIVE_JS.NOT_OPTIMIZED: renderLayerData = await this.#prepareRenderLayerOld(renderLayer, pageData); break; case CONST.OPTIMIZATION.WEB_ASSEMBLY.ASSEMBLY_SCRIPT: case CONST.OPTIMIZATION.WEB_ASSEMBLY.NATIVE_WAT: renderLayerData = await this.#prepareRenderLayerWM(renderLayer, pageData); break; case CONST.OPTIMIZATION.NATIVE_JS.OPTIMIZED: default: renderLayerData = await this.#prepareRenderLayer(renderLayer, pageData); } const translation = [0, 0], scale = renderLayer.scale, rotation = renderLayer.rotation || 0, drawMask = ["ONE", "ONE_MINUS_SRC_ALPHA"], shapeMaskId = renderLayer._maskId; /* const c = Math.cos(renderLayer.rotation || 0), s = Math.sin(renderLayer.rotation || 0), translationMatrix = [ 1, 0, translation[0], 0, 1, translation[1], 0, 0, 1], rotationMatrix = [ c, -s, 0, s, c, 0, 0, 0, 1 ], scaleMatrix = [ scale[0], 0, 0, 0, scale[1], 0, 0, 0, 1 ]; const matMultiply = utils.mat3Multiply(utils.mat3Multiply(translationMatrix, rotationMatrix), scaleMatrix); for (let i = 0; i < renderLayerData.length; i++) { renderLayerData[i][0] = utils.mat3MultiplyPosCoords(matMultiply, renderLayerData[i][0]); }*/ //console.log("mat1: ", matMult1); //console.log("mat2: ", matMult2); /* const x1y1 = utils.mat3MultiplyVector(matMultiply, [vecX1, vecY1, 1]), x2y1 = utils.mat3MultiplyVector(matMultiply, [vecX2, vecY1, 1]), x1y2 = utils.mat3MultiplyVector(matMultiply, [vecX1, vecY2, 1]), x2y2 = utils.mat3MultiplyVector(matMultiply, [vecX2, vecY2, 1]); */ const nextObject = this.getNextRenderObject(renderLayer, pageData); if (this._canMergeNextTileObject(renderLayer, nextObject)) { if (this.#currentVertices === null) { this.#currentVertices = renderLayerData[0][0]; this.#currentTextures = renderLayerData[0][1]; return Promise.resolve(0); } else { this.#currentVertices.push(...renderLayerData[0][0]); this.#currentTextures.push(...renderLayerData[0][1]); return Promise.resolve(0); } } else { let verticesNumber = 0, isTextureBind = false, renderLayerDataLen = renderLayerData.length; gl.enableVertexAttribArray(positionAttributeLocation); gl.enableVertexAttribArray(texCoordLocation); // set the resolution gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height); //gl.uniform2f(translationLocation,translation[0], translation[1]); //gl.uniform2f(scaleLocation, scale[0], scale[1]); //gl.uniform1f(rotationRotation, rotation); // MULTIPLE_IMAGE_TILESET drawing, no merging possible if (renderLayerDataLen > 1) { for (let i = 0; i < renderLayerDataLen; i++) { const data = renderLayerData[i], vectors = data[0], textures = data[1], image_name = data[2], image = data[3]; // if layer use multiple tilesets // the issue is: when we add some layer data to the temp arrays, and then // process empty layer, it actually skips the draw with this check if (vectors.length > 0 && textures.length > 0) { // need to have additional draw call for each new texture added // probably it could be combined in one draw call if multiple textures // could be used in one draw call if (isTextureBind) { await this._render(verticesNumber, gl.TRIANGLES); } gl.bindBuffer(gl.ARRAY_BUFFER, this.#positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vectors), gl.STATIC_DRAW); //Tell the attribute how to get data out of positionBuffer const size = 2, type = gl.FLOAT, // data is 32bit floats normalize = false, stride = 0, // move forward size * sizeof(type) each iteration to get next position offset = 0; // verticesNumber * 4; // start of beginning of the buffer gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset); //textures buffer gl.bindBuffer(gl.ARRAY_BUFFER, this.#texCoordBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textures), gl.STATIC_DRAW); gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, offset); let textureStorage = renderLayer._textureStorages[i]; if (!textureStorage) { textureStorage = new ImageTempStorage(gl.createTexture(), i); renderLayer._setTextureStorage(i, textureStorage); } if (textureStorage._isTextureRecalculated === true) { this.#updateWebGlTexture(gl, textureStorage._texture, image, textureStorage._textureIndex); textureStorage._isTextureRecalculated = false; } else { //console.log("bind texture"); this.#bindTexture(gl, textureStorage._texture, textureStorage._textureIndex); } gl.uniform1i(u_imageLocation, textureStorage._textureIndex); gl.blendFunc(gl[drawMask[0]], gl[drawMask[1]]); verticesNumber = vectors.length / 2; if (shapeMaskId) { gl.stencilFunc(gl.EQUAL, shapeMaskId, 0xFF); } isTextureBind = true; } } // Single image tileset draw, with merging } else { const data = renderLayerData[0], vectors = data[0], textures = data[1], image_name = data[2], image = data[3]; // if layer use multiple tilesets // the issue is: when we add some layer data to the temp arrays, and then // process empty layer, it actually skips the draw with this check if (this.#currentVertices === null) { this.#currentVertices = vectors; this.#currentTextures = textures; } else { this.#currentVertices.push(...vectors); this.#currentTextures.push(...textures); } gl.bindBuffer(gl.ARRAY_BUFFER, this.#positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.#currentVertices), gl.STATIC_DRAW); //Tell the attribute how to get data out of positionBuffer const size = 2, type = gl.FLOAT, // data is 32bit floats normalize = false, stride = 0, // move forward size * sizeof(type) each iteration to get next position offset = 0; // verticesNumber * 4; // start of beginning of the buffer gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset); //textures buffer gl.bindBuffer(gl.ARRAY_BUFFER, this.#texCoordBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.#currentTextures), gl.STATIC_DRAW); gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, offset); let textureStorage = renderLayer._textureStorages[0]; if (!textureStorage) { textureStorage = new ImageTempStorage(gl.createTexture(), 0); renderLayer._setTextureStorage(0, textureStorage); } if (textureStorage._isTextureRecalculated === true) { this.#updateWebGlTexture(gl, textureStorage._texture, image, textureStorage._textureIndex); textureStorage._isTextureRecalculated = false; } else { //console.log("bind texture"); this.#bindTexture(gl, textureStorage._texture, textureStorage._textureIndex); } gl.uniform1i(u_imageLocation, textureStorage._textureIndex); gl.blendFunc(gl[drawMask[0]], gl[drawMask[1]]); verticesNumber = this.#currentVertices.length / 2; if (shapeMaskId) { gl.stencilFunc(gl.EQUAL, shapeMaskId, 0xFF); } this.#currentVertices = null; this.#currentTextures = null; } renderLayerData = null; return this._render(verticesNumber, gl.TRIANGLES); } }; _drawPolygon(renderObject, pageData) { const [ xOffset, yOffset ] = renderObject.isOffsetTurnedOff === true ? [0,0] : pageData.worldOffset, x = renderObject.x - xOffset, y = renderObject.y - yOffset, rotation = renderObject.rotation || 0, scale = renderObject.scale || [1, 1], vertices = renderObject.vertices, color = this.#gameOptions.debug.boundaries.boundariesColor; const program = this.getProgram(CONST.WEBGL.DRAW_PROGRAMS.PRIMITIVES); const { u_translation: translationLocation, u_rotation: rotationRotation, u_scale: scaleLocation, u_resolution: resolutionUniformLocation, u_color: colorUniformLocation, a_position: positionAttributeLocation, u_fade_max: fadeMaxLocation,