playcanvas
Version:
PlayCanvas WebGL game engine
1,005 lines (1,002 loc) • 46.2 kB
JavaScript
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 { CLEARFLAG_COLOR, CLEARFLAG_DEPTH, CLEARFLAG_STENCIL, CULLFACE_FRONT, CULLFACE_BACK, CULLFACE_NONE, UNIFORMTYPE_MAT4, UNIFORMTYPE_MAT3, UNIFORMTYPE_VEC3, UNIFORMTYPE_FLOAT, UNIFORMTYPE_VEC2, UNIFORMTYPE_INT, BINDGROUP_VIEW, BINDGROUP_MESH, BINDGROUP_MESH_UB, UNIFORM_BUFFER_DEFAULT_SLOT_NAME, SHADERSTAGE_VERTEX, SHADERSTAGE_FRAGMENT } 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, PROJECTION_ORTHOGRAPHIC, 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 { RenderPassUpdateClustered } from './render-pass-update-clustered.js';
/**
* @import { Camera } from '../camera.js'
* @import { CulledInstances } from '../layer.js'
* @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js'
* @import { LayerComposition } from '../composition/layer-composition.js'
* @import { Light } from '../light.js'
* @import { MeshInstance } from '../mesh-instance.js'
* @import { RenderTarget } from '../../platform/graphics/render-target.js'
* @import { Scene } from '../scene.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 _flipYMat = new Mat4().setScale(1, -1, 1);
const _tempLightSet = new Set();
const _tempLayerSet = new Set();
const _dynamicBindGroup = new DynamicBindGroup();
// Converts a projection matrix in OpenGL style (depth range of -1..1) to a DirectX style (depth range of 0..1).
const _fixProjRangeMat = new Mat4().set([
1,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0.5,
0,
0,
0,
0.5,
1
]);
// helton sequence of 2d offsets for jittering
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.370370),
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 _tempProjMat2 = new Mat4();
const _tempProjMat3 = new Mat4();
const _tempProjMat4 = new Mat4();
const _tempProjMat5 = new Mat4();
const _tempSet = new Set();
const _tempMeshInstances = [];
const _tempMeshInstancesSkinned = [];
/**
* The base renderer functionality to allow implementation of specialized renderers.
*
* @ignore
*/ class Renderer {
/**
* Create a new instance.
*
* @param {GraphicsDevice} graphicsDevice - The graphics device used by the renderer.
*/ constructor(graphicsDevice){
/** @type {boolean} */ this.clustersDebugRendered = false;
/**
* A set of visible mesh instances which need further processing before being rendered, e.g.
* skinning or morphing. Extracted during culling.
*
* @type {Set<MeshInstance>}
* @private
*/ this.processingMeshInstances = new Set();
/**
* A list of all unique lights in the layer composition.
*
* @type {Light[]}
*/ this.lights = [];
/**
* A list of all unique local lights (spot & omni) in the layer composition.
*
* @type {Light[]}
*/ 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>>}
*/ this.cameraDirShadowLights = 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>}
*/ this.dirLightShadows = new Map();
this.blueNoise = new BlueNoise(123);
this.device = graphicsDevice;
/** @type {Scene|null} */ this.scene = null;
// TODO: allocate only when the scene has clustered lighting enabled
this.worldClustersAllocator = new WorldClustersAllocator(graphicsDevice);
// texture atlas managing shadow map / cookie texture atlassing for omni and spot lights
this.lightTextureAtlas = new LightTextureAtlas(graphicsDevice);
// shadows
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);
// clustered passes
this._renderPassUpdateClustered = new RenderPassUpdateClustered(this.device, this, this.shadowRenderer, this._shadowRendererLocal, this.lightTextureAtlas);
// view bind group format with its uniform buffer format
this.viewUniformFormat = null;
this.viewBindGroupFormat = null;
// timing
this._skinTime = 0;
this._morphTime = 0;
this._cullTime = 0;
this._shadowMapTime = 0;
this._lightClustersTime = 0;
this._layerCompositionUpdateTime = 0;
// stats
this._shadowDrawCalls = 0;
this._skinDrawCalls = 0;
this._instancedDrawCalls = 0;
this._shadowMapUpdates = 0;
this._numDrawCallsCulled = 0;
this._camerasRendered = 0;
this._lightClusters = 0;
// Uniforms
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.nearClipId = scope.resolve('camera_near');
this.farClipId = scope.resolve('camera_far');
this.cameraParams = new Float32Array(4);
this.cameraParamsId = scope.resolve('camera_params');
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.twoSidedLightingNegScaleFactorId = scope.resolve('twoSidedLightingNegScaleFactor');
this.twoSidedLightingNegScaleFactorId.setValue(0);
this.morphPositionTex = scope.resolve('morphPositionTex');
this.morphNormalTex = scope.resolve('morphNormalTex');
this.morphTexParams = scope.resolve('morph_tex_params');
// a single instance of light cube
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;
}
/**
* 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);
// use viewport rectangle by default. Use scissor rectangle when required.
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) {
// flipping proj matrix
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);
camera.frustum.setFromMat4(view.projViewOffMat);
}
} else {
// Projection Matrix
let projMat = camera.projectionMatrix;
if (camera.calculateProjection) {
camera.calculateProjection(projMat, VIEW_CENTER);
}
let projMatSkybox = camera.getProjectionMatrixSkybox();
// flip projection matrices
if (flipY) {
projMat = _tempProjMat0.mul2(_flipYMat, projMat);
projMatSkybox = _tempProjMat1.mul2(_flipYMat, projMatSkybox);
}
// update depth range of projection matrices (-1..1 to 0..1)
if (this.device.isWebGPU) {
projMat = _tempProjMat2.mul2(_fixProjRangeMat, projMat);
projMatSkybox = _tempProjMat3.mul2(_fixProjRangeMat, projMatSkybox);
}
// camera jitter
const { jitter } = camera;
let jitterX = 0;
let jitterY = 0;
if (jitter > 0) {
// render target size
const targetWidth = target ? target.width : this.device.width;
const targetHeight = target ? target.height : this.device.height;
// offsets
const offset = _haltonSequence[this.device.renderVersion % _haltonSequence.length];
jitterX = jitter * (offset.x * 2 - 1) / targetWidth;
jitterY = jitter * (offset.y * 2 - 1) / targetHeight;
// apply offset to projection matrix
projMat = _tempProjMat4.copy(projMat);
projMat.data[8] = jitterX;
projMat.data[9] = jitterY;
// apply offset to skybox projection matrix
projMatSkybox = _tempProjMat5.copy(projMatSkybox);
projMatSkybox.data[8] = jitterX;
projMatSkybox.data[9] = jitterY;
// blue noise vec4 - only use when jitter is enabled
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);
// ViewInverse Matrix
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);
// View Matrix
viewMat.copy(viewInvMat).invert();
this.viewId.setValue(viewMat.data);
// View 3x3
viewMat3.setFromMat4(viewMat);
this.viewId3.setValue(viewMat3.data);
// ViewProjection Matrix
viewProjMat.mul2(projMat, viewMat);
this.viewProjId.setValue(viewProjMat.data);
// store matrices needed by TAA
camera._storeShaderMatrices(viewProjMat, jitterX, jitterY, this.device.renderVersion);
this.flipYId.setValue(flipY ? -1 : 1);
// View Position (world space)
this.dispatchViewPos(camera._node.getPosition());
camera.frustum.setFromMat4(viewProjMat);
}
this.tbnBasis.setValue(flipY ? -1 : 1);
// Near and far clip values
const n = camera._nearClip;
const f = camera._farClip;
this.nearClipId.setValue(n);
this.farClipId.setValue(f);
// camera params
this.cameraParams[0] = 1 / f;
this.cameraParams[1] = f;
this.cameraParams[2] = n;
this.cameraParams[3] = camera.projection === PROJECTION_ORTHOGRAPHIC ? 1 : 0;
this.cameraParamsId.setValue(this.cameraParams);
// exposure
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');
device.clear({
color: [
camera._clearColor.r,
camera._clearColor.g,
camera._clearColor.b,
camera._clearColor.a
],
depth: camera._clearDepth,
stencil: camera._clearStencil,
flags: flags
});
DebugGraphics.popGpuMarker(device);
}
}
// make sure colorWrite is set to true to all channels, if you want to fully clear the target
// TODO: this function is only used from outside of forward renderer, and should be deprecated
// when the functionality moves to the render passes. Note that Editor uses it as well.
setCamera(camera, target, clear, renderAction = null) {
this.setCameraUniforms(camera, target);
this.clearView(camera, target, clear, false);
}
// TODO: this is currently used by the lightmapper and the Editor,
// and will be removed when those call are removed.
clearView(camera, target, clear, forceWrite) {
const device = this.device;
DebugGraphics.pushGpuMarker(device, 'CLEAR-VIEW');
device.setRenderTarget(target);
device.updateBegin();
if (forceWrite) {
device.setColorWrite(true, true, true, true);
device.setDepthWrite(true);
}
this.setupViewport(camera, target);
if (clear) {
// use camera clear options if any
const options = camera._clearOptions;
device.clear(options ? options : {
color: [
camera._clearColor.r,
camera._clearColor.g,
camera._clearColor.b,
camera._clearColor.a
],
depth: camera._clearDepth,
flags: (camera._clearColorBuffer ? CLEARFLAG_COLOR : 0) | (camera._clearDepthBuffer ? CLEARFLAG_DEPTH : 0) | (camera._clearStencilBuffer ? CLEARFLAG_STENCIL : 0),
stencil: camera._clearStencil
});
}
DebugGraphics.popGpuMarker(device);
}
setupCullMode(cullFaces, flipFactor, drawCall) {
const material = drawCall.material;
let mode = CULLFACE_NONE;
if (cullFaces) {
let flipFaces = 1;
if (material.cull === CULLFACE_FRONT || material.cull === CULLFACE_BACK) {
flipFaces = flipFactor * drawCall.flipFacesFactor * drawCall.node.worldScaleSign;
}
if (flipFaces < 0) {
mode = material.cull === CULLFACE_FRONT ? CULLFACE_BACK : CULLFACE_FRONT;
} else {
mode = material.cull;
}
}
this.device.setCullMode(mode);
if (mode === CULLFACE_NONE && material.cull === CULLFACE_NONE) {
this.twoSidedLightingNegScaleFactorId.setValue(drawCall.node.worldScaleSign);
}
}
updateCameraFrustum(camera) {
if (camera.xr && camera.xr.views.list.length) {
// calculate frustum based on XR view
const view = camera.xr.views.list[0];
viewProjMat.mul2(view.projMat, view.viewOffMat);
camera.frustum.setFromMat4(viewProjMat);
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) {
// Cull mode
device.setCullMode(material.cull);
// Alpha test
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) {
// Note that drawCalls can be either a Set or an Array and contains mesh instances
// that are visible in this frame
this.updateGpuSkinMatrices(drawCalls);
this.updateMorphing(drawCalls);
this.updateGSplats(drawCalls);
}
setVertexBuffers(device, mesh) {
// main vertex buffer
device.setVertexBuffer(mesh.vertexBuffer);
}
setMorphing(device, morphInstance) {
if (morphInstance) {
morphInstance.prepareRendering(device);
// vertex buffer with vertex ids
device.setVertexBuffer(morphInstance.morph.vertexBufferIds);
// textures
this.morphPositionTex.setValue(morphInstance.texturePositions);
this.morphNormalTex.setValue(morphInstance.textureNormals);
// texture params
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; // note that this reuses an array
vp[0] = position.x;
vp[1] = position.y;
vp[2] = position.z;
this.viewPosId.setValue(vp);
}
initViewBindGroupFormat(isClustered) {
if (this.device.supportsUniformBuffers && !this.viewUniformFormat) {
// format of the view uniform buffer
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('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('clusterTextureSize', UNIFORMTYPE_VEC3),
new UniformFormat('clusterBoundsMin', UNIFORMTYPE_VEC3),
new UniformFormat('clusterBoundsDelta', UNIFORMTYPE_VEC3),
new UniformFormat('clusterCellsDot', UNIFORMTYPE_VEC3),
new UniformFormat('clusterCellsMax', UNIFORMTYPE_VEC3),
new UniformFormat('shadowAtlasParams', UNIFORMTYPE_VEC2),
new UniformFormat('clusterMaxCells', UNIFORMTYPE_INT),
new UniformFormat('clusterSkip', UNIFORMTYPE_FLOAT)
]);
}
this.viewUniformFormat = new UniformBufferFormat(this.device, uniforms);
// format of the view bind group - contains single uniform buffer, and some textures
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
// if (isClustered) {
// formats.push(...[
// new BindTextureFormat('clusterWorldTexture', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_UNFILTERABLE_FLOAT)
// ]);
// }
this.viewBindGroupFormat = new BindGroupFormat(this.device, formats);
}
}
/**
* Set up uniforms for an XR view.
*/ setupViewUniforms(view, index) {
// any view uniforms need to be part of the view uniform buffer, see initViewBindGroupFormat
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;
// make sure we have bind group for each view
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++){
// set up view uniforms
const view = viewList[i];
this.setupViewUniforms(view, i);
// update view bind group / uniforms
const viewBindGroup = viewBindGroups[i];
viewBindGroup.defaultUniformBuffer.update();
viewBindGroup.update();
}
} else {
const viewBindGroup = viewBindGroups[0];
viewBindGroup.defaultUniformBuffer.update();
viewBindGroup.update();
}
// bind it when a single view is used, otherwise this is handled per view inside rendering loop
if (!viewList) {
device.setBindGroup(BINDGROUP_VIEW, viewBindGroups[0]);
}
}
setupMeshUniformBuffers(shaderInstance) {
const device = this.device;
if (device.supportsUniformBuffers) {
// update mesh bind group / uniform buffer
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;
// sort mesh instance into the right bucket based on its transparency
const bucket = drawCall.transparent ? transparent : opaque;
bucket.push(drawCall);
if (drawCall.skinInstance || drawCall.morphInstance || drawCall.gsplatInstance) {
this.processingMeshInstances.add(drawCall);
// register visible cameras
if (drawCall.gsplatInstance) {
drawCall.gsplatInstance.cameras.push(camera);
}
}
}
}
}
this._cullTime += now() - cullTime;
this._numDrawCallsCulled += doCull ? count : 0;
}
collectLights(comp) {
// build a list and of all unique lights from all layers
this.lights.length = 0;
this.localLights.length = 0;
// stats
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];
// layer can be in the list two times (opaque, transp), process it only one time
if (!_tempLayerSet.has(layer)) {
_tempLayerSet.add(layer);
const lights = layer._lights;
for(let j = 0; j < lights.length; j++){
const light = lights[j];
// add new light
if (!_tempLightSet.has(light)) {
_tempLightSet.add(light);
this.lights.push(light);
if (light._type !== LIGHTTYPE_DIRECTIONAL) {
this.localLights.push(light);
}
// if affects dynamic or baked objects in real-time
if (light.mask & MASK_AFFECT_DYNAMIC || light.mask & MASK_AFFECT_LIGHTMAPPED) {
stats.dynamicLights++;
}
// bake lights
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) {
// directional lights are marked visible at the start of the frame
if (light._type !== LIGHTTYPE_DIRECTIONAL) {
light.getBoundingSphere(tempSphere);
if (camera.frustum.containsSphere(tempSphere)) {
light.visibleThisFrame = true;
light.usePhysicalUnits = physicalUnits;
// maximum screen area taken by the light
const screenSize = camera.getScreenSize(tempSphere);
light.maxScreenSize = Math.max(light.maxScreenSize, screenSize);
} else {
// if shadow casting light does not have shadow map allocated, mark it visible to allocate shadow map
// Note: This won't be needed when clustered shadows are used, but at the moment even culled out lights
// are used for rendering, and need shadow map to be allocated
// TODO: delete this code when clusteredLightingEnabled is being removed and is on by default.
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;
// shadow casters culling for local (point and spot) lights
for(let i = 0; i < this.localLights.length; i++){
const light = this.localLights[i];
if (light._type !== LIGHTTYPE_DIRECTIONAL) {
if (isClustered) {
// if atlas slot is reassigned, make sure to update the shadow map, including the culling
if (light.atlasSlotUpdated && light.shadowUpdateMode === SHADOWUPDATE_NONE) {
light.shadowUpdateMode = SHADOWUPDATE_THISFRAME;
}
} else {
// force rendering shadow at least once to allocate the shadow map needed by the shaders
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);
}
}
}
// shadow casters culling for directional lights - start with none and collect lights for cameras
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;
// get directional lights from all layers of the 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];
// unique shadow casting lights
if (light.castShadows && !_tempSet.has(light)) {
_tempSet.add(light);
lightList = lightList ?? [];
lightList.push(light);
// frustum culling for the directional shadow when rendering the camera
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();
// for all cameras
const numCameras = comp.cameras.length;
this._camerasRendered += numCameras;
for(let i = 0; i < numCameras; i++){
const camera = comp.cameras[i];
// event before the camera is culling
scene?.fire(EVENT_PRECULL, camera);
// update camera and frustum
const renderTarget = camera.renderTarget;
camera.frameUpdate(renderTarget);
this.updateCameraFrustum(camera.camera);
// for all of its enabled layers
const layerIds = camera.layers;
for(let j = 0; j < layerIds.length; j++){
const layer = comp.getLayerById(layerIds[j]);
if (layer && layer.enabled) {
// cull each layer's non-directional lights once with each camera
// lights aren't collected anywhere, but marked as visible
this.cullLights(camera.camera, layer._lights);
// cull mesh instances
const culledInstances = layer.getCulledInstances(camera.camera);
this.cull(camera.camera, layer.meshInstances, culledInstances);
}
}
// event after the camera is done with culling
scene?.fire(EVENT_POSTCULL, camera);
}
// update shadow / cookie atlas allocation for the visible lights. Update it after the ligthts were culled,
// but before shadow maps were culling, as it might force some 'update once' shadows to cull.
if (scene.clusteredLightingEnabled) {
this.updateLightTextureAtlas();
}
// cull shadow casters for all lights
this.cullShadowmaps(comp);
// event after the engine has finished culling all cameras
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) {
// material not processed yet
if (!_tempSet.has(mat)) {
_tempSet.add(mat);
// skip this for materials not using variants
if (mat.getShaderVariant !== Material.prototype.getShaderVariant) {
if (onlyLitShaders) {
// skip materials not using lighting
if (!mat.useLighting || mat.emitter && !mat.emitter.lighting) {
continue;
}
}
// clear shader variants on the material and also on mesh instances that use it
mat.clearVariants();
}
}
}
}
// keep temp set empty
_tempSet.clear();
}
updateFrameUniforms() {
// blue noise texture
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];
// clear visibility
meshInst.visibleThisFrame = false;
// collect all mesh instances if we need to update their shaders. Note that there could
// be duplicates, which is not a problem for the shader updates, so we do not filter them out.
if (updateShaders) {
_tempMeshInstances.push(meshInst);
}
// collect skinned mesh instances
if (meshInst.skinInstance) {
_tempMeshInstancesSkinned.push(meshInst);
}
}
}
scene._stats.meshInstances = totalMeshInstances;
// update shaders if needed
if (updateShaders) {
const onlyLitShaders = !scene.updateShaders || !this.device._shadersDirty;
this.updateShaders(_tempMeshInstances, onlyLitShaders);
scene.updateShaders = false;
this.device._shadersDirty = false;
scene._shaderVersion++;
}
this.updateFrameUniforms();
// Update all skin matrices to properly cull skinned objects (but don't update rendering data yet)
this.updateCpuSkinMatrices(_tempMeshInstancesSkinned);
// clear light arrays
_tempMeshInstances.length = 0;
_tempMeshInstancesSkinned.length = 0;
// clear light visibility
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;
}
// update composition
comp._update();
this._layerCompositionUpdateTime += now() - layerCompositionUpdateTime;
}
frameUpdate() {
this.clustersDebugRendered = false;
this.initViewBindGroupFormat(this.scene.clusteredLightingEnabled);
// no valid shadows at the start of the frame
this.dirLightShadows.clear();
}
}
export { Renderer };