UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

909 lines (906 loc) 43.2 kB
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 };