@lightningjs/renderer
Version:
Lightning 3 Renderer
737 lines (642 loc) • 22.6 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 { CoreRenderer, type BufferInfo } from '../CoreRenderer.js';
import type { CoreContextTexture } from '../CoreContextTexture.js';
import {
createIndexBuffer,
type CoreWebGlParameters,
type CoreWebGlExtensions,
getWebGlParameters,
getWebGlExtensions,
type WebGlColor,
} from './internal/RendererUtils.js';
import { WebGlCtxTexture } from './WebGlCtxTexture.js';
import {
Texture,
TextureType,
type TextureCoords,
} from '../../textures/Texture.js';
import { SubTexture } from '../../textures/SubTexture.js';
import { WebGlCtxSubTexture } from './WebGlCtxSubTexture.js';
import { BufferCollection } from './internal/BufferCollection.js';
import { compareRect, getNormalizedRgbaComponents } from '../../lib/utils.js';
import { WebGlShaderProgram } from './WebGlShaderProgram.js';
import { RenderTexture } from '../../textures/RenderTexture.js';
import { CoreNodeRenderState, CoreNode } from '../../CoreNode.js';
import { WebGlCtxRenderTexture } from './WebGlCtxRenderTexture.js';
import { Default } from '../../shaders/webgl/Default.js';
import type { WebGlShaderType } from './WebGlShaderNode.js';
import { WebGlShaderNode } from './WebGlShaderNode.js';
import type { Dimensions } from '../../../common/CommonTypes.js';
import type { GlContextWrapper } from '../../platforms/GlContextWrapper.js';
import type { Stage } from '../../Stage.js';
import type { CoreTextNode } from '../../CoreTextNode.js';
interface CoreWebGlSystem {
parameters: CoreWebGlParameters;
extensions: CoreWebGlExtensions;
}
export type WebGlRenderOp = CoreNode | CoreTextNode;
export class WebGlRenderer extends CoreRenderer {
//// WebGL Native Context and Data
glw: GlContextWrapper;
system: CoreWebGlSystem;
//// Persistent data
quadBuffer: ArrayBuffer;
fQuadBuffer: Float32Array;
uiQuadBuffer: Uint32Array;
renderOps: WebGlRenderOp[] = [];
//// Render Op / Buffer Filling State
curBufferIdx = 0;
curRenderOp: WebGlRenderOp | null = null;
override rttNodes: CoreNode[] = [];
activeRttNode: CoreNode | null = null;
override defaultTextureCoords: TextureCoords = {
x1: 0,
y1: 0,
x2: 1,
y2: 1,
};
//// Default Shader
defaultShaderNode: WebGlShaderNode | null = null;
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(stage: Stage) {
super(stage);
this.quadBuffer = new ArrayBuffer(stage.options.quadBufferSize);
this.fQuadBuffer = new Float32Array(this.quadBuffer);
this.uiQuadBuffer = new Uint32Array(this.quadBuffer);
this.mode = 'webgl';
const platform = stage.platform;
const canvas = platform.canvas!;
const glw = (this.glw = platform.createContext() as GlContextWrapper);
glw.viewport(0, 0, canvas.width, canvas.height);
this.updateClearColor(stage.clearColor);
glw.setBlend(true);
glw.blendFunc(glw.ONE, glw.ONE_MINUS_SRC_ALPHA);
createIndexBuffer(glw, stage.bufferMemory);
this.system = {
parameters: getWebGlParameters(this.glw),
extensions: getWebGlExtensions(this.glw),
};
const quadBuffer = glw.createBuffer();
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_textureCoords: {
name: 'a_textureCoords',
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_nodeCoords: {
name: 'a_nodeCoords',
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);
if (this.stage.options.enableClear !== false) {
glw.clear();
}
}
createShaderProgram(
shaderType: WebGlShaderType,
props: Record<string, unknown>,
): WebGlShaderProgram {
return new WebGlShaderProgram(this, shaderType, props);
}
createShaderNode(
shaderKey: string,
shaderType: WebGlShaderType,
props?: Record<string, unknown>,
program?: WebGlShaderProgram,
) {
return new WebGlShaderNode(
shaderKey,
shaderType,
program!,
this.stage,
props,
);
}
override supportsShaderType(shaderType: Readonly<WebGlShaderType>): boolean {
//if shadertype doesnt have a fragment source we cant use it
return shaderType.fragment !== undefined;
}
createCtxTexture(textureSource: Texture): CoreContextTexture {
if (textureSource instanceof SubTexture) {
return new WebGlCtxSubTexture(
this.glw,
this.stage.txMemManager,
textureSource,
);
} else if (textureSource instanceof RenderTexture) {
return new WebGlCtxRenderTexture(
this.glw,
this.stage.txMemManager,
textureSource,
);
}
return new WebGlCtxTexture(
this.glw,
this.stage.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(node: CoreNode) {
const f = this.fQuadBuffer;
const u = this.uiQuadBuffer;
let i = this.curBufferIdx;
const reuse = this.reuseRenderOp(node);
if (reuse === false) {
this.newRenderOp(node, i);
}
let tx = (node.props.texture || this.stage.defaultTexture) as Texture;
if (tx.type === TextureType.subTexture) {
tx = (tx as SubTexture).parentTexture;
}
const ctx = tx.ctxTexture as WebGlCtxTexture | undefined;
if (ctx === undefined) return;
let tidx = this.curRenderOp!.addTexture(ctx);
if (tidx === 0xffffffff) {
this.newRenderOp(node, i);
tidx = this.curRenderOp!.addTexture(ctx);
}
const rc = node.renderCoords!;
const tc = node.textureCoords || this.defaultTextureCoords;
const cTl = node.premultipliedColorTl;
const cTr = node.premultipliedColorTr;
const cBl = node.premultipliedColorBl;
const cBr = node.premultipliedColorBr;
// Upper-Left
f[i] = rc.x1;
f[i + 1] = rc.y1;
f[i + 2] = tc.x1;
f[i + 3] = tc.y1;
u[i + 4] = cTl;
f[i + 5] = tidx;
f[i + 6] = 0;
f[i + 7] = 0;
// Upper-Right
f[i + 8] = rc.x2;
f[i + 9] = rc.y2;
f[i + 10] = tc.x2;
f[i + 11] = tc.y1;
u[i + 12] = cTr;
f[i + 13] = tidx;
f[i + 14] = 1;
f[i + 15] = 0;
// Lower-Left
f[i + 16] = rc.x4;
f[i + 17] = rc.y4;
f[i + 18] = tc.x1;
f[i + 19] = tc.y2;
u[i + 20] = cBl;
f[i + 21] = tidx;
f[i + 22] = 0;
f[i + 23] = 1;
// Lower-Right
f[i + 24] = rc.x3;
f[i + 25] = rc.y3;
f[i + 26] = tc.x2;
f[i + 27] = tc.y2;
u[i + 28] = cBr;
f[i + 29] = tidx;
f[i + 30] = 1;
f[i + 31] = 1;
this.curRenderOp!.numQuads++;
this.curBufferIdx = i + 32;
}
/**
* 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(node: CoreNode, bufferIdx: number) {
const curRenderOp = node;
curRenderOp.renderOpBufferIdx = bufferIdx;
curRenderOp.numQuads = 0;
curRenderOp.renderOpTextures.length = 0;
this.curRenderOp = curRenderOp;
this.renderOps.push(curRenderOp);
}
/**
* Test if the current Render operation can be reused for the specified parameters.
* @param params
* @returns
*/
reuseRenderOp(node: CoreNode): boolean {
const curRenderOp = this.curRenderOp;
if (curRenderOp === null) {
return false;
}
const shader = node.props.shader as WebGlShaderNode;
const curShader = curRenderOp.shader as WebGlShaderNode;
if (curShader?.shaderKey !== shader?.shaderKey) {
return false;
}
// Switching clipping rect will require a new render operation
if (compareRect(curRenderOp.clippingRect, node.clippingRect) === false) {
return false;
}
// Force new render operation if rendering to texture is different
const curRtt = curRenderOp.rtt;
if (
curRenderOp.parentHasRenderTexture !== node.parentHasRenderTexture ||
curRtt !== (node.props.rtt === true)
) {
return false;
}
if (
node.parentHasRenderTexture === true &&
node.parentFramebufferDimensions !== null
) {
const curFbDims = curRenderOp.isCoreNode
? curRenderOp.parentFramebufferDimensions
: curRenderOp.framebufferDimensions;
if (
curFbDims === null ||
curFbDims.w !== node.parentFramebufferDimensions.w ||
curFbDims.h !== node.parentFramebufferDimensions.h
) {
return false;
}
}
if (curShader?.shaderKey === 'default' && shader?.shaderKey === 'default') {
return true;
}
// Check if the shader can batch the shader properties
if (curShader?.program.reuseRenderOp(node, curRenderOp) === false) {
return false;
}
return true;
}
/**
* add RenderOp to the render pipeline
*/
addRenderOp(renderable: WebGlRenderOp) {
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++) {
this.renderOps[i]!.draw(this);
}
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
// Build a one-shot index map so all lookups below are O(1) instead of O(n).
const rttNodes = this.rttNodes;
const indexMap = new Map<number, number>();
for (let i = 0; i < rttNodes.length; i++) {
indexMap.set(rttNodes[i]!.id, i);
}
// 1. Traverse upwards to ensure the node is placed before its RTT parent (if any).
let currentNode: CoreNode = node;
while (currentNode.parent !== null) {
const parentIndex = indexMap.get(currentNode.parent.id);
if (parentIndex !== undefined) {
insertIndex = parentIndex;
break;
}
currentNode = currentNode.parent;
}
// 2. Traverse downwards to ensure the node is placed after any RTT children.
const maxChildIndex = this.findMaxChildRTTIndex(node, indexMap);
if (maxChildIndex !== -1) {
insertIndex = Math.max(insertIndex, maxChildIndex + 1);
}
// 3. Insert the node at the calculated position
this.rttNodes.splice(insertIndex, 0, node);
}
// Iterative DFS to find the highest rttNodes index among all RTT descendants of node.
private findMaxChildRTTIndex(
node: CoreNode,
indexMap: Map<number, number>,
): number {
let maxIndex = -1;
// Explicit stack avoids recursive arrow function allocation and call-stack growth.
const stack: CoreNode[] = [node];
while (stack.length !== 0) {
const current = stack.pop()!;
const idx = indexMap.get(current.id);
if (idx !== undefined && idx > maxIndex) {
maxIndex = idx;
}
const children = current.children;
for (let i = 0; i < children.length; i++) {
stack.push(children[i]!);
}
}
return maxIndex;
}
renderRTTNodes() {
const { glw } = this;
// 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.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;
const ctxTexture = node.texture.ctxTexture as WebGlCtxRenderTexture;
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;
}
updateViewport(): void {
this.glw.viewport(0, 0, this.glw.canvas.width, this.glw.canvas.height);
}
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;
}
getDefaultShaderNode(): WebGlShaderNode {
if (this.defaultShaderNode !== null) {
return this.defaultShaderNode as WebGlShaderNode;
}
this.stage.shManager.registerShaderType('default', Default);
this.defaultShaderNode = this.stage.shManager.createShader(
'default',
) as WebGlShaderNode;
return this.defaultShaderNode;
}
override getTextureCoords(node: CoreNode): TextureCoords | undefined {
const texture = node.texture;
if (texture === null) {
return undefined;
}
//this stuff needs to be properly moved to CtxSubTexture at some point in the future.
const ctxTexture =
(texture as SubTexture).parentTexture !== undefined
? (texture as SubTexture).parentTexture.ctxTexture
: texture.ctxTexture;
if (ctxTexture === undefined) {
return undefined;
}
const textureOptions = node.props.textureOptions;
//early exit for textures with no options unless its a subtexture
if (
texture.type !== TextureType.subTexture &&
textureOptions === undefined
) {
return (ctxTexture as WebGlCtxTexture).txCoords;
}
let { x1, x2, y1, y2 } = (ctxTexture as WebGlCtxTexture).txCoords;
if (texture.type === TextureType.subTexture) {
const { w: parentW, h: parentH } = (texture as SubTexture).parentTexture
.dimensions!;
const { x, y, w, h } = (texture as SubTexture).props;
x1 = x / parentW;
y1 = y / parentH;
x2 = x1 + w / parentW;
y2 = y1 + h / parentH;
}
const resizeMode = textureOptions.resizeMode;
if (
resizeMode !== undefined &&
resizeMode.type === 'cover' &&
texture.dimensions !== null
) {
const dimensions = texture.dimensions as Dimensions;
const w = node.props.w;
const h = node.props.h;
const scaleX = w / dimensions.w;
const scaleY = h / dimensions.h;
const scale = Math.max(scaleX, scaleY);
const precision = 1 / scale;
// Determine based on width
if (scaleX < scale) {
const desiredSize = precision * node.props.w;
x1 = (1 - desiredSize / dimensions.w) * (resizeMode.clipX ?? 0.5);
x2 = x1 + desiredSize / dimensions.w;
}
// Determine based on height
if (scaleY < scale) {
const desiredSize = precision * node.props.h;
y1 = (1 - desiredSize / dimensions.h) * (resizeMode.clipY ?? 0.5);
y2 = y1 + desiredSize / dimensions.h;
}
}
if (textureOptions.flipX === true) {
[x1, x2] = [x2, x1];
}
if (textureOptions.flipY === true) {
[y1, y2] = [y2, y1];
}
return {
x1,
y1,
x2,
y2,
};
}
/**
* Sets the glClearColor to the specified color. *
* @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,
};
}
override destroy(): void {
const loseCtx = this.glw.getExtension(
'WEBGL_lose_context',
) as WEBGL_lose_context | null;
loseCtx?.loseContext();
}
override deleteBuffer(buffer: WebGLBuffer): void {
this.glw.deleteBuffer(buffer);
}
}