UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

922 lines (921 loc) 36.1 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Debug, DebugHelper } from "../../core/debug.js"; import { now } from "../../core/time.js"; import { BlueNoise } from "../../core/math/blue-noise.js"; import { Vec2 } from "../../core/math/vec2.js"; import { Vec3 } from "../../core/math/vec3.js"; import { Vec4 } from "../../core/math/vec4.js"; import { Mat3 } from "../../core/math/mat3.js"; import { Mat4 } from "../../core/math/mat4.js"; import { BoundingSphere } from "../../core/shape/bounding-sphere.js"; import { Frustum } from "../../core/shape/frustum.js"; import { CLEARFLAG_COLOR, CLEARFLAG_DEPTH, CLEARFLAG_STENCIL, BINDGROUP_MESH, BINDGROUP_VIEW, UNIFORM_BUFFER_DEFAULT_SLOT_NAME, UNIFORMTYPE_MAT4, UNIFORMTYPE_MAT3, UNIFORMTYPE_VEC4, UNIFORMTYPE_VEC3, UNIFORMTYPE_IVEC3, UNIFORMTYPE_VEC2, UNIFORMTYPE_FLOAT, UNIFORMTYPE_INT, SHADERSTAGE_VERTEX, SHADERSTAGE_FRAGMENT, CULLFACE_NONE, BINDGROUP_MESH_UB, FRONTFACE_CCW, FRONTFACE_CW } from "../../platform/graphics/constants.js"; import { DebugGraphics } from "../../platform/graphics/debug-graphics.js"; import { UniformBuffer } from "../../platform/graphics/uniform-buffer.js"; import { BindGroup, DynamicBindGroup } from "../../platform/graphics/bind-group.js"; import { UniformFormat, UniformBufferFormat } from "../../platform/graphics/uniform-buffer-format.js"; import { BindGroupFormat, BindUniformBufferFormat } from "../../platform/graphics/bind-group-format.js"; import { VIEW_CENTER, LIGHTTYPE_DIRECTIONAL, MASK_AFFECT_DYNAMIC, MASK_AFFECT_LIGHTMAPPED, MASK_BAKE, SHADOWUPDATE_NONE, SHADOWUPDATE_THISFRAME, EVENT_PRECULL, EVENT_POSTCULL, EVENT_CULL_END } from "../constants.js"; import { LightCube } from "../graphics/light-cube.js"; import { getBlueNoiseTexture } from "../graphics/noise-textures.js"; import { LightTextureAtlas } from "../lighting/light-texture-atlas.js"; import { Material } from "../materials/material.js"; import { ShadowMapCache } from "./shadow-map-cache.js"; import { ShadowRendererLocal } from "./shadow-renderer-local.js"; import { ShadowRendererDirectional } from "./shadow-renderer-directional.js"; import { ShadowRenderer } from "./shadow-renderer.js"; import { WorldClustersAllocator } from "./world-clusters-allocator.js"; import { FramePassUpdateClustered } from "./frame-pass-update-clustered.js"; import { Camera } from "../camera.js"; let _skinUpdateIndex = 0; const viewProjMat = new Mat4(); const viewInvMat = new Mat4(); const viewMat = new Mat4(); const viewMat3 = new Mat3(); const tempSphere = new BoundingSphere(); const tempFrustum = new Frustum(); const _tempLightSet = /* @__PURE__ */ new Set(); const _tempLayerSet = /* @__PURE__ */ new Set(); const _dynamicBindGroup = new DynamicBindGroup(); const _tempClearColor = [0, 0, 0, 1]; const _tempClearOptions = { color: _tempClearColor, depth: 1, stencil: 0, flags: 0 }; const _haltonSequence = [ new Vec2(0.5, 0.333333), new Vec2(0.25, 0.666667), new Vec2(0.75, 0.111111), new Vec2(0.125, 0.444444), new Vec2(0.625, 0.777778), new Vec2(0.375, 0.222222), new Vec2(0.875, 0.555556), new Vec2(0.0625, 0.888889), new Vec2(0.5625, 0.037037), new Vec2(0.3125, 0.37037), new Vec2(0.8125, 0.703704), new Vec2(0.1875, 0.148148), new Vec2(0.6875, 0.481481), new Vec2(0.4375, 0.814815), new Vec2(0.9375, 0.259259), new Vec2(0.03125, 0.592593) ]; const _tempProjMat0 = new Mat4(); const _tempProjMat1 = new Mat4(); const _tempProjMat4 = new Mat4(); const _tempProjMat5 = new Mat4(); const _tempSet = /* @__PURE__ */ new Set(); const _tempMeshInstances = []; const _tempMeshInstancesSkinned = []; class Renderer { /** * Create a new instance. * * @param {GraphicsDevice} graphicsDevice - The graphics device used by the renderer. * @param {Scene} scene - The scene. */ constructor(graphicsDevice, scene) { /** @type {boolean} */ __publicField(this, "clustersDebugRendered", false); /** @type {Scene} */ __publicField(this, "scene"); /** * A set of visible mesh instances which need further processing before being rendered, e.g. * skinning or morphing. Extracted during culling. * * @type {Set<MeshInstance>} * @protected */ __publicField(this, "processingMeshInstances", /* @__PURE__ */ new Set()); /** * @type {WorldClustersAllocator} * @ignore */ __publicField(this, "worldClustersAllocator"); /** * A list of all unique lights in the layer composition. * * @type {Light[]} */ __publicField(this, "lights", []); /** * A list of all unique local lights (spot & omni) in the layer composition. * * @type {Light[]} */ __publicField(this, "localLights", []); /** * A list of unique directional shadow casting lights for each enabled camera. This is generated * each frame during light culling. * * @type {Map<Camera, Array<Light>>} */ __publicField(this, "cameraDirShadowLights", /* @__PURE__ */ new Map()); /** * A mapping of a directional light to a camera, for which the shadow is currently valid. This * is cleared each frame, and updated each time a directional light shadow is rendered for a * camera, and allows us to manually schedule shadow passes when a new camera needs a shadow. * * @type {Map<Light, Camera>} */ __publicField(this, "dirLightShadows", /* @__PURE__ */ new Map()); __publicField(this, "blueNoise", new BlueNoise(123)); /** * A gsplat director for unified splat rendering. * * @type {GSplatDirector|null} */ __publicField(this, "gsplatDirector", null); this.device = graphicsDevice; this.scene = scene; this.worldClustersAllocator = new WorldClustersAllocator(graphicsDevice); this.lightTextureAtlas = new LightTextureAtlas(graphicsDevice); this.shadowMapCache = new ShadowMapCache(); this.shadowRenderer = new ShadowRenderer(this, this.lightTextureAtlas); this._shadowRendererLocal = new ShadowRendererLocal(this, this.shadowRenderer); this._shadowRendererDirectional = new ShadowRendererDirectional(this, this.shadowRenderer); if (this.scene.clusteredLightingEnabled) { this._renderPassUpdateClustered = new FramePassUpdateClustered( this.device, this, this.shadowRenderer, this._shadowRendererLocal, this.lightTextureAtlas ); } this.viewUniformFormat = null; this.viewBindGroupFormat = null; this._skinTime = 0; this._morphTime = 0; this._cullTime = 0; this._shadowMapTime = 0; this._lightClustersTime = 0; this._layerCompositionUpdateTime = 0; this._shadowDrawCalls = 0; this._skinDrawCalls = 0; this._instancedDrawCalls = 0; this._shadowMapUpdates = 0; this._numDrawCallsCulled = 0; this._camerasRendered = 0; this._lightClusters = 0; this._gsplatCount = 0; const scope = graphicsDevice.scope; this.boneTextureId = scope.resolve("texture_poseMap"); this.modelMatrixId = scope.resolve("matrix_model"); this.normalMatrixId = scope.resolve("matrix_normal"); this.viewInvId = scope.resolve("matrix_viewInverse"); this.viewPos = new Float32Array(3); this.viewPosId = scope.resolve("view_position"); this.projId = scope.resolve("matrix_projection"); this.projSkyboxId = scope.resolve("matrix_projectionSkybox"); this.viewId = scope.resolve("matrix_view"); this.viewId3 = scope.resolve("matrix_view3"); this.viewProjId = scope.resolve("matrix_viewProjection"); this.flipYId = scope.resolve("projectionFlipY"); this.tbnBasis = scope.resolve("tbnBasis"); this.cameraParams = new Float32Array(4); this.cameraParamsId = scope.resolve("camera_params"); this.viewportSize = new Float32Array(4); this.viewportSizeId = scope.resolve("viewport_size"); this.viewIndexId = scope.resolve("view_index"); this.viewIndexId.setValue(0); this.blueNoiseJitterVersion = 0; this.blueNoiseJitterVec = new Vec4(); this.blueNoiseJitterData = new Float32Array(4); this.blueNoiseJitterId = scope.resolve("blueNoiseJitter"); this.blueNoiseTextureId = scope.resolve("blueNoiseTex32"); this.alphaTestId = scope.resolve("alpha_ref"); this.opacityMapId = scope.resolve("texture_opacityMap"); this.exposureId = scope.resolve("exposure"); this.morphPositionTex = scope.resolve("morphPositionTex"); this.morphNormalTex = scope.resolve("morphNormalTex"); this.morphTexParams = scope.resolve("morph_tex_params"); this.lightCube = new LightCube(); this.constantLightCube = scope.resolve("lightCube[0]"); } destroy() { this.shadowRenderer = null; this._shadowRendererLocal = null; this._shadowRendererDirectional = null; this.shadowMapCache.destroy(); this.shadowMapCache = null; this._renderPassUpdateClustered?.destroy(); this._renderPassUpdateClustered = null; this.lightTextureAtlas.destroy(); this.lightTextureAtlas = null; this.gsplatDirector?.destroy(); this.gsplatDirector = null; } /** * Set up the viewport and the scissor for camera rendering. * * @param {Camera} camera - The camera containing the viewport information. * @param {RenderTarget} [renderTarget] - The render target. NULL for the default one. */ setupViewport(camera, renderTarget) { const device = this.device; const pixelWidth = renderTarget ? renderTarget.width : device.width; const pixelHeight = renderTarget ? renderTarget.height : device.height; const rect = camera.rect; let x = Math.floor(rect.x * pixelWidth); let y = Math.floor(rect.y * pixelHeight); let w = Math.floor(rect.z * pixelWidth); let h = Math.floor(rect.w * pixelHeight); device.setViewport(x, y, w, h); if (camera._scissorRectClear) { const scissorRect = camera.scissorRect; x = Math.floor(scissorRect.x * pixelWidth); y = Math.floor(scissorRect.y * pixelHeight); w = Math.floor(scissorRect.z * pixelWidth); h = Math.floor(scissorRect.w * pixelHeight); } device.setScissor(x, y, w, h); } setCameraUniforms(camera, target) { const flipY = target?.flipY; let viewList = null; if (camera.xr && camera.xr.session) { const transform = camera._node?.parent?.getWorldTransform() || null; const views = camera.xr.views; viewList = views.list; for (let v = 0; v < viewList.length; v++) { const view = viewList[v]; view.updateTransforms(transform); } } else { let projMat = camera.projectionMatrix; if (camera.calculateProjection) { camera.calculateProjection(projMat, VIEW_CENTER); } let projMatSkybox = camera.getProjectionMatrixSkybox(); const webgpu = this.device.isWebGPU; projMat = Camera.applyShaderProjectionTransform(projMat, _tempProjMat0, flipY, webgpu); projMatSkybox = Camera.applyShaderProjectionTransform(projMatSkybox, _tempProjMat1, flipY, webgpu); const { jitter } = camera; let jitterX = 0; let jitterY = 0; if (jitter > 0) { const targetWidth = target ? target.width : this.device.width; const targetHeight = target ? target.height : this.device.height; const offset = _haltonSequence[this.device.renderVersion % _haltonSequence.length]; jitterX = jitter * (offset.x * 2 - 1) / targetWidth; jitterY = jitter * (offset.y * 2 - 1) / targetHeight; projMat = _tempProjMat4.copy(projMat); projMat.data[8] = jitterX; projMat.data[9] = jitterY; projMatSkybox = _tempProjMat5.copy(projMatSkybox); projMatSkybox.data[8] = jitterX; projMatSkybox.data[9] = jitterY; if (this.blueNoiseJitterVersion !== this.device.renderVersion) { this.blueNoiseJitterVersion = this.device.renderVersion; this.blueNoise.vec4(this.blueNoiseJitterVec); } } const jitterVec = jitter > 0 ? this.blueNoiseJitterVec : Vec4.ZERO; this.blueNoiseJitterData[0] = jitterVec.x; this.blueNoiseJitterData[1] = jitterVec.y; this.blueNoiseJitterData[2] = jitterVec.z; this.blueNoiseJitterData[3] = jitterVec.w; this.blueNoiseJitterId.setValue(this.blueNoiseJitterData); this.projId.setValue(projMat.data); this.projSkyboxId.setValue(projMatSkybox.data); if (camera.calculateTransform) { camera.calculateTransform(viewInvMat, VIEW_CENTER); } else { const pos = camera._node.getPosition(); const rot = camera._node.getRotation(); viewInvMat.setTRS(pos, rot, Vec3.ONE); } this.viewInvId.setValue(viewInvMat.data); viewMat.copy(viewInvMat).invert(); this.viewId.setValue(viewMat.data); viewMat3.setFromMat4(viewMat); this.viewId3.setValue(viewMat3.data); viewProjMat.mul2(projMat, viewMat); this.viewProjId.setValue(viewProjMat.data); camera._storeShaderMatrices(viewProjMat, jitterX, jitterY, this.device.renderVersion); this.flipYId.setValue(flipY ? -1 : 1); this.dispatchViewPos(camera._node.getPosition()); camera.frustum.setFromMat4(viewProjMat); } this.tbnBasis.setValue(flipY ? -1 : 1); this.cameraParamsId.setValue(camera.fillShaderParams(this.cameraParams)); const xrView = camera.xr?.session ? camera.xr.views.list[0] : null; let viewportWidth = xrView ? xrView.viewport.z : target ? target.width : this.device.width; let viewportHeight = xrView ? xrView.viewport.w : target ? target.height : this.device.height; viewportWidth *= camera.rect.z; viewportHeight *= camera.rect.w; this.viewportSize[0] = viewportWidth; this.viewportSize[1] = viewportHeight; this.viewportSize[2] = 1 / viewportWidth; this.viewportSize[3] = 1 / viewportHeight; this.viewportSizeId.setValue(this.viewportSize); this.exposureId.setValue(this.scene.physicalUnits ? camera.getExposure() : this.scene.exposure); return viewList; } /** * Clears the active render target. If the viewport is already set up, only its area is cleared. * * @param {Camera} camera - The camera supplying the value to clear to. * @param {boolean} [clearColor] - True if the color buffer should be cleared. Uses the value * from the camera if not supplied. * @param {boolean} [clearDepth] - True if the depth buffer should be cleared. Uses the value * from the camera if not supplied. * @param {boolean} [clearStencil] - True if the stencil buffer should be cleared. Uses the * value from the camera if not supplied. */ clear(camera, clearColor, clearDepth, clearStencil) { const flags = (clearColor ?? camera._clearColorBuffer ? CLEARFLAG_COLOR : 0) | (clearDepth ?? camera._clearDepthBuffer ? CLEARFLAG_DEPTH : 0) | (clearStencil ?? camera._clearStencilBuffer ? CLEARFLAG_STENCIL : 0); if (flags) { const device = this.device; DebugGraphics.pushGpuMarker(device, "CLEAR"); const c = camera._clearColor; _tempClearColor[0] = c.r; _tempClearColor[1] = c.g; _tempClearColor[2] = c.b; _tempClearColor[3] = c.a; _tempClearOptions.depth = camera._clearDepth; _tempClearOptions.stencil = camera._clearStencil; _tempClearOptions.flags = flags; device.clear(_tempClearOptions); DebugGraphics.popGpuMarker(device); } } setupCullModeAndFrontFace(cullFaces, flipFactor, drawCall) { const material = drawCall.material; const flipFaces = flipFactor * drawCall.flipFacesFactor * drawCall.node.worldScaleSign; let frontFace = material.frontFace; if (flipFaces < 0) { frontFace = frontFace === FRONTFACE_CCW ? FRONTFACE_CW : FRONTFACE_CCW; } this.device.setCullMode(cullFaces ? material.cull : CULLFACE_NONE); this.device.setFrontFace(frontFace); } setupCullMode(cullFaces, flipFactor, drawCall) { Debug.deprecated("pc.Renderer.setupCullMode is deprecated. Use 'pc.Renderer.setupCullModeAndFrontFace(cullFaces, flipFactor, drawCall);' format instead."); this.setupCullModeAndFrontFace(cullFaces, flipFactor, drawCall); } updateCameraFrustum(camera) { if (camera.xr && camera.xr.views.list.length) { const views = camera.xr.views.list; viewProjMat.mul2(views[0].projMat, views[0].viewOffMat); camera.frustum.setFromMat4(viewProjMat); for (let v = 1; v < views.length; v++) { viewProjMat.mul2(views[v].projMat, views[v].viewOffMat); tempFrustum.setFromMat4(viewProjMat); camera.frustum.add(tempFrustum); } return; } const projMat = camera.projectionMatrix; if (camera.calculateProjection) { camera.calculateProjection(projMat, VIEW_CENTER); } if (camera.calculateTransform) { camera.calculateTransform(viewInvMat, VIEW_CENTER); } else { const pos = camera._node.getPosition(); const rot = camera._node.getRotation(); viewInvMat.setTRS(pos, rot, Vec3.ONE); this.viewInvId.setValue(viewInvMat.data); } viewMat.copy(viewInvMat).invert(); viewProjMat.mul2(projMat, viewMat); camera.frustum.setFromMat4(viewProjMat); } setBaseConstants(device, material) { device.setCullMode(material.cull); device.setFrontFace(material.frontFace); if (material.opacityMap) { this.opacityMapId.setValue(material.opacityMap); } if (material.opacityMap || material.alphaTest > 0) { this.alphaTestId.setValue(material.alphaTest); } } updateCpuSkinMatrices(drawCalls) { _skinUpdateIndex++; const drawCallsCount = drawCalls.length; if (drawCallsCount === 0) return; const skinTime = now(); for (let i = 0; i < drawCallsCount; i++) { const si = drawCalls[i].skinInstance; if (si) { si.updateMatrices(drawCalls[i].node, _skinUpdateIndex); si._dirty = true; } } this._skinTime += now() - skinTime; } /** * Update skin matrices ahead of rendering. * * @param {MeshInstance[]|Set<MeshInstance>} drawCalls - MeshInstances containing skinInstance. * @ignore */ updateGpuSkinMatrices(drawCalls) { const skinTime = now(); for (const drawCall of drawCalls) { const skin = drawCall.skinInstance; if (skin && skin._dirty) { skin.updateMatrixPalette(drawCall.node, _skinUpdateIndex); skin._dirty = false; } } this._skinTime += now() - skinTime; } /** * Update morphing ahead of rendering. * * @param {MeshInstance[]|Set<MeshInstance>} drawCalls - MeshInstances containing morphInstance. * @ignore */ updateMorphing(drawCalls) { const morphTime = now(); for (const drawCall of drawCalls) { const morphInst = drawCall.morphInstance; if (morphInst && morphInst._dirty) { morphInst.update(); } } this._morphTime += now() - morphTime; } /** * Update gsplats ahead of rendering. * * @param {MeshInstance[]|Set<MeshInstance>} drawCalls - MeshInstances containing gsplatInstances. * @ignore */ updateGSplats(drawCalls) { for (const drawCall of drawCalls) { drawCall.gsplatInstance?.update(); } } /** * Update draw calls ahead of rendering. * * @param {MeshInstance[]|Set<MeshInstance>} drawCalls - MeshInstances requiring updates. * @ignore */ gpuUpdate(drawCalls) { this.updateGpuSkinMatrices(drawCalls); this.updateMorphing(drawCalls); this.updateGSplats(drawCalls); } setVertexBuffers(device, mesh) { device.setVertexBuffer(mesh.vertexBuffer); } setMorphing(device, morphInstance) { if (morphInstance) { morphInstance.prepareRendering(device); device.setVertexBuffer(morphInstance.morph.vertexBufferIds); this.morphPositionTex.setValue(morphInstance.texturePositions); this.morphNormalTex.setValue(morphInstance.textureNormals); this.morphTexParams.setValue(morphInstance._textureParams); } } setSkinning(device, meshInstance) { const skinInstance = meshInstance.skinInstance; if (skinInstance) { this._skinDrawCalls++; const boneTexture = skinInstance.boneTexture; this.boneTextureId.setValue(boneTexture); } } // sets Vec3 camera position uniform dispatchViewPos(position) { const vp = this.viewPos; vp[0] = position.x; vp[1] = position.y; vp[2] = position.z; this.viewPosId.setValue(vp); } initViewBindGroupFormat(isClustered) { if (this.device.supportsUniformBuffers && !this.viewUniformFormat) { const uniforms = [ new UniformFormat("matrix_view", UNIFORMTYPE_MAT4), new UniformFormat("matrix_viewInverse", UNIFORMTYPE_MAT4), new UniformFormat("matrix_projection", UNIFORMTYPE_MAT4), new UniformFormat("matrix_projectionSkybox", UNIFORMTYPE_MAT4), new UniformFormat("matrix_viewProjection", UNIFORMTYPE_MAT4), new UniformFormat("matrix_view3", UNIFORMTYPE_MAT3), new UniformFormat("cubeMapRotationMatrix", UNIFORMTYPE_MAT3), new UniformFormat("view_position", UNIFORMTYPE_VEC3), new UniformFormat("viewport_size", UNIFORMTYPE_VEC4), new UniformFormat("skyboxIntensity", UNIFORMTYPE_FLOAT), new UniformFormat("exposure", UNIFORMTYPE_FLOAT), new UniformFormat("textureBias", UNIFORMTYPE_FLOAT), new UniformFormat("view_index", UNIFORMTYPE_FLOAT) ]; if (isClustered) { uniforms.push(...[ new UniformFormat("clusterCellsCountByBoundsSize", UNIFORMTYPE_VEC3), new UniformFormat("clusterBoundsMin", UNIFORMTYPE_VEC3), new UniformFormat("clusterBoundsDelta", UNIFORMTYPE_VEC3), new UniformFormat("clusterCellsDot", UNIFORMTYPE_IVEC3), new UniformFormat("clusterCellsMax", UNIFORMTYPE_IVEC3), new UniformFormat("shadowAtlasParams", UNIFORMTYPE_VEC2), new UniformFormat("clusterMaxCells", UNIFORMTYPE_INT), new UniformFormat("numClusteredLights", UNIFORMTYPE_INT), new UniformFormat("clusterTextureWidth", UNIFORMTYPE_INT) ]); } this.viewUniformFormat = new UniformBufferFormat(this.device, uniforms); const formats = [ // uniform buffer needs to be first, as the shader processor assumes slot 0 for it new BindUniformBufferFormat(UNIFORM_BUFFER_DEFAULT_SLOT_NAME, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT) // disable view level textures, as they consume texture slots. They get automatically added to mesh bind group // for the meshes that uses them // new BindTextureFormat('lightsTexture', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_UNFILTERABLE_FLOAT), // new BindTextureFormat('shadowAtlasTexture', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_DEPTH), // new BindTextureFormat('cookieAtlasTexture', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_FLOAT), // new BindTextureFormat('areaLightsLutTex1', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_FLOAT), // new BindTextureFormat('areaLightsLutTex2', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_FLOAT) ]; this.viewBindGroupFormat = new BindGroupFormat(this.device, formats); } } /** * Set up uniforms for an XR view. */ setupViewUniforms(view, index) { this.projId.setValue(view.projMat.data); this.projSkyboxId.setValue(view.projMat.data); this.viewId.setValue(view.viewOffMat.data); this.viewInvId.setValue(view.viewInvOffMat.data); this.viewId3.setValue(view.viewMat3.data); this.viewProjId.setValue(view.projViewOffMat.data); this.viewPosId.setValue(view.positionData); this.viewIndexId.setValue(index); } setupViewUniformBuffers(viewBindGroups, viewUniformFormat, viewBindGroupFormat, viewList) { Debug.assert(Array.isArray(viewBindGroups), "viewBindGroups must be an array"); const { device } = this; const viewCount = viewList?.length ?? 1; while (viewBindGroups.length < viewCount) { const ub = new UniformBuffer(device, viewUniformFormat, false); const bg = new BindGroup(device, viewBindGroupFormat, ub); DebugHelper.setName(bg, `ViewBindGroup_${bg.id}`); viewBindGroups.push(bg); } if (viewList) { for (let i = 0; i < viewCount; i++) { const view = viewList[i]; this.setupViewUniforms(view, i); const viewBindGroup = viewBindGroups[i]; viewBindGroup.defaultUniformBuffer.update(); viewBindGroup.update(); } } else { const viewBindGroup = viewBindGroups[0]; viewBindGroup.defaultUniformBuffer.update(); viewBindGroup.update(); } if (!viewList) { device.setBindGroup(BINDGROUP_VIEW, viewBindGroups[0]); } } setupMeshUniformBuffers(shaderInstance) { const device = this.device; if (device.supportsUniformBuffers) { const meshBindGroup = shaderInstance.getBindGroup(device); meshBindGroup.update(); device.setBindGroup(BINDGROUP_MESH, meshBindGroup); const meshUniformBuffer = shaderInstance.getUniformBuffer(device); meshUniformBuffer.update(_dynamicBindGroup); device.setBindGroup(BINDGROUP_MESH_UB, _dynamicBindGroup.bindGroup, _dynamicBindGroup.offsets); } } setMeshInstanceMatrices(meshInstance, setNormalMatrix = false) { const modelMatrix = meshInstance.node.worldTransform; this.modelMatrixId.setValue(modelMatrix.data); if (setNormalMatrix) { this.normalMatrixId.setValue(meshInstance.node.normalMatrix.data); } } /** * @param {Camera} camera - The camera used for culling. * @param {MeshInstance[]} drawCalls - Draw calls to cull. * @param {CulledInstances} culledInstances - Stores culled instances. */ cull(camera, drawCalls, culledInstances) { const cullTime = now(); const opaque = culledInstances.opaque; opaque.length = 0; const transparent = culledInstances.transparent; transparent.length = 0; const doCull = camera.frustumCulling; const count = drawCalls.length; for (let i = 0; i < count; i++) { const drawCall = drawCalls[i]; if (drawCall.visible) { const visible = !doCull || !drawCall.cull || drawCall._isVisible(camera); if (visible) { drawCall.visibleThisFrame = true; const bucket = drawCall.transparent ? transparent : opaque; bucket.push(drawCall); if (drawCall.skinInstance || drawCall.morphInstance || drawCall.gsplatInstance) { this.processingMeshInstances.add(drawCall); if (drawCall.gsplatInstance) { drawCall.gsplatInstance.cameras.push(camera); } } } } } this._cullTime += now() - cullTime; this._numDrawCallsCulled += doCull ? count : 0; } collectLights(comp) { this.lights.length = 0; this.localLights.length = 0; const stats = this.scene._stats; stats.dynamicLights = 0; stats.bakedLights = 0; const count = comp.layerList.length; for (let i = 0; i < count; i++) { const layer = comp.layerList[i]; if (!_tempLayerSet.has(layer)) { _tempLayerSet.add(layer); const lights = layer._lights; for (let j = 0; j < lights.length; j++) { const light = lights[j]; if (!_tempLightSet.has(light)) { _tempLightSet.add(light); this.lights.push(light); if (light._type !== LIGHTTYPE_DIRECTIONAL) { this.localLights.push(light); } if (light.mask & MASK_AFFECT_DYNAMIC || light.mask & MASK_AFFECT_LIGHTMAPPED) { stats.dynamicLights++; } if (light.mask & MASK_BAKE) { stats.bakedLights++; } } } } } stats.lights = this.lights.length; _tempLightSet.clear(); _tempLayerSet.clear(); } cullLights(camera, lights) { const clusteredLightingEnabled = this.scene.clusteredLightingEnabled; const physicalUnits = this.scene.physicalUnits; for (let i = 0; i < lights.length; i++) { const light = lights[i]; if (light.enabled) { if (light._type !== LIGHTTYPE_DIRECTIONAL) { light.getBoundingSphere(tempSphere); if (camera.frustum.containsSphere(tempSphere)) { light.visibleThisFrame = true; light.usePhysicalUnits = physicalUnits; const screenSize = camera.getScreenSize(tempSphere); light.maxScreenSize = Math.max(light.maxScreenSize, screenSize); } else { if (!clusteredLightingEnabled) { if (light.castShadows && !light.shadowMap) { light.visibleThisFrame = true; } } } } else { light.usePhysicalUnits = this.scene.physicalUnits; } } } } /** * Shadow map culling for directional and visible local lights visible meshInstances are * collected into light._renderData, and are marked as visible for directional lights also * shadow camera matrix is set up. * * @param {LayerComposition} comp - The layer composition. */ cullShadowmaps(comp) { const isClustered = this.scene.clusteredLightingEnabled; for (let i = 0; i < this.localLights.length; i++) { const light = this.localLights[i]; if (light._type !== LIGHTTYPE_DIRECTIONAL) { if (isClustered) { if (light.atlasSlotUpdated && light.shadowUpdateMode === SHADOWUPDATE_NONE) { light.shadowUpdateMode = SHADOWUPDATE_THISFRAME; } } else { if (light.shadowUpdateMode === SHADOWUPDATE_NONE && light.castShadows) { if (!light.getRenderData(null, 0).shadowCamera.renderTarget) { light.shadowUpdateMode = SHADOWUPDATE_THISFRAME; } } } if (light.visibleThisFrame && light.castShadows && light.shadowUpdateMode !== SHADOWUPDATE_NONE) { this._shadowRendererLocal.cull(light, comp); } } } this.cameraDirShadowLights.clear(); const cameras = comp.cameras; for (let i = 0; i < cameras.length; i++) { const cameraComponent = cameras[i]; if (cameraComponent.enabled) { const camera = cameraComponent.camera; let lightList; const cameraLayers = camera.layers; for (let l = 0; l < cameraLayers.length; l++) { const cameraLayer = comp.getLayerById(cameraLayers[l]); if (cameraLayer) { const layerDirLights = cameraLayer.splitLights[LIGHTTYPE_DIRECTIONAL]; for (let j = 0; j < layerDirLights.length; j++) { const light = layerDirLights[j]; if (light.castShadows && !_tempSet.has(light)) { _tempSet.add(light); lightList = lightList ?? []; lightList.push(light); this._shadowRendererDirectional.cull(light, comp, camera); } } } } if (lightList) { this.cameraDirShadowLights.set(camera, lightList); } _tempSet.clear(); } } } /** * visibility culling of lights, meshInstances, shadows casters. Also applies * `meshInstance.visible`. * * @param {LayerComposition} comp - The layer composition. */ cullComposition(comp) { const cullTime = now(); const { scene } = this; this.processingMeshInstances.clear(); const numCameras = comp.cameras.length; this._camerasRendered += numCameras; for (let i = 0; i < numCameras; i++) { const camera = comp.cameras[i]; const renderTarget = camera.renderTarget; camera.frameUpdate(renderTarget); this.updateCameraFrustum(camera.camera); scene?.fire(EVENT_PRECULL, camera); const layerIds = camera.layers; for (let j = 0; j < layerIds.length; j++) { const layer = comp.getLayerById(layerIds[j]); if (layer && layer.enabled) { this.cullLights(camera.camera, layer._lights); const culledInstances = layer.getCulledInstances(camera.camera); this.cull(camera.camera, layer.meshInstances, culledInstances); } } scene?.fire(EVENT_POSTCULL, camera); } if (scene.clusteredLightingEnabled) { this.updateLightTextureAtlas(); } this.cullShadowmaps(comp); scene?.fire(EVENT_CULL_END); this._cullTime += now() - cullTime; } /** * @param {MeshInstance[]} drawCalls - Mesh instances. * @param {boolean} onlyLitShaders - Limits the update to shaders affected by lighting. */ updateShaders(drawCalls, onlyLitShaders) { const count = drawCalls.length; for (let i = 0; i < count; i++) { const mat = drawCalls[i].material; if (mat) { if (!_tempSet.has(mat)) { _tempSet.add(mat); if (mat.getShaderVariant !== Material.prototype.getShaderVariant) { if (onlyLitShaders) { if (!mat.useLighting || mat.emitter && !mat.emitter.lighting) { continue; } } mat.clearVariants(); } } } } _tempSet.clear(); } updateFrameUniforms() { this.blueNoiseTextureId.setValue(getBlueNoiseTexture(this.device)); } /** * @param {LayerComposition} comp - The layer composition to update. */ beginFrame(comp) { const scene = this.scene; const updateShaders = scene.updateShaders || this.device._shadersDirty; let totalMeshInstances = 0; const layers = comp.layerList; const layerCount = layers.length; for (let i = 0; i < layerCount; i++) { const layer = layers[i]; const meshInstances = layer.meshInstances; const count = meshInstances.length; totalMeshInstances += count; for (let j = 0; j < count; j++) { const meshInst = meshInstances[j]; meshInst.visibleThisFrame = false; if (updateShaders) { _tempMeshInstances.push(meshInst); } if (meshInst.skinInstance) { _tempMeshInstancesSkinned.push(meshInst); } } } scene._stats.meshInstances = totalMeshInstances; if (updateShaders) { const onlyLitShaders = !scene.updateShaders || !this.device._shadersDirty; this.updateShaders(_tempMeshInstances, onlyLitShaders); scene.updateShaders = false; this.device._shadersDirty = false; scene._shaderVersion++; } this.updateFrameUniforms(); this.updateCpuSkinMatrices(_tempMeshInstancesSkinned); _tempMeshInstances.length = 0; _tempMeshInstancesSkinned.length = 0; const lights = this.lights; const lightCount = lights.length; for (let i = 0; i < lightCount; i++) { lights[i].beginFrame(); } } updateLightTextureAtlas() { this.lightTextureAtlas.update(this.localLights, this.scene.lighting); } /** * Updates the layer composition for rendering. * * @param {LayerComposition} comp - The layer composition to update. */ updateLayerComposition(comp) { const layerCompositionUpdateTime = now(); const len = comp.layerList.length; const scene = this.scene; const shaderVersion = scene._shaderVersion; for (let i = 0; i < len; i++) { const layer = comp.layerList[i]; layer._shaderVersion = shaderVersion; layer._skipRenderCounter = 0; layer._forwardDrawCalls = 0; layer._shadowDrawCalls = 0; layer._renderTime = 0; } comp._update(); this._layerCompositionUpdateTime += now() - layerCompositionUpdateTime; } frameUpdate() { this.clustersDebugRendered = false; this.initViewBindGroupFormat(this.scene.clusteredLightingEnabled); this.dirLightShadows.clear(); } } export { Renderer };