UNPKG

@lightningjs/renderer

Version:
318 lines (284 loc) 9.94 kB
/* * 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; }