@lightningjs/renderer
Version:
Lightning 3 Renderer
849 lines (749 loc) • 28.7 kB
text/typescript
/*
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2023 Comcast Cable Communications Management, LLC.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assertTruthy, createWebGLContext, hasOwn } from '../../../utils.js';
import {
CoreRenderer,
type BufferInfo,
type CoreRendererOptions,
type QuadOptions,
} from '../CoreRenderer.js';
import { WebGlCoreRenderOp } from './WebGlCoreRenderOp.js';
import type { CoreContextTexture } from '../CoreContextTexture.js';
import {
createIndexBuffer,
type CoreWebGlParameters,
type CoreWebGlExtensions,
getWebGlParameters,
getWebGlExtensions,
type WebGlColor,
} from './internal/RendererUtils.js';
import { WebGlCoreCtxTexture } from './WebGlCoreCtxTexture.js';
import { Texture, TextureType } from '../../textures/Texture.js';
import { SubTexture } from '../../textures/SubTexture.js';
import { WebGlCoreCtxSubTexture } from './WebGlCoreCtxSubTexture.js';
import { CoreShaderManager } from '../../CoreShaderManager.js';
import { BufferCollection } from './internal/BufferCollection.js';
import {
compareRect,
getNormalizedRgbaComponents,
type RectWithValid,
} from '../../lib/utils.js';
import type { Dimensions } from '../../../common/CommonTypes.js';
import { WebGlCoreShader } from './WebGlCoreShader.js';
import { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js';
import { RenderTexture } from '../../textures/RenderTexture.js';
import { CoreNodeRenderState, type CoreNode } from '../../CoreNode.js';
import { WebGlCoreCtxRenderTexture } from './WebGlCoreCtxRenderTexture.js';
import type { BaseShaderController } from '../../../main-api/ShaderController.js';
const WORDS_PER_QUAD = 24;
// const BYTES_PER_QUAD = WORDS_PER_QUAD * 4;
export type WebGlCoreRendererOptions = CoreRendererOptions;
interface CoreWebGlSystem {
parameters: CoreWebGlParameters;
extensions: CoreWebGlExtensions;
}
export class WebGlCoreRenderer extends CoreRenderer {
//// WebGL Native Context and Data
glw: WebGlContextWrapper;
system: CoreWebGlSystem;
//// Persistent data
quadBuffer: ArrayBuffer;
fQuadBuffer: Float32Array;
uiQuadBuffer: Uint32Array;
renderOps: WebGlCoreRenderOp[] = [];
//// Render Op / Buffer Filling State
curBufferIdx = 0;
curRenderOp: WebGlCoreRenderOp | null = null;
override rttNodes: CoreNode[] = [];
activeRttNode: CoreNode | null = null;
//// Default Shader
defShaderCtrl: BaseShaderController;
defaultShader: WebGlCoreShader;
quadBufferCollection: BufferCollection;
clearColor: WebGlColor = {
raw: 0x00000000,
normalized: [0, 0, 0, 0],
};
/**
* White pixel texture used by default when no texture is specified.
*/
quadBufferUsage = 0;
numQuadsRendered = 0;
/**
* Whether the renderer is currently rendering to a texture.
*/
public renderToTextureActive = false;
constructor(options: WebGlCoreRendererOptions) {
super(options);
this.quadBuffer = new ArrayBuffer(this.stage.options.quadBufferSize);
this.fQuadBuffer = new Float32Array(this.quadBuffer);
this.uiQuadBuffer = new Uint32Array(this.quadBuffer);
this.mode = 'webgl';
const { canvas, clearColor, bufferMemory } = options;
const gl = createWebGLContext(
canvas,
options.forceWebGL2,
options.contextSpy,
);
const glw = (this.glw = new WebGlContextWrapper(gl));
glw.viewport(0, 0, canvas.width, canvas.height);
this.updateClearColor(clearColor);
glw.setBlend(true);
glw.blendFunc(glw.ONE, glw.ONE_MINUS_SRC_ALPHA);
createIndexBuffer(glw, bufferMemory);
this.system = {
parameters: getWebGlParameters(this.glw),
extensions: getWebGlExtensions(this.glw),
};
this.shManager.renderer = this;
this.defShaderCtrl = this.shManager.loadShader('DefaultShader');
this.defaultShader = this.defShaderCtrl.shader as WebGlCoreShader;
const quadBuffer = glw.createBuffer();
assertTruthy(quadBuffer);
const stride = 8 * Float32Array.BYTES_PER_ELEMENT;
this.quadBufferCollection = new BufferCollection([
{
buffer: quadBuffer,
attributes: {
a_position: {
name: 'a_position',
size: 2, // 2 components per iteration
type: glw.FLOAT, // the data is 32bit floats
normalized: false, // don't normalize the data
stride, // 0 = move forward size * sizeof(type) each iteration to get the next position
offset: 0, // start at the beginning of the buffer
},
a_textureCoordinate: {
name: 'a_textureCoordinate',
size: 2,
type: glw.FLOAT,
normalized: false,
stride,
offset: 2 * Float32Array.BYTES_PER_ELEMENT,
},
a_color: {
name: 'a_color',
size: 4,
type: glw.UNSIGNED_BYTE,
normalized: true,
stride,
offset: 4 * Float32Array.BYTES_PER_ELEMENT,
},
a_textureIndex: {
name: 'a_textureIndex',
size: 1,
type: glw.FLOAT,
normalized: false,
stride,
offset: 5 * Float32Array.BYTES_PER_ELEMENT,
},
a_nodeCoordinate: {
name: 'a_nodeCoordinate',
size: 2,
type: glw.FLOAT,
normalized: false,
stride,
offset: 6 * Float32Array.BYTES_PER_ELEMENT,
},
},
},
]);
}
reset() {
const { glw } = this;
this.curBufferIdx = 0;
this.curRenderOp = null;
this.renderOps.length = 0;
glw.setScissorTest(false);
glw.clear();
}
override getShaderManager(): CoreShaderManager {
return this.shManager;
}
override createCtxTexture(textureSource: Texture): CoreContextTexture {
if (textureSource instanceof SubTexture) {
return new WebGlCoreCtxSubTexture(
this.glw,
this.txMemManager,
textureSource,
);
} else if (textureSource instanceof RenderTexture) {
return new WebGlCoreCtxRenderTexture(
this.glw,
this.txMemManager,
textureSource,
);
}
return new WebGlCoreCtxTexture(this.glw, this.txMemManager, textureSource);
}
/**
* This function adds a quad (a rectangle composed of two triangles) to the WebGL rendering pipeline.
*
* It takes a set of options that define the quad's properties, such as its dimensions, colors, texture, shader, and transformation matrix.
* The function first updates the shader properties with the current dimensions if necessary, then sets the default texture if none is provided.
* It then checks if a new render operation is needed, based on the current shader and clipping rectangle.
* If a new render operation is needed, it creates one and updates the current render operation.
* The function then adjusts the texture coordinates based on the texture options and adds the texture to the texture manager.
*
* Finally, it calculates the vertices for the quad, taking into account any transformations, and adds them to the quad buffer.
* The function updates the length and number of quads in the current render operation, and updates the current buffer index.
*/
addQuad(params: QuadOptions) {
const { fQuadBuffer, uiQuadBuffer } = this;
let texture = params.texture;
assertTruthy(texture !== null, 'Texture is required');
/**
* If the shader props contain any automatic properties, update it with the
* current dimensions and or alpha that will be used to render the quad.
*/
if (params.shader !== this.defaultShader) {
if (hasOwn(params.shaderProps, '$dimensions') == true) {
const dimensions = params.shaderProps.$dimensions as Dimensions;
dimensions.width = params.width;
dimensions.height = params.height;
}
if (hasOwn(params.shaderProps, '$alpha') === true) {
params.shaderProps.$alpha = params.alpha;
}
}
let { curBufferIdx: bufferIdx, curRenderOp } = this;
const targetDims = { width: params.width, height: params.height };
if (this.reuseRenderOp(params) === false) {
this.newRenderOp(
params.shader as WebGlCoreShader,
params.shaderProps as Record<string, unknown>,
params.alpha,
targetDims,
params.clippingRect,
bufferIdx,
params.rtt,
params.parentHasRenderTexture,
params.framebufferDimensions,
);
curRenderOp = this.curRenderOp;
assertTruthy(curRenderOp);
}
let ctxTexture = undefined;
let texCoordX1 = 0;
let texCoordY1 = 0;
let texCoordX2 = 1;
let texCoordY2 = 1;
if (texture.type === TextureType.subTexture) {
const {
x: tx,
y: ty,
width: tw,
height: th,
} = (texture as SubTexture).props;
const { width: parentW = 0, height: parentH = 0 } = (
texture as SubTexture
).parentTexture.dimensions || { width: 0, height: 0 };
texCoordX1 = tx / parentW;
texCoordX2 = texCoordX1 + tw / parentW;
texCoordY1 = ty / parentH;
texCoordY2 = texCoordY1 + th / parentH;
texture = (texture as SubTexture).parentTexture;
ctxTexture = texture.ctxTexture as WebGlCoreCtxTexture;
} else {
ctxTexture = texture.ctxTexture as WebGlCoreCtxTexture;
if (ctxTexture === undefined) {
ctxTexture = this.stage.defaultTexture?.ctxTexture as WebGlCoreCtxTexture;
console.warn(
'WebGL Renderer: Texture does not have a ctxTexture, using default texture instead',
);
}
texCoordX1 = ctxTexture.txCoordX1;
texCoordY1 = ctxTexture.txCoordY1;
texCoordX2 = ctxTexture.txCoordX2;
texCoordY2 = ctxTexture.txCoordY2;
}
if (
texture.type === TextureType.image &&
params.textureOptions !== null &&
params.textureOptions.resizeMode !== undefined &&
texture.dimensions !== null
) {
const resizeMode = params.textureOptions.resizeMode;
const { width: tw, height: th } = texture.dimensions;
if (resizeMode.type === 'cover') {
const scaleX = params.width / tw;
const scaleY = params.height / th;
const scale = Math.max(scaleX, scaleY);
const precision = 1 / scale;
// Determine based on width
if (scale && scaleX && scaleX < scale) {
const desiredSize = precision * params.width;
texCoordX1 = (1 - desiredSize / tw) * (resizeMode.clipX ?? 0.5);
texCoordX2 = texCoordX1 + desiredSize / tw;
}
// Determine based on height
if (scale && scaleY && scaleY < scale) {
const desiredSize = precision * params.height;
texCoordY1 = (1 - desiredSize / th) * (resizeMode.clipY ?? 0.5);
texCoordY2 = texCoordY1 + desiredSize / th;
}
}
}
// Flip texture coordinates if dictated by texture options
let flipY = 0;
if (params.textureOptions !== null) {
if (params.textureOptions.flipX === true) {
[texCoordX1, texCoordX2] = [texCoordX2, texCoordX1];
}
// convert to integer for bitwise operation below
flipY = +(params.textureOptions.flipY || false);
}
// Eitherone should be true
if (flipY ^ +(texture.type === TextureType.renderToTexture)) {
[texCoordY1, texCoordY2] = [texCoordY2, texCoordY1];
}
const textureIdx = this.addTexture(ctxTexture, bufferIdx);
assertTruthy(this.curRenderOp !== null);
if (params.renderCoords) {
// Upper-Left
fQuadBuffer[bufferIdx++] = params.renderCoords.x1; // vertexX
fQuadBuffer[bufferIdx++] = params.renderCoords.y1; // vertexY
fQuadBuffer[bufferIdx++] = texCoordX1; // texCoordX
fQuadBuffer[bufferIdx++] = texCoordY1; // texCoordY
uiQuadBuffer[bufferIdx++] = params.colorTl; // color
fQuadBuffer[bufferIdx++] = textureIdx; // texIndex
fQuadBuffer[bufferIdx++] = 0;
fQuadBuffer[bufferIdx++] = 0;
// Upper-Right
fQuadBuffer[bufferIdx++] = params.renderCoords.x2;
fQuadBuffer[bufferIdx++] = params.renderCoords.y2;
fQuadBuffer[bufferIdx++] = texCoordX2;
fQuadBuffer[bufferIdx++] = texCoordY1;
uiQuadBuffer[bufferIdx++] = params.colorTr;
fQuadBuffer[bufferIdx++] = textureIdx;
fQuadBuffer[bufferIdx++] = 1;
fQuadBuffer[bufferIdx++] = 0;
// Lower-Left
fQuadBuffer[bufferIdx++] = params.renderCoords.x4;
fQuadBuffer[bufferIdx++] = params.renderCoords.y4;
fQuadBuffer[bufferIdx++] = texCoordX1;
fQuadBuffer[bufferIdx++] = texCoordY2;
uiQuadBuffer[bufferIdx++] = params.colorBl;
fQuadBuffer[bufferIdx++] = textureIdx;
fQuadBuffer[bufferIdx++] = 0;
fQuadBuffer[bufferIdx++] = 1;
// Lower-Right
fQuadBuffer[bufferIdx++] = params.renderCoords.x3;
fQuadBuffer[bufferIdx++] = params.renderCoords.y3;
fQuadBuffer[bufferIdx++] = texCoordX2;
fQuadBuffer[bufferIdx++] = texCoordY2;
uiQuadBuffer[bufferIdx++] = params.colorBr;
fQuadBuffer[bufferIdx++] = textureIdx;
fQuadBuffer[bufferIdx++] = 1;
fQuadBuffer[bufferIdx++] = 1;
} else if (params.tb !== 0 || params.tc !== 0) {
// Upper-Left
fQuadBuffer[bufferIdx++] = params.tx; // vertexX
fQuadBuffer[bufferIdx++] = params.ty; // vertexY
fQuadBuffer[bufferIdx++] = texCoordX1; // texCoordX
fQuadBuffer[bufferIdx++] = texCoordY1; // texCoordY
uiQuadBuffer[bufferIdx++] = params.colorTl; // color
fQuadBuffer[bufferIdx++] = textureIdx; // texIndex
fQuadBuffer[bufferIdx++] = 0;
fQuadBuffer[bufferIdx++] = 0;
// Upper-Right
fQuadBuffer[bufferIdx++] = params.tx + params.width * params.ta;
fQuadBuffer[bufferIdx++] = params.ty + params.width * params.tc;
fQuadBuffer[bufferIdx++] = texCoordX2;
fQuadBuffer[bufferIdx++] = texCoordY1;
uiQuadBuffer[bufferIdx++] = params.colorTr;
fQuadBuffer[bufferIdx++] = textureIdx;
fQuadBuffer[bufferIdx++] = 1;
fQuadBuffer[bufferIdx++] = 0;
// Lower-Left
fQuadBuffer[bufferIdx++] = params.tx + params.height * params.tb;
fQuadBuffer[bufferIdx++] = params.ty + params.height * params.td;
fQuadBuffer[bufferIdx++] = texCoordX1;
fQuadBuffer[bufferIdx++] = texCoordY2;
uiQuadBuffer[bufferIdx++] = params.colorBl;
fQuadBuffer[bufferIdx++] = textureIdx;
fQuadBuffer[bufferIdx++] = 0;
fQuadBuffer[bufferIdx++] = 1;
// Lower-Right
fQuadBuffer[bufferIdx++] =
params.tx + params.width * params.ta + params.height * params.tb;
fQuadBuffer[bufferIdx++] =
params.ty + params.width * params.tc + params.height * params.td;
fQuadBuffer[bufferIdx++] = texCoordX2;
fQuadBuffer[bufferIdx++] = texCoordY2;
uiQuadBuffer[bufferIdx++] = params.colorBr;
fQuadBuffer[bufferIdx++] = textureIdx;
fQuadBuffer[bufferIdx++] = 1;
fQuadBuffer[bufferIdx++] = 1;
} else {
// Calculate the right corner of the quad
// multiplied by the scale
const rightCornerX = params.tx + params.width * params.ta;
const rightCornerY = params.ty + params.height * params.td;
// Upper-Left
fQuadBuffer[bufferIdx++] = params.tx; // vertexX
fQuadBuffer[bufferIdx++] = params.ty; // vertexY
fQuadBuffer[bufferIdx++] = texCoordX1; // texCoordX
fQuadBuffer[bufferIdx++] = texCoordY1; // texCoordY
uiQuadBuffer[bufferIdx++] = params.colorTl; // color
fQuadBuffer[bufferIdx++] = textureIdx; // texIndex
fQuadBuffer[bufferIdx++] = 0;
fQuadBuffer[bufferIdx++] = 0;
// Upper-Right
fQuadBuffer[bufferIdx++] = rightCornerX;
fQuadBuffer[bufferIdx++] = params.ty;
fQuadBuffer[bufferIdx++] = texCoordX2;
fQuadBuffer[bufferIdx++] = texCoordY1;
uiQuadBuffer[bufferIdx++] = params.colorTr;
fQuadBuffer[bufferIdx++] = textureIdx;
fQuadBuffer[bufferIdx++] = 1;
fQuadBuffer[bufferIdx++] = 0;
// Lower-Left
fQuadBuffer[bufferIdx++] = params.tx;
fQuadBuffer[bufferIdx++] = rightCornerY;
fQuadBuffer[bufferIdx++] = texCoordX1;
fQuadBuffer[bufferIdx++] = texCoordY2;
uiQuadBuffer[bufferIdx++] = params.colorBl;
fQuadBuffer[bufferIdx++] = textureIdx;
fQuadBuffer[bufferIdx++] = 0;
fQuadBuffer[bufferIdx++] = 1;
// Lower-Right
fQuadBuffer[bufferIdx++] = rightCornerX;
fQuadBuffer[bufferIdx++] = rightCornerY;
fQuadBuffer[bufferIdx++] = texCoordX2;
fQuadBuffer[bufferIdx++] = texCoordY2;
uiQuadBuffer[bufferIdx++] = params.colorBr;
fQuadBuffer[bufferIdx++] = textureIdx;
fQuadBuffer[bufferIdx++] = 1;
fQuadBuffer[bufferIdx++] = 1;
}
// Update the length of the current render op
this.curRenderOp.numQuads++;
this.curBufferIdx = bufferIdx;
}
/**
* Replace the existing RenderOp with a new one that uses the specified Shader
* and starts at the specified buffer index.
*
* @param shader
* @param bufferIdx
*/
private newRenderOp(
shader: WebGlCoreShader,
shaderProps: Record<string, unknown>,
alpha: number,
dimensions: Dimensions,
clippingRect: RectWithValid,
bufferIdx: number,
renderToTexture?: boolean,
parentHasRenderTexture?: boolean,
framebufferDimensions?: Dimensions,
) {
const curRenderOp = new WebGlCoreRenderOp(
this.glw,
this.options,
this.quadBufferCollection,
shader,
shaderProps,
alpha,
clippingRect,
dimensions,
bufferIdx,
0, // Z-Index is only used for explictly added Render Ops
renderToTexture,
parentHasRenderTexture,
framebufferDimensions,
);
this.curRenderOp = curRenderOp;
this.renderOps.push(curRenderOp);
}
/**
* Add a texture to the current RenderOp. If the texture cannot be added to the
* current RenderOp, a new RenderOp will be created and the texture will be added
* to that one.
*
* If the texture cannot be added to the new RenderOp, an error will be thrown.
*
* @param texture
* @param bufferIdx
* @param recursive
* @returns Assigned Texture Index of the texture in the render op
*/
private addTexture(
texture: WebGlCoreCtxTexture,
bufferIdx: number,
recursive?: boolean,
): number {
const { curRenderOp } = this;
assertTruthy(curRenderOp);
const textureIdx = curRenderOp.addTexture(texture);
// TODO: Refactor to be more DRY
if (textureIdx === 0xffffffff) {
if (recursive) {
throw new Error('Unable to add texture to render op');
}
this.newRenderOp(
curRenderOp.shader,
curRenderOp.shaderProps,
curRenderOp.alpha,
curRenderOp.dimensions,
curRenderOp.clippingRect,
bufferIdx,
);
return this.addTexture(texture, bufferIdx, true);
}
return textureIdx;
}
/**
* Test if the current Render operation can be reused for the specified parameters.
* @param params
* @returns
*/
reuseRenderOp(params: QuadOptions): boolean {
const { shader, shaderProps, parentHasRenderTexture, rtt, clippingRect } =
params;
// Switching shader program will require a new render operation
if (this.curRenderOp?.shader !== shader) {
return false;
}
// Switching clipping rect will require a new render operation
if (compareRect(this.curRenderOp.clippingRect, clippingRect) === false) {
return false;
}
// Force new render operation if rendering to texture
// @todo: This needs to be improved, render operations could also be reused
// for rendering to texture
if (parentHasRenderTexture === true || rtt === true) {
return false;
}
// Check if the shader can batch the shader properties
if (
this.curRenderOp.shader !== this.defaultShader &&
this.curRenderOp.shader.canBatchShaderProps(
this.curRenderOp.shaderProps,
shaderProps,
) === false
) {
return false;
}
// Render operation can be reused
return true;
}
/**
* add RenderOp to the render pipeline
*/
addRenderOp(renderable: WebGlCoreRenderOp) {
this.renderOps.push(renderable);
this.curRenderOp = null;
}
/**
* Render the current set of RenderOps to render to the specified surface.
*
* TODO: 'screen' is the only supported surface at the moment.
*
* @param surface
*/
render(surface: 'screen' | CoreContextTexture = 'screen'): void {
const { glw, quadBuffer } = this;
const arr = new Float32Array(quadBuffer, 0, this.curBufferIdx);
const buffer = this.quadBufferCollection.getBuffer('a_position') || null;
glw.arrayBufferData(buffer, arr, glw.STATIC_DRAW);
for (let i = 0, length = this.renderOps.length; i < length; i++) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.renderOps[i]!.draw();
}
this.quadBufferUsage = this.curBufferIdx * arr.BYTES_PER_ELEMENT;
// Calculate the size of each quad in bytes (4 vertices per quad) times the size of each vertex in bytes
const QUAD_SIZE_IN_BYTES = 4 * (8 * arr.BYTES_PER_ELEMENT); // 8 attributes per vertex
this.numQuadsRendered = this.quadBufferUsage / QUAD_SIZE_IN_BYTES;
}
getQuadCount(): number {
return this.numQuadsRendered;
}
renderToTexture(node: CoreNode) {
for (let i = 0; i < this.rttNodes.length; i++) {
if (this.rttNodes[i] === node) {
return;
}
}
this.insertRTTNodeInOrder(node);
}
/**
* Inserts an RTT node into `this.rttNodes` while maintaining the correct rendering order based on hierarchy.
*
* Rendering order for RTT nodes is critical when nested RTT nodes exist in a parent-child relationship.
* Specifically:
* - Child RTT nodes must be rendered before their RTT-enabled parents to ensure proper texture composition.
* - If an RTT node is added and it has existing RTT children, it should be rendered after those children.
*
* This function addresses both cases by:
* 1. **Checking Upwards**: It traverses the node's hierarchy upwards to identify any RTT parent
* already in `rttNodes`. If an RTT parent is found, the new node is placed before this parent.
* 2. **Checking Downwards**: It traverses the node’s children recursively to find any RTT-enabled
* children that are already in `rttNodes`. If such children are found, the new node is inserted
* after the last (highest index) RTT child node.
*
* The final calculated insertion index ensures the new node is positioned in `rttNodes` to respect
* both parent-before-child and child-before-parent rendering rules, preserving the correct order
* for the WebGL renderer.
*
* @param node - The RTT-enabled CoreNode to be added to `rttNodes` in the appropriate hierarchical position.
*/
private insertRTTNodeInOrder(node: CoreNode) {
let insertIndex = this.rttNodes.length; // Default to the end of the array
// 1. Traverse upwards to ensure the node is placed before its RTT parent (if any).
let currentNode: CoreNode = node;
while (currentNode) {
if (!currentNode.parent) {
break;
}
const parentIndex = this.rttNodes.indexOf(currentNode.parent);
if (parentIndex !== -1) {
// Found an RTT parent in the list; set insertIndex to place node before the parent
insertIndex = parentIndex;
break;
}
currentNode = currentNode.parent;
}
// 2. Traverse downwards to ensure the node is placed after any RTT children.
// Look through each child recursively to see if any are already in rttNodes.
const maxChildIndex = this.findMaxChildRTTIndex(node);
if (maxChildIndex !== -1) {
// Adjust insertIndex to be after the last child RTT node
insertIndex = Math.max(insertIndex, maxChildIndex + 1);
}
// 3. Insert the node at the calculated position
this.rttNodes.splice(insertIndex, 0, node);
}
// Helper function to find the highest index of any RTT children of a node within rttNodes
private findMaxChildRTTIndex(node: CoreNode): number {
let maxIndex = -1;
const traverseChildren = (currentNode: CoreNode) => {
const currentIndex = this.rttNodes.indexOf(currentNode);
if (currentIndex !== -1) {
maxIndex = Math.max(maxIndex, currentIndex);
}
// Recursively check all children of the current node
for (const child of currentNode.children) {
traverseChildren(child);
}
};
// Start traversal directly with the provided node
traverseChildren(node);
return maxIndex;
}
renderRTTNodes() {
const { glw } = this;
const { txManager } = this.stage;
// Render all associated RTT nodes to their textures
for (let i = 0; i < this.rttNodes.length; i++) {
const node = this.rttNodes[i];
// Skip nodes that don't have RTT updates
if (node === undefined || node.hasRTTupdates === false) {
continue;
}
// Skip nodes that are not visible
if (
node.worldAlpha === 0 ||
(node.strictBounds === true &&
node.renderState === CoreNodeRenderState.OutOfBounds)
) {
continue;
}
// Skip nodes that do not have a loaded texture
if (node.texture === null || node.texture.state !== 'loaded') {
continue;
}
// Set the active RTT node to the current node
// So we can prevent rendering children of nested RTT nodes
this.activeRttNode = node;
assertTruthy(node.texture, 'RTT node missing texture');
const ctxTexture = node.texture.ctxTexture;
assertTruthy(ctxTexture instanceof WebGlCoreCtxRenderTexture);
this.renderToTextureActive = true;
// Bind the the texture's framebuffer
glw.bindFramebuffer(ctxTexture.framebuffer);
glw.viewport(0, 0, ctxTexture.w, ctxTexture.h);
// Set the clear color to transparent
glw.clearColor(0, 0, 0, 0);
glw.clear();
// Render all associated quads to the texture
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (child === undefined) {
continue;
}
this.stage.addQuads(child);
child.hasRTTupdates = false;
}
// Render all associated quads to the texture
this.render();
// Reset render operations
this.renderOps.length = 0;
node.hasRTTupdates = false;
}
const clearColor = this.clearColor.normalized;
// Restore the default clear color
glw.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
// Bind the default framebuffer
glw.bindFramebuffer(null);
glw.viewport(0, 0, this.glw.canvas.width, this.glw.canvas.height);
this.renderToTextureActive = false;
}
removeRTTNode(node: CoreNode) {
const index = this.rttNodes.indexOf(node);
if (index === -1) {
return;
}
this.rttNodes.splice(index, 1);
}
getBufferInfo(): BufferInfo | null {
const bufferInfo: BufferInfo = {
totalAvailable: this.stage.options.quadBufferSize,
totalUsed: this.quadBufferUsage,
};
return bufferInfo;
}
override getDefShaderCtr(): BaseShaderController {
return this.defShaderCtrl;
}
/**
* Updates the WebGL context's clear color and clears the color buffer.
*
* @param color - The color to set as the clear color, represented as a 32-bit integer.
*/
updateClearColor(color: number) {
if (this.clearColor.raw === color) {
return;
}
const glw = this.glw;
const normalizedColor = getNormalizedRgbaComponents(color);
glw.clearColor(
normalizedColor[0],
normalizedColor[1],
normalizedColor[2],
normalizedColor[3],
);
this.clearColor = {
raw: color,
normalized: normalizedColor,
};
glw.clear();
}
}