UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

467 lines (466 loc) 20.7 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { SEMANTIC_ATTR8, SEMANTIC_ATTR9, SEMANTIC_ATTR12, SEMANTIC_ATTR11, SEMANTIC_ATTR14, SEMANTIC_ATTR15, SEMANTIC_BLENDINDICES, SEMANTIC_BLENDWEIGHT, SEMANTIC_COLOR, SEMANTIC_NORMAL, SEMANTIC_POSITION, SEMANTIC_TANGENT, SEMANTIC_TEXCOORD0, SEMANTIC_TEXCOORD1, SHADERLANGUAGE_GLSL, SHADERLANGUAGE_WGSL, primitiveGlslToWgslTypeMap } from "../../../platform/graphics/constants.js"; import { LIGHTSHAPE_PUNCTUAL, LIGHTTYPE_DIRECTIONAL, LIGHTTYPE_OMNI, LIGHTTYPE_SPOT, SHADER_PICK, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, shadowTypeInfo, SHADER_PREPASS, lightTypeNames, lightShapeNames, spriteRenderModeNames, fresnelNames, blendNames, lightFalloffNames, cubemaProjectionNames, specularOcclusionNames, reflectionSrcNames, ambientSrcNames, ditherNames, REFLECTIONSRC_NONE } from "../../constants.js"; import { ChunkUtils } from "../chunk-utils.js"; import { ShaderPass } from "../../shader-pass.js"; import { validateUserChunks } from "../glsl/chunks/chunk-validation.js"; import { Debug } from "../../../core/debug.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 { /** * @param {GraphicsDevice} device - The graphics device. * @param {LitShaderOptions} options - The lit options. * @param {boolean} [allowWGSL] - Whether to allow WGSL shader language. */ constructor(device, options, allowWGSL = true) { /** * Shader code representing varyings. */ __publicField(this, "varyingsCode", ""); /** * The graphics device. * * @type {GraphicsDevice} */ __publicField(this, "device"); /** * The lit options. * * @type {LitShaderOptions} */ __publicField(this, "options"); /** * The shader language, {@link SHADERLANGUAGE_GLSL} or {@link SHADERLANGUAGE_WGSL}. * * @type {string} */ __publicField(this, "shaderLanguage"); /** * The vertex shader defines needed for the shader compilation. * * @type {Map<string, string>} */ __publicField(this, "vDefines", /* @__PURE__ */ new Map()); /** * The fragment shader defines needed for the shader compilation. * * @type {Map<string, string>} */ __publicField(this, "fDefines", /* @__PURE__ */ new Map()); /** * The vertex and fragment shader includes needed for the shader compilation. * * @type {Map<string, string>} */ __publicField(this, "includes", /* @__PURE__ */ new Map()); /** * The shader chunks to use for the shader generation. * * @type {Map<string, string>} */ __publicField(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) { Debug.errorOnce("Cannot use GLSL shader on WebGPU without transpilers", { litShader: this }); } } 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; Debug.call(() => { validateUserChunks(userChunkMap, userChunks.version); }); userChunkMap.forEach((chunk, chunkName) => { Debug.assert(chunk); 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; } /** * Helper function to define a value in the fragment shader. * * @param {boolean} condition - The define is added if the condition is true. * @param {string} name - The define name. * @param {string} [value] - The define value. */ fDefineSet(condition, name, value = "") { if (condition) { this.fDefines.set(name, value); } } /** * The function generates defines for the lit vertex shader, and handles required attributes and * varyings. The source code of the shader is supplied by litMainVS chunk. This vertex shader is * used for all render passes. * * @param {any} useUv - Info about used UVs. * @param {any} useUnmodifiedUv - Info about used unmodified UVs. * @param {any} mapTransforms - Info about used texture transforms. */ generateVertexShader(useUv, useUnmodifiedUv, mapTransforms) { const { options, vDefines, attributes } = this; const varyings = /* @__PURE__ */ 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 = /* @__PURE__ */ 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()} `; this.varyingsCode += this.shaderLanguage === SHADERLANGUAGE_WGSL ? `varying ${name}: ${primitiveGlslToWgslTypeMap.get(type)}; ` : `varying ${type} ${name}; `; }); this.includes.set("varyingsVS", this.varyingsCode); this.includes.set("varyingsPS", this.varyingsCode); this.vshader = ` #include "litMainVS" `; } /** * Generate defines for lighting environment as well as individual lights. * * @param {boolean} hasAreaLights - Whether any of the lights are area lights. * @param {boolean} clusteredLightingEnabled - Whether clustered lighting is enabled. */ _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); Debug.assert(shadowInfo); 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); Debug.assert(shadowInfo); 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"); } /** * Generates a fragment shader. * * @param {string} frontendDecl - Frontend declarations like `float dAlpha;` * @param {string} frontendCode - Frontend code containing `getOpacity()` etc. * @param {string} lightingUv - E.g. `vUv0` */ 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" `; } } export { LitShader };