playcanvas
Version:
PlayCanvas WebGL game engine
909 lines (906 loc) • 43.2 kB
JavaScript
import { Debug } from '../../core/debug.js';
import { now } from '../../core/time.js';
import { Color } from '../../core/math/color.js';
import { math } from '../../core/math/math.js';
import { Vec3 } from '../../core/math/vec3.js';
import { BoundingBox } from '../../core/shape/bounding-box.js';
import { PIXELFORMAT_RGBA8, TEXTURETYPE_RGBM, CHUNKAPI_1_65, CULLFACE_NONE, ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, TEXTURETYPE_DEFAULT, TEXHINT_LIGHTMAP, FILTER_LINEAR } from '../../platform/graphics/constants.js';
import { DebugGraphics } from '../../platform/graphics/debug-graphics.js';
import { RenderTarget } from '../../platform/graphics/render-target.js';
import { drawQuadWithShader } from '../../scene/graphics/quad-render-utils.js';
import { Texture } from '../../platform/graphics/texture.js';
import { PROJECTION_ORTHOGRAPHIC, GAMMA_NONE, TONEMAP_LINEAR, MASK_AFFECT_LIGHTMAPPED, SHADERDEF_LM, MASK_BAKE, MASK_AFFECT_DYNAMIC, SHADOWUPDATE_REALTIME, SHADOWUPDATE_THISFRAME, LIGHTTYPE_DIRECTIONAL, LIGHTTYPE_SPOT, PROJECTION_PERSPECTIVE, LIGHTTYPE_OMNI, SHADER_FORWARD, BAKE_COLORDIR, SHADERDEF_DIRLM, SHADERDEF_LMAMBIENT } from '../../scene/constants.js';
import { MeshInstance } from '../../scene/mesh-instance.js';
import { LightingParams } from '../../scene/lighting/lighting-params.js';
import { WorldClusters } from '../../scene/lighting/world-clusters.js';
import { shaderChunks } from '../../scene/shader-lib/chunks/chunks.js';
import { shaderChunksLightmapper } from '../../scene/shader-lib/chunks/chunks-lightmapper.js';
import { Camera } from '../../scene/camera.js';
import { GraphNode } from '../../scene/graph-node.js';
import { StandardMaterial } from '../../scene/materials/standard-material.js';
import { BakeLightSimple } from './bake-light-simple.js';
import { BakeLightAmbient } from './bake-light-ambient.js';
import { BakeMeshNode } from './bake-mesh-node.js';
import { LightmapCache } from '../../scene/graphics/lightmap-cache.js';
import { LightmapFilters } from './lightmap-filters.js';
import { BlendState } from '../../platform/graphics/blend-state.js';
import { DepthState } from '../../platform/graphics/depth-state.js';
import { RenderPassLightmapper } from './render-pass-lightmapper.js';
/**
* @import { AssetRegistry } from '../asset/asset-registry.js'
* @import { Entity } from '../entity.js'
* @import { ForwardRenderer } from '../../scene/renderer/forward-renderer.js'
* @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js'
* @import { Scene } from '../../scene/scene.js'
*/ var MAX_LIGHTMAP_SIZE = 2048;
var PASS_COLOR = 0;
var PASS_DIR = 1;
var tempVec = new Vec3();
/**
* The lightmapper is used to bake scene lights into textures.
*
* @category Graphics
*/ class Lightmapper {
destroy() {
var _this_camera;
// release reference to the texture
LightmapCache.decRef(this.blackTex);
this.blackTex = null;
// destroy all lightmaps
LightmapCache.destroy();
this.device = null;
this.root = null;
this.scene = null;
this.renderer = null;
this.assets = null;
(_this_camera = this.camera) == null ? void 0 : _this_camera.destroy();
this.camera = null;
}
initBake(device) {
this.bakeHDR = this.scene.lightmapPixelFormat !== PIXELFORMAT_RGBA8;
// only initialize one time
if (!this._initCalled) {
this._initCalled = true;
// lightmap filtering shaders
this.lightmapFilters = new LightmapFilters(device);
// shader related
this.constantBakeDir = device.scope.resolve('bakeDir');
this.materials = [];
// small black texture
this.blackTex = new Texture(this.device, {
width: 4,
height: 4,
format: PIXELFORMAT_RGBA8,
type: TEXTURETYPE_RGBM,
name: 'lightmapBlack'
});
// incref black texture in the cache to avoid it being destroyed
LightmapCache.incRef(this.blackTex);
// camera used for baking
var camera = new Camera();
camera.clearColor.set(0, 0, 0, 0);
camera.clearColorBuffer = true;
camera.clearDepthBuffer = false;
camera.clearStencilBuffer = false;
camera.frustumCulling = false;
camera.projection = PROJECTION_ORTHOGRAPHIC;
camera.aspectRatio = 1;
camera.node = new GraphNode();
this.camera = camera;
// baking uses HDR (no gamma / tone mapping)
this.camera.shaderParams.gammaCorrection = GAMMA_NONE;
this.camera.shaderParams.toneMapping = TONEMAP_LINEAR;
}
// create light cluster structure
if (this.scene.clusteredLightingEnabled) {
// create light params, and base most parameters on the lighting params of the scene
var lightingParams = new LightingParams(device.supportsAreaLights, device.maxTextureSize, ()=>{});
this.lightingParams = lightingParams;
var srcParams = this.scene.lighting;
lightingParams.shadowsEnabled = srcParams.shadowsEnabled;
lightingParams.shadowAtlasResolution = srcParams.shadowAtlasResolution;
lightingParams.cookiesEnabled = srcParams.cookiesEnabled;
lightingParams.cookieAtlasResolution = srcParams.cookieAtlasResolution;
lightingParams.areaLightsEnabled = srcParams.areaLightsEnabled;
// some custom lightmapping params - we bake single light a time
lightingParams.cells = new Vec3(3, 3, 3);
lightingParams.maxLightsPerCell = 4;
this.worldClusters = new WorldClusters(device);
this.worldClusters.name = 'ClusterLightmapper';
}
}
finishBake(bakeNodes) {
this.materials = [];
function destroyRT(rt) {
// this can cause ref count to be 0 and texture destroyed
LightmapCache.decRef(rt.colorBuffer);
// destroy render target itself
rt.destroy();
}
// spare render targets including color buffer
this.renderTargets.forEach((rt)=>{
destroyRT(rt);
});
this.renderTargets.clear();
// destroy render targets from nodes (but not color buffer)
bakeNodes.forEach((node)=>{
node.renderTargets.forEach((rt)=>{
destroyRT(rt);
});
node.renderTargets.length = 0;
});
// this shader is only valid for specific brightness and contrast values, dispose it
this.ambientAOMaterial = null;
// delete light cluster
if (this.worldClusters) {
this.worldClusters.destroy();
this.worldClusters = null;
}
}
createMaterialForPass(device, scene, pass, addAmbient) {
var material = new StandardMaterial();
material.name = "lmMaterial-pass:" + pass + "-ambient:" + addAmbient;
material.chunks.APIVersion = CHUNKAPI_1_65;
material.setDefine('UV1LAYOUT', ''); // draw into UV1 texture space
if (pass === PASS_COLOR) {
var bakeLmEndChunk = shaderChunksLightmapper.bakeLmEndPS; // encode to RGBM
if (addAmbient) {
// diffuse light stores accumulated AO, apply contrast and brightness to it
// and multiply ambient light color by the AO
bakeLmEndChunk = "\n dDiffuseLight = ((dDiffuseLight - 0.5) * max(" + scene.ambientBakeOcclusionContrast.toFixed(1) + " + 1.0, 0.0)) + 0.5;\n dDiffuseLight += vec3(" + scene.ambientBakeOcclusionBrightness.toFixed(1) + ");\n dDiffuseLight = saturate(dDiffuseLight);\n dDiffuseLight *= dAmbientLight;\n " + bakeLmEndChunk;
} else {
material.ambient = new Color(0, 0, 0); // don't bake ambient
}
material.chunks.basePS = shaderChunks.basePS + (this.bakeHDR ? '' : '\n#define LIGHTMAP_RGBM\n');
material.chunks.endPS = bakeLmEndChunk;
material.lightMap = this.blackTex;
} else {
material.chunks.basePS = "" + shaderChunks.basePS + "\nuniform sampler2D texture_dirLightMap;\nuniform float bakeDir;\n";
material.chunks.endPS = shaderChunksLightmapper.bakeDirLmEndPS;
}
// avoid writing unrelated things to alpha
material.chunks.outputAlphaPS = '\n';
material.cull = CULLFACE_NONE;
material.forceUv1 = true; // provide data to xformUv1
material.update();
return material;
}
createMaterials(device, scene, passCount) {
for(var pass = 0; pass < passCount; pass++){
if (!this.passMaterials[pass]) {
this.passMaterials[pass] = this.createMaterialForPass(device, scene, pass, false);
}
}
// material used on last render of ambient light to multiply accumulated AO in lightmap by ambient light
if (!this.ambientAOMaterial) {
this.ambientAOMaterial = this.createMaterialForPass(device, scene, 0, true);
this.ambientAOMaterial.onUpdateShader = function(options) {
// mark LM as without ambient, to add it
options.litOptions.lightMapWithoutAmbient = true;
// don't add ambient to diffuse directly but keep it separate, to allow AO to be multiplied in
options.litOptions.separateAmbient = true;
return options;
};
}
}
createTexture(size, name) {
return new Texture(this.device, {
profilerHint: TEXHINT_LIGHTMAP,
width: size,
height: size,
format: this.scene.lightmapPixelFormat,
mipmaps: false,
type: this.bakeHDR ? TEXTURETYPE_DEFAULT : TEXTURETYPE_RGBM,
minFilter: FILTER_NEAREST,
magFilter: FILTER_NEAREST,
addressU: ADDRESS_CLAMP_TO_EDGE,
addressV: ADDRESS_CLAMP_TO_EDGE,
name: name
});
}
// recursively walk the hierarchy of nodes starting at the specified node
// collect all nodes that need to be lightmapped to bakeNodes array
// collect all nodes with geometry to allNodes array
collectModels(node, bakeNodes, allNodes) {
var _node_model, _node_model1, _node_render;
if (!node.enabled) return;
// mesh instances from model component
var meshInstances;
if (((_node_model = node.model) == null ? void 0 : _node_model.model) && ((_node_model1 = node.model) == null ? void 0 : _node_model1.enabled)) {
if (allNodes) allNodes.push(new BakeMeshNode(node));
if (node.model.lightmapped) {
if (bakeNodes) {
meshInstances = node.model.model.meshInstances;
}
}
}
// mesh instances from render component
if ((_node_render = node.render) == null ? void 0 : _node_render.enabled) {
if (allNodes) allNodes.push(new BakeMeshNode(node));
if (node.render.lightmapped) {
if (bakeNodes) {
meshInstances = node.render.meshInstances;
}
}
}
if (meshInstances) {
var hasUv1 = true;
for(var i = 0; i < meshInstances.length; i++){
if (!meshInstances[i].mesh.vertexBuffer.format.hasUv1) {
Debug.log("Lightmapper - node [" + node.name + "] contains meshes without required uv1, excluding it from baking.");
hasUv1 = false;
break;
}
}
if (hasUv1) {
var notInstancedMeshInstances = [];
for(var i1 = 0; i1 < meshInstances.length; i1++){
var mesh = meshInstances[i1].mesh;
// is this mesh an instance of already used mesh in this node
if (this._tempSet.has(mesh)) {
// collect each instance (object with shared VB) as separate "node"
bakeNodes.push(new BakeMeshNode(node, [
meshInstances[i1]
]));
} else {
notInstancedMeshInstances.push(meshInstances[i1]);
}
this._tempSet.add(mesh);
}
this._tempSet.clear();
// collect all non-shared objects as one "node"
if (notInstancedMeshInstances.length > 0) {
bakeNodes.push(new BakeMeshNode(node, notInstancedMeshInstances));
}
}
}
for(var i2 = 0; i2 < node._children.length; i2++){
this.collectModels(node._children[i2], bakeNodes, allNodes);
}
}
// prepare all meshInstances that cast shadows into lightmaps
prepareShadowCasters(nodes) {
var casters = [];
for(var n = 0; n < nodes.length; n++){
var component = nodes[n].component;
component.castShadows = component.castShadowsLightmap;
if (component.castShadowsLightmap) {
var meshes = nodes[n].meshInstances;
for(var i = 0; i < meshes.length; i++){
meshes[i].visibleThisFrame = true;
casters.push(meshes[i]);
}
}
}
return casters;
}
// updates world transform for nodes
updateTransforms(nodes) {
for(var i = 0; i < nodes.length; i++){
var meshInstances = nodes[i].meshInstances;
for(var j = 0; j < meshInstances.length; j++){
meshInstances[j].node.getWorldTransform();
}
}
}
// Note: this function is also called by the Editor to display estimated LM size in the inspector,
// do not change its signature.
calculateLightmapSize(node) {
var data;
var sizeMult = this.scene.lightmapSizeMultiplier || 16;
var scale = tempVec;
var srcArea, lightmapSizeMultiplier;
if (node.model) {
lightmapSizeMultiplier = node.model.lightmapSizeMultiplier;
if (node.model.asset) {
data = this.assets.get(node.model.asset).data;
if (data.area) {
srcArea = data.area;
}
} else if (node.model._area) {
data = node.model;
if (data._area) {
srcArea = data._area;
}
}
} else if (node.render) {
lightmapSizeMultiplier = node.render.lightmapSizeMultiplier;
if (node.render.type !== 'asset') {
if (node.render._area) {
data = node.render;
if (data._area) {
srcArea = data._area;
}
}
}
}
// copy area
var area = {
x: 1,
y: 1,
z: 1,
uv: 1
};
if (srcArea) {
area.x = srcArea.x;
area.y = srcArea.y;
area.z = srcArea.z;
area.uv = srcArea.uv;
}
var areaMult = lightmapSizeMultiplier || 1;
area.x *= areaMult;
area.y *= areaMult;
area.z *= areaMult;
// bounds of the component
var component = node.render || node.model;
var bounds = this.computeNodeBounds(component.meshInstances);
// total area in the lightmap is based on the world space bounds of the mesh
scale.copy(bounds.halfExtents);
var totalArea = area.x * scale.y * scale.z + area.y * scale.x * scale.z + area.z * scale.x * scale.y;
totalArea /= area.uv;
totalArea = Math.sqrt(totalArea);
var lightmapSize = Math.min(math.nextPowerOfTwo(totalArea * sizeMult), this.scene.lightmapMaxResolution || MAX_LIGHTMAP_SIZE);
return lightmapSize;
}
setLightmapping(nodes, value, passCount, shaderDefs) {
for(var i = 0; i < nodes.length; i++){
var node = nodes[i];
var meshInstances = node.meshInstances;
for(var j = 0; j < meshInstances.length; j++){
var meshInstance = meshInstances[j];
meshInstance.setLightmapped(value);
if (value) {
if (shaderDefs) {
meshInstance._shaderDefs |= shaderDefs;
}
// only lights that affect lightmapped objects are used on this mesh now that it is baked
meshInstance.mask = MASK_AFFECT_LIGHTMAPPED;
// textures
for(var pass = 0; pass < passCount; pass++){
var tex = node.renderTargets[pass].colorBuffer;
tex.minFilter = FILTER_LINEAR;
tex.magFilter = FILTER_LINEAR;
meshInstance.setRealtimeLightmap(MeshInstance.lightmapParamNames[pass], tex);
}
}
}
}
}
/**
* Generates and applies the lightmaps.
*
* @param {Entity[]|null} nodes - An array of entities (with model or render components) to
* render lightmaps for. If not supplied, the entire scene will be baked.
* @param {number} [mode] - Baking mode. Can be:
*
* - {@link BAKE_COLOR}: single color lightmap
* - {@link BAKE_COLORDIR}: single color lightmap + dominant light direction (used for
* bump/specular)
*
* Only lights with bakeDir=true will be used for generating the dominant light direction.
* Defaults to {@link BAKE_COLORDIR}.
*/ bake(nodes, mode) {
if (mode === void 0) mode = BAKE_COLORDIR;
var device = this.device;
var startTime = now();
// update skybox
this.scene._updateSkyMesh();
device.fire('lightmapper:start', {
timestamp: startTime,
target: this
});
this.stats.renderPasses = 0;
this.stats.shadowMapTime = 0;
this.stats.forwardTime = 0;
var startShaders = device._shaderStats.linked;
var startFboTime = device._renderTargetCreationTime;
var startCompileTime = device._shaderStats.compileTime;
// BakeMeshNode objects for baking
var bakeNodes = [];
// all BakeMeshNode objects
var allNodes = [];
// collect nodes / meshInstances for baking
if (nodes) {
// collect nodes for baking based on specified list of nodes
for(var i = 0; i < nodes.length; i++){
this.collectModels(nodes[i], bakeNodes, null);
}
// collect all nodes from the scene
this.collectModels(this.root, null, allNodes);
} else {
// collect nodes from the root of the scene
this.collectModels(this.root, bakeNodes, allNodes);
}
DebugGraphics.pushGpuMarker(this.device, 'LMBake');
// bake nodes
if (bakeNodes.length > 0) {
this.renderer.shadowRenderer.frameUpdate();
// disable lightmapping
var passCount = mode === BAKE_COLORDIR ? 2 : 1;
this.setLightmapping(bakeNodes, false, passCount);
this.initBake(device);
this.bakeInternal(passCount, bakeNodes, allNodes);
// Enable new lightmaps
var shaderDefs = SHADERDEF_LM;
if (mode === BAKE_COLORDIR) {
shaderDefs |= SHADERDEF_DIRLM;
}
// mark lightmap as containing ambient lighting
if (this.scene.ambientBake) {
shaderDefs |= SHADERDEF_LMAMBIENT;
}
this.setLightmapping(bakeNodes, true, passCount, shaderDefs);
// clean up memory
this.finishBake(bakeNodes);
}
DebugGraphics.popGpuMarker(this.device);
var nowTime = now();
this.stats.totalRenderTime = nowTime - startTime;
this.stats.shadersLinked = device._shaderStats.linked - startShaders;
this.stats.compileTime = device._shaderStats.compileTime - startCompileTime;
this.stats.fboTime = device._renderTargetCreationTime - startFboTime;
this.stats.lightmapCount = bakeNodes.length;
device.fire('lightmapper:end', {
timestamp: nowTime,
target: this
});
}
// this allocates lightmap textures and render targets.
allocateTextures(bakeNodes, passCount) {
for(var i = 0; i < bakeNodes.length; i++){
// required lightmap size
var bakeNode = bakeNodes[i];
var size = this.calculateLightmapSize(bakeNode.node);
// texture and render target for each pass, stored per node
for(var pass = 0; pass < passCount; pass++){
var tex = this.createTexture(size, "lightmapper_lightmap_" + i);
LightmapCache.incRef(tex);
bakeNode.renderTargets[pass] = new RenderTarget({
colorBuffer: tex,
depth: false
});
}
// single temporary render target of each size
if (!this.renderTargets.has(size)) {
var tex1 = this.createTexture(size, "lightmapper_temp_lightmap_" + size);
LightmapCache.incRef(tex1);
this.renderTargets.set(size, new RenderTarget({
colorBuffer: tex1,
depth: false
}));
}
}
}
prepareLightsToBake(allLights, bakeLights) {
// ambient light
if (this.scene.ambientBake) {
var ambientLight = new BakeLightAmbient(this);
bakeLights.push(ambientLight);
}
// scene lights
var sceneLights = this.renderer.lights;
for(var i = 0; i < sceneLights.length; i++){
var light = sceneLights[i];
// store all lights and their original settings we need to temporarily modify
var bakeLight = new BakeLightSimple(this, light);
allLights.push(bakeLight);
// bake light
if (light.enabled && (light.mask & MASK_BAKE) !== 0) {
light.mask = MASK_BAKE | MASK_AFFECT_LIGHTMAPPED | MASK_AFFECT_DYNAMIC;
light.shadowUpdateMode = light.type === LIGHTTYPE_DIRECTIONAL ? SHADOWUPDATE_REALTIME : SHADOWUPDATE_THISFRAME;
bakeLights.push(bakeLight);
}
}
// sort bake lights by type to minimize shader switches
bakeLights.sort();
}
restoreLights(allLights) {
for(var i = 0; i < allLights.length; i++){
allLights[i].restore();
}
}
setupScene() {
// backup
this.ambientLight.copy(this.scene.ambientLight);
// if not baking ambient, set it to black
if (!this.scene.ambientBake) {
this.scene.ambientLight.set(0, 0, 0);
}
// apply scene settings
this.renderer.setSceneConstants();
}
restoreScene() {
this.scene.ambientLight.copy(this.ambientLight);
}
// compute bounding box for a single node
computeNodeBounds(meshInstances) {
var bounds = new BoundingBox();
if (meshInstances.length > 0) {
bounds.copy(meshInstances[0].aabb);
for(var m = 1; m < meshInstances.length; m++){
bounds.add(meshInstances[m].aabb);
}
}
return bounds;
}
// compute bounding box for each node
computeNodesBounds(nodes) {
for(var i = 0; i < nodes.length; i++){
var meshInstances = nodes[i].meshInstances;
nodes[i].bounds = this.computeNodeBounds(meshInstances);
}
}
// compute compound bounding box for an array of mesh instances
computeBounds(meshInstances) {
var bounds = new BoundingBox();
for(var i = 0; i < meshInstances.length; i++){
bounds.copy(meshInstances[0].aabb);
for(var m = 1; m < meshInstances.length; m++){
bounds.add(meshInstances[m].aabb);
}
}
return bounds;
}
backupMaterials(meshInstances) {
for(var i = 0; i < meshInstances.length; i++){
this.materials[i] = meshInstances[i].material;
}
}
restoreMaterials(meshInstances) {
for(var i = 0; i < meshInstances.length; i++){
meshInstances[i].material = this.materials[i];
}
}
lightCameraPrepare(device, bakeLight) {
var light = bakeLight.light;
var shadowCam;
// only prepare camera for spot light, other cameras need to be adjusted per cubemap face / per node later
if (light.type === LIGHTTYPE_SPOT) {
var lightRenderData = light.getRenderData(null, 0);
shadowCam = lightRenderData.shadowCamera;
shadowCam._node.setPosition(light._node.getPosition());
shadowCam._node.setRotation(light._node.getRotation());
shadowCam._node.rotateLocal(-90, 0, 0);
shadowCam.projection = PROJECTION_PERSPECTIVE;
shadowCam.nearClip = light.attenuationEnd / 1000;
shadowCam.farClip = light.attenuationEnd;
shadowCam.aspectRatio = 1;
shadowCam.fov = light._outerConeAngle * 2;
this.renderer.updateCameraFrustum(shadowCam);
}
return shadowCam;
}
// prepares camera / frustum of the light for rendering the bakeNode
// returns true if light affects the bakeNode
lightCameraPrepareAndCull(bakeLight, bakeNode, shadowCam, casterBounds) {
var light = bakeLight.light;
var lightAffectsNode = true;
if (light.type === LIGHTTYPE_DIRECTIONAL) {
// tweak directional light camera to fully see all casters and they are fully inside the frustum
tempVec.copy(casterBounds.center);
tempVec.y += casterBounds.halfExtents.y;
this.camera.node.setPosition(tempVec);
this.camera.node.setEulerAngles(-90, 0, 0);
this.camera.nearClip = 0;
this.camera.farClip = casterBounds.halfExtents.y * 2;
var frustumSize = Math.max(casterBounds.halfExtents.x, casterBounds.halfExtents.z);
this.camera.orthoHeight = frustumSize;
} else {
// for other light types, test if light affects the node
if (!bakeLight.lightBounds.intersects(bakeNode.bounds)) {
lightAffectsNode = false;
}
}
// per meshInstance culling for spot light only
// (omni lights cull per face later, directional lights don't cull)
if (light.type === LIGHTTYPE_SPOT) {
var nodeVisible = false;
var meshInstances = bakeNode.meshInstances;
for(var i = 0; i < meshInstances.length; i++){
if (meshInstances[i]._isVisible(shadowCam)) {
nodeVisible = true;
break;
}
}
if (!nodeVisible) {
lightAffectsNode = false;
}
}
return lightAffectsNode;
}
// set up light array for a single light
setupLightArray(lightArray, light) {
lightArray[LIGHTTYPE_DIRECTIONAL].length = 0;
lightArray[LIGHTTYPE_OMNI].length = 0;
lightArray[LIGHTTYPE_SPOT].length = 0;
lightArray[light.type][0] = light;
light.visibleThisFrame = true;
}
renderShadowMap(comp, shadowMapRendered, casters, bakeLight) {
var light = bakeLight.light;
var isClustered = this.scene.clusteredLightingEnabled;
var castShadow = light.castShadows && (!isClustered || this.scene.lighting.shadowsEnabled);
if (!shadowMapRendered && castShadow) {
// allocate shadow map from the cache to avoid per light allocation
if (!light.shadowMap && !isClustered) {
light.shadowMap = this.shadowMapCache.get(this.device, light);
}
if (light.type === LIGHTTYPE_DIRECTIONAL) {
this.renderer._shadowRendererDirectional.cull(light, comp, this.camera, casters);
var shadowPass = this.renderer._shadowRendererDirectional.getLightRenderPass(light, this.camera);
shadowPass == null ? void 0 : shadowPass.render();
} else {
// TODO: lightmapper on WebGPU does not yet support spot and omni shadows
if (this.device.isWebGPU) {
Debug.warnOnce('Lightmapper on WebGPU does not yet support spot and omni shadows.');
return true;
}
this.renderer._shadowRendererLocal.cull(light, comp, casters);
// TODO: this needs to use render passes to work on WebGPU
var insideRenderPass = false;
this.renderer.shadowRenderer.render(light, this.camera, insideRenderPass);
}
}
return true;
}
postprocessTextures(device, bakeNodes, passCount) {
var numDilates2x = 1; // 1 or 2 dilates (depending on filter being enabled)
var dilateShader = this.lightmapFilters.getDilate(device, this.bakeHDR);
var denoiseShader;
// bilateral denoise filter - runs as a first pass, before dilate
var filterLightmap = this.scene.lightmapFilterEnabled;
if (filterLightmap) {
this.lightmapFilters.prepareDenoise(this.scene.lightmapFilterRange, this.scene.lightmapFilterSmoothness, this.bakeHDR);
denoiseShader = this.lightmapFilters.getDenoise(this.bakeHDR);
}
device.setBlendState(BlendState.NOBLEND);
device.setDepthState(DepthState.NODEPTH);
device.setStencilState(null, null);
for(var node = 0; node < bakeNodes.length; node++){
var bakeNode = bakeNodes[node];
DebugGraphics.pushGpuMarker(this.device, "LMPost:" + node);
for(var pass = 0; pass < passCount; pass++){
var nodeRT = bakeNode.renderTargets[pass];
var lightmap = nodeRT.colorBuffer;
var tempRT = this.renderTargets.get(lightmap.width);
var tempTex = tempRT.colorBuffer;
this.lightmapFilters.prepare(lightmap.width, lightmap.height);
// bounce dilate between textures, execute denoise on the first pass
for(var i = 0; i < numDilates2x; i++){
this.lightmapFilters.setSourceTexture(lightmap);
var bilateralFilterEnabled = filterLightmap && pass === 0 && i === 0;
drawQuadWithShader(device, tempRT, bilateralFilterEnabled ? denoiseShader : dilateShader);
this.lightmapFilters.setSourceTexture(tempTex);
drawQuadWithShader(device, nodeRT, dilateShader);
}
}
DebugGraphics.popGpuMarker(this.device);
}
}
bakeInternal(passCount, bakeNodes, allNodes) {
var scene = this.scene;
var comp = scene.layers;
var device = this.device;
var clusteredLightingEnabled = scene.clusteredLightingEnabled;
this.createMaterials(device, scene, passCount);
this.setupScene();
// update layer composition
comp._update();
// compute bounding boxes for nodes
this.computeNodesBounds(bakeNodes);
// Calculate lightmap sizes and allocate textures
this.allocateTextures(bakeNodes, passCount);
// Collect bakeable lights, and also keep allLights along with their properties we change to restore them later
this.renderer.collectLights(comp);
var allLights = [], bakeLights = [];
this.prepareLightsToBake(allLights, bakeLights);
// update transforms
this.updateTransforms(allNodes);
// get all meshInstances that cast shadows into lightmap and set them up for realtime shadow casting
var casters = this.prepareShadowCasters(allNodes);
// update skinned and morphed meshes
this.renderer.updateCpuSkinMatrices(casters);
this.renderer.gpuUpdate(casters);
// compound bounding box for all casters, used to compute shared directional light shadow
var casterBounds = this.computeBounds(casters);
var i, j, rcv, m;
// Prepare models
for(i = 0; i < bakeNodes.length; i++){
var bakeNode = bakeNodes[i];
rcv = bakeNode.meshInstances;
for(j = 0; j < rcv.length; j++){
// patch meshInstance
m = rcv[j];
m.setLightmapped(false);
m.mask = MASK_BAKE; // only affected by LM lights
// patch material
m.setRealtimeLightmap(MeshInstance.lightmapParamNames[0], this.blackTex);
m.setRealtimeLightmap(MeshInstance.lightmapParamNames[1], this.blackTex);
}
}
// Disable all bakeable lights
for(j = 0; j < bakeLights.length; j++){
bakeLights[j].light.enabled = false;
}
var lightArray = [
[],
[],
[]
];
var pass, node;
var shadersUpdatedOn1stPass = false;
// Accumulate lights into RGBM textures
for(i = 0; i < bakeLights.length; i++){
var bakeLight = bakeLights[i];
var isAmbientLight = bakeLight instanceof BakeLightAmbient;
var isDirectional = bakeLight.light.type === LIGHTTYPE_DIRECTIONAL;
// light can be baked using many virtual lights to create soft effect
var numVirtualLights = bakeLight.numVirtualLights;
// direction baking is not currently compatible with virtual lights, as we end up with no valid direction in lights penumbra
if (passCount > 1 && numVirtualLights > 1 && bakeLight.light.bakeDir) {
numVirtualLights = 1;
Debug.warn('Lightmapper\'s BAKE_COLORDIR mode is not compatible with Light\'s bakeNumSamples larger than one. Forcing it to one.');
}
for(var virtualLightIndex = 0; virtualLightIndex < numVirtualLights; virtualLightIndex++){
DebugGraphics.pushGpuMarker(device, "Light:" + bakeLight.light._node.name + ":" + virtualLightIndex);
// prepare virtual light
if (numVirtualLights > 1) {
bakeLight.prepareVirtualLight(virtualLightIndex, numVirtualLights);
}
bakeLight.startBake();
var shadowMapRendered = false;
var shadowCam = this.lightCameraPrepare(device, bakeLight);
for(node = 0; node < bakeNodes.length; node++){
var bakeNode1 = bakeNodes[node];
rcv = bakeNode1.meshInstances;
var lightAffectsNode = this.lightCameraPrepareAndCull(bakeLight, bakeNode1, shadowCam, casterBounds);
if (!lightAffectsNode) {
continue;
}
this.setupLightArray(lightArray, bakeLight.light);
var clusterLights = isDirectional ? [] : [
bakeLight.light
];
if (clusteredLightingEnabled) {
this.renderer.lightTextureAtlas.update(clusterLights, this.lightingParams);
}
// render light shadow map needs to be rendered
shadowMapRendered = this.renderShadowMap(comp, shadowMapRendered, casters, bakeLight);
if (clusteredLightingEnabled) {
this.worldClusters.update(clusterLights, this.lightingParams);
}
// Store original materials
this.backupMaterials(rcv);
for(pass = 0; pass < passCount; pass++){
// only bake first virtual light for pass 1, as it does not handle overlapping lights
if (pass > 0 && virtualLightIndex > 0) {
break;
}
// don't bake ambient light in pass 1, as there's no main direction
if (isAmbientLight && pass > 0) {
break;
}
DebugGraphics.pushGpuMarker(device, "LMPass:" + pass);
// lightmap size
var nodeRT = bakeNode1.renderTargets[pass];
var lightmapSize = bakeNode1.renderTargets[pass].colorBuffer.width;
// get matching temp render target to render to
var tempRT = this.renderTargets.get(lightmapSize);
var tempTex = tempRT.colorBuffer;
if (pass === 0) {
shadersUpdatedOn1stPass = scene.updateShaders;
} else if (shadersUpdatedOn1stPass) {
scene.updateShaders = true;
}
var passMaterial = this.passMaterials[pass];
if (isAmbientLight) {
// for last virtual light of ambient light, multiply accumulated AO lightmap with ambient light
var lastVirtualLightForPass = virtualLightIndex + 1 === numVirtualLights;
if (lastVirtualLightForPass && pass === 0) {
passMaterial = this.ambientAOMaterial;
}
}
// set up material for baking a pass
for(j = 0; j < rcv.length; j++){
rcv[j].material = passMaterial;
}
// update shader
this.renderer.updateShaders(rcv);
// render receivers to the tempRT
if (pass === PASS_DIR) {
this.constantBakeDir.setValue(bakeLight.light.bakeDir ? 1 : 0);
}
if (device.isWebGPU) {
// TODO: On WebGPU we use a render pass, but this has some issue it seems,
// and needs to be investigated and fixed. In the LightsBaked example, edges of
// some geometry are not lit correctly, especially visible on boxes. Most likely
// some global per frame / per camera constants are not set up or similar, that
// renderForward sets up.
var renderPass = new RenderPassLightmapper(device, this.renderer, this.camera, clusteredLightingEnabled ? this.worldClusters : null, rcv, lightArray);
renderPass.init(tempRT);
renderPass.render();
renderPass.destroy();
} else {
// ping-ponging output
this.renderer.setCamera(this.camera, tempRT, true);
// prepare clustered lighting
if (clusteredLightingEnabled) {
this.worldClusters.activate();
}
this.renderer._forwardTime = 0;
this.renderer._shadowMapTime = 0;
this.renderer.renderForward(this.camera, tempRT, rcv, lightArray, SHADER_FORWARD);
device.updateEnd();
}
this.stats.shadowMapTime += this.renderer._shadowMapTime;
this.stats.forwardTime += this.renderer._forwardTime;
this.stats.renderPasses++;
// temp render target now has lightmap, store it for the node
bakeNode1.renderTargets[pass] = tempRT;
// and release previous lightmap into temp render target pool
this.renderTargets.set(lightmapSize, nodeRT);
for(j = 0; j < rcv.length; j++){
m = rcv[j];
m.setRealtimeLightmap(MeshInstance.lightmapParamNames[pass], tempTex); // ping-ponging input
m._shaderDefs |= SHADERDEF_LM; // force using LM even if material doesn't have it
}
DebugGraphics.popGpuMarker(device);
}
// Revert to original materials
this.restoreMaterials(rcv);
}
bakeLight.endBake(this.shadowMapCache);
DebugGraphics.popGpuMarker(device);
}
}
this.postprocessTextures(device, bakeNodes, passCount);
// restore changes
for(node = 0; node < allNodes.length; node++){
allNodes[node].restore();
}
this.restoreLights(allLights);
this.restoreScene();
// empty cache to minimize persistent memory use .. if some cached textures are needed,
// they will be allocated again as needed
if (!clusteredLightingEnabled) {
this.shadowMapCache.clear();
}
}
/**
* Create a new Lightmapper instance.
*
* @param {GraphicsDevice} device - The graphics device used by the lightmapper.
* @param {Entity} root - The root entity of the scene.
* @param {Scene} scene - The scene to lightmap.
* @param {ForwardRenderer} renderer - The renderer.
* @param {AssetRegistry} assets - Registry of assets to lightmap.
* @ignore
*/ constructor(device, root, scene, renderer, assets){
this.device = device;
this.root = root;
this.scene = scene;
this.renderer = renderer;
this.assets = assets;
this.shadowMapCache = renderer.shadowMapCache;
this._tempSet = new Set();
this._initCalled = false;
// internal materials used by baking
this.passMaterials = [];
this.ambientAOMaterial = null;
this.fog = '';
this.ambientLight = new Color();
// dictionary of spare render targets with color buffer for each used size
this.renderTargets = new Map();
this.stats = {
renderPasses: 0,
lightmapCount: 0,
totalRenderTime: 0,
forwardTime: 0,
fboTime: 0,
shadowMapTime: 0,
compileTime: 0,
shadersLinked: 0
};
}
}
export { Lightmapper };