UNPKG

@animech-public/playcanvas

Version:
480 lines (434 loc) 18.9 kB
import { Vec3 } from '../../core/math/vec3.js'; import { PIXELFORMAT_RGBA8, PIXELFORMAT_RGBA32F, ADDRESS_CLAMP_TO_EDGE, TEXTURETYPE_DEFAULT, FILTER_NEAREST } from '../../platform/graphics/constants.js'; import { FloatPacking } from '../../core/math/float-packing.js'; import { MASK_AFFECT_DYNAMIC, MASK_AFFECT_LIGHTMAPPED, LIGHTTYPE_SPOT, LIGHTSHAPE_PUNCTUAL } from '../constants.js'; import { Texture } from '../../platform/graphics/texture.js'; import { DeviceCache } from '../../platform/graphics/device-cache.js'; import { LightCamera } from '../renderer/light-camera.js'; const epsilon = 0.000001; const tempVec3 = new Vec3(); const tempAreaLightSizes = new Float32Array(6); const areaHalfAxisWidth = new Vec3(-0.5, 0, 0); const areaHalfAxisHeight = new Vec3(0, 0, 0.5); // format of a row in 8 bit texture used to encode light data // this is used to store data in the texture correctly, and also use to generate defines for the shader const TextureIndex8 = { // always 8bit texture data, regardless of float texture support FLAGS: 0, // lightType, lightShape, fallofMode, castShadows COLOR_A: 1, // color.r, color.r, color.g, color.g // HDR color is stored using 2 bytes per channel COLOR_B: 2, // color.b, color.b, useCookie, lightMask SPOT_ANGLES: 3, // spotInner, spotInner, spotOuter, spotOuter SHADOW_BIAS: 4, // bias, bias, normalBias, normalBias COOKIE_A: 5, // cookieIntensity, cookieIsRgb, -, - COOKIE_B: 6, // cookieChannelMask.xyzw // leave in-between COUNT_ALWAYS: 7, // 8bit texture data used when float texture is not supported POSITION_X: 7, // position.x POSITION_Y: 8, // position.y POSITION_Z: 9, // position.z RANGE: 10, // range SPOT_DIRECTION_X: 11, // spot direction x SPOT_DIRECTION_Y: 12, // spot direction y SPOT_DIRECTION_Z: 13, // spot direction z PROJ_MAT_00: 14, // light projection matrix, mat4, 16 floats ATLAS_VIEWPORT_A: 14, // viewport.x, viewport.x, viewport.y, viewport.y PROJ_MAT_01: 15, ATLAS_VIEWPORT_B: 15, // viewport.z, viewport.z, -, - PROJ_MAT_02: 16, PROJ_MAT_03: 17, PROJ_MAT_10: 18, PROJ_MAT_11: 19, PROJ_MAT_12: 20, PROJ_MAT_13: 21, PROJ_MAT_20: 22, PROJ_MAT_21: 23, PROJ_MAT_22: 24, PROJ_MAT_23: 25, PROJ_MAT_30: 26, PROJ_MAT_31: 27, PROJ_MAT_32: 28, PROJ_MAT_33: 29, AREA_DATA_WIDTH_X: 30, AREA_DATA_WIDTH_Y: 31, AREA_DATA_WIDTH_Z: 32, AREA_DATA_HEIGHT_X: 33, AREA_DATA_HEIGHT_Y: 34, AREA_DATA_HEIGHT_Z: 35, // leave last COUNT: 36 }; // format of the float texture const TextureIndexFloat = { POSITION_RANGE: 0, // positions.xyz, range SPOT_DIRECTION: 1, // spot direction.xyz, - PROJ_MAT_0: 2, // projection matrix row 0 (spot light) ATLAS_VIEWPORT: 2, // atlas viewport data (omni light) PROJ_MAT_1: 3, // projection matrix row 1 (spot light) PROJ_MAT_2: 4, // projection matrix row 2 (spot light) PROJ_MAT_3: 5, // projection matrix row 3 (spot light) AREA_DATA_WIDTH: 6, // area light half-width.xyz, - AREA_DATA_HEIGHT: 7, // area light half-height.xyz, - // leave last COUNT: 8 }; // format for high precision light texture - float const FORMAT_FLOAT = 0; // format for high precision light texture - 8bit const FORMAT_8BIT = 1; // device cache storing shader defines for the device const shaderDefinesDeviceCache = new DeviceCache(); // A class used by clustered lighting, responsible for encoding light properties into textures for the use on the GPU class LightsBuffer { static getLightTextureFormat(device) { // precision for texture storage // don't use float texture on devices with small number of texture units (as it uses both float and 8bit textures at the same time) return device.extTextureFloat && device.maxTextures > 8 ? FORMAT_FLOAT : FORMAT_8BIT; } static getShaderDefines(device) { // return defines for this device from the cache, or create them if not cached yet return shaderDefinesDeviceCache.get(device, () => { // converts object with properties to a list of these as an example: "#define CLUSTER_TEXTURE_8_BLAH 1.5" const buildShaderDefines = (device, object, prefix, floatOffset) => { return Object.keys(object).map(key => `#define ${prefix}${key} ${object[key]}${floatOffset}`).join('\n'); }; const lightTextureFormat = LightsBuffer.getLightTextureFormat(device); const clusterTextureFormat = lightTextureFormat === FORMAT_FLOAT ? 'FLOAT' : '8BIT'; // on webgl2 and WebGPU we use texelFetch instruction to read data textures, and don't need the offset const floatOffset = device.supportsTextureFetch ? '' : '.5'; return ` \n#define CLUSTER_TEXTURE_${clusterTextureFormat} ${buildShaderDefines(device, TextureIndex8, 'CLUSTER_TEXTURE_8_', floatOffset)} ${buildShaderDefines(device, TextureIndexFloat, 'CLUSTER_TEXTURE_F_', floatOffset)} `; }); } constructor(device) { this.device = device; // features this.cookiesEnabled = false; this.shadowsEnabled = false; this.areaLightsEnabled = false; // using 8 bit index so this is maximum supported number of lights this.maxLights = 255; // shared 8bit texture pixels: let pixelsPerLight8 = TextureIndex8.COUNT_ALWAYS; let pixelsPerLightFloat = 0; this.lightTextureFormat = LightsBuffer.getLightTextureFormat(device); // float texture format if (this.lightTextureFormat === FORMAT_FLOAT) { pixelsPerLightFloat = TextureIndexFloat.COUNT; } else { // 8bit texture pixelsPerLight8 = TextureIndex8.COUNT; } // 8bit texture - to store data that can fit into 8bits to lower the bandwidth requirements this.lights8 = new Uint8ClampedArray(4 * pixelsPerLight8 * this.maxLights); this.lightsTexture8 = this.createTexture(this.device, pixelsPerLight8, this.maxLights, PIXELFORMAT_RGBA8, 'LightsTexture8'); this._lightsTexture8Id = this.device.scope.resolve('lightsTexture8'); // float texture if (pixelsPerLightFloat) { this.lightsFloat = new Float32Array(4 * pixelsPerLightFloat * this.maxLights); this.lightsTextureFloat = this.createTexture(this.device, pixelsPerLightFloat, this.maxLights, PIXELFORMAT_RGBA32F, 'LightsTextureFloat'); this._lightsTextureFloatId = this.device.scope.resolve('lightsTextureFloat'); } else { this.lightsFloat = null; this.lightsTextureFloat = null; this._lightsTextureFloatId = undefined; } // inverse sizes for both textures this._lightsTextureInvSizeId = this.device.scope.resolve('lightsTextureInvSize'); this._lightsTextureInvSizeData = new Float32Array(4); this._lightsTextureInvSizeData[0] = pixelsPerLightFloat ? 1.0 / this.lightsTextureFloat.width : 0; this._lightsTextureInvSizeData[1] = pixelsPerLightFloat ? 1.0 / this.lightsTextureFloat.height : 0; this._lightsTextureInvSizeData[2] = 1.0 / this.lightsTexture8.width; this._lightsTextureInvSizeData[3] = 1.0 / this.lightsTexture8.height; // compression ranges this.invMaxColorValue = 0; this.invMaxAttenuation = 0; this.boundsMin = new Vec3(); this.boundsDelta = new Vec3(); } destroy() { // release textures if (this.lightsTexture8) { this.lightsTexture8.destroy(); this.lightsTexture8 = null; } if (this.lightsTextureFloat) { this.lightsTextureFloat.destroy(); this.lightsTextureFloat = null; } } createTexture(device, width, height, format, name) { const tex = new Texture(device, { name: name, width: width, height: height, mipmaps: false, format: format, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, type: TEXTURETYPE_DEFAULT, magFilter: FILTER_NEAREST, minFilter: FILTER_NEAREST, anisotropy: 1 }); return tex; } setCompressionRanges(maxAttenuation, maxColorValue) { this.invMaxColorValue = 1 / maxColorValue; this.invMaxAttenuation = 1 / maxAttenuation; } setBounds(min, delta) { this.boundsMin.copy(min); this.boundsDelta.copy(delta); } uploadTextures() { if (this.lightsTextureFloat) { this.lightsTextureFloat.lock().set(this.lightsFloat); this.lightsTextureFloat.unlock(); } this.lightsTexture8.lock().set(this.lights8); this.lightsTexture8.unlock(); } updateUniforms() { // textures this._lightsTexture8Id.setValue(this.lightsTexture8); if (this.lightTextureFormat === FORMAT_FLOAT) { this._lightsTextureFloatId.setValue(this.lightsTextureFloat); } this._lightsTextureInvSizeId.setValue(this._lightsTextureInvSizeData); } getSpotDirection(direction, spot) { // Spots shine down the negative Y axis const mat = spot._node.getWorldTransform(); mat.getY(direction).mulScalar(-1); direction.normalize(); } // half sizes of area light in world space, returned as an array of 6 floats getLightAreaSizes(light) { const mat = light._node.getWorldTransform(); mat.transformVector(areaHalfAxisWidth, tempVec3); tempAreaLightSizes[0] = tempVec3.x; tempAreaLightSizes[1] = tempVec3.y; tempAreaLightSizes[2] = tempVec3.z; mat.transformVector(areaHalfAxisHeight, tempVec3); tempAreaLightSizes[3] = tempVec3.x; tempAreaLightSizes[4] = tempVec3.y; tempAreaLightSizes[5] = tempVec3.z; return tempAreaLightSizes; } addLightDataFlags(data8, index, light, isSpot, castShadows, shadowIntensity) { data8[index + 0] = isSpot ? 255 : 0; data8[index + 1] = light._shape * 64; // value 0..3 data8[index + 2] = light._falloffMode * 255; // value 0..1 data8[index + 3] = castShadows ? shadowIntensity * 255 : 0; } addLightDataColor(data8, index, light, gammaCorrection, isCookie) { const invMaxColorValue = this.invMaxColorValue; const color = gammaCorrection ? light._linearFinalColor : light._finalColor; FloatPacking.float2Bytes(color[0] * invMaxColorValue, data8, index + 0, 2); FloatPacking.float2Bytes(color[1] * invMaxColorValue, data8, index + 2, 2); FloatPacking.float2Bytes(color[2] * invMaxColorValue, data8, index + 4, 2); // cookie data8[index + 6] = isCookie ? 255 : 0; // lightMask // 0: MASK_AFFECT_DYNAMIC // 127: MASK_AFFECT_DYNAMIC && MASK_AFFECT_LIGHTMAPPED // 255: MASK_AFFECT_LIGHTMAPPED const isDynamic = !!(light.mask & MASK_AFFECT_DYNAMIC); const isLightmapped = !!(light.mask & MASK_AFFECT_LIGHTMAPPED); data8[index + 7] = isDynamic && isLightmapped ? 127 : isLightmapped ? 255 : 0; } addLightDataSpotAngles(data8, index, light) { // 2 bytes each FloatPacking.float2Bytes(light._innerConeAngleCos * (0.5 - epsilon) + 0.5, data8, index + 0, 2); FloatPacking.float2Bytes(light._outerConeAngleCos * (0.5 - epsilon) + 0.5, data8, index + 2, 2); } addLightDataShadowBias(data8, index, light) { const lightRenderData = light.getRenderData(null, 0); const biases = light._getUniformBiasValues(lightRenderData); FloatPacking.float2BytesRange(biases.bias, data8, index, -1, 20, 2); // bias: -1 to 20 range FloatPacking.float2Bytes(biases.normalBias, data8, index + 2, 2); // normalBias: 0 to 1 range } addLightDataPositionRange(data8, index, light, pos) { // position and range scaled to 0..1 range const normPos = tempVec3.sub2(pos, this.boundsMin).div(this.boundsDelta); FloatPacking.float2Bytes(normPos.x, data8, index + 0, 4); FloatPacking.float2Bytes(normPos.y, data8, index + 4, 4); FloatPacking.float2Bytes(normPos.z, data8, index + 8, 4); FloatPacking.float2Bytes(light.attenuationEnd * this.invMaxAttenuation, data8, index + 12, 4); } addLightDataSpotDirection(data8, index, light) { this.getSpotDirection(tempVec3, light); FloatPacking.float2Bytes(tempVec3.x * (0.5 - epsilon) + 0.5, data8, index + 0, 4); FloatPacking.float2Bytes(tempVec3.y * (0.5 - epsilon) + 0.5, data8, index + 4, 4); FloatPacking.float2Bytes(tempVec3.z * (0.5 - epsilon) + 0.5, data8, index + 8, 4); } addLightDataLightProjMatrix(data8, index, lightProjectionMatrix) { const matData = lightProjectionMatrix.data; // these are in -2..2 range for (let m = 0; m < 12; m++) { FloatPacking.float2BytesRange(matData[m], data8, index + 4 * m, -2, 2, 4); } for (let m = 12; m < 16; m++) { // these are full float range FloatPacking.float2MantissaExponent(matData[m], data8, index + 4 * m, 4); } } addLightDataCookies(data8, index, light) { const isRgb = light._cookieChannel === 'rgb'; data8[index + 0] = Math.floor(light.cookieIntensity * 255); data8[index + 1] = isRgb ? 255 : 0; // we have two unused bytes here if (!isRgb) { const channel = light._cookieChannel; data8[index + 4] = channel === 'rrr' ? 255 : 0; data8[index + 5] = channel === 'ggg' ? 255 : 0; data8[index + 6] = channel === 'bbb' ? 255 : 0; data8[index + 7] = channel === 'aaa' ? 255 : 0; } } addLightAtlasViewport(data8, index, atlasViewport) { // all these are in 0..1 range FloatPacking.float2Bytes(atlasViewport.x, data8, index + 0, 2); FloatPacking.float2Bytes(atlasViewport.y, data8, index + 2, 2); FloatPacking.float2Bytes(atlasViewport.z / 3, data8, index + 4, 2); // we have two unused bytes here } addLightAreaSizes(data8, index, light) { const areaSizes = this.getLightAreaSizes(light); for (let i = 0; i < 6; i++) { // these are full float range FloatPacking.float2MantissaExponent(areaSizes[i], data8, index + 4 * i, 4); } } // fill up both float and 8bit texture data with light properties addLightData(light, lightIndex, gammaCorrection) { const isSpot = light._type === LIGHTTYPE_SPOT; const hasAtlasViewport = light.atlasViewportAllocated; // if the light does not have viewport, it does not fit to the atlas const isCookie = this.cookiesEnabled && !!light._cookie && hasAtlasViewport; const isArea = this.areaLightsEnabled && light.shape !== LIGHTSHAPE_PUNCTUAL; const castShadows = this.shadowsEnabled && light.castShadows && hasAtlasViewport; const pos = light._node.getPosition(); let lightProjectionMatrix = null; // light projection matrix - used for shadow map and cookie of spot light let atlasViewport = null; // atlas viewport info - used for shadow map and cookie of omni light if (isSpot) { if (castShadows) { const lightRenderData = light.getRenderData(null, 0); lightProjectionMatrix = lightRenderData.shadowMatrix; } else if (isCookie) { lightProjectionMatrix = LightCamera.evalSpotCookieMatrix(light); } } else { if (castShadows || isCookie) { atlasViewport = light.atlasViewport; } } // data always stored in 8bit texture const data8 = this.lights8; const data8Start = lightIndex * this.lightsTexture8.width * 4; // flags this.addLightDataFlags(data8, data8Start + 4 * TextureIndex8.FLAGS, light, isSpot, castShadows, light.shadowIntensity); // light color this.addLightDataColor(data8, data8Start + 4 * TextureIndex8.COLOR_A, light, gammaCorrection, isCookie); // spot light angles if (isSpot) { this.addLightDataSpotAngles(data8, data8Start + 4 * TextureIndex8.SPOT_ANGLES, light); } // shadow biases if (light.castShadows) { this.addLightDataShadowBias(data8, data8Start + 4 * TextureIndex8.SHADOW_BIAS, light); } // cookie properties if (isCookie) { this.addLightDataCookies(data8, data8Start + 4 * TextureIndex8.COOKIE_A, light); } // high precision data stored using float texture if (this.lightTextureFormat === FORMAT_FLOAT) { const dataFloat = this.lightsFloat; const dataFloatStart = lightIndex * this.lightsTextureFloat.width * 4; // pos and range dataFloat[dataFloatStart + 4 * TextureIndexFloat.POSITION_RANGE + 0] = pos.x; dataFloat[dataFloatStart + 4 * TextureIndexFloat.POSITION_RANGE + 1] = pos.y; dataFloat[dataFloatStart + 4 * TextureIndexFloat.POSITION_RANGE + 2] = pos.z; dataFloat[dataFloatStart + 4 * TextureIndexFloat.POSITION_RANGE + 3] = light.attenuationEnd; // spot direction if (isSpot) { this.getSpotDirection(tempVec3, light); dataFloat[dataFloatStart + 4 * TextureIndexFloat.SPOT_DIRECTION + 0] = tempVec3.x; dataFloat[dataFloatStart + 4 * TextureIndexFloat.SPOT_DIRECTION + 1] = tempVec3.y; dataFloat[dataFloatStart + 4 * TextureIndexFloat.SPOT_DIRECTION + 2] = tempVec3.z; // here we have unused float } // light projection matrix if (lightProjectionMatrix) { const matData = lightProjectionMatrix.data; for (let m = 0; m < 16; m++) { dataFloat[dataFloatStart + 4 * TextureIndexFloat.PROJ_MAT_0 + m] = matData[m]; } } if (atlasViewport) { dataFloat[dataFloatStart + 4 * TextureIndexFloat.ATLAS_VIEWPORT + 0] = atlasViewport.x; dataFloat[dataFloatStart + 4 * TextureIndexFloat.ATLAS_VIEWPORT + 1] = atlasViewport.y; dataFloat[dataFloatStart + 4 * TextureIndexFloat.ATLAS_VIEWPORT + 2] = atlasViewport.z / 3; // size of a face slot (3x3 grid) } // area light sizes if (isArea) { const areaSizes = this.getLightAreaSizes(light); dataFloat[dataFloatStart + 4 * TextureIndexFloat.AREA_DATA_WIDTH + 0] = areaSizes[0]; dataFloat[dataFloatStart + 4 * TextureIndexFloat.AREA_DATA_WIDTH + 1] = areaSizes[1]; dataFloat[dataFloatStart + 4 * TextureIndexFloat.AREA_DATA_WIDTH + 2] = areaSizes[2]; dataFloat[dataFloatStart + 4 * TextureIndexFloat.AREA_DATA_HEIGHT + 0] = areaSizes[3]; dataFloat[dataFloatStart + 4 * TextureIndexFloat.AREA_DATA_HEIGHT + 1] = areaSizes[4]; dataFloat[dataFloatStart + 4 * TextureIndexFloat.AREA_DATA_HEIGHT + 2] = areaSizes[5]; } } else { // high precision data stored using 8bit texture this.addLightDataPositionRange(data8, data8Start + 4 * TextureIndex8.POSITION_X, light, pos); // spot direction if (isSpot) { this.addLightDataSpotDirection(data8, data8Start + 4 * TextureIndex8.SPOT_DIRECTION_X, light); } // light projection matrix if (lightProjectionMatrix) { this.addLightDataLightProjMatrix(data8, data8Start + 4 * TextureIndex8.PROJ_MAT_00, lightProjectionMatrix); } if (atlasViewport) { this.addLightAtlasViewport(data8, data8Start + 4 * TextureIndex8.ATLAS_VIEWPORT_A, atlasViewport); } // area light sizes if (isArea) { this.addLightAreaSizes(data8, data8Start + 4 * TextureIndex8.AREA_DATA_WIDTH_X, light); } } } } export { LightsBuffer };