playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
333 lines (330 loc) • 17.3 kB
JavaScript
import { SEMANTIC_ATTR11, SEMANTIC_ATTR12, SEMANTIC_ATTR14, SEMANTIC_ATTR15, SEMANTIC_NORMAL, SEMANTIC_TANGENT, SEMANTIC_COLOR, SEMANTIC_ATTR8, SEMANTIC_ATTR9, SEMANTIC_BLENDINDICES, SEMANTIC_BLENDWEIGHT, SHADERLANGUAGE_WGSL, primitiveGlslToWgslTypeMap, SHADERLANGUAGE_GLSL, SEMANTIC_POSITION, SEMANTIC_TEXCOORD1, SEMANTIC_TEXCOORD0 } from '../../../platform/graphics/constants.js';
import { SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, shadowTypeInfo, LIGHTTYPE_DIRECTIONAL, LIGHTSHAPE_PUNCTUAL, lightTypeNames, lightShapeNames, lightFalloffNames, LIGHTTYPE_SPOT, LIGHTTYPE_OMNI, fresnelNames, spriteRenderModeNames, blendNames, cubemaProjectionNames, specularOcclusionNames, reflectionSrcNames, ambientSrcNames, ditherNames, SHADER_PICK, SHADER_PREPASS, REFLECTIONSRC_NONE } from '../../constants.js';
import { ChunkUtils } from '../chunk-utils.js';
import { ShaderPass } from '../../shader-pass.js';
import { ShaderChunks } from '../shader-chunks.js';
const 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
};
class LitShader {
fDefineSet(condition, name, value = '') {
if (condition) {
this.fDefines.set(name, value);
}
}
generateVertexShader(useUv, useUnmodifiedUv, mapTransforms) {
const { options, vDefines, attributes } = this;
const varyings = new Map();
varyings.set('vPositionW', 'vec3');
if (options.nineSlicedMode === SPRITE_RENDERMODE_SLICED || options.nineSlicedMode === SPRITE_RENDERMODE_TILED) {
vDefines.set('NINESLICED', true);
}
if (this.options.linearDepth) {
vDefines.set('LINEAR_DEPTH', true);
varyings.set('vLinearDepth', 'float');
}
if (this.needsNormal) vDefines.set('NORMALS', true);
if (this.options.useInstancing) {
const languageChunks = ShaderChunks.get(this.device, this.shaderLanguage);
if (this.chunks.get('transformInstancingVS') === languageChunks.get('transformInstancingVS')) {
attributes.instance_line1 = SEMANTIC_ATTR11;
attributes.instance_line2 = SEMANTIC_ATTR12;
attributes.instance_line3 = SEMANTIC_ATTR14;
attributes.instance_line4 = SEMANTIC_ATTR15;
}
}
if (this.needsNormal) {
attributes.vertex_normal = SEMANTIC_NORMAL;
varyings.set('vNormalW', 'vec3');
if (options.hasTangents && (options.useHeights || options.useNormals || options.useClearCoatNormals || options.enableGGXSpecular)) {
vDefines.set('TANGENTS', true);
attributes.vertex_tangent = SEMANTIC_TANGENT;
varyings.set('vTangentW', 'vec3');
varyings.set('vBinormalW', 'vec3');
} else if (options.enableGGXSpecular) {
vDefines.set('GGX_SPECULAR', true);
varyings.set('vObjectSpaceUpW', 'vec3');
}
}
const maxUvSets = 2;
for(let i = 0; i < maxUvSets; i++){
if (useUv[i]) {
vDefines.set(`UV${i}`, true);
attributes[`vertex_texCoord${i}`] = `TEXCOORD${i}`;
}
if (useUnmodifiedUv[i]) {
vDefines.set(`UV${i}_UNMODIFIED`, true);
varyings.set(`vUv${i}`, 'vec2');
}
}
let numTransforms = 0;
const transformDone = new Set();
mapTransforms.forEach((mapTransform)=>{
const { id, uv, name } = mapTransform;
const checkId = id + uv * 100;
if (!transformDone.has(checkId)) {
transformDone.add(checkId);
varyings.set(`vUV${uv}_${id}`, 'vec2');
const varName = `texture_${name}MapTransform`;
vDefines.set(`{TRANSFORM_NAME_${numTransforms}}`, varName);
vDefines.set(`{TRANSFORM_UV_${numTransforms}}`, uv);
vDefines.set(`{TRANSFORM_ID_${numTransforms}}`, id);
numTransforms++;
}
});
vDefines.set('UV_TRANSFORMS_COUNT', numTransforms);
if (options.vertexColors) {
attributes.vertex_color = SEMANTIC_COLOR;
vDefines.set('VERTEX_COLOR', true);
varyings.set('vVertexColor', 'vec4');
if (options.useVertexColorGamma) {
vDefines.set('STD_VERTEX_COLOR_GAMMA', '');
}
}
if (options.useMsdf && options.msdfTextAttribute) {
attributes.vertex_outlineParameters = SEMANTIC_ATTR8;
attributes.vertex_shadowParameters = SEMANTIC_ATTR9;
vDefines.set('MSDF', true);
}
if (options.useMorphPosition || options.useMorphNormal) {
vDefines.set('MORPHING', true);
if (options.useMorphTextureBasedInt) vDefines.set('MORPHING_INT', true);
if (options.useMorphPosition) vDefines.set('MORPHING_POSITION', true);
if (options.useMorphNormal) vDefines.set('MORPHING_NORMAL', true);
attributes.morph_vertex_id = SEMANTIC_ATTR15;
}
if (options.skin) {
attributes.vertex_boneIndices = SEMANTIC_BLENDINDICES;
if (options.batch) {
vDefines.set('BATCH', true);
} else {
attributes.vertex_boneWeights = SEMANTIC_BLENDWEIGHT;
vDefines.set('SKIN', true);
}
}
if (options.useInstancing) vDefines.set('INSTANCING', true);
if (options.screenSpace) vDefines.set('SCREENSPACE', true);
if (options.pixelSnap) vDefines.set('PIXELSNAP', true);
varyings.forEach((type, name)=>{
this.varyingsCode += `#define VARYING_${name.toUpperCase()}\n`;
this.varyingsCode += this.shaderLanguage === SHADERLANGUAGE_WGSL ? `varying ${name}: ${primitiveGlslToWgslTypeMap.get(type)};\n` : `varying ${type} ${name};\n`;
});
this.includes.set('varyingsVS', this.varyingsCode);
this.includes.set('varyingsPS', this.varyingsCode);
this.vshader = `
#include "litMainVS"
`;
}
_setupLightingDefines(hasAreaLights, clusteredLightingEnabled) {
const fDefines = this.fDefines;
const options = this.options;
this.fDefines.set('LIGHT_COUNT', options.lights.length);
if (hasAreaLights) fDefines.set('AREA_LIGHTS', true);
if (clusteredLightingEnabled && this.lighting) {
fDefines.set('LIT_CLUSTERED_LIGHTS', true);
if (options.clusteredLightingCookiesEnabled) fDefines.set('CLUSTER_COOKIES', true);
if (options.clusteredLightingAreaLightsEnabled) fDefines.set('CLUSTER_AREALIGHTS', true);
if (options.lightMaskDynamic) fDefines.set('CLUSTER_MESH_DYNAMIC_LIGHTS', true);
if (options.clusteredLightingShadowsEnabled && !options.noShadow) {
const clusteredShadowInfo = shadowTypeInfo.get(options.clusteredLightingShadowType);
fDefines.set('CLUSTER_SHADOWS', true);
fDefines.set(`SHADOW_KIND_${clusteredShadowInfo.kind}`, true);
fDefines.set(`CLUSTER_SHADOW_TYPE_${clusteredShadowInfo.kind}`, true);
}
}
for(let i = 0; i < options.lights.length; i++){
const light = options.lights[i];
const lightType = light._type;
if (clusteredLightingEnabled && lightType !== LIGHTTYPE_DIRECTIONAL) {
continue;
}
const lightShape = hasAreaLights && light._shape ? light._shape : LIGHTSHAPE_PUNCTUAL;
const shadowType = light._shadowType;
const castShadow = light.castShadows && !options.noShadow;
const shadowInfo = shadowTypeInfo.get(shadowType);
fDefines.set(`LIGHT${i}`, true);
fDefines.set(`LIGHT${i}TYPE`, `${lightTypeNames[lightType]}`);
fDefines.set(`LIGHT${i}SHADOWTYPE`, `${shadowInfo.name}`);
fDefines.set(`LIGHT${i}SHAPE`, `${lightShapeNames[lightShape]}`);
fDefines.set(`LIGHT${i}FALLOFF`, `${lightFalloffNames[light._falloffMode]}`);
if (light.affectSpecularity) fDefines.set(`LIGHT${i}AFFECT_SPECULARITY`, true);
if (light._cookie) {
if (lightType === LIGHTTYPE_SPOT && !light._cookie._cubemap || lightType === LIGHTTYPE_OMNI && light._cookie._cubemap) {
fDefines.set(`LIGHT${i}COOKIE`, true);
fDefines.set(`{LIGHT${i}COOKIE_CHANNEL}`, light._cookieChannel);
if (lightType === LIGHTTYPE_SPOT) {
if (light._cookieTransform) fDefines.set(`LIGHT${i}COOKIE_TRANSFORM`, true);
if (light._cookieFalloff) fDefines.set(`LIGHT${i}COOKIE_FALLOFF`, true);
}
}
}
if (castShadow) {
fDefines.set(`LIGHT${i}CASTSHADOW`, true);
if (shadowInfo.pcf) fDefines.set(`LIGHT${i}SHADOW_PCF`, true);
if (light._normalOffsetBias && !light._isVsm) fDefines.set(`LIGHT${i}_SHADOW_SAMPLE_NORMAL_OFFSET`, true);
if (lightType === LIGHTTYPE_DIRECTIONAL) {
fDefines.set(`LIGHT${i}_SHADOW_SAMPLE_ORTHO`, true);
if (light.cascadeBlend > 0) fDefines.set(`LIGHT${i}_SHADOW_CASCADE_BLEND`, true);
if (light.numCascades > 1) fDefines.set(`LIGHT${i}_SHADOW_CASCADES`, true);
}
if (shadowInfo.pcf || shadowInfo.pcss || this.device.isWebGPU) fDefines.set(`LIGHT${i}_SHADOW_SAMPLE_SOURCE_ZBUFFER`, true);
if (lightType === LIGHTTYPE_OMNI) fDefines.set(`LIGHT${i}_SHADOW_SAMPLE_POINT`, true);
}
if (castShadow) {
fDefines.set(`SHADOW_KIND_${shadowInfo.kind}`, true);
if (lightType === LIGHTTYPE_DIRECTIONAL) fDefines.set('SHADOW_DIRECTIONAL', true);
}
}
}
prepareForwardPass(lightingUv) {
const { options } = this;
const clusteredAreaLights = options.clusteredLightingEnabled && options.clusteredLightingAreaLightsEnabled;
const hasAreaLights = clusteredAreaLights || options.lights.some((light)=>{
return light._shape && light._shape !== LIGHTSHAPE_PUNCTUAL;
});
const addAmbient = !options.lightMapEnabled || options.lightMapWithoutAmbient;
const hasTBN = this.needsNormal && (options.useNormals || options.useClearCoatNormals || options.enableGGXSpecular && !options.useHeights);
if (options.useSpecular) {
this.fDefineSet(true, 'LIT_SPECULAR');
this.fDefineSet(this.reflections, 'LIT_REFLECTIONS');
this.fDefineSet(options.useClearCoat, 'LIT_CLEARCOAT');
this.fDefineSet(options.fresnelModel > 0, 'LIT_SPECULAR_FRESNEL');
this.fDefineSet(options.useSheen, 'LIT_SHEEN');
this.fDefineSet(options.useIridescence, 'LIT_IRIDESCENCE');
}
this.fDefineSet(this.lighting && options.useSpecular || this.reflections, 'LIT_SPECULAR_OR_REFLECTION');
this.fDefineSet(this.needsSceneColor, 'LIT_SCENE_COLOR');
this.fDefineSet(this.needsScreenSize, 'LIT_SCREEN_SIZE');
this.fDefineSet(this.needsTransforms, 'LIT_TRANSFORMS');
this.fDefineSet(this.needsNormal, 'LIT_NEEDS_NORMAL');
this.fDefineSet(this.lighting, 'LIT_LIGHTING');
this.fDefineSet(options.useMetalness, 'LIT_METALNESS');
this.fDefineSet(options.enableGGXSpecular, 'LIT_GGX_SPECULAR');
this.fDefineSet(options.useAnisotropy, 'LIT_ANISOTROPY');
this.fDefineSet(options.useSpecularityFactor, 'LIT_SPECULARITY_FACTOR');
this.fDefineSet(options.useCubeMapRotation, 'CUBEMAP_ROTATION');
this.fDefineSet(options.occludeSpecularFloat, 'LIT_OCCLUDE_SPECULAR_FLOAT');
this.fDefineSet(options.separateAmbient, 'LIT_SEPARATE_AMBIENT');
this.fDefineSet(options.twoSidedLighting, 'LIT_TWO_SIDED_LIGHTING');
this.fDefineSet(options.lightMapEnabled, 'LIT_LIGHTMAP');
this.fDefineSet(options.dirLightMapEnabled, 'LIT_DIR_LIGHTMAP');
this.fDefineSet(options.skyboxIntensity > 0, 'LIT_SKYBOX_INTENSITY');
this.fDefineSet(options.clusteredLightingShadowsEnabled, 'LIT_CLUSTERED_SHADOWS');
this.fDefineSet(options.clusteredLightingAreaLightsEnabled, 'LIT_CLUSTERED_AREA_LIGHTS');
this.fDefineSet(hasTBN, 'LIT_TBN');
this.fDefineSet(addAmbient, 'LIT_ADD_AMBIENT');
this.fDefineSet(options.hasTangents, 'LIT_TANGENTS');
this.fDefineSet(options.useNormals, 'LIT_USE_NORMALS');
this.fDefineSet(options.useClearCoatNormals, 'LIT_USE_CLEARCOAT_NORMALS');
this.fDefineSet(options.useRefraction, 'LIT_REFRACTION');
this.fDefineSet(options.useDynamicRefraction, 'LIT_DYNAMIC_REFRACTION');
this.fDefineSet(options.dispersion, 'LIT_DISPERSION');
this.fDefineSet(options.useHeights, 'LIT_HEIGHTS');
this.fDefineSet(options.opacityFadesSpecular, 'LIT_OPACITY_FADES_SPECULAR');
this.fDefineSet(options.alphaToCoverage, 'LIT_ALPHA_TO_COVERAGE');
this.fDefineSet(options.alphaTest, 'LIT_ALPHA_TEST');
this.fDefineSet(options.useMsdf, 'LIT_MSDF');
this.fDefineSet(options.ssao, 'LIT_SSAO');
this.fDefineSet(options.useAo, 'LIT_AO');
this.fDefineSet(options.occludeDirect, 'LIT_OCCLUDE_DIRECT');
this.fDefineSet(options.msdfTextAttribute, 'LIT_MSDF_TEXT_ATTRIBUTE');
this.fDefineSet(options.diffuseMapEnabled, 'LIT_DIFFUSE_MAP');
this.fDefineSet(options.shadowCatcher, 'LIT_SHADOW_CATCHER');
this.fDefineSet(true, 'LIT_FRESNEL_MODEL', fresnelNames[options.fresnelModel]);
this.fDefineSet(true, 'LIT_NONE_SLICE_MODE', spriteRenderModeNames[options.nineSlicedMode]);
this.fDefineSet(true, 'LIT_BLEND_TYPE', blendNames[options.blendType]);
this.fDefineSet(true, 'LIT_CUBEMAP_PROJECTION', cubemaProjectionNames[options.cubeMapProjection]);
this.fDefineSet(true, 'LIT_OCCLUDE_SPECULAR', specularOcclusionNames[options.occludeSpecular]);
this.fDefineSet(true, 'LIT_REFLECTION_SOURCE', reflectionSrcNames[options.reflectionSource]);
this.fDefineSet(true, 'LIT_AMBIENT_SOURCE', ambientSrcNames[options.ambientSource]);
this.fDefineSet(true, '{lightingUv}', lightingUv ?? '');
this.fDefineSet(true, '{reflectionDecode}', ChunkUtils.decodeFunc(options.reflectionEncoding));
this.fDefineSet(true, '{reflectionCubemapDecode}', ChunkUtils.decodeFunc(options.reflectionCubemapEncoding));
this.fDefineSet(true, '{ambientDecode}', ChunkUtils.decodeFunc(options.ambientEncoding));
this._setupLightingDefines(hasAreaLights, options.clusteredLightingEnabled);
}
preparePrepassPass() {
const { options } = this;
this.fDefineSet(options.alphaTest, 'LIT_ALPHA_TEST');
this.fDefineSet(true, 'STD_OPACITY_DITHER', ditherNames[options.opacityShadowDither]);
}
prepareShadowPass() {
const { options } = this;
const lightType = this.shaderPassInfo.lightType;
const shadowType = this.shaderPassInfo.shadowType;
const shadowInfo = shadowTypeInfo.get(shadowType);
const usePerspectiveDepth = lightType === LIGHTTYPE_DIRECTIONAL || !shadowInfo.vsm && lightType === LIGHTTYPE_SPOT;
this.fDefineSet(usePerspectiveDepth, 'PERSPECTIVE_DEPTH');
this.fDefineSet(true, 'LIGHT_TYPE', `${lightTypeNames[lightType]}`);
this.fDefineSet(true, 'SHADOW_TYPE', `${shadowInfo.name}`);
this.fDefineSet(options.alphaTest, 'LIT_ALPHA_TEST');
}
generateFragmentShader(frontendDecl, frontendCode, lightingUv) {
const options = this.options;
this.includes.set('frontendDeclPS', frontendDecl ?? '');
this.includes.set('frontendCodePS', frontendCode ?? '');
if (options.pass === SHADER_PICK) ; else if (options.pass === SHADER_PREPASS) {
this.preparePrepassPass();
} else if (this.shadowPass) {
this.prepareShadowPass();
} else {
this.prepareForwardPass(lightingUv);
}
this.fshader = `
#include "litMainPS"
`;
}
constructor(device, options, allowWGSL = true){
this.varyingsCode = '';
this.vDefines = new Map();
this.fDefines = new Map();
this.includes = new Map();
this.chunks = null;
this.device = device;
this.options = options;
const userChunks = options.shaderChunks;
this.shaderLanguage = device.isWebGPU && allowWGSL && (!userChunks || userChunks.useWGSL) ? SHADERLANGUAGE_WGSL : SHADERLANGUAGE_GLSL;
if (device.isWebGPU && this.shaderLanguage === SHADERLANGUAGE_GLSL) {
if (!device.hasTranspilers) ;
}
this.attributes = {
vertex_position: SEMANTIC_POSITION
};
if (options.userAttributes) {
for (const [semantic, name] of Object.entries(options.userAttributes)){
this.attributes[name] = semantic;
}
}
const engineChunks = ShaderChunks.get(device, this.shaderLanguage);
this.chunks = new Map(engineChunks);
if (userChunks) {
const userChunkMap = this.shaderLanguage === SHADERLANGUAGE_GLSL ? userChunks.glsl : userChunks.wgsl;
userChunkMap.forEach((chunk, chunkName)=>{
for(const a in builtinAttributes){
if (builtinAttributes.hasOwnProperty(a) && chunk.indexOf(a) >= 0) {
this.attributes[a] = builtinAttributes[a];
}
}
this.chunks.set(chunkName, chunk);
});
}
this.shaderPassInfo = ShaderPass.get(this.device).getByIndex(options.pass);
this.shadowPass = this.shaderPassInfo.isShadow;
this.lighting = options.lights.length > 0 || options.dirLightMapEnabled || options.clusteredLightingEnabled;
this.reflections = options.reflectionSource !== REFLECTIONSRC_NONE;
this.needsNormal = this.lighting || this.reflections || options.useSpecular || options.ambientSH || options.useHeights || options.enableGGXSpecular || options.clusteredLightingEnabled && !this.shadowPass || options.useClearCoatNormals;
this.needsNormal = this.needsNormal && !this.shadowPass;
this.needsSceneColor = options.useDynamicRefraction;
this.needsScreenSize = options.useDynamicRefraction;
this.needsTransforms = options.useDynamicRefraction;
this.vshader = null;
this.fshader = null;
}
}
export { LitShader };