UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

320 lines (317 loc) 14.7 kB
import { Vec2 } from '../../core/math/vec2.js'; import { Vec4 } from '../../core/math/vec4.js'; import { ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, PIXELFORMAT_SRGBA8 } from '../../platform/graphics/constants.js'; import { RenderTarget } from '../../platform/graphics/render-target.js'; import { Texture } from '../../platform/graphics/texture.js'; import { shadowTypeInfo, LIGHTTYPE_SPOT, LIGHTTYPE_OMNI, SHADOW_PCF3_32F } from '../constants.js'; import { ShadowMap } from '../renderer/shadow-map.js'; var _tempArray = []; var _tempArray2 = []; var _viewport = new Vec4(); var _scissor = new Vec4(); class Slot { constructor(rect){ this.size = Math.floor(rect.w * 1024); // size normalized to 1024 atlas this.used = false; this.lightId = -1; // id of the light using the slot this.rect = rect; } } // A class handling runtime allocation of slots in a texture. It is used to allocate slots in the shadow and cookie atlas. class LightTextureAtlas { destroy() { this.destroyShadowAtlas(); this.destroyCookieAtlas(); } destroyShadowAtlas() { var _this_shadowAtlas; (_this_shadowAtlas = this.shadowAtlas) == null ? void 0 : _this_shadowAtlas.destroy(); this.shadowAtlas = null; } destroyCookieAtlas() { var _this_cookieAtlas, _this_cookieRenderTarget; (_this_cookieAtlas = this.cookieAtlas) == null ? void 0 : _this_cookieAtlas.destroy(); this.cookieAtlas = null; (_this_cookieRenderTarget = this.cookieRenderTarget) == null ? void 0 : _this_cookieRenderTarget.destroy(); this.cookieRenderTarget = null; } allocateShadowAtlas(resolution, shadowType) { if (shadowType === void 0) shadowType = SHADOW_PCF3_32F; var _this_shadowAtlas; var existingFormat = (_this_shadowAtlas = this.shadowAtlas) == null ? void 0 : _this_shadowAtlas.texture.format; var requiredFormat = shadowTypeInfo.get(shadowType).format; if (!this.shadowAtlas || this.shadowAtlas.texture.width !== resolution || existingFormat !== requiredFormat) { // content of atlas is lost, force re-render of static shadows this.version++; this.destroyShadowAtlas(); this.shadowAtlas = ShadowMap.createAtlas(this.device, resolution, shadowType); // avoid it being destroyed by lights this.shadowAtlas.cached = true; // leave gap between individual tiles to avoid shadow / cookie sampling other tiles (enough for PCF5) // note that this only fades / removes shadows on the edges, which is still not correct - a shader clipping is needed? var scissorOffset = 4 / this.shadowAtlasResolution; this.scissorVec.set(scissorOffset, scissorOffset, -2 * scissorOffset, -2 * scissorOffset); } } allocateCookieAtlas(resolution) { // resize atlas if (this.cookieAtlas.width !== resolution) { this.cookieRenderTarget.resize(resolution, resolution); // content of atlas is lost, force re-render of static cookies this.version++; } } allocateUniforms() { this._shadowAtlasTextureId = this.device.scope.resolve('shadowAtlasTexture'); this._shadowAtlasParamsId = this.device.scope.resolve('shadowAtlasParams'); this._shadowAtlasParams = new Float32Array(2); this._cookieAtlasTextureId = this.device.scope.resolve('cookieAtlasTexture'); } updateUniforms() { // shadow atlas texture var rt = this.shadowAtlas.renderTargets[0]; var shadowBuffer = rt.depthBuffer; this._shadowAtlasTextureId.setValue(shadowBuffer); // shadow atlas params this._shadowAtlasParams[0] = this.shadowAtlasResolution; this._shadowAtlasParams[1] = this.shadowEdgePixels; this._shadowAtlasParamsId.setValue(this._shadowAtlasParams); // cookie atlas textures this._cookieAtlasTextureId.setValue(this.cookieAtlas); } subdivide(numLights, lightingParams) { var atlasSplit = lightingParams.atlasSplit; // if no user specified subdivision if (!atlasSplit) { // split to equal number of squares var gridSize = Math.ceil(Math.sqrt(numLights)); atlasSplit = _tempArray2; atlasSplit[0] = gridSize; atlasSplit.length = 1; } // compare two arrays var arraysEqual = (a, b)=>a.length === b.length && a.every((v, i)=>v === b[i]); // if the split has changed, regenerate slots if (!arraysEqual(atlasSplit, this.atlasSplit)) { this.version++; this.slots.length = 0; // store current settings this.atlasSplit.length = 0; this.atlasSplit.push(...atlasSplit); // generate top level split var splitCount = this.atlasSplit[0]; if (splitCount > 1) { var invSize = 1 / splitCount; for(var i = 0; i < splitCount; i++){ for(var j = 0; j < splitCount; j++){ var rect = new Vec4(i * invSize, j * invSize, invSize, invSize); var nextLevelSplit = this.atlasSplit[1 + i * splitCount + j]; // if need to split again if (nextLevelSplit > 1) { for(var x = 0; x < nextLevelSplit; x++){ for(var y = 0; y < nextLevelSplit; y++){ var invSizeNext = invSize / nextLevelSplit; var rectNext = new Vec4(rect.x + x * invSizeNext, rect.y + y * invSizeNext, invSizeNext, invSizeNext); this.slots.push(new Slot(rectNext)); } } } else { this.slots.push(new Slot(rect)); } } } } else { // single slot this.slots.push(new Slot(new Vec4(0, 0, 1, 1))); } // sort slots descending this.slots.sort((a, b)=>{ return b.size - a.size; }); } } collectLights(localLights, lightingParams) { var cookiesEnabled = lightingParams.cookiesEnabled; var shadowsEnabled = lightingParams.shadowsEnabled; // get all lights that need shadows or cookies, if those are enabled var needsShadowAtlas = false; var needsCookieAtlas = false; var lights = _tempArray; lights.length = 0; var processLights = (list)=>{ for(var i = 0; i < list.length; i++){ var light = list[i]; if (light.visibleThisFrame) { var lightShadow = shadowsEnabled && light.castShadows; var lightCookie = cookiesEnabled && !!light.cookie; needsShadowAtlas || (needsShadowAtlas = lightShadow); needsCookieAtlas || (needsCookieAtlas = lightCookie); if (lightShadow || lightCookie) { lights.push(light); } } } }; if (cookiesEnabled || shadowsEnabled) { processLights(localLights); } // sort lights by maxScreenSize - to have them ordered by atlas slot size lights.sort((a, b)=>{ return b.maxScreenSize - a.maxScreenSize; }); if (needsShadowAtlas) { this.allocateShadowAtlas(this.shadowAtlasResolution, lightingParams.shadowType); } if (needsCookieAtlas) { this.allocateCookieAtlas(this.cookieAtlasResolution); } if (needsShadowAtlas || needsCookieAtlas) { this.subdivide(lights.length, lightingParams); } return lights; } // configure light to use assigned slot setupSlot(light, rect) { light.atlasViewport.copy(rect); var faceCount = light.numShadowFaces; for(var face = 0; face < faceCount; face++){ // setup slot for shadow and cookie if (light.castShadows || light._cookie) { _viewport.copy(rect); _scissor.copy(rect); // for spot lights in the atlas, make viewport slightly smaller to avoid sampling past the edges if (light._type === LIGHTTYPE_SPOT) { _viewport.add(this.scissorVec); } // for cube map, allocate part of the slot if (light._type === LIGHTTYPE_OMNI) { var smallSize = _viewport.z / 3; var offset = this.cubeSlotsOffsets[face]; _viewport.x += smallSize * offset.x; _viewport.y += smallSize * offset.y; _viewport.z = smallSize; _viewport.w = smallSize; _scissor.copy(_viewport); } if (light.castShadows) { var lightRenderData = light.getRenderData(null, face); lightRenderData.shadowViewport.copy(_viewport); lightRenderData.shadowScissor.copy(_scissor); } } } } // assign a slot to the light assignSlot(light, slotIndex, slotReassigned) { light.atlasViewportAllocated = true; var slot = this.slots[slotIndex]; slot.lightId = light.id; slot.used = true; // slot is reassigned (content needs to be updated) if (slotReassigned) { light.atlasSlotUpdated = true; light.atlasVersion = this.version; light.atlasSlotIndex = slotIndex; } } // update texture atlas for a list of lights update(localLights, lightingParams) { // update texture resolutions this.shadowAtlasResolution = lightingParams.shadowAtlasResolution; this.cookieAtlasResolution = lightingParams.cookieAtlasResolution; // collect lights requiring atlas var lights = this.collectLights(localLights, lightingParams); if (lights.length > 0) { // mark all slots as unused var slots = this.slots; for(var i = 0; i < slots.length; i++){ slots[i].used = false; } // assign slots to lights // The slot to light assignment logic: // - internally the atlas slots are sorted in the descending order (done when atlas split changes) // - every frame all visible lights are sorted by their screen space size (this handles all cameras where lights // are visible using max value) // - all lights in this order get a slot size from the slot list in the same order. Care is taken to not reassign // slot if the size of it is the same and only index changes - this is done using two pass assignment var assignCount = Math.min(lights.length, slots.length); // first pass - preserve allocated slots for lights requiring slot of the same size for(var i1 = 0; i1 < assignCount; i1++){ var light = lights[i1]; if (light.castShadows) { light._shadowMap = this.shadowAtlas; } // if currently assigned slot is the same size as what is needed, and was last used by this light, reuse it var previousSlot = slots[light.atlasSlotIndex]; if (light.atlasVersion === this.version && light.id === (previousSlot == null ? void 0 : previousSlot.lightId)) { var previousSlot1 = slots[light.atlasSlotIndex]; if (previousSlot1.size === slots[i1].size && !previousSlot1.used) { this.assignSlot(light, light.atlasSlotIndex, false); } } } // second pass - assign slots to unhandled lights var usedCount = 0; for(var i2 = 0; i2 < assignCount; i2++){ // skip already used slots while(usedCount < slots.length && slots[usedCount].used){ usedCount++; } var light1 = lights[i2]; if (!light1.atlasViewportAllocated) { this.assignSlot(light1, usedCount, true); } // set up all slots var slot = slots[light1.atlasSlotIndex]; this.setupSlot(light1, slot.rect); } } this.updateUniforms(); } constructor(device){ this.device = device; this.version = 1; // incremented each time slot configuration changes this.shadowAtlasResolution = 2048; this.shadowAtlas = null; // number of additional pixels to render past the required shadow camera angle (90deg for omni, outer for spot) of the shadow camera for clustered lights. // This needs to be a pixel more than a shadow filter needs to access. this.shadowEdgePixels = 3; this.cookieAtlasResolution = 4; this.cookieAtlas = new Texture(this.device, { name: 'CookieAtlas', width: this.cookieAtlasResolution, height: this.cookieAtlasResolution, format: PIXELFORMAT_SRGBA8, cubemap: false, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }); this.cookieRenderTarget = new RenderTarget({ colorBuffer: this.cookieAtlas, depth: false, flipY: true }); // available slots (of type Slot) this.slots = []; // current subdivision strategy - matches format of LightingParams.atlasSplit this.atlasSplit = []; // offsets to individual faces of a cubemap inside 3x3 grid in an atlas slot this.cubeSlotsOffsets = [ new Vec2(0, 0), new Vec2(0, 1), new Vec2(1, 0), new Vec2(1, 1), new Vec2(2, 0), new Vec2(2, 1) ]; // handles gap between slots this.scissorVec = new Vec4(); this.allocateShadowAtlas(1); // placeholder as shader requires it this.allocateCookieAtlas(1); // placeholder as shader requires it this.allocateUniforms(); } } export { LightTextureAtlas };