@lightningjs/renderer
Version:
Lightning 3 Renderer
365 lines (310 loc) • 10.9 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 type { GlContextWrapper } from '../../platforms/GlContextWrapper.js';
import { Default } from '../../shaders/webgl/Default.js';
import type { CoreShaderProgram } from '../CoreShaderProgram.js';
import type { WebGlCtxTexture } from './WebGlCtxTexture.js';
import type { WebGlRenderer, WebGlRenderOp } from './WebGlRenderer.js';
import type { WebGlShaderType } from './WebGlShaderNode.js';
import { WebGlShaderNode } from './WebGlShaderNode.js';
import type { BufferCollection } from './internal/BufferCollection.js';
import {
createProgram,
createShader,
type UniformSet1Param,
type UniformSet2Params,
type UniformSet3Params,
type UniformSet4Params,
} from './internal/ShaderUtils.js';
import { CoreNode } from '../../CoreNode.js';
import type { CoreTextNode } from '../../CoreTextNode.js';
import type { SdfShaderProps } from '../../shaders/webgl/SdfShader.js';
export class WebGlShaderProgram implements CoreShaderProgram {
protected program: WebGLProgram | null;
/**
* Vertex Array Object
*
* @remarks
* Used by WebGL2 Only
*/
protected vao: WebGLVertexArrayObject | undefined;
protected renderer: WebGlRenderer;
protected glw: GlContextWrapper;
protected attributeLocations: string[];
protected uniformLocations: Record<string, WebGLUniformLocation> | null;
protected lifecycle: Pick<WebGlShaderType, 'update' | 'canBatch'>;
protected useSystemAlpha = false;
protected useSystemDimensions = false;
protected useTimeValue = false;
public isDestroyed = false;
supportsIndexedTextures = false;
constructor(
renderer: WebGlRenderer,
config: WebGlShaderType,
resolvedProps: Record<string, any>,
) {
this.renderer = renderer;
const glw = (this.glw = renderer.glw);
// Check that extensions are supported
const webGl2 = glw.isWebGl2();
let requiredExtensions: string[] = [];
this.supportsIndexedTextures =
config.supportsIndexedTextures || this.supportsIndexedTextures;
requiredExtensions =
(webGl2 && config.webgl2Extensions) ||
(!webGl2 && config.webgl1Extensions) ||
[];
const glVersion = webGl2 ? '2.0' : '1.0';
requiredExtensions.forEach((extensionName) => {
if (!glw.getExtension(extensionName)) {
throw new Error(
`Shader "${this.constructor.name}" requires extension "${extensionName}" for WebGL ${glVersion} but wasn't found`,
);
}
});
let vertexSource =
config.vertex instanceof Function
? config.vertex(renderer, resolvedProps)
: config.vertex;
if (vertexSource === undefined) {
vertexSource = Default.vertex as string;
}
const fragmentSource =
config.fragment instanceof Function
? config.fragment(renderer, resolvedProps)
: config.fragment;
const vertexShader = createShader(glw, glw.VERTEX_SHADER, vertexSource);
if (!vertexShader) {
throw new Error('Vertex shader creation failed');
}
const fragmentShader = createShader(
glw,
glw.FRAGMENT_SHADER,
fragmentSource,
);
if (!fragmentShader) {
throw new Error('fragment shader creation failed');
}
const program = createProgram(glw, vertexShader, fragmentShader);
this.program = program;
this.attributeLocations = glw.getAttributeLocations(program);
const uniLocs = (this.uniformLocations = glw.getUniformLocations(program));
this.useSystemAlpha = uniLocs['u_alpha'] !== undefined;
this.useSystemDimensions = uniLocs['u_dimensions'] !== undefined;
this.useTimeValue =
this.glw.getUniformLocation(program, 'u_dimensions') !== null &&
config.time !== undefined;
this.lifecycle = {
update: config.update,
canBatch: config.canBatch,
};
}
disableAttribute(location: number) {
this.glw.disableVertexAttribArray(location);
}
disableAttributes() {
const glw = this.glw;
const attribLen = this.attributeLocations.length;
for (let i = 0; i < attribLen; i++) {
glw.disableVertexAttribArray(i);
}
}
reuseRenderOp(node: CoreNode, currentRenderOp: WebGlRenderOp): boolean {
if (this.lifecycle.canBatch !== undefined) {
return this.lifecycle.canBatch(node, currentRenderOp);
}
const { time, worldAlpha, w, h } = node;
if (this.useTimeValue === true) {
if (time !== currentRenderOp.time) {
return false;
}
}
if (this.useSystemAlpha === true) {
if (worldAlpha !== currentRenderOp.worldAlpha) {
return false;
}
}
if (this.useSystemDimensions === true) {
if (w !== currentRenderOp.w || h !== currentRenderOp.h) {
return false;
}
}
let shaderPropsA: Record<string, unknown> | undefined = undefined;
let shaderPropsB: Record<string, unknown> | undefined = undefined;
const shader = node.props.shader;
if (shader !== null) {
shaderPropsA = (shader as WebGlShaderNode).resolvedProps;
}
const opShader = currentRenderOp.shader;
if (opShader !== null) {
shaderPropsB = (opShader as WebGlShaderNode).resolvedProps;
}
if (
(shaderPropsA === undefined && shaderPropsB !== undefined) ||
(shaderPropsA !== undefined && shaderPropsB === undefined)
) {
return false;
}
if (shaderPropsA !== undefined && shaderPropsB !== undefined) {
for (const key in shaderPropsA) {
if (shaderPropsA[key] !== shaderPropsB[key]) {
return false;
}
}
}
return true;
}
bindRenderOp(renderOp: WebGlRenderOp) {
this.bindTextures(renderOp.renderOpTextures);
this.bindBufferCollection(renderOp.quadBufferCollection);
const parentHasRenderTexture = renderOp.parentHasRenderTexture;
const framebufferDimensions = renderOp.isCoreNode
? renderOp.parentFramebufferDimensions
: renderOp.framebufferDimensions;
// Skip if the parent and current operation both have render textures
if (renderOp.rtt === true && parentHasRenderTexture === true) {
return;
}
// Bind render texture framebuffer dimensions as resolution
// if the parent has a render texture
if (parentHasRenderTexture === true && framebufferDimensions) {
const { w, h } = framebufferDimensions;
// Force pixel ratio to 1.0 for render textures since they are always 1:1
// the final render texture will be rendered to the screen with the correct pixel ratio
this.glw.uniform1f('u_pixelRatio', 1.0);
// Set resolution to the framebuffer dimensions
this.glw.uniform2f('u_resolution', w, h);
} else {
this.glw.uniform1f('u_pixelRatio', renderOp.stage.pixelRatio);
this.glw.uniform2f(
'u_resolution',
this.glw.canvas.width,
this.glw.canvas.height,
);
}
if (this.useTimeValue === true) {
this.glw.uniform1f('u_time', renderOp.time);
}
if (this.useSystemAlpha === true) {
this.glw.uniform1f('u_alpha', renderOp.worldAlpha);
}
if (this.useSystemDimensions === true) {
this.glw.uniform2f('u_dimensions', renderOp.w, renderOp.h);
}
/**temporary fix to make sdf texts work */
if (renderOp.isSdfRenderOp === true) {
const opShader = renderOp.shader!; // SdfRenderOp has .shader
(opShader.shaderType as WebGlShaderType<SdfShaderProps>).onSdfBind?.call(
this.glw,
(renderOp as CoreTextNode).sdfShaderProps,
);
return;
}
const shader = renderOp.shader as WebGlShaderNode;
const uniforms = shader.uniforms;
if (uniforms.hasStoredUniforms === true) {
/**
* loop over all precalculated uniform types
*/
for (const key in uniforms.single) {
const { method, value } = uniforms.single[key]!;
this.glw[method as keyof UniformSet1Param](key, value as never);
}
for (const key in uniforms.vec2) {
const { method, value } = uniforms.vec2[key]!;
this.glw[method as keyof UniformSet2Params](key, value[0], value[1]);
}
for (const key in uniforms.vec3) {
const { method, value } = uniforms.vec3[key]!;
this.glw[method as keyof UniformSet3Params](
key,
value[0],
value[1],
value[2],
);
}
for (const key in uniforms.vec4) {
const { method, value } = uniforms.vec4[key]!;
this.glw[method as keyof UniformSet4Params](
key,
value[0],
value[1],
value[2],
value[3],
);
}
}
}
bindBufferCollection(buffer: BufferCollection) {
const { glw } = this;
const attribs = this.attributeLocations;
const attribLen = attribs.length;
for (let i = 0; i < attribLen; i++) {
const name = attribs[i]!;
const resolvedBuffer = buffer.getBuffer(name);
const resolvedInfo = buffer.getAttributeInfo(name);
if (resolvedBuffer === undefined || resolvedInfo === undefined) {
continue;
}
glw.enableVertexAttribArray(i);
glw.vertexAttribPointer(
resolvedBuffer,
i,
resolvedInfo.size,
resolvedInfo.type,
resolvedInfo.normalized,
resolvedInfo.stride,
resolvedInfo.offset,
);
}
}
bindTextures(textures: WebGlCtxTexture[]) {
const t = textures[0];
if (t === undefined) return;
this.glw.activeTexture(0);
this.glw.bindTexture(t.ctxTexture);
}
attach(): void {
if (this.isDestroyed === true) {
return;
}
this.glw.useProgram(this.program, this.uniformLocations!);
if (this.glw.isWebGl2() && this.vao) {
this.glw.bindVertexArray(this.vao);
}
}
detach(): void {
this.disableAttributes();
}
destroy() {
if (this.isDestroyed === true) {
return;
}
const glw = this.glw;
this.detach();
glw.deleteProgram(this.program!);
this.program = null;
this.uniformLocations = null;
const attribs = this.attributeLocations;
const attribLen = this.attributeLocations.length;
for (let i = 0; i < attribLen; i++) {
this.glw.deleteBuffer(attribs[i]!);
}
}
}