playcanvas
Version:
PlayCanvas WebGL game engine
266 lines (263 loc) • 12.9 kB
JavaScript
import { Vec3 } from '../../core/math/vec3.js';
import { FILTER_NEAREST, TEXTURETYPE_DEFAULT, ADDRESS_CLAMP_TO_EDGE, PIXELFORMAT_RGBA8, PIXELFORMAT_RGBA32F } 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 { LightCamera } from '../renderer/light-camera.js';
import { shaderChunks } from '../shader-lib/chunks/chunks.js';
import { shaderChunksWGSL } from '../shader-lib/chunks-wgsl/chunks-wgsl.js';
var epsilon = 0.000001;
var tempVec3 = new Vec3();
var tempAreaLightSizes = new Float32Array(6);
var areaHalfAxisWidth = new Vec3(-0.5, 0, 0);
var 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
var TextureIndex8 = {
// format of the 8bit texture data
FLAGS: 0,
COLOR_A: 1,
COLOR_B: 2,
SPOT_ANGLES: 3,
SHADOW_BIAS: 4,
COOKIE_A: 5,
COOKIE_B: 6,
// leave last
COUNT: 7
};
// format of the float texture data
var TextureIndexFloat = {
POSITION_RANGE: 0,
SPOT_DIRECTION: 1,
PROJ_MAT_0: 2,
ATLAS_VIEWPORT: 2,
PROJ_MAT_1: 3,
PROJ_MAT_2: 4,
PROJ_MAT_3: 5,
AREA_DATA_WIDTH: 6,
AREA_DATA_HEIGHT: 7,
// leave last
COUNT: 8
};
// converts object with properties to a list of these as an example: "#define CLUSTER_TEXTURE_8_BLAH 1"
var buildShaderDefines = (object, prefix)=>{
return Object.keys(object).map((key)=>"#define " + prefix + key + " " + object[key]).join('\n');
};
// create a shader chunk with defines for the light buffer textures
shaderChunks.lightBufferDefinesPS = shaderChunksWGSL.lightBufferDefinesPS = "\n\n " + buildShaderDefines(TextureIndex8, 'CLUSTER_TEXTURE_8_') + "\n " + buildShaderDefines(TextureIndexFloat, 'CLUSTER_TEXTURE_F_') + "\n";
// A class used by clustered lighting, responsible for encoding light properties into textures for the use on the GPU
class LightsBuffer {
destroy() {
var // release textures
_this_lightsTexture8, _this_lightsTextureFloat;
(_this_lightsTexture8 = this.lightsTexture8) == null ? void 0 : _this_lightsTexture8.destroy();
this.lightsTexture8 = null;
(_this_lightsTextureFloat = this.lightsTextureFloat) == null ? void 0 : _this_lightsTextureFloat.destroy();
this.lightsTextureFloat = null;
}
createTexture(device, width, height, format, name) {
var 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() {
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);
this._lightsTextureFloatId.setValue(this.lightsTextureFloat);
}
getSpotDirection(direction, spot) {
// Spots shine down the negative Y axis
var 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) {
var 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] = this.areaLightsEnabled ? light._shape * 64 : 0; // value 0..3
data8[index + 2] = light._falloffMode * 255; // value 0..1
data8[index + 3] = castShadows ? shadowIntensity * 255 : 0;
}
addLightDataColor(data8, index, light, isCookie) {
var invMaxColorValue = this.invMaxColorValue;
var color = light._colorLinear;
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
var isDynamic = !!(light.mask & MASK_AFFECT_DYNAMIC);
var 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) {
var lightRenderData = light.getRenderData(null, 0);
var 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
}
addLightDataCookies(data8, index, light) {
var 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) {
var 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;
}
}
// fill up both float and 8bit texture data with light properties
addLightData(light, lightIndex) {
var isSpot = light._type === LIGHTTYPE_SPOT;
var hasAtlasViewport = light.atlasViewportAllocated; // if the light does not have viewport, it does not fit to the atlas
var isCookie = this.cookiesEnabled && !!light._cookie && hasAtlasViewport;
var isArea = this.areaLightsEnabled && light.shape !== LIGHTSHAPE_PUNCTUAL;
var castShadows = this.shadowsEnabled && light.castShadows && hasAtlasViewport;
var pos = light._node.getPosition();
var lightProjectionMatrix = null; // light projection matrix - used for shadow map and cookie of spot light
var atlasViewport = null; // atlas viewport info - used for shadow map and cookie of omni light
if (isSpot) {
if (castShadows) {
var 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
var data8 = this.lights8;
var 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, 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);
}
var dataFloat = this.lightsFloat;
var 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) {
var matData = lightProjectionMatrix.data;
for(var 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) {
var 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];
}
}
constructor(device){
this.areaLightsEnabled = false;
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;
// 8bit texture - to store data that can fit into 8bits to lower the bandwidth requirements
var pixelsPerLight8 = TextureIndex8.COUNT;
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
var pixelsPerLightFloat = TextureIndexFloat.COUNT;
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');
// compression ranges
this.invMaxColorValue = 0;
this.invMaxAttenuation = 0;
this.boundsMin = new Vec3();
this.boundsDelta = new Vec3();
}
}
export { LightsBuffer };