jsge
Version:
Javascript Game Engine
1,188 lines (1,054 loc) • 99.2 kB
JavaScript
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,