UNPKG

gpu-curtains

Version:

gpu-curtains is a 3D WebGPU rendering engine. It can be used as a standalone 3D engine, but also includes extra classes focused on mapping 3d objects to DOM elements; It allows users to synchronize values such as position, sizing, or scale between them.

691 lines (690 loc) 26.2 kB
import { throwWarning } from "../../utils/utils.mjs"; import { BufferBinding } from "../bindings/BufferBinding.mjs"; import { BindGroup } from "../bindGroups/BindGroup.mjs"; import { Texture } from "../textures/Texture.mjs"; import { OrthographicCamera } from "../cameras/OrthographicCamera.mjs"; import { PerspectiveCamera } from "../cameras/PerspectiveCamera.mjs"; import { Sampler } from "../samplers/Sampler.mjs"; import { directionalShadowStruct } from "../shadows/DirectionalShadow.mjs"; import { pointShadowStruct } from "../shadows/PointShadow.mjs"; import { spotShadowStruct } from "../shadows/SpotShadow.mjs"; import { GPURenderer } from "./GPURenderer.mjs"; //#region src/core/renderers/GPUCameraRenderer.ts /** * This renderer is meant to render meshes projected by a {@link RendererCamera}. It therefore creates a {@link RendererCamera} with its associated {@link bindings} as well as lights and shadows {@link bindings} used for lighting and their associated {@link cameraLightsBindGroup | bind group}.<br> * Can be safely used to render compute passes and meshes if they do not need to be tied to the DOM. * * @example * ```javascript * // first, we need a WebGPU device, that's what GPUDeviceManager is for * const gpuDeviceManager = new GPUDeviceManager({ * label: 'Custom device manager', * }) * * // we need to wait for the WebGPU device to be created * await gpuDeviceManager.init() * * // then we can create a camera renderer * const gpuCameraRenderer = new GPUCameraRenderer({ * deviceManager: gpuDeviceManager, // we need the WebGPU device to create the renderer context * container: document.querySelector('#canvas'), * }) * ``` * @template TCamera - The camera type parameter which extends {@link RendererCamera}. Default is {@link PerspectiveCamera}. */ var GPUCameraRenderer = class extends GPURenderer { /** @ignore */ #shouldUpdateCameraLightsBindGroup; /** * GPUCameraRenderer constructor * @param parameters - {@link GPUCameraRendererParams | parameters} used to create this {@link GPUCameraRenderer} */ constructor({ deviceManager, label, container, pixelRatio = 1, autoResize = true, context = {}, renderPass, camera = {}, lights = {} }) { super({ deviceManager, label, container, pixelRatio, autoResize, context, renderPass }); this.type = "GPUCameraRenderer"; camera = { fov: 50, near: .1, far: 1e3, ...camera }; if (lights !== false) lights = { maxAmbientLights: 2, maxDirectionalLights: 5, maxPointLights: 5, maxSpotLights: 5, useUniformsForShadows: this.device?.limits.maxStorageBuffersInVertexStage === 0, ...lights }; this.options = { ...this.options, camera, lights }; this.bindings = {}; this.#shouldUpdateCameraLightsBindGroup = true; this.lights = []; this.cameraViewport = null; this.setCamera(camera); this.setCameraBinding(); if (this.options.lights) this.#initLights(); this.setCameraLightsBindGroup(); } /** * Called when the {@link core/renderers/GPUDeviceManager.GPUDeviceManager#device | device} is lost. * Reset all our samplers, force all our scene objects and camera bind group to lose context. */ loseContext() { super.loseContext(); this.cameraLightsBindGroup.loseContext(); this.pointShadowsCubeFaceBindGroups.forEach((bindGroup) => bindGroup.loseContext()); } /** * Called when the {@link core/renderers/GPUDeviceManager.GPUDeviceManager#device | device} has been restored. * Configure the context again, resize the {@link core/renderPasses/RenderTarget.RenderTarget | render targets} and {@link core/textures/Texture.Texture | textures}, restore our {@link renderedObjects | rendered objects} context, re-write our {@link cameraLightsBindGroup | camera, lights and shadows bind group} bindings. */ restoreContext() { super.restoreContext(); this.cameraLightsBindGroup?.restoreContext(); this.pointShadowsCubeFaceBindGroups.forEach((bindGroup) => bindGroup.restoreContext()); this.updateCameraBindings(); } /** * Set our {@link renderPass | main render pass} and our {@link transmissionTarget} sampler. */ setMainRenderPasses() { super.setMainRenderPasses(); this.transmissionTarget = { sampler: new Sampler(this, { label: "Transmission sampler", name: "transmissionSampler", magFilter: "linear", minFilter: "linear", mipmapFilter: "linear", addressModeU: "clamp-to-edge", addressModeV: "clamp-to-edge" }) }; } /** * Set the default {@link camera}. * @param cameraParameters - {@link PerspectiveCameraBaseOptions | parameters} used to create the {@link camera}. */ setCamera(cameraParameters) { const { width, height } = this.rectBBox; this.useCamera(new PerspectiveCamera({ label: `${this.options.label} default perspective camera`, fov: cameraParameters.fov, near: cameraParameters.near, far: cameraParameters.far, width, height, pixelRatio: this.pixelRatio, onMatricesChanged: () => { this.onCameraMatricesChanged(); } })); } /** * Tell our {@link GPUCameraRenderer} to use this {@link RendererCamera}. If a {@link RendererCamera | camera} has already been set, reset the {@link GPUCameraRenderer#bindings.camera | camera binding} inputs view values and the {@link meshes} {@link RendererCamera} object. * @param camera - New {@link RendererCamera} to use. * @returns - This {@link GPUCameraRenderer} with its {@link RendererCamera | camera} type updated. */ useCamera(camera) { if (this.camera && camera && this.camera.uuid === camera.uuid) return; if (this.camera) { if (this.camera.parent && this.camera.parent.object3DIndex === this.scene.object3DIndex) this.camera.parent = null; this.camera.onMatricesChanged = () => {}; } this.camera = camera; if (!this.camera.parent) this.camera.parent = this.scene; this.resizeCamera(); this.camera.shouldUpdateProjectionMatrices(); if (this.bindings.camera) { this.camera.onMatricesChanged = () => this.onCameraMatricesChanged(); this.bindings.camera.inputs.view.value = this.camera.viewMatrix; this.bindings.camera.inputs.projection.value = this.camera.projectionMatrix; this.bindings.camera.inputs.position.value = this.camera.actualPosition; this.bindings.camera.inputs.view.shouldUpdate = true; this.bindings.camera.inputs.projection.shouldUpdate = true; this.bindings.camera.inputs.position.shouldUpdate = true; for (const mesh of this.meshes) if ("modelViewMatrix" in mesh) mesh.camera = this.camera; } return this; } /** * Update the {@link cameraViewport} if needed (i.e. if the camera use a different aspect ratio than the renderer). */ updateCameraViewport() { let { width, height } = this.canvas; if (this.viewport) { width = Math.min(width, this.viewport.width); height = Math.min(height, this.viewport.height); } if (this.camera instanceof PerspectiveCamera && this.camera.forceAspect) { width = Math.min(width, height * this.camera.forceAspect); height = Math.min(width / this.camera.forceAspect, height); this.setCameraViewport({ width, height, top: (this.canvas.height - height) * .5, left: (this.canvas.width - width) * .5, minDepth: 0, maxDepth: 1 }); } else if (this.camera instanceof OrthographicCamera) { const aspectRatio = (this.camera.right - this.camera.left) / (this.camera.top - this.camera.bottom); width = Math.min(width, height * aspectRatio); height = Math.min(width / aspectRatio, height); this.setCameraViewport({ width, height, top: (this.canvas.height - height) * .5, left: (this.canvas.width - width) * .5, minDepth: 0, maxDepth: 1 }); } else this.setCameraViewport(); } /** * Resize the {@link camera}, first by updating the {@link cameraViewport} and then resetting the {@link camera} projection. */ resizeCamera() { this.updateCameraViewport(); const { width, height } = this.cameraViewport ?? this.viewport ?? this.canvas; if (this.camera instanceof PerspectiveCamera) this.camera?.setPerspective({ width, height, pixelRatio: this.pixelRatio }); else if (this.camera instanceof OrthographicCamera) { const aspect = width / height; const frustumSize = this.camera.top * 2; this.camera.setOrthographic({ left: -frustumSize * aspect / 2, right: frustumSize * aspect / 2, pixelRatio: this.pixelRatio }); } } /** * Set the {@link cameraViewport} (that should be contained within the renderer {@link GPURenderer#viewport | viewport} if any) and update the {@link renderPass} and {@link postProcessingPass} {@link GPURenderer#viewport | viewport} values. * @param viewport - {@link RenderPassViewport} settings to use if any. */ setCameraViewport(viewport = null) { this.cameraViewport = viewport; if (!this.cameraViewport) { this.renderPass?.setViewport(this.viewport); this.postProcessingPass?.setViewport(this.viewport); } else { if (this.viewport) { const aspect = this.cameraViewport.width / this.cameraViewport.height; this.cameraViewport.width = Math.min(this.viewport.width, this.viewport.height * aspect); this.cameraViewport.height = Math.min(this.cameraViewport.width / aspect, this.viewport.height); this.cameraViewport.left = Math.max(0, (this.viewport.width - this.cameraViewport.width) * .5); this.cameraViewport.top = Math.max(0, (this.viewport.height - this.cameraViewport.height) * .5); } this.renderPass?.setViewport(this.cameraViewport); this.postProcessingPass?.setViewport(this.cameraViewport); } } /** * Resize the {@link camera} whenever the {@link GPURenderer#viewport | viewport} is updated. * @param viewport - {@link RenderPassViewport} settings to use if any. Can be set to `null` to cancel the {@link GPURenderer#viewport | viewport}. */ setViewport(viewport = null) { super.setViewport(viewport); this.resizeCamera(); } /** * Update the {@link core/renderers/GPURenderer.ProjectedMesh | projected meshes} sizes and positions when the {@link camera} {@link Camera#position | position} changes. */ onCameraMatricesChanged() { this.updateCameraBindings(); for (const mesh of this.meshes) if ("modelViewMatrix" in mesh) mesh.shouldUpdateProjectionMatrixStack(); } /** * Create a {@link BufferBinding} from a given {@link Camera}. Used internally but can also be used to create a new {@link BufferBinding} from a different camera than this renderer's {@link RendererCamera | camera}. * @param camera - {@link Camera} to use to create the {@link BufferBinding}. * @param label - Optional label to use for the {@link BufferBinding}. * @returns - Newly created {@link BufferBinding}. */ createCameraBinding(camera, label = "Camera") { return new BufferBinding({ label, name: "camera", visibility: ["vertex", "fragment"], struct: { view: { type: "mat4x4f", value: camera.viewMatrix }, projection: { type: "mat4x4f", value: camera.projectionMatrix }, position: { type: "vec3f", value: camera.actualPosition } } }); } /** * Set the {@link GPUCameraRenderer#bindings.camera | camera BufferBinding}. */ setCameraBinding() { this.bindings.camera = this.createCameraBinding(this.camera); } /** * Initialize the lights and shadows bindings. * @private */ #initLights() { if (!this.options.lights) this.options.lights = { maxAmbientLights: 2, maxDirectionalLights: 5, maxPointLights: 5, maxSpotLights: 5, useUniformsForShadows: this.device?.limits.maxStorageBuffersInVertexStage === 0 || false }; this.setLightsBinding(); this.setShadowsBinding(); } /** * Add a {@link Light} to the {@link lights} array. * @param light - {@link Light} to add. */ addLight(light) { this.lights.push(light); this.#updateLightsCount(light.type); } /** * Remove a {@link Light} from the {@link lights} array. * @param light - {@link Light} to remove. */ removeLight(light) { this.lights = this.lights.filter((l) => l.uuid !== light.uuid); this.#updateLightsCount(light.type); } /** * Update the lights count for the given {@link LightsType} based on the max light index. * @param lightsType - {@link LightsType} for which to update the light count. * @private */ #updateLightsCount(lightsType) { let maxIndex = 0; this.lights.filter((light) => light.type === lightsType).forEach((light) => { maxIndex = Math.max(maxIndex, light.index + 1); }); this.bindings[lightsType].inputs.count.value = maxIndex; this.bindings[lightsType].inputs.count.shouldUpdate = true; } /** * Set the lights {@link BufferBinding} based on the {@link lightsBindingParams}. */ setLightsBinding() { if (!this.options.lights) return; this.lightsBindingParams = { ambientLights: { max: this.options.lights.maxAmbientLights, label: "Ambient lights", params: { color: { type: "array<vec3f>", size: 3 } } }, directionalLights: { max: this.options.lights.maxDirectionalLights, label: "Directional lights", params: { color: { type: "array<vec3f>", size: 3 }, direction: { type: "array<vec3f>", size: 3 } } }, pointLights: { max: this.options.lights.maxPointLights, label: "Point lights", params: { color: { type: "array<vec3f>", size: 3 }, position: { type: "array<vec3f>", size: 3 }, range: { type: "array<f32>", size: 1 } } }, spotLights: { max: this.options.lights.maxSpotLights, label: "Spot lights", params: { color: { type: "array<vec3f>", size: 3 }, direction: { type: "array<vec3f>", size: 3 }, position: { type: "array<vec3f>", size: 3 }, coneCos: { type: "array<f32>", size: 1 }, penumbraCos: { type: "array<f32>", size: 1 }, range: { type: "array<f32>", size: 1 } } } }; Object.keys({ ambientLights: null, directionalLights: null, pointLights: null, spotLights: null }).forEach((lightsType) => { this.setLightsTypeBinding(lightsType); }); } /** * Set or reset the {@link BufferBinding} for a given {@link LightsType | type of light}. * @param lightsType - {@link LightsType | Type of light} for which to create the {@link BufferBinding}. */ setLightsTypeBinding(lightsType) { const structParams = Object.keys(this.lightsBindingParams[lightsType].params).map((paramKey) => { return { key: paramKey, type: this.lightsBindingParams[lightsType].params[paramKey].type, size: this.lightsBindingParams[lightsType].params[paramKey].size }; }).reduce((acc, binding) => { acc[binding.key] = { type: binding.type, value: new Float32Array(Math.max(this.lightsBindingParams[lightsType].max, 1) * binding.size) }; return acc; }, {}); this.bindings[lightsType] = new BufferBinding({ label: this.lightsBindingParams[lightsType].label, name: lightsType, bindingType: "storage", visibility: ["fragment", "compute"], struct: { count: { type: "i32", value: 0 }, ...structParams } }); } /** * Called when a {@link LightsType | type of light} has overflown its maximum capacity. Destroys the associated {@link BufferBinding} (and eventually the associated shadow {@link BufferBinding}), recreates the {@link cameraLightsBindGroup | camera, lights and shadows bind group} and reset all lights for this {@link LightsType | type of light}. * @param lightsType - {@link LightsType | Type of light} that has overflown its maximum capacity. * @param lightIndex - The {@link Light#index | light index} that caused overflow. Will be used to reset the new max light count. */ onMaxLightOverflow(lightsType, lightIndex = 0) { if (!this.options.lights) { if (!this.production) throwWarning(`${this.options.label} (${this.type}): You are adding a light (${lightsType}) to a renderer that should not initially handle lights. The renderer bind group will be re-created. This should be avoided.`); this.#initLights(); this.cameraLightsBindGroup.destroy(); this.setCameraLightsBindGroup(); } else { if (!this.production) throwWarning(`${this.options.label} (${this.type}): You are overflowing the current max lights count of '${this.lightsBindingParams[lightsType].max}' for this type of lights: ${lightsType}. This should be avoided by setting a larger ${"max" + lightsType.charAt(0).toUpperCase() + lightsType.slice(1)} when instancing your ${this.type}.`); this.lightsBindingParams[lightsType].max = Math.max(this.lightsBindingParams[lightsType].max, lightIndex + 1); const oldLightBinding = this.cameraLightsBindGroup.getBindingByName(lightsType); if (oldLightBinding) this.cameraLightsBindGroup.destroyBufferBinding(oldLightBinding); this.setLightsTypeBinding(lightsType); const lightBindingIndex = this.cameraLightsBindGroup.bindings.findIndex((binding) => binding.name === lightsType); if (lightBindingIndex !== -1) this.cameraLightsBindGroup.bindings[lightBindingIndex] = this.bindings[lightsType]; else { this.bindings[lightsType].shouldResetBindGroup = true; this.bindings[lightsType].shouldResetBindGroupLayout = true; this.cameraLightsBindGroup.addBinding(this.bindings[lightsType]); this.shouldUpdateCameraLightsBindGroup(); } if (lightsType === "directionalLights" || lightsType === "pointLights" || lightsType === "spotLights") { const shadowsType = lightsType.replace("Lights", "") + "Shadows"; const oldShadowsBinding = this.cameraLightsBindGroup.getBindingByName(shadowsType); if (oldShadowsBinding) this.cameraLightsBindGroup.destroyBufferBinding(oldShadowsBinding); this.setShadowsTypeBinding(lightsType); const shadowsBindingIndex = this.cameraLightsBindGroup.bindings.findIndex((binding) => binding.name === shadowsType); if (shadowsBindingIndex !== -1) this.cameraLightsBindGroup.bindings[shadowsBindingIndex] = this.bindings[shadowsType]; else { this.bindings[shadowsType].shouldResetBindGroup = true; this.bindings[shadowsType].shouldResetBindGroupLayout = true; this.cameraLightsBindGroup.addBinding(this.bindings[shadowsType]); this.shouldUpdateCameraLightsBindGroup(); } } this.cameraLightsBindGroup.resetEntries(); this.cameraLightsBindGroup.createBindGroup(); this.lights.forEach((light) => { if (light.type === lightsType) light.reset(); }); } } /** * Get all the current {@link ShadowCastingLights | lights that can cast shadows}. * @returns - All {@link ShadowCastingLights | lights that can cast shadows}. */ get shadowCastingLights() { return this.lights?.filter((light) => light.type === "directionalLights" || light.type === "pointLights" || light.type === "spotLights"); } /** * Set the shadows {@link BufferBinding} based on the {@link shadowsBindingsStruct}. */ setShadowsBinding() { this.shadowsBindingsStruct = { directional: directionalShadowStruct, point: pointShadowStruct, spot: spotShadowStruct }; this.setShadowsTypeBinding("directionalLights"); this.setShadowsTypeBinding("pointLights"); this.setShadowsTypeBinding("spotLights"); } /** * Set or reset the associated shadow {@link BufferBinding} for a given {@link LightsType | type of light}. * @param lightsType - {@link LightsType | Type of light} for which to create the associated shadow {@link BufferBinding}. */ setShadowsTypeBinding(lightsType) { const type = lightsType.replace("Lights", ""); const shadowsType = type + "Shadows"; const struct = this.shadowsBindingsStruct[type]; const label = type.charAt(0).toUpperCase() + type.slice(1) + " shadows"; this.bindings[shadowsType] = new BufferBinding({ label, name: shadowsType, bindingType: this.options.lights && (this.options.lights.useUniformsForShadows || this.device?.limits.maxStorageBuffersInVertexStage === 0) ? "uniform" : "storage", visibility: [ "vertex", "fragment", "compute" ], childrenBindings: [{ binding: new BufferBinding({ label: label + " element", name: shadowsType + "Elements", bindingType: "uniform", visibility: ["vertex", "fragment"], struct }), count: Math.max(1, this.lightsBindingParams[lightsType].max), forceArray: true }] }); } /** * Set the {@link cameraLightsBindGroup | camera, lights and shadows bind group}. */ setCameraLightsBindGroup() { this.cameraLightsBindGroup = new BindGroup(this, { label: this.options.label + ": Camera and lights uniform bind group", bindings: Object.keys(this.bindings).map((bindingName) => this.bindings[bindingName]).flat() }); this.cameraLightsBindGroup.consumers.add(this.uuid); this.pointShadowsCubeFaceBindGroups = []; for (let face = 0; face < 6; face++) { const cubeFace = new BufferBinding({ label: "Cube face", name: "cubeFace", bindingType: "uniform", visibility: ["vertex"], struct: { face: { type: "u32", value: face } } }); const cubeBindGroup = new BindGroup(this, { label: `Cube face bind group ${face}`, bindings: [cubeFace] }); cubeBindGroup.createBindGroup(); cubeBindGroup.consumers.add(this.uuid); this.pointShadowsCubeFaceBindGroups.push(cubeBindGroup); } if (this.device) this.createCameraLightsBindGroup(); } /** * Create the {@link cameraLightsBindGroup | camera, lights and shadows bind group} buffers */ createCameraLightsBindGroup() { if (this.cameraLightsBindGroup && this.cameraLightsBindGroup.shouldCreateBindGroup) { this.cameraLightsBindGroup.setIndex(0); this.cameraLightsBindGroup.createBindGroup(); } } /** * Tell our {@link cameraLightsBindGroup | camera, lights and shadows bind group} to update. */ shouldUpdateCameraLightsBindGroup() { this.#shouldUpdateCameraLightsBindGroup = true; } /** * Tell our {@link GPUCameraRenderer#bindings.camera | camera buffer binding} that we should update its bindings and update the bind group. Called each time the camera matrices change. */ updateCameraBindings() { this.bindings.camera?.shouldUpdateBinding("view"); this.bindings.camera?.shouldUpdateBinding("projection"); this.bindings.camera?.shouldUpdateBinding("position"); this.shouldUpdateCameraLightsBindGroup(); } /** * Update the {@link cameraLightsBindGroup | camera and lights BindGroup}. */ updateCameraLightsBindGroup() { if (this.cameraLightsBindGroup && this.#shouldUpdateCameraLightsBindGroup) { this.cameraLightsBindGroup.update(); this.#shouldUpdateCameraLightsBindGroup = false; } } /** * Get all objects ({@link core/renderers/GPURenderer.RenderedMesh | rendered meshes} or {@link core/computePasses/ComputePass.ComputePass | compute passes}) using a given {@link AllowedBindGroups | bind group}, including {@link cameraLightsBindGroup | camera and lights bind group}. * Useful to know if a resource is used by multiple objects and if it is safe to destroy it or not. * @param bindGroup - {@link AllowedBindGroups | bind group} to check */ getObjectsByBindGroup(bindGroup) { return this.deviceRenderedObjects.filter((object) => { return [ ...object.material.bindGroups, ...object.material.inputsBindGroups, ...object.material.clonedBindGroups, this.cameraLightsBindGroup ].some((bG) => bG.uuid === bindGroup.uuid); }); } /** * Create the {@link transmissionTarget} {@link Texture} and {@link RenderPassEntry} if not already created. */ createTransmissionTarget() { if (!this.transmissionTarget.texture) { this.transmissionTarget.passEntry = this.scene.createScreenPassEntry("Transmission scene screen render pass"); this.transmissionTarget.texture = new Texture(this, { label: "Transmission background scene render target output", name: "transmissionBackgroundTexture", generateMips: true, format: this.options.context.format, autoDestroy: false }); this.transmissionTarget.passEntry.onBeforeRenderPass = (commandEncoder, swapChainTexture) => { this.copyGPUTextureToTexture(swapChainTexture, this.transmissionTarget.texture, commandEncoder); }; const mainPassEntry = this.scene.getRenderTargetPassEntry(); this.transmissionTarget.passEntry.stack.projected.transparent = mainPassEntry.stack.projected.transparent; mainPassEntry.stack.projected.transparent = []; } } /** * Destroy the {@link transmissionTarget} {@link Texture} and {@link RenderPassEntry} if already created. */ destroyTransmissionTarget() { if (this.transmissionTarget.texture) { this.transmissionTarget.texture.destroy(); this.scene.renderPassEntries.screen = this.scene.renderPassEntries.screen.filter((passEntry) => passEntry.label !== "Transmission scene screen render pass"); this.transmissionTarget.texture = null; this.transmissionTarget.passEntry = null; } } /** * Resize our {@link GPUCameraRenderer} and resize our {@link camera} before anything else. * @param rectBBox - the optional new {@link canvas} {@link RectBBox} to set */ resize(rectBBox = null) { this.setSize(rectBBox); this.resizeCamera(); this._onResizeCallback && this._onResizeCallback(); this.resizeObjects(); this._onAfterResizeCallback && this._onAfterResizeCallback(); } /** * {@link createCameraLightsBindGroup | Set the camera bind group if needed} and then call our {@link GPURenderer#render | GPURenderer render method}. * @param commandEncoder - Current {@link GPUCommandEncoder}. */ render(commandEncoder) { if (!this.ready) return; this.createCameraLightsBindGroup(); this.updateCameraLightsBindGroup(); super.render(commandEncoder); if (this.cameraLightsBindGroup) this.cameraLightsBindGroup.needsPipelineFlush = false; } /** * Destroy our {@link GPUCameraRenderer} */ destroy() { this.cameraLightsBindGroup?.destroy(); this.pointShadowsCubeFaceBindGroups.forEach((bindGroup) => bindGroup.destroy()); this.destroyTransmissionTarget(); this.lights.forEach((light) => light.destroy()); super.destroy(); this.lights.forEach((light) => this.removeLight(light)); } }; //#endregion export { GPUCameraRenderer };