@lightningjs/renderer
Version:
Lightning 3 Renderer
318 lines (284 loc) • 9.94 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 { Dimensions } from '../../../common/CommonTypes.js';
import { assertTruthy, hasOwn } from '../../../utils.js';
import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js';
import { CoreShader } from '../CoreShader.js';
import type { WebGlCoreCtxTexture } from './WebGlCoreCtxTexture.js';
import type { WebGlCoreRenderOp } from './WebGlCoreRenderOp.js';
import type { WebGlCoreRenderer } from './WebGlCoreRenderer.js';
import type { BufferCollection } from './internal/BufferCollection.js';
import {
createProgram,
createShader,
type AttributeInfo,
type ShaderOptions,
type UniformInfo,
type UniformMethodMap,
type ShaderProgramSources,
} from './internal/ShaderUtils.js';
/**
* Automatic shader prop for the dimensions of the Node being rendered
*
* @remarks
* Shader's who's rendering depends on the dimensions of the Node being rendered
* should extend this interface from their Prop interface type.
*/
export interface DimensionsShaderProp {
/**
* Dimensions of the Node being rendered (Auto-set by the renderer)
*
* @remarks
* DO NOT SET THIS. It is set automatically by the renderer.
* Any values set here will be ignored.
*/
$dimensions?: Dimensions;
}
export interface AlphaShaderProp {
/**
* Alpha of the Node being rendered (Auto-set by the renderer)
*
* @remarks
* DO NOT SET THIS. It is set automatically by the renderer.
* Any values set here will be ignored.
*/
$alpha?: number;
}
export abstract class WebGlCoreShader extends CoreShader {
protected buffersBound = false;
protected program: WebGLProgram;
/**
* Vertex Array Object
*
* @remarks
* Used by WebGL2 Only
*/
protected vao: WebGLVertexArrayObject | undefined;
protected renderer: WebGlCoreRenderer;
protected glw: WebGlContextWrapper;
protected attributeLocations: string[];
protected uniformLocations: Record<string, WebGLUniformLocation>;
readonly supportsIndexedTextures: boolean;
constructor(options: ShaderOptions) {
super();
const renderer = (this.renderer = options.renderer);
const glw = (this.glw = this.renderer.glw);
this.supportsIndexedTextures = options.supportsIndexedTextures || false;
// Check that extensions are supported
const webGl2 = glw.isWebGl2();
const requiredExtensions =
(webGl2 && options.webgl2Extensions) ||
(!webGl2 && options.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`,
);
}
});
// Gather shader sources
// - If WebGL 2 and special WebGL 2 sources are provided, we copy those sources and delete
// the extra copy of them to save memory.
// TODO: This could be further made optimal by just caching the compiled shaders and completely deleting
// the source code
const shaderSources =
options.shaderSources ||
(this.constructor as typeof WebGlCoreShader).shaderSources;
if (!shaderSources) {
throw new Error(
`Shader "${this.constructor.name}" is missing shaderSources.`,
);
} else if (webGl2 && shaderSources?.webGl2) {
shaderSources.fragment = shaderSources.webGl2.fragment;
shaderSources.vertex = shaderSources.webGl2.vertex;
delete shaderSources.webGl2;
}
const textureUnits =
renderer.system.parameters.MAX_VERTEX_TEXTURE_IMAGE_UNITS;
const vertexSource =
shaderSources.vertex instanceof Function
? shaderSources.vertex(textureUnits)
: shaderSources.vertex;
const fragmentSource =
shaderSources.fragment instanceof Function
? shaderSources.fragment(textureUnits)
: shaderSources.fragment;
const vertexShader = createShader(glw, glw.VERTEX_SHADER, vertexSource);
const fragmentShader = createShader(
glw,
glw.FRAGMENT_SHADER,
fragmentSource,
);
if (!vertexShader || !fragmentShader) {
throw new Error(
`Unable to create the following shader(s): ${[
!vertexShader && 'VERTEX_SHADER',
!fragmentShader && 'FRAGMENT_SHADER',
]
.filter(Boolean)
.join(' and ')}`,
);
}
const program = createProgram(glw, vertexShader, fragmentShader);
if (!program) {
throw new Error('Unable to create program');
}
this.program = program;
this.attributeLocations = glw.getAttributeLocations(this.program);
this.uniformLocations = glw.getUniformLocations(this.program);
}
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);
}
}
/**
* Given two sets of Shader props destined for this Shader, determine if they can be batched together
* to reduce the number of draw calls.
*
* @remarks
* This is used by the {@link WebGlCoreRenderer} to determine if it can batch multiple consecutive draw
* calls into a single draw call.
*
* By default, this returns false (meaning no batching is allowed), but can be
* overridden by child classes to provide more efficient batching.
*
* @param propsA
* @param propsB
* @returns
*/
canBatchShaderProps(
propsA: Record<string, unknown>,
propsB: Record<string, unknown>,
): boolean {
return false;
}
bindRenderOp(
renderOp: WebGlCoreRenderOp,
props: Record<string, unknown> | null,
) {
this.bindBufferCollection(renderOp.buffers);
// Since we're not using batched rendering yet we can safely test
// for first texture only
if (renderOp.textures.length > 0 && renderOp.textures[0]?.ctxTexture) {
this.bindTextures(renderOp.textures);
}
const { glw, parentHasRenderTexture, renderToTexture } = renderOp;
// Skip if the parent and current operation both have render textures
if (renderToTexture && parentHasRenderTexture) {
return;
}
// Bind render texture framebuffer dimensions as resolution
// if the parent has a render texture
if (parentHasRenderTexture) {
const { width, height } = renderOp.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
glw.uniform1f(this.getUniformLocation('u_pixelRatio'), 1.0);
// Set resolution to the framebuffer dimensions
glw.uniform2f(
this.getUniformLocation('u_resolution'),
width ?? 0,
height ?? 0,
);
} else {
glw.uniform1f(
this.getUniformLocation('u_pixelRatio'),
renderOp.options.pixelRatio,
);
glw.uniform2f(
this.getUniformLocation('u_resolution'),
glw.canvas.width,
glw.canvas.height,
);
}
if (props) {
// Bind optional automatic uniforms
// These are only bound if their keys are present in the props.
if (hasOwn(props, '$dimensions')) {
let dimensions = props.$dimensions as Dimensions | null;
if (!dimensions) {
dimensions = renderOp.dimensions;
}
glw.uniform2f(
this.getUniformLocation('u_dimensions'),
dimensions.width,
dimensions.height,
);
}
if (hasOwn(props, '$alpha')) {
let alpha = props.$alpha as number | null;
if (!alpha) {
alpha = renderOp.alpha;
}
glw.uniform1f(this.getUniformLocation('u_alpha'), alpha);
}
this.bindProps(props);
}
}
getUniformLocation(name: string): WebGLUniformLocation | null {
return this.uniformLocations[name] || null;
}
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,
);
}
}
protected override bindProps(props: Record<string, unknown>) {
// Implement in child class
}
bindTextures(textures: WebGlCoreCtxTexture[]) {
// no defaults
}
override attach(): void {
this.glw.useProgram(this.program);
if (this.glw.isWebGl2() && this.vao) {
this.glw.bindVertexArray(this.vao);
}
}
override detach(): void {
this.disableAttributes();
}
protected static shaderSources?: ShaderProgramSources;
}