playcanvas
Version:
PlayCanvas WebGL game engine
944 lines (942 loc) • 66.1 kB
JavaScript
import { SEMANTIC_ATTR12, SEMANTIC_ATTR13, SEMANTIC_ATTR14, SEMANTIC_ATTR15, SEMANTIC_NORMAL, SEMANTIC_TANGENT, SEMANTIC_COLOR, SEMANTIC_ATTR8, SEMANTIC_ATTR9, SEMANTIC_BLENDINDICES, SEMANTIC_BLENDWEIGHT, SHADERTAG_MATERIAL, SEMANTIC_POSITION, SEMANTIC_TEXCOORD0, SEMANTIC_TEXCOORD1 } from '../../../platform/graphics/constants.js';
import { SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, LIGHTSHAPE_SPHERE, LIGHTSHAPE_DISK, LIGHTSHAPE_RECT, LIGHTTYPE_DIRECTIONAL, shadowTypeInfo, SHADOW_PCSS_32F, LIGHTSHAPE_PUNCTUAL, LIGHTTYPE_SPOT, LIGHTTYPE_OMNI, FRESNEL_SCHLICK, SPECOCC_GLOSSDEPENDENT, SPECOCC_AO, SHADOW_PCF3_32F, SHADOW_PCF5_32F, SHADOW_PCF1_32F, SHADOW_PCF1_16F, SHADOW_PCF3_16F, SHADOW_PCF5_16F, SHADOW_VSM_16F, SHADOW_VSM_32F, LIGHTFALLOFF_LINEAR, BLEND_NORMAL, BLEND_PREMULTIPLIED, BLEND_ADDITIVEALPHA, SHADER_PICK, SHADER_DEPTH, SHADER_PREPASS } from '../../constants.js';
import { shaderChunks } from '../chunks/chunks.js';
import { ChunkUtils } from '../chunk-utils.js';
import { LightsBuffer } from '../../lighting/lights-buffer.js';
import { ShaderPass } from '../../shader-pass.js';
import { validateUserChunks } from '../chunks/chunk-validation.js';
import { ShaderUtils } from '../../../platform/graphics/shader-utils.js';
import { ChunkBuilder } from '../chunk-builder.js';
import { ShaderGenerator } from './shader-generator.js';
import { Debug } from '../../../core/debug.js';
/**
* @import { GraphicsDevice } from '../../../platform/graphics/graphics-device.js'
* @import { LitShaderOptions } from './lit-shader-options.js'
*/ var builtinAttributes = {
vertex_normal: SEMANTIC_NORMAL,
vertex_tangent: SEMANTIC_TANGENT,
vertex_texCoord0: SEMANTIC_TEXCOORD0,
vertex_texCoord1: SEMANTIC_TEXCOORD1,
vertex_color: SEMANTIC_COLOR,
vertex_boneWeights: SEMANTIC_BLENDWEIGHT,
vertex_boneIndices: SEMANTIC_BLENDINDICES
};
var builtinVaryings = {
vVertexColor: 'vec4',
vPositionW: 'vec3',
vNormalV: 'vec3',
vNormalW: 'vec3',
vTangentW: 'vec3',
vBinormalW: 'vec3',
vObjectSpaceUpW: 'vec3',
vUv0: 'vec2',
vUv1: 'vec2',
vLinearDepth: 'float'
};
class LitShader {
_vsAddBaseCode(code, chunks, options) {
code += chunks.baseVS;
if (options.nineSlicedMode === SPRITE_RENDERMODE_SLICED || options.nineSlicedMode === SPRITE_RENDERMODE_TILED) {
code += chunks.baseNineSlicedVS;
}
return code;
}
_setMapTransform(codes, name, id, uv) {
var checkId = id + uv * 100;
if (!codes[3][checkId]) {
// upload a 3x2 matrix and manually perform the multiplication
var varName = "texture_" + name + "MapTransform";
codes[0] += "uniform vec3 " + varName + "0;\n";
codes[0] += "uniform vec3 " + varName + "1;\n";
codes[1] += "varying vec2 vUV" + uv + "_" + id + ";\n";
codes[2] += " vUV" + uv + "_" + id + " = vec2(dot(vec3(uv" + uv + ", 1), " + varName + "0), dot(vec3(uv" + uv + ", 1), " + varName + "1));\n";
codes[3][checkId] = true;
}
return codes;
}
// Add "Base" Code section to fragment shader.
_fsGetBaseCode() {
var options = this.options;
var chunks = this.chunks;
var result = this.chunks.basePS;
if (options.nineSlicedMode === SPRITE_RENDERMODE_SLICED) {
result += chunks.baseNineSlicedPS;
} else if (options.nineSlicedMode === SPRITE_RENDERMODE_TILED) {
result += chunks.baseNineSlicedTiledPS;
}
return result;
}
// Add "Start" Code section to fragment shader.
_fsGetStartCode(code, device, chunks, options) {
var result = chunks.startPS;
if (options.nineSlicedMode === SPRITE_RENDERMODE_SLICED) {
result += chunks.startNineSlicedPS;
} else if (options.nineSlicedMode === SPRITE_RENDERMODE_TILED) {
result += chunks.startNineSlicedTiledPS;
}
return result;
}
_getLightSourceShapeString(shape) {
switch(shape){
case LIGHTSHAPE_RECT:
return 'Rect';
case LIGHTSHAPE_DISK:
return 'Disk';
case LIGHTSHAPE_SPHERE:
return 'Sphere';
default:
return '';
}
}
generateVertexShader(useUv, useUnmodifiedUv, mapTransforms) {
var device = this.device;
var options = this.options;
var chunks = this.chunks;
var code = '';
var codeBody = '';
var codeDefines = '';
code = this._vsAddBaseCode(code, chunks, options);
codeBody += ' vPositionW = getWorldPosition();\n';
if (this.options.linearDepth) {
codeDefines += "\n #ifndef VIEWMATRIX\n #define VIEWMATRIX\n uniform mat4 matrix_view;\n #endif\n ";
codeBody += "\n // linear depth from the worldPosition, see getLinearDepth\n vLinearDepth = -(matrix_view * vec4(vPositionW, 1.0)).z;\n ";
}
if (this.options.useInstancing) {
// only attach these if the default instancing chunk is used, otherwise it is expected
// for the user to provide required attributes using material.setAttribute
if (this.chunks.transformInstancingVS === shaderChunks.transformInstancingVS) {
this.attributes.instance_line1 = SEMANTIC_ATTR12;
this.attributes.instance_line2 = SEMANTIC_ATTR13;
this.attributes.instance_line3 = SEMANTIC_ATTR14;
this.attributes.instance_line4 = SEMANTIC_ATTR15;
}
}
code += chunks.transformVS;
if (this.needsNormal) {
code += chunks.normalCoreVS;
code += chunks.normalVS;
}
if (this.needsNormal) {
this.attributes.vertex_normal = SEMANTIC_NORMAL;
codeBody += ' vNormalW = getNormal();\n';
if (options.reflectionSource === 'sphereMap' && device.fragmentUniformsCount <= 16) {
code += chunks.viewNormalVS;
codeBody += ' vNormalV = getViewNormal();\n';
}
if (options.hasTangents && (options.useHeights || options.useNormals || options.enableGGXSpecular)) {
this.attributes.vertex_tangent = SEMANTIC_TANGENT;
code += chunks.tangentBinormalVS;
codeBody += ' vTangentW = getTangent();\n';
codeBody += ' vBinormalW = getBinormal();\n';
} else if (options.enableGGXSpecular) {
codeBody += ' vObjectSpaceUpW = normalize(dNormalMatrix * vec3(0, 1, 0));\n';
}
}
var maxUvSets = 2;
for(var i = 0; i < maxUvSets; i++){
if (useUv[i]) {
this.attributes["vertex_texCoord" + i] = "TEXCOORD" + i;
code += chunks["uv" + i + "VS"];
codeBody += " vec2 uv" + i + " = getUv" + i + "();\n";
}
if (useUnmodifiedUv[i]) {
codeBody += " vUv" + i + " = uv" + i + ";\n";
}
}
var codes = [
code,
this.varyings,
codeBody,
[]
];
mapTransforms.forEach((mapTransform)=>{
this._setMapTransform(codes, mapTransform.name, mapTransform.id, mapTransform.uv);
});
code = codes[0];
this.varyings = codes[1];
codeBody = codes[2];
if (options.vertexColors) {
this.attributes.vertex_color = SEMANTIC_COLOR;
codeBody += ' vVertexColor = vertex_color;\n';
}
if (options.useMsdf && options.msdfTextAttribute) {
this.attributes.vertex_outlineParameters = SEMANTIC_ATTR8;
this.attributes.vertex_shadowParameters = SEMANTIC_ATTR9;
codeBody += ' unpackMsdfParams();\n';
code += chunks.msdfVS;
}
// morphing
if (options.useMorphPosition || options.useMorphNormal) {
codeDefines += '#define MORPHING\n';
if (options.useMorphTextureBasedInt) {
codeDefines += '#define MORPHING_INT\n';
}
if (options.useMorphPosition) {
codeDefines += '#define MORPHING_POSITION\n';
}
if (options.useMorphNormal) {
codeDefines += '#define MORPHING_NORMAL\n';
}
// vertex ids attributes
this.attributes.morph_vertex_id = SEMANTIC_ATTR15;
}
if (options.skin) {
this.attributes.vertex_boneIndices = SEMANTIC_BLENDINDICES;
if (options.batch) {
codeDefines += '#define BATCH\n';
} else {
this.attributes.vertex_boneWeights = SEMANTIC_BLENDWEIGHT;
codeDefines += '#define SKIN\n';
}
} else if (options.useInstancing) {
codeDefines += '#define INSTANCING\n';
}
if (options.screenSpace) {
codeDefines += '#define SCREENSPACE\n';
}
if (options.pixelSnap) {
codeDefines += '#define PIXELSNAP\n';
}
code += '\n';
code += chunks.startVS;
code += codeBody;
code += chunks.endVS;
code += '}';
// build varyings
Object.keys(builtinVaryings).forEach((v)=>{
if (code.indexOf(v) >= 0) {
this.varyings += "varying " + builtinVaryings[v] + " " + v + ";\n";
this.varyingDefines += "#define VARYING_" + v.toUpperCase() + "\n";
}
});
var shaderPassDefines = this.shaderPassInfo.shaderDefines;
this.vshader = shaderPassDefines + codeDefines + this.varyings + code;
}
_fsGetBeginCode() {
var code = this.shaderPassInfo.shaderDefines;
for(var i = 0; i < this.defines.length; i++){
code += "#define " + this.defines[i] + "\n";
}
return code;
}
_fsGetPickPassCode() {
return "\n " + this._fsGetBeginCode() + "\n " + this.varyings + "\n " + this.varyingDefines + "\n " + this.frontendDecl + "\n " + this.frontendCode + "\n " + this.chunks.pickPS + "\n\n void main(void) {\n " + this.frontendFunc + "\n gl_FragColor = getPickOutput();\n }\n ";
}
_fsGetDepthPassCode() {
var code = this._fsGetBeginCode();
code += this.varyings;
code += this.varyingDefines;
code += this.frontendDecl;
code += this.frontendCode;
code += ShaderGenerator.begin();
code += this.frontendFunc;
code += ' gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n';
code += ShaderGenerator.end();
return code;
}
_fsGetPrePassCode() {
var code = this._fsGetBeginCode();
code += this.varyings;
code += this.varyingDefines;
code += this.chunks.floatAsUintPS;
code += this.frontendDecl;
code += this.frontendCode;
code += ShaderGenerator.begin();
code += this.frontendFunc;
code += this.device.textureFloatRenderable ? "\n gl_FragColor = vec4(vLinearDepth, 1.0, 1.0, 1.0);\n " : "\n gl_FragColor = float2uint(vLinearDepth);\n ";
code += ShaderGenerator.end();
return code;
}
_fsGetShadowPassCode() {
var options = this.options;
var chunks = this.chunks;
var varyings = this.varyings;
var lightType = this.shaderPassInfo.lightType;
var shadowType = this.shaderPassInfo.shadowType;
// If not a directional light and using clustered, fall back to using PCF3x3 if shadow type isn't supported
if (lightType !== LIGHTTYPE_DIRECTIONAL && options.clusteredLightingEnabled) {
if (shadowType === SHADOW_VSM_16F || shadowType === SHADOW_VSM_32F || shadowType === SHADOW_PCSS_32F) {
shadowType = SHADOW_PCF3_32F;
}
}
var shadowInfo = shadowTypeInfo.get(shadowType);
Debug.assert(shadowInfo);
var _shadowInfo_vsm;
var isVsm = (_shadowInfo_vsm = shadowInfo == null ? undefined : shadowInfo.vsm) != null ? _shadowInfo_vsm : false;
var _shadowInfo_pcss;
var isPcss = (_shadowInfo_pcss = shadowInfo == null ? undefined : shadowInfo.pcss) != null ? _shadowInfo_pcss : false;
var code = this._fsGetBeginCode();
if (shadowType === SHADOW_VSM_32F) {
code += '#define VSM_EXPONENT 15.0\n\n';
} else if (shadowType === SHADOW_VSM_16F) {
code += '#define VSM_EXPONENT 5.54\n\n';
}
if (lightType !== LIGHTTYPE_DIRECTIONAL) {
code += 'uniform vec3 view_position;\n';
code += 'uniform float light_radius;\n';
}
code += varyings;
code += this.varyingDefines;
code += this.frontendDecl;
code += this.frontendCode;
if (shadowType === SHADOW_PCSS_32F) {
code += shaderChunks.linearizeDepthPS;
}
code += ShaderGenerator.begin();
code += this.frontendFunc;
// Use perspective depth for:
// Directional: Always since light has no position
// Spot: If not using VSM
// Point: Never
var usePerspectiveDepth = lightType === LIGHTTYPE_DIRECTIONAL || !isVsm && lightType === LIGHTTYPE_SPOT;
// Flag if we are using non-standard depth, i.e gl_FragCoord.z
var hasModifiedDepth = false;
if (usePerspectiveDepth) {
code += ' float depth = gl_FragCoord.z;\n';
if (isPcss) {
// spot/omni shadows currently use linear depth.
// TODO: use perspective depth for spot/omni the same way as directional
if (lightType !== LIGHTTYPE_DIRECTIONAL) {
code += ' depth = linearizeDepth(depth, camera_params);\n';
}
}
} else {
code += ' float depth = min(distance(view_position, vPositionW) / light_radius, 0.99999);\n';
hasModifiedDepth = true;
}
if (!isVsm) {
var exportR32 = isPcss;
if (exportR32) {
code += ' gl_FragColor.r = depth;\n';
} else {
// If we end up using modified depth, it needs to be explicitly written to gl_FragDepth
if (hasModifiedDepth) {
code += ' gl_FragDepth = depth;\n';
}
code += ' gl_FragColor = vec4(1.0);\n'; // just the simplest code, color is not written anyway
}
} else {
code += chunks.storeEVSMPS;
}
code += ShaderGenerator.end();
return code;
}
// the big one
_fsGetLitPassCode() {
var device = this.device;
var options = this.options;
var chunks = this.chunks;
var decl = new ChunkBuilder();
var func = new ChunkBuilder();
var backend = new ChunkBuilder();
var code = new ChunkBuilder();
if (options.opacityFadesSpecular === false) {
decl.append('uniform float material_alphaFade;');
}
if (options.useSpecular) {
this.defines.push('LIT_SPECULAR');
if (this.reflections) {
this.defines.push('LIT_REFLECTIONS');
}
if (options.useClearCoat) {
this.defines.push('LIT_CLEARCOAT');
}
if (options.fresnelModel > 0) {
this.defines.push('LIT_SPECULAR_FRESNEL');
}
if (options.useSheen) {
this.defines.push('LIT_SHEEN');
}
if (options.useIridescence) {
this.defines.push('LIT_IRIDESCENCE');
}
}
// FRAGMENT SHADER INPUTS: UNIFORMS
var shadowTypeUsed = [];
var numShadowLights = 0;
var shadowedDirectionalLightUsed = false;
var useVsm = false;
var usePcss = false;
var hasAreaLights = options.lights.some((light)=>{
return light._shape && light._shape !== LIGHTSHAPE_PUNCTUAL;
});
// if clustered lighting has area lights enabled, it always runs in 'area lights mode'
// TODO: maybe we should always use it and remove the other way?
if (options.clusteredLightingEnabled && options.clusteredLightingAreaLightsEnabled) {
hasAreaLights = true;
}
if (hasAreaLights || options.clusteredLightingEnabled) {
decl.append('#define AREA_LIGHTS');
decl.append('uniform highp sampler2D areaLightsLutTex1;');
decl.append('uniform highp sampler2D areaLightsLutTex2;');
}
for(var i = 0; i < options.lights.length; i++){
var light = options.lights[i];
var lightType = light._type;
// skip uniform generation for local lights if clustered lighting is enabled
if (options.clusteredLightingEnabled && lightType !== LIGHTTYPE_DIRECTIONAL) {
continue;
}
var lightShape = hasAreaLights && light._shape ? light._shape : LIGHTSHAPE_PUNCTUAL;
decl.append("uniform vec3 light" + i + "_color;");
if (light._shadowType === SHADOW_PCSS_32F && light.castShadows && !options.noShadow) {
decl.append("uniform float light" + i + "_shadowSearchArea;");
decl.append("uniform vec4 light" + i + "_cameraParams;");
if (lightType === LIGHTTYPE_DIRECTIONAL) {
decl.append("uniform vec4 light" + i + "_softShadowParams;");
}
}
if (lightType === LIGHTTYPE_DIRECTIONAL) {
decl.append("uniform vec3 light" + i + "_direction;");
} else {
decl.append("uniform vec3 light" + i + "_position;");
decl.append("uniform float light" + i + "_radius;");
if (lightType === LIGHTTYPE_SPOT) {
decl.append("uniform vec3 light" + i + "_direction;");
decl.append("uniform float light" + i + "_innerConeAngle;");
decl.append("uniform float light" + i + "_outerConeAngle;");
}
}
if (lightShape !== LIGHTSHAPE_PUNCTUAL) {
if (lightType === LIGHTTYPE_DIRECTIONAL) {
decl.append("uniform vec3 light" + i + "_position;");
}
decl.append("uniform vec3 light" + i + "_halfWidth;");
decl.append("uniform vec3 light" + i + "_halfHeight;");
}
if (light.castShadows && !options.noShadow) {
decl.append("uniform mat4 light" + i + "_shadowMatrix;");
decl.append("uniform float light" + i + "_shadowIntensity;");
// directional (cascaded) shadows
if (lightType === LIGHTTYPE_DIRECTIONAL) {
decl.append("uniform mat4 light" + i + "_shadowMatrixPalette[4];");
decl.append("uniform vec4 light" + i + "_shadowCascadeDistances;");
decl.append("uniform int light" + i + "_shadowCascadeCount;");
decl.append("uniform float light" + i + "_shadowCascadeBlend;");
}
decl.append("uniform vec4 light" + i + "_shadowParams;"); // Width, height, bias, radius
if (lightType === LIGHTTYPE_DIRECTIONAL) {
shadowedDirectionalLightUsed = true;
}
if (lightType === LIGHTTYPE_OMNI) {
decl.append("uniform " + (light._isPcf ? 'samplerCubeShadow' : 'samplerCube') + " light" + i + "_shadowMap;");
} else {
decl.append("uniform " + (light._isPcf ? 'sampler2DShadow' : 'sampler2D') + " light" + i + "_shadowMap;");
}
numShadowLights++;
shadowTypeUsed[light._shadowType] = true;
if (light._isVsm) useVsm = true;
if (light._shadowType === SHADOW_PCSS_32F) usePcss = true;
}
if (light._cookie) {
if (light._cookie._cubemap) {
if (lightType === LIGHTTYPE_OMNI) {
decl.append("uniform samplerCube light" + i + "_cookie;");
decl.append("uniform float light" + i + "_cookieIntensity;");
if (!light.castShadows || options.noShadow) {
decl.append("uniform mat4 light" + i + "_shadowMatrix;");
}
}
} else {
if (lightType === LIGHTTYPE_SPOT) {
decl.append("uniform sampler2D light" + i + "_cookie;");
decl.append("uniform float light" + i + "_cookieIntensity;");
if (!light.castShadows || options.noShadow) {
decl.append("uniform mat4 light" + i + "_shadowMatrix;");
}
if (light._cookieTransform) {
decl.append("uniform vec4 light" + i + "_cookieMatrix;");
decl.append("uniform vec2 light" + i + "_cookieOffset;");
}
}
}
}
}
// TBN
var hasTBN = this.needsNormal && (options.useNormals || options.useClearCoatNormals || options.enableGGXSpecular && !options.useHeights);
if (hasTBN) {
if (options.hasTangents) {
func.append(chunks.TBNPS);
} else {
if (options.useNormals || options.useClearCoatNormals) {
func.append(chunks.TBNderivativePS.replace(/\$UV/g, this.lightingUv));
} else {
func.append(chunks.TBNObjectSpacePS);
}
}
if (options.twoSidedLighting) {
func.append(chunks.twoSidedLightingPS);
}
}
// FIXME: only add these when needed
func.append(chunks.sphericalPS);
func.append(chunks.decodePS);
func.append(ShaderGenerator.gammaCode(options.gamma, chunks));
func.append(ShaderGenerator.tonemapCode(options.toneMap, chunks));
func.append(ShaderGenerator.fogCode(options.fog, chunks));
// frontend
func.append(this.frontendCode);
if (options.useCubeMapRotation) {
decl.append('#define CUBEMAP_ROTATION');
}
if (this.needsNormal) {
func.append(chunks.cubeMapRotatePS);
func.append(options.cubeMapProjection > 0 ? chunks.cubeMapProjectBoxPS : chunks.cubeMapProjectNonePS);
func.append(options.skyboxIntensity ? chunks.envMultiplyPS : chunks.envConstPS);
}
if (this.lighting && options.useSpecular || this.reflections) {
if (options.useMetalness) {
func.append(chunks.metalnessModulatePS);
}
if (options.fresnelModel === FRESNEL_SCHLICK) {
func.append(chunks.fresnelSchlickPS);
}
if (options.useIridescence) {
func.append(chunks.iridescenceDiffractionPS);
}
}
if (options.useAo) {
func.append(chunks.aoDiffuseOccPS);
switch(options.occludeSpecular){
case SPECOCC_AO:
func.append(options.occludeSpecularFloat ? chunks.aoSpecOccSimplePS : chunks.aoSpecOccConstSimplePS);
break;
case SPECOCC_GLOSSDEPENDENT:
func.append(options.occludeSpecularFloat ? chunks.aoSpecOccPS : chunks.aoSpecOccConstPS);
break;
}
}
if (options.reflectionSource === 'envAtlasHQ') {
func.append(chunks.envAtlasPS);
func.append(chunks.reflectionEnvHQPS.replace(/\$DECODE_CUBEMAP/g, ChunkUtils.decodeFunc(options.reflectionCubemapEncoding)).replace(/\$DECODE/g, ChunkUtils.decodeFunc(options.reflectionEncoding)));
} else if (options.reflectionSource === 'envAtlas') {
func.append(chunks.envAtlasPS);
func.append(chunks.reflectionEnvPS.replace(/\$DECODE/g, ChunkUtils.decodeFunc(options.reflectionEncoding)));
} else if (options.reflectionSource === 'cubeMap') {
func.append(chunks.reflectionCubePS.replace(/\$DECODE/g, ChunkUtils.decodeFunc(options.reflectionEncoding)));
} else if (options.reflectionSource === 'sphereMap') {
func.append(chunks.reflectionSpherePS.replace(/\$DECODE/g, ChunkUtils.decodeFunc(options.reflectionEncoding)));
}
if (this.reflections) {
if (options.useClearCoat) {
func.append(chunks.reflectionCCPS);
}
if (options.useSheen) {
func.append(chunks.reflectionSheenPS);
}
}
if (options.useRefraction) {
if (options.useDynamicRefraction) {
if (options.dispersion) {
decl.append('uniform float material_dispersion;');
decl.append('#define DISPERSION\n');
}
func.append(chunks.refractionDynamicPS);
} else if (this.reflections) {
func.append(chunks.refractionCubePS);
}
}
if (options.useSheen) {
func.append(chunks.lightSheenPS);
}
// clustered lighting
if (options.clusteredLightingEnabled) {
// include this before shadow / cookie code
func.append(chunks.clusteredLightUtilsPS);
if (options.clusteredLightingCookiesEnabled) {
func.append(chunks.clusteredLightCookiesPS);
}
// include shadow chunks clustered lights support
if (options.clusteredLightingShadowsEnabled && !options.noShadow) {
shadowTypeUsed[SHADOW_PCF3_32F] = true;
shadowTypeUsed[SHADOW_PCF5_32F] = true;
shadowTypeUsed[SHADOW_PCSS_32F] = true;
}
}
if (numShadowLights > 0 || options.clusteredLightingEnabled) {
if (shadowedDirectionalLightUsed) {
func.append(chunks.shadowCascadesPS);
}
if (shadowTypeUsed[SHADOW_PCF1_32F] || shadowTypeUsed[SHADOW_PCF3_32F] || shadowTypeUsed[SHADOW_PCF1_16F] || shadowTypeUsed[SHADOW_PCF3_16F]) {
func.append(chunks.shadowStandardPS);
}
if (shadowTypeUsed[SHADOW_PCF5_32F] || shadowTypeUsed[SHADOW_PCF5_16F]) {
func.append(chunks.shadowStandardGL2PS);
}
if (useVsm) {
func.append(chunks.shadowVSM_commonPS);
if (shadowTypeUsed[SHADOW_VSM_16F]) {
func.append(chunks.shadowEVSMPS.replace(/\$/g, '16'));
}
if (shadowTypeUsed[SHADOW_VSM_32F]) {
func.append(device.extTextureFloatLinear ? chunks.shadowEVSMPS.replace(/\$/g, '32') : chunks.shadowEVSMnPS.replace(/\$/g, '32'));
}
}
if (usePcss) {
func.append(chunks.linearizeDepthPS);
func.append(chunks.shadowPCSSPS);
func.append(chunks.shadowSoftPS);
}
}
if (options.enableGGXSpecular) func.append('uniform float material_anisotropy;');
if (this.lighting) {
func.append(chunks.lightDiffuseLambertPS);
if (hasAreaLights || options.clusteredLightingAreaLightsEnabled) {
func.append(chunks.ltcPS);
}
}
var useOldAmbient = false;
if (options.useSpecular) {
if (this.lighting) {
func.append(options.enableGGXSpecular ? chunks.lightSpecularAnisoGGXPS : chunks.lightSpecularBlinnPS);
}
if (!options.fresnelModel && !this.reflections && !options.diffuseMapEnabled) {
decl.append('uniform vec3 material_ambient;');
decl.append('#define LIT_OLD_AMBIENT');
useOldAmbient = true;
}
}
func.append(chunks.combinePS);
// lightmap support
if (options.lightMapEnabled) {
func.append(options.useSpecular && options.dirLightMapEnabled ? chunks.lightmapDirAddPS : chunks.lightmapAddPS);
}
var addAmbient = !options.lightMapEnabled || options.lightMapWithoutAmbient;
if (addAmbient) {
if (options.ambientSource === 'ambientSH') {
func.append(chunks.ambientSHPS);
} else if (options.ambientSource === 'envAtlas') {
if (options.reflectionSource !== 'envAtlas' && options.reflectionSource !== 'envAtlasHQ') {
func.append(chunks.envAtlasPS);
}
func.append(chunks.ambientEnvPS.replace(/\$DECODE/g, ChunkUtils.decodeFunc(options.ambientEncoding)));
} else {
func.append(chunks.ambientConstantPS);
}
}
if (!useOldAmbient) {
decl.append('uniform vec3 material_ambient;');
}
if (options.useMsdf) {
if (!options.msdfTextAttribute) {
decl.append('#define UNIFORM_TEXT_PARAMETERS');
}
func.append(chunks.msdfPS);
}
if (this.needsNormal) {
func.append(chunks.viewDirPS);
if (options.useSpecular) {
func.append(options.enableGGXSpecular ? chunks.reflDirAnisoPS : chunks.reflDirPS);
}
}
var hasPointLights = false;
var usesLinearFalloff = false;
var usesInvSquaredFalloff = false;
var usesSpot = false;
var usesCookie = false;
var usesCookieNow;
// clustered lighting
if (options.clusteredLightingEnabled && this.lighting) {
usesSpot = true;
hasPointLights = true;
usesLinearFalloff = true;
usesCookie = true;
func.append(chunks.floatUnpackingPS);
if (options.lightMaskDynamic) {
decl.append('#define CLUSTER_MESH_DYNAMIC_LIGHTS');
}
if (options.clusteredLightingCookiesEnabled) {
decl.append('#define CLUSTER_COOKIES');
}
if (options.clusteredLightingShadowsEnabled && !options.noShadow) {
var _shadowTypeInfo_get;
var shadowTypeName = (_shadowTypeInfo_get = shadowTypeInfo.get(options.clusteredLightingShadowType)) == null ? undefined : _shadowTypeInfo_get.name;
Debug.assert(shadowTypeName);
var clusteredSampleType = shadowTypeName.substring(0, 4); // PCF1 from PCF1_FLOAT16
decl.append('#define CLUSTER_SHADOWS');
decl.append("#define CLUSTER_SHADOW_TYPE_" + clusteredSampleType);
}
if (options.clusteredLightingAreaLightsEnabled) {
decl.append('#define CLUSTER_AREALIGHTS');
}
decl.append(LightsBuffer.getShaderDefines());
if (options.clusteredLightingShadowsEnabled && !options.noShadow) {
func.append(chunks.clusteredLightShadowsPS);
}
func.append(chunks.clusteredLightPS);
}
// FRAGMENT SHADER BODY
code.append(this._fsGetStartCode(code, device, chunks, options));
if (this.needsNormal) {
code.append(' dVertexNormalW = normalize(vNormalW);');
if ((options.useHeights || options.useNormals) && options.hasTangents) {
code.append(' dTangentW = vTangentW;');
code.append(' dBinormalW = vBinormalW;');
}
code.append(' getViewDir();');
if (hasTBN) {
code.append(' getTBN(dTangentW, dBinormalW, dVertexNormalW);');
if (options.twoSidedLighting) {
code.append(' handleTwoSidedLighting();');
}
}
}
// invoke frontend functions
code.append(this.frontendFunc);
// apply SSAO
if (options.ssao) {
func.append("\n uniform sampler2D ssaoTexture;\n uniform vec2 ssaoTextureSizeInv;\n ");
backend.append('litArgs_ao *= texture2DLod(ssaoTexture, gl_FragCoord.xy * ssaoTextureSizeInv, 0.0).r;');
}
// transform tangent space normals to world space
if (this.needsNormal) {
if (options.useSpecular) {
backend.append(' getReflDir(litArgs_worldNormal, dViewDirW, litArgs_gloss, dTBN);');
}
if (options.useClearCoat) {
backend.append(' ccReflDirW = normalize(-reflect(dViewDirW, litArgs_clearcoat_worldNormal));');
}
}
if (this.lighting && options.useSpecular || this.reflections) {
if (options.useMetalness) {
backend.append(' float f0 = 1.0 / litArgs_ior; f0 = (f0 - 1.0) / (f0 + 1.0); f0 *= f0;');
backend.append(' litArgs_specularity = getSpecularModulate(litArgs_specularity, litArgs_albedo, litArgs_metalness, f0);');
backend.append(' litArgs_albedo = getAlbedoModulate(litArgs_albedo, litArgs_metalness);');
}
if (options.useIridescence) {
backend.append(' vec3 iridescenceFresnel = getIridescence(saturate(dot(dViewDirW, litArgs_worldNormal)), litArgs_specularity, litArgs_iridescence_thickness);');
}
}
if (addAmbient) {
backend.append(' addAmbient(litArgs_worldNormal);');
if (options.useSpecular) {
backend.append(' dDiffuseLight = dDiffuseLight * (1.0 - litArgs_specularity);');
}
// move ambient color out of diffuse (used by Lightmapper, to multiply ambient color by accumulated AO)
if (options.separateAmbient) {
backend.append("\n vec3 dAmbientLight = dDiffuseLight;\n dDiffuseLight = vec3(0);\n ");
}
}
if (!useOldAmbient) {
backend.append(' dDiffuseLight *= material_ambient;');
}
if (options.useAo && !options.occludeDirect) {
backend.append(' occludeDiffuse(litArgs_ao);');
}
if (options.lightMapEnabled) {
backend.append(" addLightMap(\n litArgs_lightmap, \n litArgs_lightmapDir, \n litArgs_worldNormal, \n dViewDirW, \n dReflDirW, \n litArgs_gloss, \n litArgs_specularity, \n dVertexNormalW,\n dTBN\n #if defined(LIT_IRIDESCENCE)\n , iridescenceFresnel,\n litArgs_iridescence_intensity\n #endif\n );");
}
if (this.lighting || this.reflections) {
if (this.reflections) {
if (options.useClearCoat) {
backend.append(' addReflectionCC(ccReflDirW, litArgs_clearcoat_gloss);');
if (options.fresnelModel > 0) {
backend.append(' ccFresnel = getFresnelCC(dot(dViewDirW, litArgs_clearcoat_worldNormal));');
backend.append(' ccReflection.rgb *= ccFresnel;');
} else {
backend.append(' ccFresnel = 0.0;');
}
}
if (options.useSpecularityFactor) {
backend.append(' ccReflection.rgb *= litArgs_specularityFactor;');
}
if (options.useSheen) {
backend.append(' addReflectionSheen(litArgs_worldNormal, dViewDirW, litArgs_sheen_gloss);');
}
// Fresnel has to be applied to reflections
backend.append(' addReflection(dReflDirW, litArgs_gloss);');
if (options.fresnelModel > 0) {
backend.append(" dReflection.rgb *= \n getFresnel(\n dot(dViewDirW, litArgs_worldNormal), \n litArgs_gloss, \n litArgs_specularity\n #if defined(LIT_IRIDESCENCE)\n , iridescenceFresnel,\n litArgs_iridescence_intensity\n #endif\n );");
} else {
backend.append(' dReflection.rgb *= litArgs_specularity;');
}
if (options.useSpecularityFactor) {
backend.append(' dReflection.rgb *= litArgs_specularityFactor;');
}
}
if (hasAreaLights) {
// specular has to be accumulated differently if we want area lights to look correct
backend.append(' dSpecularLight *= litArgs_specularity;');
// code += " float roughness = max((1.0 - dGlossiness) * (1.0 - dGlossiness), 0.001);\n";
// evaluate material based area lights data, shared by all area lights
if (options.useSpecular) {
backend.append(' calcLTCLightValues(litArgs_gloss, litArgs_worldNormal, dViewDirW, litArgs_specularity, litArgs_clearcoat_gloss, litArgs_clearcoat_worldNormal, litArgs_clearcoat_specularity);');
}
}
for(var i1 = 0; i1 < options.lights.length; i1++){
var light1 = options.lights[i1];
var lightType1 = light1._type;
// if clustered lights are used, skip normal lights other than directional
if (options.clusteredLightingEnabled && lightType1 !== LIGHTTYPE_DIRECTIONAL) {
continue;
}
// The following code is not decoupled to separate shader files, because most of it can be actually changed to achieve different behaviors like:
// - different falloffs
// - different shadow coords (omni shadows will use drastically different genShadowCoord)
// - different shadow filter modes
// - different light source shapes
// getLightDiffuse and getLightSpecular is BRDF itself.
usesCookieNow = false;
var lightShape1 = hasAreaLights && light1._shape ? light1.shape : LIGHTSHAPE_PUNCTUAL;
var shapeString = hasAreaLights && light1._shape ? this._getLightSourceShapeString(lightShape1) : '';
if (lightShape1 !== LIGHTSHAPE_PUNCTUAL) {
backend.append(" calc" + shapeString + "LightValues(light" + i1 + "_position, light" + i1 + "_halfWidth, light" + i1 + "_halfHeight);");
}
if (lightType1 === LIGHTTYPE_DIRECTIONAL) {
// directional
backend.append(" dLightDirNormW = light" + i1 + "_direction;");
backend.append(' dAtten = 1.0;');
} else {
if (light1._cookie) {
if (lightType1 === LIGHTTYPE_SPOT && !light1._cookie._cubemap) {
usesCookie = true;
usesCookieNow = true;
} else if (lightType1 === LIGHTTYPE_OMNI && light1._cookie._cubemap) {
usesCookie = true;
usesCookieNow = true;
}
}
backend.append(" getLightDirPoint(light" + i1 + "_position);");
hasPointLights = true;
if (usesCookieNow) {
if (lightType1 === LIGHTTYPE_SPOT) {
backend.append(" dAtten3 = getCookie2D" + (light1._cookieFalloff ? '' : 'Clip') + (light1._cookieTransform ? 'Xform' : '') + "(light" + i1 + "_cookie, light" + i1 + "_shadowMatrix, light" + i1 + "_cookieIntensity" + (light1._cookieTransform ? ", light" + i1 + "_cookieMatrix, light" + i1 + "_cookieOffset" : '') + ")." + light1._cookieChannel + ";");
} else {
backend.append(" dAtten3 = getCookieCube(light" + i1 + "_cookie, light" + i1 + "_shadowMatrix, light" + i1 + "_cookieIntensity)." + light1._cookieChannel + ";");
}
}
if (lightShape1 === LIGHTSHAPE_PUNCTUAL) {
if (light1._falloffMode === LIGHTFALLOFF_LINEAR) {
backend.append(" dAtten = getFalloffLinear(light" + i1 + "_radius, dLightDirW);");
usesLinearFalloff = true;
} else {
backend.append(" dAtten = getFalloffInvSquared(light" + i1 + "_radius, dLightDirW);");
usesInvSquaredFalloff = true;
}
} else {
// non punctual lights only gets the range window here
backend.append(" dAtten = getFalloffWindow(light" + i1 + "_radius, dLightDirW);");
usesInvSquaredFalloff = true;
}
backend.append(' if (dAtten > 0.00001) {'); // BRANCH START
if (lightType1 === LIGHTTYPE_SPOT) {
if (!(usesCookieNow && !light1._cookieFalloff)) {
backend.append(" dAtten *= getSpotEffect(light" + i1 + "_direction, light" + i1 + "_innerConeAngle, light" + i1 + "_outerConeAngle, dLightDirNormW);");
usesSpot = true;
}
}
}
// diffuse lighting - LTC lights do not mix diffuse lighting into attenuation that affects specular
if (lightShape1 !== LIGHTSHAPE_PUNCTUAL) {
if (lightType1 === LIGHTTYPE_DIRECTIONAL) {
// NB: A better aproximation perhaps using wrap lighting could be implemented here
backend.append(' dAttenD = getLightDiffuse(litArgs_worldNormal, dViewDirW, dLightDirW, dLightDirNormW);');
} else {
// 16.0 is a constant that is in getFalloffInvSquared()
backend.append(" dAttenD = get" + shapeString + "LightDiffuse(litArgs_worldNormal, dViewDirW, dLightDirW, dLightDirNormW) * 16.0;");
}
} else {
backend.append(' dAtten *= getLightDiffuse(litArgs_worldNormal, dViewDirW, dLightDirW, dLightDirNormW);');
}
if (light1.castShadows && !options.noShadow) {
var shadowInfo = shadowTypeInfo.get(light1._shadowType);
Debug.assert(shadowInfo);
var pcssShadows = light1._shadowType === SHADOW_PCSS_32F;
var vsmShadows = shadowInfo == null ? undefined : shadowInfo.vsm;
var pcfShadows = shadowInfo == null ? undefined : shadowInfo.pcf;
var shadowReadMode = null;
var evsmExp = undefined;
switch(light1._shadowType){
case SHADOW_VSM_16F:
shadowReadMode = 'VSM16';
evsmExp = '5.54';
break;
case SHADOW_VSM_32F:
shadowReadMode = 'VSM32';
evsmExp = '15.0';
break;
case SHADOW_PCF1_32F:
case SHADOW_PCF1_16F:
shadowReadMode = 'PCF1x1';
break;
case SHADOW_PCF5_32F:
case SHADOW_PCF5_16F:
shadowReadMode = 'PCF5x5';
break;
case SHADOW_PCSS_32F:
shadowReadMode = 'PCSS';
break;
case SHADOW_PCF3_32F:
case SHADOW_PCF3_16F:
default:
shadowReadMode = 'PCF3x3';
break;
}
if (shadowReadMode !== null) {
if (light1._normalOffsetBias && !light1._isVsm) {
func.append('#define SHADOW_SAMPLE_NORMAL_OFFSET');
}
if (lightType1 === LIGHTTYPE_DIRECTIONAL) {
func.append('#define SHADOW_SAMPLE_ORTHO');
}
if (pcfShadows || pcssShadows || device.isWebGPU) {
func.append('#define SHADOW_SAMPLE_SOURCE_ZBUFFER');
}
if (lightType1 === LIGHTTYPE_OMNI) {
func.append('#define SHADOW_SAMPLE_POINT');
}
// Create shadow coord sampler function for this light
var coordCode = chunks.shadowSampleCoordPS;
func.append(coordCode.replace('$LIGHT', i1));
// Make sure to undefine the shadow sampler defines
func.append('#undef SHADOW_SAMPLE_NORMAL_OFFSET');
func.append('#undef SHADOW_SAMPLE_ORTHO');
func.append('#undef SHADOW_SAMPLE_SOURCE_ZBUFFER');
func.append('#undef SHADOW_SAMPLE_POINT');
var shadowMatrix = "light" + i1 + "_shadowMatrix";
if (lightType1 === LIGHTTYPE_DIRECTIONAL && light1.numCascades > 1) {
// select shadow cascade matrix
backend.append("int cascadeIndex = getShadowCascadeIndex(light" + i1 + "_shadowCascadeDistances, light" + i1 + "_shadowCascadeCount);");
if (light1.cascadeBlend > 0) {
backend.append("cascadeIndex = ditherShadowCascadeIndex(cascadeIndex, light" + i1 + "_shadowCascadeDistances, light" + i1 + "_shadowCascadeCount, light" + i1 + "_shadowCascadeBlend);");
}
backend.append("mat4 cascadeShadowMat = light" + i1 + "_shadowMatrixPalette[cascadeIndex];");
shadowMatrix = 'cascadeShadowMat';
}
backend.append(" dShadowCoord = getShadowSampleCoord" + i1 + "(" + shadowMatrix + ", light" + i1 + "_shadowParams, vPositionW, dLightPosW, dLightDirW, dLightDirNormW, dVertexNormalW);");
// Fade shadow at edges
if (lightType1 === LIGHTTYPE_DIRECTIONAL) {
backend.append(" fadeShadow(light" + i1 + "_shadowCascadeDistances);");
}
var shadowCoordArgs = "SHADOWMAP_PASS(light" + i1 + "_shadowMap), dShadowCoord, light" + i1 + "_shadowParams";
if (vsmShadows) {
// VSM
shadowCoordArgs = shadowCoordArgs + ", " + evsmExp + ", dLightDirW";
} else if (pcssShadows) {
var penumbraSizeArg = lightType1 === LIGHTTYPE_DIRECTIONAL ? "light" + i1 + "_softShadowParams" : "vec2(light" + i1 + "_shadowSearchArea)";
if (lightShape1 !== LIGHTSHAPE_PUNCTUAL) {
penumbraSizeArg = "vec2(length(light" + i1 + "_halfWidth), length(light" + i1 + "_halfHeight)) * light" + i1 + "_shadowSearchArea";
}
shadowCoordArgs = shadowCoordArgs + ", light" + i1 + "_cameraParams, " + penumbraSizeArg + ", dLightDirW";
}
if (lightType1 === LIGHTTYPE_OMNI) {
shadowReadMode = "Point" + shadowReadMode;
if (!pcssShadows) {
shadowCoordArgs = "" + shadowCoordArgs + ", dLightDirW";
}
} else if (lightType1 === LIGHTTYPE_SPOT) {
shadowReadMode = "Spot" + shadowReadMode;
}
backend.append(" float shadow" + i1 + " = getShadow" + shadowReadMode + "(" + shadowCoordArgs + ");");
backend.append(" dAtten *= mix(1.0, shadow" + i1 + ", light" + i1 + "_shadowIntensity);");
}
}
if (lightShape1 !== LIGHTSHAPE_PUNCTUAL) {
// area light - they do not mix diffuse lighting into specular attenuation
if (options.useSpecular) {
backend.append(" dDiffuseLight += ((dAttenD * dAtten) * light" + i1 + "_color" + (usesCookieNow ? ' * dAtten3' : '') + ") * (1.0 - dLTCSpecFres);");
} else {
backend.append(" dDiffuseLight += (dAttenD * dAtten) * light" + i1 + "_color" + (usesCookieNow ? ' * dAtten3' : '') + ";");
}
} else {
// punctual light