action-engine-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +348 -0
  3. package/actionengine/3rdparty/goblin/goblin.js +9609 -0
  4. package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
  5. package/actionengine/camera/actioncamera.js +90 -0
  6. package/actionengine/camera/cameracollisionhandler.js +69 -0
  7. package/actionengine/character/actioncharacter.js +360 -0
  8. package/actionengine/character/actioncharacter3D.js +61 -0
  9. package/actionengine/core/app.js +430 -0
  10. package/actionengine/debug/basedebugpanel.js +858 -0
  11. package/actionengine/display/canvasmanager.js +75 -0
  12. package/actionengine/display/gl/programmanager.js +570 -0
  13. package/actionengine/display/gl/shaders/lineshader.js +118 -0
  14. package/actionengine/display/gl/shaders/objectshader.js +1756 -0
  15. package/actionengine/display/gl/shaders/particleshader.js +43 -0
  16. package/actionengine/display/gl/shaders/shadowshader.js +319 -0
  17. package/actionengine/display/gl/shaders/spriteshader.js +100 -0
  18. package/actionengine/display/gl/shaders/watershader.js +67 -0
  19. package/actionengine/display/graphics/actionmodel3D.js +191 -0
  20. package/actionengine/display/graphics/actionsprite3D.js +230 -0
  21. package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
  22. package/actionengine/display/graphics/lighting/actionlight.js +211 -0
  23. package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
  24. package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
  25. package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
  26. package/actionengine/display/graphics/renderableobject.js +44 -0
  27. package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
  28. package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
  29. package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
  30. package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
  31. package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
  32. package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
  33. package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
  34. package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
  35. package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
  36. package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
  37. package/actionengine/display/graphics/texture/texturemanager.js +242 -0
  38. package/actionengine/display/graphics/texture/textureregistry.js +177 -0
  39. package/actionengine/input/actionscrollablearea.js +1405 -0
  40. package/actionengine/input/inputhandler.js +1647 -0
  41. package/actionengine/math/geometry/geometrybuilder.js +161 -0
  42. package/actionengine/math/geometry/glbexporter.js +364 -0
  43. package/actionengine/math/geometry/glbloader.js +722 -0
  44. package/actionengine/math/geometry/modelcodegenerator.js +97 -0
  45. package/actionengine/math/geometry/triangle.js +33 -0
  46. package/actionengine/math/geometry/triangleutils.js +34 -0
  47. package/actionengine/math/mathutils.js +25 -0
  48. package/actionengine/math/matrix4.js +785 -0
  49. package/actionengine/math/physics/actionphysics.js +108 -0
  50. package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
  51. package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
  52. package/actionengine/math/physics/actionraycast.js +129 -0
  53. package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
  54. package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
  55. package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
  56. package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
  57. package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
  58. package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
  59. package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
  60. package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
  61. package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
  62. package/actionengine/math/quaternion.js +61 -0
  63. package/actionengine/math/vector2.js +277 -0
  64. package/actionengine/math/vector3.js +318 -0
  65. package/actionengine/math/viewfrustum.js +136 -0
  66. package/actionengine/network/ACTIONNETREADME.md +810 -0
  67. package/actionengine/network/client/ActionNetManager.js +802 -0
  68. package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
  69. package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
  70. package/actionengine/network/client/SyncSystem.js +422 -0
  71. package/actionengine/network/p2p/ActionNetPeer.js +142 -0
  72. package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
  73. package/actionengine/network/p2p/DataConnection.js +282 -0
  74. package/actionengine/network/p2p/README.md +510 -0
  75. package/actionengine/network/p2p/example.html +502 -0
  76. package/actionengine/network/server/ActionNetServer.js +577 -0
  77. package/actionengine/network/server/ActionNetServerSSL.js +579 -0
  78. package/actionengine/network/server/ActionNetServerUtils.js +458 -0
  79. package/actionengine/network/server/SERVERREADME.md +314 -0
  80. package/actionengine/network/server/package-lock.json +35 -0
  81. package/actionengine/network/server/package.json +13 -0
  82. package/actionengine/network/server/start.bat +27 -0
  83. package/actionengine/network/server/start.sh +25 -0
  84. package/actionengine/network/server/startwss.bat +27 -0
  85. package/actionengine/sound/audiomanager.js +1589 -0
  86. package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
  87. package/actionengine/sound/soundfont/actionparser.js +718 -0
  88. package/actionengine/sound/soundfont/actionreverb.js +252 -0
  89. package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
  90. package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
  91. package/actionengine/sound/soundfont/soundfont.js +2 -0
  92. package/dist/action-engine.min.js +328 -0
  93. package/package.json +35 -0
@@ -0,0 +1,1756 @@
1
+ // actionengine/display/gl/shaders/objectshader.js
2
+
3
+ class ObjectShader {
4
+ constructor() {
5
+ // Store references to different object shader variants
6
+ this.variants = {
7
+ default: {
8
+ getVertexShader: this.getDefaultVertexShader,
9
+ getFragmentShader: this.getDefaultFragmentShader
10
+ },
11
+ pbr: {
12
+ getVertexShader: this.getPBRVertexShader,
13
+ getFragmentShader: this.getPBRFragmentShader
14
+ },
15
+ virtualboy: {
16
+ getVertexShader: this.getVirtualBoyVertexShader,
17
+ getFragmentShader: this.getVirtualBoyFragmentShader
18
+ }
19
+ // Additional variants can be added here
20
+ };
21
+
22
+ // Current active variant (default to 'default')
23
+ this.currentVariant = "default";
24
+ }
25
+
26
+ /**
27
+ * Set the current shader variant
28
+ * @param {string} variantName - Name of the variant to use
29
+ */
30
+ setVariant(variantName) {
31
+ if (this.variants[variantName]) {
32
+ this.currentVariant = variantName;
33
+ console.log(`[ObjectShader] Set shader variant to: ${variantName}`);
34
+ } else {
35
+ console.warn(`[ObjectShader] Unknown variant: ${variantName}, using default`);
36
+ this.currentVariant = "default";
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get the current variant name
42
+ * @returns {string} - Current variant name
43
+ */
44
+ getCurrentVariant() {
45
+ return this.currentVariant;
46
+ }
47
+
48
+ /**
49
+ * Get the current variant's vertex shader
50
+ * @param {boolean} isWebGL2 - Whether WebGL2 is being used
51
+ * @returns {string} - Vertex shader source code
52
+ */
53
+ getVertexShader(isWebGL2) {
54
+ return this.variants[this.currentVariant].getVertexShader.call(this, isWebGL2);
55
+ }
56
+
57
+ /**
58
+ * Get the current variant's fragment shader
59
+ * @param {boolean} isWebGL2 - Whether WebGL2 is being used
60
+ * @returns {string} - Fragment shader source code
61
+ */
62
+ getFragmentShader(isWebGL2) {
63
+ return this.variants[this.currentVariant].getFragmentShader.call(this, isWebGL2);
64
+ }
65
+
66
+ //--------------------------------------------------------------------------
67
+ // DEFAULT SHADER VARIANT
68
+ //--------------------------------------------------------------------------
69
+
70
+ /**
71
+ * Default object vertex shader
72
+ * @param {boolean} isWebGL2 - Whether WebGL2 is being used
73
+ * @returns {string} - Vertex shader source code
74
+ */
75
+ getDefaultVertexShader(isWebGL2) {
76
+ return `${isWebGL2 ? "#version 300 es\n" : ""}
77
+ // Add precision qualifier to make it match fragment shader
78
+ precision mediump float;
79
+
80
+ ${isWebGL2 ? "in" : "attribute"} vec3 aPosition;
81
+ ${isWebGL2 ? "in" : "attribute"} vec3 aNormal;
82
+ ${isWebGL2 ? "in" : "attribute"} vec3 aColor;
83
+ ${isWebGL2 ? "in" : "attribute"} vec2 aTexCoord;
84
+ ${isWebGL2 ? "in" : "attribute"} float aTextureIndex;
85
+ ${isWebGL2 ? "in" : "attribute"} float aUseTexture;
86
+
87
+ uniform mat4 uProjectionMatrix;
88
+ uniform mat4 uViewMatrix;
89
+ uniform mat4 uModelMatrix;
90
+ uniform mat4 uLightSpaceMatrix; // Added for shadow mapping
91
+ uniform vec3 uLightDir;
92
+
93
+ ${isWebGL2 ? "out" : "varying"} vec3 vColor;
94
+ ${isWebGL2 ? "out" : "varying"} vec2 vTexCoord;
95
+ ${isWebGL2 ? "out" : "varying"} float vLighting;
96
+ ${isWebGL2 ? "flat out" : "varying"} float vTextureIndex;
97
+ ${isWebGL2 ? "flat out" : "varying"} float vUseTexture;
98
+ ${isWebGL2 ? "out" : "varying"} vec4 vFragPosLightSpace; // Added for shadow mapping
99
+ ${isWebGL2 ? "out" : "varying"} vec3 vNormal;
100
+ ${isWebGL2 ? "out" : "varying"} vec3 vFragPos;
101
+
102
+ void main() {
103
+ vec4 worldPos = uModelMatrix * vec4(aPosition, 1.0);
104
+ vFragPos = worldPos.xyz;
105
+ gl_Position = uProjectionMatrix * uViewMatrix * worldPos;
106
+
107
+ // Position in light space for shadow mapping
108
+ vFragPosLightSpace = uLightSpaceMatrix * worldPos;
109
+
110
+ // Pass world-space normal
111
+ vNormal = mat3(uModelMatrix) * aNormal;
112
+
113
+ // Calculate basic diffuse lighting
114
+ // Note: We negate the light direction to make it consistent with shadow mapping
115
+ vec3 worldNormal = normalize(vNormal);
116
+ vLighting = max(0.3, min(1.0, dot(worldNormal, normalize(-uLightDir))));
117
+
118
+ // Pass other variables to fragment shader
119
+ vColor = aColor;
120
+ vTexCoord = aTexCoord;
121
+ vTextureIndex = aTextureIndex;
122
+ vUseTexture = aUseTexture;
123
+ }`;
124
+ }
125
+
126
+ /**
127
+ * Default object fragment shader
128
+ * @param {boolean} isWebGL2 - Whether WebGL2 is being used
129
+ * @returns {string} - Fragment shader source code
130
+ */
131
+ getDefaultFragmentShader(isWebGL2) {
132
+ // Directly include shadow calculation functions
133
+ const shadowFunctions = isWebGL2
134
+ ? `
135
+ // Sample from shadow map with hardware-enabled filtering
136
+ float shadowCalculation(vec4 fragPosLightSpace, sampler2D shadowMap) {
137
+ // Perform perspective divide to get NDC coordinates
138
+ vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
139
+
140
+ // Transform to [0,1] range for texture lookup
141
+ projCoords = projCoords * 0.5 + 0.5;
142
+
143
+ // Check if position is outside the shadow map bounds
144
+ if(projCoords.x < 0.0 || projCoords.x > 1.0 ||
145
+ projCoords.y < 0.0 || projCoords.y > 1.0 ||
146
+ projCoords.z < 0.0 || projCoords.z > 1.0) {
147
+ return 1.0; // No shadow outside shadow map
148
+ }
149
+
150
+ // Explicitly sample shadow map with explicit texture binding
151
+ // This helps avoid texture binding conflicts
152
+ float closestDepth = texture(shadowMap, projCoords.xy).r;
153
+
154
+ // Get current depth value
155
+ float currentDepth = projCoords.z;
156
+
157
+ // Apply bias from uniform to avoid shadow acne
158
+ float bias = uShadowBias;
159
+
160
+ // Check if fragment is in shadow
161
+ float shadow = currentDepth - bias > closestDepth ? 0.0 : 1.0;
162
+
163
+ return shadow;
164
+ }
165
+
166
+ // PCF shadow mapping for smoother shadows
167
+ float shadowCalculationPCF(vec4 fragPosLightSpace, sampler2D shadowMap) {
168
+ // Check if PCF is disabled - fall back to basic shadow calculation
169
+ if (!uPCFEnabled) {
170
+ return shadowCalculation(fragPosLightSpace, shadowMap);
171
+ }
172
+
173
+ // Perform perspective divide to get NDC coordinates
174
+ vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
175
+
176
+ // Transform to [0,1] range for texture lookup
177
+ projCoords = projCoords * 0.5 + 0.5;
178
+
179
+ // Check if position is outside the shadow map bounds
180
+ if(projCoords.x < 0.0 || projCoords.x > 1.0 ||
181
+ projCoords.y < 0.0 || projCoords.y > 1.0 ||
182
+ projCoords.z < 0.0 || projCoords.z > 1.0) {
183
+ return 1.0; // No shadow outside shadow map
184
+ }
185
+
186
+ // Get current depth value
187
+ float currentDepth = projCoords.z;
188
+
189
+ // Apply bias from uniform - adjust using softness factor
190
+ float softnessFactor = max(0.1, uShadowSoftness); // Ensure minimum softness
191
+ float bias = uShadowBias * softnessFactor;
192
+
193
+ // Calculate PCF with explicit shadow map sampling
194
+ float shadow = 0.0;
195
+ vec2 texelSize = 1.0 / vec2(textureSize(shadowMap, 0));
196
+
197
+ // Determine PCF kernel radius based on uPCFSize
198
+ int pcfRadius = uPCFSize / 2;
199
+ float totalSamples = 0.0;
200
+
201
+ // Dynamic PCF sampling using the specified kernel size
202
+ for(int x = -pcfRadius; x <= pcfRadius; ++x) {
203
+ for(int y = -pcfRadius; y <= pcfRadius; ++y) {
204
+ // Skip samples outside the kernel radius
205
+ // (needed for non-square kernels like 3x3, 5x5, etc.)
206
+ if (abs(x) <= pcfRadius && abs(y) <= pcfRadius) {
207
+ // Apply softness factor to sampling coordinates
208
+ vec2 offset = vec2(x, y) * texelSize * mix(1.0, 2.0, uShadowSoftness);
209
+
210
+ // Explicitly sample shadow map with clear texture binding
211
+ float pcfDepth = texture(shadowMap, projCoords.xy + offset).r;
212
+ shadow += currentDepth - bias > pcfDepth ? 0.0 : 1.0;
213
+ totalSamples += 1.0;
214
+ }
215
+ }
216
+ }
217
+
218
+ // Average samples
219
+ shadow /= max(1.0, totalSamples);
220
+
221
+ return shadow;
222
+ }`
223
+ : `
224
+ // Unpack depth from RGBA color
225
+ float unpackDepth(vec4 packedDepth) {
226
+ const vec4 bitShift = vec4(1.0, 1.0/256.0, 1.0/(256.0*256.0), 1.0/(256.0*256.0*256.0));
227
+ return dot(packedDepth, bitShift);
228
+ }
229
+
230
+ // Shadow calculation for WebGL1
231
+ float shadowCalculation(vec4 fragPosLightSpace, sampler2D shadowMap) {
232
+ // Perform perspective divide to get NDC coordinates
233
+ vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
234
+
235
+ // Transform to [0,1] range for texture lookup
236
+ projCoords = projCoords * 0.5 + 0.5;
237
+
238
+ // Check if position is outside the shadow map bounds
239
+ if(projCoords.x < 0.0 || projCoords.x > 1.0 ||
240
+ projCoords.y < 0.0 || projCoords.y > 1.0 ||
241
+ projCoords.z < 0.0 || projCoords.z > 1.0) {
242
+ return 1.0; // No shadow outside shadow map
243
+ }
244
+
245
+ // Get packed depth value
246
+ vec4 packedDepth = texture2D(shadowMap, projCoords.xy);
247
+
248
+ // Unpack the depth value
249
+ float closestDepth = unpackDepth(packedDepth);
250
+
251
+ // Get current depth value
252
+ float currentDepth = projCoords.z;
253
+
254
+ // Apply bias from uniform to avoid shadow acne
255
+ float bias = uShadowBias;
256
+
257
+ // Check if fragment is in shadow
258
+ float shadow = currentDepth - bias > closestDepth ? 0.0 : 1.0;
259
+
260
+ return shadow;
261
+ }
262
+
263
+ // PCF shadow calculation for WebGL1
264
+ float shadowCalculationPCF(vec4 fragPosLightSpace, sampler2D shadowMap) {
265
+ // Check if PCF is disabled - fall back to basic shadow calculation
266
+ if (!uPCFEnabled) {
267
+ return shadowCalculation(fragPosLightSpace, shadowMap);
268
+ }
269
+
270
+ // Perform perspective divide to get NDC coordinates
271
+ vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
272
+
273
+ // Transform to [0,1] range for texture lookup
274
+ projCoords = projCoords * 0.5 + 0.5;
275
+
276
+ // Check if position is outside the shadow map bounds
277
+ if(projCoords.x < 0.0 || projCoords.x > 1.0 ||
278
+ projCoords.y < 0.0 || projCoords.y > 1.0 ||
279
+ projCoords.z < 0.0 || projCoords.z > 1.0) {
280
+ return 1.0; // No shadow outside shadow map
281
+ }
282
+
283
+ // Get current depth value
284
+ float currentDepth = projCoords.z;
285
+
286
+ // Apply bias from uniform - adjust using softness factor
287
+ float softnessFactor = max(0.1, uShadowSoftness); // Ensure minimum softness
288
+ float bias = uShadowBias * softnessFactor;
289
+
290
+ // Calculate PCF with explicit shadow map sampling
291
+ float shadow = 0.0;
292
+ float texelSize = 1.0 / uShadowMapSize;
293
+
294
+ // Determine PCF kernel radius based on uPCFSize
295
+ int pcfRadius = int(uPCFSize) / 2;
296
+ float totalSamples = 0.0;
297
+
298
+ // WebGL1 has more limited loop support, so limit to max 9x9 kernel
299
+ // We need fixed loop bounds in WebGL1
300
+ for(int x = -4; x <= 4; ++x) {
301
+ for(int y = -4; y <= 4; ++y) {
302
+ // Skip samples outside the requested kernel radius
303
+ if (abs(x) <= pcfRadius && abs(y) <= pcfRadius) {
304
+ // Apply softness factor to sampling coordinates
305
+ vec2 offset = vec2(x, y) * texelSize * mix(1.0, 2.0, uShadowSoftness);
306
+
307
+ vec4 packedDepth = texture2D(shadowMap, projCoords.xy + offset);
308
+ float pcfDepth = unpackDepth(packedDepth);
309
+ shadow += currentDepth - bias > pcfDepth ? 0.0 : 1.0;
310
+ totalSamples += 1.0;
311
+ }
312
+ }
313
+ }
314
+
315
+ // Average samples
316
+ shadow /= max(1.0, totalSamples);
317
+
318
+ return shadow;
319
+ }`;
320
+
321
+ return `${isWebGL2 ? "#version 300 es\n" : ""}
322
+ precision mediump float;
323
+ ${isWebGL2 ? "precision mediump sampler2DArray;\n" : ""}
324
+
325
+ ${isWebGL2 ? "in" : "varying"} vec3 vColor;
326
+ ${isWebGL2 ? "in" : "varying"} vec2 vTexCoord;
327
+ ${isWebGL2 ? "in" : "varying"} float vLighting;
328
+ ${isWebGL2 ? "flat in" : "varying"} float vTextureIndex;
329
+ ${isWebGL2 ? "flat in" : "varying"} float vUseTexture;
330
+ ${isWebGL2 ? "in" : "varying"} vec4 vFragPosLightSpace;
331
+ ${isWebGL2 ? "in" : "varying"} vec3 vNormal;
332
+ ${isWebGL2 ? "in" : "varying"} vec3 vFragPos;
333
+
334
+ // Texture array for albedo textures
335
+ ${isWebGL2 ? "uniform sampler2DArray uTextureArray;" : "uniform sampler2D uTexture;"}
336
+
337
+ // Shadow map with explicit separate binding
338
+ // Always use sampler2D for shadow maps
339
+ uniform sampler2D uShadowMap;
340
+ ${isWebGL2 ? "uniform samplerCube uPointShadowMap;" : "uniform sampler2D uPointShadowMap;"}
341
+
342
+ // Light counts
343
+ uniform int uDirectionalLightCount;
344
+ uniform int uPointLightCount;
345
+ uniform int uSpotLightCount;
346
+
347
+ // Light data textures - each light has multiple pixels for all properties
348
+ uniform sampler2D uDirectionalLightData;
349
+ uniform vec2 uDirectionalLightTextureSize;
350
+ uniform sampler2D uPointLightData;
351
+ uniform vec2 uPointLightTextureSize;
352
+
353
+ // Legacy directional light uniforms (for backward compatibility)
354
+ uniform vec3 uLightPos;
355
+ uniform vec3 uLightDir;
356
+ uniform float uLightIntensity;
357
+ uniform vec3 uLightColor;
358
+
359
+ // Legacy point light uniforms (for backward compatibility)
360
+ uniform vec3 uPointLightPos;
361
+ uniform float uPointLightIntensity;
362
+ uniform float uLightRadius;
363
+ uniform vec3 uPointLightColor;
364
+
365
+ // Legacy second point light uniforms
366
+ uniform vec3 uPointLightPos1;
367
+ uniform float uPointLightIntensity1;
368
+ uniform float uPointLightRadius1;
369
+ uniform vec3 uPointLightColor1;
370
+ uniform samplerCube uPointShadowMap1;
371
+ uniform bool uPointShadowsEnabled1;
372
+
373
+ // Additional point light shadow maps
374
+ uniform samplerCube uPointShadowMap2;
375
+ uniform bool uPointShadowsEnabled2;
376
+ uniform samplerCube uPointShadowMap3;
377
+ uniform bool uPointShadowsEnabled3;
378
+
379
+ uniform float uIntensityFactor; // Factor controlling intensity effect in default shader
380
+ uniform bool uShadowsEnabled;
381
+ uniform bool uPointShadowsEnabled; // Enable point light shadows
382
+ //uniform int uPointLightCount; // Number of point lights
383
+ uniform float uShadowBias; // Shadow bias uniform for controlling shadow acne
384
+ uniform float uShadowMapSize; // Shadow map size for texture calculations
385
+ uniform float uShadowSoftness; // Controls shadow edge softness (0-1)
386
+ uniform int uPCFSize; // Controls PCF kernel size (1, 3, 5, 7, 9)
387
+ uniform bool uPCFEnabled; // Controls whether PCF filtering is enabled
388
+ uniform float uFarPlane; // Far plane for point light shadows
389
+
390
+ // Structures to hold light data
391
+ struct DirectionalLight {
392
+ vec3 position;
393
+ vec3 direction;
394
+ vec3 color;
395
+ float intensity;
396
+ bool shadowsEnabled;
397
+ };
398
+
399
+ struct PointLight {
400
+ vec3 position;
401
+ vec3 color;
402
+ float intensity;
403
+ float radius;
404
+ bool shadowsEnabled;
405
+ };
406
+
407
+ // Functions to extract light data from textures
408
+ DirectionalLight getDirectionalLight(int index) {
409
+ // Each light takes 3 pixels horizontally
410
+ int basePixel = index * 3;
411
+
412
+ // Calculate UV coordinates for each pixel
413
+ // First pixel: position + enabled
414
+ float u1 = (float(basePixel) + 0.5) / uDirectionalLightTextureSize.x;
415
+ // Second pixel: direction + shadowEnabled
416
+ float u2 = (float(basePixel + 1) + 0.5) / uDirectionalLightTextureSize.x;
417
+ // Third pixel: color + intensity
418
+ float u3 = (float(basePixel + 2) + 0.5) / uDirectionalLightTextureSize.x;
419
+
420
+ // Use centered V coordinate (there's only one row)
421
+ float v = 0.5 / uDirectionalLightTextureSize.y;
422
+
423
+ // Sample pixels from texture
424
+ vec4 posData = texture(uDirectionalLightData, vec2(u1, v));
425
+ vec4 dirData = texture(uDirectionalLightData, vec2(u2, v));
426
+ vec4 colorData = texture(uDirectionalLightData, vec2(u3, v));
427
+
428
+ // Create and populate the light structure
429
+ DirectionalLight light;
430
+ light.position = posData.xyz;
431
+ light.direction = dirData.xyz;
432
+ light.color = colorData.rgb;
433
+ light.intensity = colorData.a;
434
+ light.shadowsEnabled = dirData.a > 0.5;
435
+
436
+ return light;
437
+ }
438
+
439
+ PointLight getPointLight(int index) {
440
+ // Each light takes 3 pixels horizontally
441
+ int basePixel = index * 3;
442
+
443
+ // Calculate UV coordinates for each pixel
444
+ // First pixel: position + enabled
445
+ float u1 = (float(basePixel) + 0.5) / uPointLightTextureSize.x;
446
+ // Second pixel: color + intensity
447
+ float u2 = (float(basePixel + 1) + 0.5) / uPointLightTextureSize.x;
448
+ // Third pixel: radius + shadowEnabled + padding
449
+ float u3 = (float(basePixel + 2) + 0.5) / uPointLightTextureSize.x;
450
+
451
+ // Use centered V coordinate (there's only one row)
452
+ float v = 0.5 / uPointLightTextureSize.y;
453
+
454
+ // Sample pixels from texture
455
+ vec4 posData = texture(uPointLightData, vec2(u1, v));
456
+ vec4 colorData = texture(uPointLightData, vec2(u2, v));
457
+ vec4 radiusData = texture(uPointLightData, vec2(u3, v));
458
+
459
+ // Create and populate the light structure
460
+ PointLight light;
461
+ light.position = posData.xyz;
462
+ light.color = colorData.rgb;
463
+ light.intensity = colorData.a;
464
+ light.radius = radiusData.r;
465
+ light.shadowsEnabled = radiusData.g > 0.5;
466
+
467
+ return light;
468
+ }
469
+
470
+ ${isWebGL2 ? "out vec4 fragColor;" : ""}
471
+
472
+ // Shadow mapping functions
473
+ ${shadowFunctions}
474
+
475
+ // Point light shadow functions
476
+ ${
477
+ isWebGL2
478
+ ? `
479
+ // Calculate shadow for omnidirectional point light with cubemap shadow
480
+ float pointShadowCalculation(vec3 fragPos, vec3 lightPos, samplerCube shadowMap, float farPlane) {
481
+ // Calculate fragment-to-light vector
482
+ vec3 fragToLight = fragPos - lightPos;
483
+
484
+ // Get current distance from fragment to light
485
+ float currentDepth = length(fragToLight);
486
+
487
+ // Normalize to [0,1] range using far plane
488
+ currentDepth = currentDepth / farPlane;
489
+
490
+ // Apply bias
491
+ float bias = uShadowBias;
492
+
493
+ // Sample from cubemap shadow map in the direction of fragToLight
494
+ float closestDepth = texture(shadowMap, fragToLight).r;
495
+
496
+ // Check if fragment is in shadow
497
+ float shadow = currentDepth - bias > closestDepth ? 0.0 : 1.0;
498
+
499
+ return shadow;
500
+ }
501
+
502
+ // PCF shadow calculation for omnidirectional point light
503
+ float pointShadowCalculationPCF(vec3 fragPos, vec3 lightPos, samplerCube shadowMap, float farPlane) {
504
+ // Check if PCF is disabled - fall back to basic shadow calculation
505
+ if (!uPCFEnabled) {
506
+ return pointShadowCalculation(fragPos, lightPos, shadowMap, farPlane);
507
+ }
508
+
509
+ // Calculate fragment-to-light vector (will be used as cubemap direction)
510
+ vec3 fragToLight = fragPos - lightPos;
511
+
512
+ // Get current distance from fragment to light
513
+ float currentDepth = length(fragToLight);
514
+
515
+ // Normalize to [0,1] range using far plane
516
+ currentDepth = currentDepth / farPlane;
517
+
518
+ // Apply bias, adjusted by softness
519
+ float softnessFactor = max(0.1, uShadowSoftness); // Ensure minimum softness
520
+ float bias = uShadowBias * softnessFactor;
521
+
522
+ // Set up PCF sampling
523
+ float shadow = 0.0;
524
+ int samples = 0;
525
+ float diskRadius = 0.01 * softnessFactor; // Adjust based on softness and distance
526
+
527
+ // Generate a tangent space TBN matrix for sampling in a cone
528
+ vec3 absFragToLight = abs(fragToLight);
529
+ vec3 tangent, bitangent;
530
+
531
+ // Find least used axis to avoid precision issues
532
+ if (absFragToLight.x <= absFragToLight.y && absFragToLight.x <= absFragToLight.z) {
533
+ tangent = vec3(0.0, fragToLight.z, -fragToLight.y);
534
+ } else if (absFragToLight.y <= absFragToLight.x && absFragToLight.y <= absFragToLight.z) {
535
+ tangent = vec3(fragToLight.z, 0.0, -fragToLight.x);
536
+ } else {
537
+ tangent = vec3(fragToLight.y, -fragToLight.x, 0.0);
538
+ }
539
+
540
+ tangent = normalize(tangent);
541
+ bitangent = normalize(cross(fragToLight, tangent));
542
+
543
+ // Determine sample count based on PCF size
544
+ int pcfRadius = uPCFSize / 2;
545
+ int maxSamples = (pcfRadius * 2 + 1) * (pcfRadius * 2 + 1);
546
+
547
+ for (int i = 0; i < maxSamples; i++) {
548
+ // Skip if we exceed the requested PCF size
549
+ int x = (i % 9) - 4;
550
+ int y = (i / 9) - 4;
551
+
552
+ if (abs(x) <= pcfRadius && abs(y) <= pcfRadius) {
553
+ // Generate offset direction based on x, y grid position
554
+ float angle = float(x) * (3.14159265359 / float(pcfRadius + 1)); // Convert x to angle
555
+ float distance = float(y) + 0.1; // Add small offset to avoid zero
556
+
557
+ // Calculate offset direction in tangent space
558
+ vec3 offset = tangent * (cos(angle) * distance * diskRadius) +
559
+ bitangent * (sin(angle) * distance * diskRadius);
560
+
561
+ // Sample from the cubemap with offset
562
+ float closestDepth = texture(shadowMap, normalize(fragToLight + offset)).r;
563
+
564
+ // Check if fragment is in shadow with bias
565
+ shadow += currentDepth - bias > closestDepth ? 0.0 : 1.0;
566
+ samples++;
567
+ }
568
+ }
569
+
570
+ // Average all samples
571
+ shadow /= float(max(samples, 1));
572
+
573
+ return shadow;
574
+ }`
575
+ : `
576
+ // For WebGL1 without cubemap support, calculate shadow from a single face
577
+ float pointShadowCalculation(vec3 fragPos, vec3 lightPos, sampler2D shadowMap, float farPlane) {
578
+ // We can't do proper cubemap in WebGL1, so this is just an approximation
579
+ // using the first face of what would be a cubemap
580
+ vec3 fragToLight = fragPos - lightPos;
581
+
582
+ // Get current distance from fragment to light
583
+ float currentDepth = length(fragToLight);
584
+
585
+ // Normalize to [0,1] range using far plane
586
+ currentDepth = currentDepth / farPlane;
587
+
588
+ // Simple planar mapping for the single shadow map face
589
+ // This is just a fallback - won't look great but better than nothing
590
+ vec2 shadowCoord = vec2(
591
+ (fragToLight.x / abs(fragToLight.x + 0.0001) + 1.0) * 0.25,
592
+ (fragToLight.y / abs(fragToLight.y + 0.0001) + 1.0) * 0.25
593
+ );
594
+
595
+ // Apply bias
596
+ float bias = uShadowBias;
597
+
598
+ // Sample from shadow map
599
+ vec4 packedDepth = texture2D(shadowMap, shadowCoord);
600
+ float closestDepth = unpackDepth(packedDepth);
601
+
602
+ // Check if fragment is in shadow
603
+ float shadow = currentDepth - bias > closestDepth ? 0.0 : 1.0;
604
+
605
+ return shadow;
606
+ }
607
+
608
+ // Simplified PCF for WebGL1 single-face approximation
609
+ float pointShadowCalculationPCF(vec3 fragPos, vec3 lightPos, sampler2D shadowMap, float farPlane) {
610
+ // Check if PCF is disabled - fall back to basic shadow calculation
611
+ if (!uPCFEnabled) {
612
+ return pointShadowCalculation(fragPos, lightPos, shadowMap, farPlane);
613
+ }
614
+
615
+ // Calculate fragment-to-light vector
616
+ vec3 fragToLight = fragPos - lightPos;
617
+
618
+ // Get current distance from fragment to light
619
+ float currentDepth = length(fragToLight);
620
+
621
+ // Normalize to [0,1] range using far plane
622
+ currentDepth = currentDepth / farPlane;
623
+
624
+ // Simple planar mapping for the single shadow map face
625
+ vec2 shadowCoord = vec2(
626
+ (fragToLight.x / abs(fragToLight.x + 0.0001) + 1.0) * 0.25,
627
+ (fragToLight.y / abs(fragToLight.y + 0.0001) + 1.0) * 0.25
628
+ );
629
+
630
+ // Apply bias
631
+ float softnessFactor = max(0.1, uShadowSoftness); // Ensure minimum softness
632
+ float bias = uShadowBias * softnessFactor;
633
+
634
+ // Set up PCF sampling
635
+ float shadow = 0.0;
636
+ float texelSize = 1.0 / uShadowMapSize;
637
+
638
+ // Determine PCF kernel radius based on uPCFSize
639
+ int pcfRadius = int(uPCFSize) / 2;
640
+ float totalSamples = 0.0;
641
+
642
+ // Limit loop size for WebGL1
643
+ for(int x = -4; x <= 4; ++x) {
644
+ for(int y = -4; y <= 4; ++y) {
645
+ // Skip samples outside the requested kernel radius
646
+ if (abs(x) <= pcfRadius && abs(y) <= pcfRadius) {
647
+ // Apply softness factor to sampling coordinates
648
+ vec2 offset = vec2(x, y) * texelSize * mix(1.0, 2.0, uShadowSoftness);
649
+
650
+ vec4 packedDepth = texture2D(shadowMap, shadowCoord + offset);
651
+ float pcfDepth = unpackDepth(packedDepth);
652
+ shadow += currentDepth - bias > pcfDepth ? 0.0 : 1.0;
653
+ totalSamples += 1.0;
654
+ }
655
+ }
656
+ }
657
+
658
+ // Average samples
659
+ shadow /= max(1.0, totalSamples);
660
+
661
+ return shadow;
662
+ }`
663
+ }
664
+
665
+ void main() {
666
+ // Base color calculation
667
+ vec4 baseColor;
668
+ if (vUseTexture > 0.5) { // Check if this fragment uses texture
669
+ ${isWebGL2 ? "baseColor = texture(uTextureArray, vec3(vTexCoord, vTextureIndex));" : "baseColor = texture2D(uTexture, vTexCoord);"}
670
+ } else {
671
+ baseColor = vec4(vColor, 1.0);
672
+ }
673
+
674
+ // Apply ambient and diffuse lighting
675
+ float ambient = 0.3; // Higher ambient to ensure dungeon isn't too dark
676
+ // Negate light direction to be consistent with shadow mapping convention
677
+ float diffuse = max(0.0, dot(normalize(vNormal), normalize(-uLightDir)));
678
+
679
+ // Apply light intensity directly - use a more dramatic effect
680
+ // Scale from 0 (no light) to very bright at high intensity values
681
+ float intensity = uLightIntensity / 100.0; // More aggressive scaling
682
+ diffuse = diffuse * clamp(intensity, 0.1, 10.0); // Allow for dramatically brighter light
683
+
684
+ // Calculate shadow factor for directional light
685
+ float shadow = 1.0;
686
+ if (uShadowsEnabled) {
687
+ // Use explicit texture lookup to avoid sampler conflicts
688
+ float shadowFactor = shadowCalculationPCF(vFragPosLightSpace, uShadowMap);
689
+ // Match the PBR shader calculation - shadows should be darker
690
+ shadow = 1.0 - (1.0 - shadowFactor) * 0.8;
691
+ }
692
+
693
+ // Calculate point light contributions
694
+ vec3 pointLightColors = vec3(0.0);
695
+
696
+ // Process all point lights from the data texture
697
+ for (int i = 0; i < uPointLightCount; i++) {
698
+ if (i >= 100) break; // Reasonable safety limit
699
+
700
+ // Extract light data from texture
701
+ PointLight light = getPointLight(i);
702
+
703
+ vec3 lightDir = normalize(vFragPos - light.position);
704
+ float pointDiffuse = max(0.0, dot(normalize(vNormal), -lightDir));
705
+
706
+ // Calculate distance attenuation
707
+ float distance = length(vFragPos - light.position);
708
+ float attenuation = 1.0 / (1.0 + (distance * distance) / (light.radius * light.radius));
709
+
710
+ // Calculate shadow for point light
711
+ float pointShadow = 1.0;
712
+ if (light.shadowsEnabled) {
713
+ // Handle first 8 shadow maps with a switch statement
714
+ int lightIdx = i % 8; // Limit to 8 shadowed lights
715
+ switch(lightIdx) {
716
+ case 0:
717
+ if (uPointShadowsEnabled) {
718
+ float pointShadowFactor = pointShadowCalculationPCF(vFragPos, light.position, uPointShadowMap, uFarPlane);
719
+ pointShadow = 1.0 - (1.0 - pointShadowFactor) * 0.8;
720
+ }
721
+ break;
722
+ case 1:
723
+ if (uPointShadowsEnabled1) {
724
+ float pointShadowFactor = pointShadowCalculationPCF(vFragPos, light.position, uPointShadowMap1, uFarPlane);
725
+ pointShadow = 1.0 - (1.0 - pointShadowFactor) * 0.8;
726
+ }
727
+ break;
728
+ case 2:
729
+ if (uPointShadowsEnabled2) {
730
+ float pointShadowFactor = pointShadowCalculationPCF(vFragPos, light.position, uPointShadowMap2, uFarPlane);
731
+ pointShadow = 1.0 - (1.0 - pointShadowFactor) * 0.8;
732
+ }
733
+ break;
734
+ case 3:
735
+ if (uPointShadowsEnabled3) {
736
+ float pointShadowFactor = pointShadowCalculationPCF(vFragPos, light.position, uPointShadowMap3, uFarPlane);
737
+ pointShadow = 1.0 - (1.0 - pointShadowFactor) * 0.8;
738
+ }
739
+ break;
740
+ }
741
+ }
742
+
743
+ // Calculate final point light contribution
744
+ float pointLightFactor = max(0.0, pointDiffuse * attenuation * pointShadow);
745
+
746
+ // Add contribution from this light
747
+ pointLightColors += baseColor.rgb * pointLightFactor * light.color * light.intensity;
748
+ }
749
+
750
+ // Legacy point light handling - only use if no new lights are available
751
+ if (uPointLightCount == 0) {
752
+ // Backward compatibility for first point light
753
+ vec3 lightDir = normalize(vFragPos - uPointLightPos);
754
+ float pointDiffuse = max(0.0, dot(normalize(vNormal), -lightDir));
755
+
756
+ float distance = length(vFragPos - uPointLightPos);
757
+ float attenuation = 1.0 / (1.0 + (distance * distance) / (uLightRadius * uLightRadius));
758
+
759
+ float pointShadow = 1.0;
760
+ if (uPointShadowsEnabled) {
761
+ float pointShadowFactor = pointShadowCalculationPCF(vFragPos, uPointLightPos, uPointShadowMap, uFarPlane);
762
+ pointShadow = 1.0 - (1.0 - pointShadowFactor) * 0.8;
763
+ }
764
+
765
+ float pointLightFactor = max(0.0, pointDiffuse * attenuation * pointShadow);
766
+ pointLightColors = baseColor.rgb * pointLightFactor * uPointLightColor * uPointLightIntensity;
767
+
768
+ // Legacy second point light
769
+ if (uPointLightCount > 1) {
770
+ lightDir = normalize(vFragPos - uPointLightPos1);
771
+ pointDiffuse = max(0.0, dot(normalize(vNormal), -lightDir));
772
+
773
+ distance = length(vFragPos - uPointLightPos1);
774
+ attenuation = 1.0 / (1.0 + (distance * distance) / (uPointLightRadius1 * uPointLightRadius1));
775
+
776
+ pointShadow = 1.0;
777
+ if (uPointShadowsEnabled1) {
778
+ float pointShadowFactor = pointShadowCalculationPCF(vFragPos, uPointLightPos1, uPointShadowMap1, uFarPlane);
779
+ pointShadow = 1.0 - (1.0 - pointShadowFactor) * 0.8;
780
+ }
781
+
782
+ pointLightFactor = max(0.0, pointDiffuse * attenuation * pointShadow);
783
+ pointLightColors += baseColor.rgb * pointLightFactor * uPointLightColor1 * uPointLightIntensity1;
784
+ }
785
+ }
786
+
787
+ // Get total directional light contribution
788
+ float lighting = ambient; // Start with ambient
789
+ vec3 directionalContribution = vec3(0.0);
790
+
791
+ // Process all directional lights from the data texture
792
+ for (int i = 0; i < uDirectionalLightCount; i++) {
793
+ if (i >= 100) break; // Reasonable safety limit
794
+
795
+ // Extract light data from texture
796
+ DirectionalLight light = getDirectionalLight(i);
797
+
798
+ // Calculate diffuse component
799
+ float lightDiffuse = max(0.0, dot(normalize(vNormal), normalize(-light.direction)));
800
+
801
+ // Calculate shadow - currently only first light gets shadow mapping
802
+ float lightShadow = 1.0;
803
+ if (light.shadowsEnabled && i == 0 && uShadowsEnabled) {
804
+ lightShadow = shadow;
805
+ }
806
+
807
+ // Add this light's contribution
808
+ float contribution = lightDiffuse * lightShadow * light.intensity * uIntensityFactor;
809
+ directionalContribution += baseColor.rgb * contribution * light.color;
810
+ }
811
+
812
+ // If there are no directional lights or for backward compatibility
813
+ if (uDirectionalLightCount == 0 && uShadowsEnabled) {
814
+ // Legacy directional light calculation
815
+ float legacyContribution = diffuse * shadow * uLightIntensity * uIntensityFactor;
816
+ directionalContribution = baseColor.rgb * (ambient + legacyContribution);
817
+ } else if (uDirectionalLightCount == 0) {
818
+ // Just ambient with no directional lights
819
+ directionalContribution = baseColor.rgb * ambient;
820
+ } else {
821
+ // Add ambient to the direct contribution
822
+ directionalContribution += baseColor.rgb * ambient;
823
+ }
824
+
825
+ // Properly handle the combination of both light types
826
+ // This ensures they're physically correctly combined and don't over-brighten
827
+ vec3 result = directionalContribution;
828
+
829
+ // Only add point light if it exists
830
+ if (uPointLightCount > 0) {
831
+ // NO BLENDING - JUST ADD THE LIGHT CONTRIBUTIONS DIRECTLY
832
+ // This eliminates the intensity-based blend factor that was causing inversion
833
+ result = directionalContribution + pointLightColors;
834
+
835
+ // OPTIONAL: To prevent over-brightening, we could clamp the result
836
+ // but for now let's see what happens with direct addition
837
+ // result = min(vec3(1.0), result);
838
+ }
839
+
840
+ ${isWebGL2 ? "fragColor" : "gl_FragColor"} = vec4(result, baseColor.a);
841
+ }`;
842
+ }
843
+
844
+ //--------------------------------------------------------------------------
845
+ // PBR SHADER VARIANT
846
+ //--------------------------------------------------------------------------
847
+
848
+ /**
849
+ * PBR vertex shader
850
+ * @param {boolean} isWebGL2 - Whether WebGL2 is being used
851
+ * @returns {string} - Vertex shader source code
852
+ */
853
+ getPBRVertexShader(isWebGL2) {
854
+ return `${isWebGL2 ? "#version 300 es\n" : ""}
855
+ // Attributes - data coming in per vertex
856
+ ${isWebGL2 ? "in" : "attribute"} vec3 aPosition;
857
+ ${isWebGL2 ? "in" : "attribute"} vec3 aNormal;
858
+ ${isWebGL2 ? "in" : "attribute"} vec3 aColor;
859
+ ${isWebGL2 ? "in" : "attribute"} vec2 aTexCoord;
860
+ ${isWebGL2 ? "in" : "attribute"} float aTextureIndex;
861
+ ${isWebGL2 ? "in" : "attribute"} float aUseTexture;
862
+
863
+ // Uniforms - shared data for all vertices
864
+ uniform mat4 uProjectionMatrix;
865
+ uniform mat4 uViewMatrix;
866
+ uniform mat4 uModelMatrix;
867
+ uniform mat4 uLightSpaceMatrix; // Added for shadow mapping
868
+
869
+ uniform vec3 uLightDir;
870
+ uniform vec3 uCameraPos;
871
+
872
+ // Outputs to fragment shader
873
+ ${isWebGL2 ? "out" : "varying"} vec3 vNormal; // Surface normal
874
+ ${isWebGL2 ? "out" : "varying"} vec3 vWorldPos; // Position in world space
875
+ ${isWebGL2 ? "out" : "varying"} vec4 vFragPosLightSpace; // Added for shadow mapping
876
+ ${isWebGL2 ? "out" : "varying"} vec3 vFragPos;
877
+ ${isWebGL2 ? "out" : "varying"} vec3 vColor;
878
+ ${isWebGL2 ? "out" : "varying"} vec3 vViewDir; // Direction to camera
879
+ ${isWebGL2 ? "flat out" : "varying"} float vTextureIndex;
880
+ ${isWebGL2 ? "out" : "varying"} vec2 vTexCoord;
881
+ ${isWebGL2 ? "flat out" : "varying"} float vUseTexture;
882
+
883
+ void main() {
884
+ // Calculate world position
885
+ vec4 worldPos = uModelMatrix * vec4(aPosition, 1.0);
886
+ vWorldPos = worldPos.xyz;
887
+ vFragPos = worldPos.xyz;
888
+ // Transform normal to world space
889
+ vNormal = mat3(uModelMatrix) * aNormal;
890
+
891
+ // Calculate view direction
892
+ vViewDir = normalize(uCameraPos - worldPos.xyz);
893
+
894
+ // Position in light space for shadow mapping
895
+ vFragPosLightSpace = uLightSpaceMatrix * worldPos;
896
+
897
+ // Pass color and texture info to fragment shader
898
+ vColor = aColor;
899
+ vTexCoord = aTexCoord;
900
+ vTextureIndex = aTextureIndex;
901
+ vUseTexture = aUseTexture;
902
+
903
+ // Final position
904
+ gl_Position = uProjectionMatrix * uViewMatrix * worldPos;
905
+ }`;
906
+ }
907
+
908
+ /**
909
+ * PBR fragment shader
910
+ * @param {boolean} isWebGL2 - Whether WebGL2 is being used
911
+ * @returns {string} - Fragment shader source code
912
+ */
913
+ getPBRFragmentShader(isWebGL2) {
914
+ // Directly include shadow calculation functions
915
+ const shadowFunctions = isWebGL2
916
+ ? `
917
+ // Sample from shadow map with hardware-enabled filtering
918
+ float shadowCalculation(vec4 fragPosLightSpace, sampler2D shadowMap) {
919
+ // Perform perspective divide to get NDC coordinates
920
+ vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
921
+
922
+ // Transform to [0,1] range for texture lookup
923
+ projCoords = projCoords * 0.5 + 0.5;
924
+
925
+ // Check if position is outside the shadow map bounds
926
+ if(projCoords.x < 0.0 || projCoords.x > 1.0 ||
927
+ projCoords.y < 0.0 || projCoords.y > 1.0 ||
928
+ projCoords.z < 0.0 || projCoords.z > 1.0) {
929
+ return 1.0; // No shadow outside shadow map
930
+ }
931
+
932
+ // Explicitly sample shadow map with explicit texture binding
933
+ // This helps avoid texture binding conflicts
934
+ float closestDepth = texture(shadowMap, projCoords.xy).r;
935
+
936
+ // Get current depth value
937
+ float currentDepth = projCoords.z;
938
+
939
+ // Apply bias from uniform to avoid shadow acne
940
+ float bias = uShadowBias;
941
+
942
+ // Check if fragment is in shadow
943
+ float shadow = currentDepth - bias > closestDepth ? 0.0 : 1.0;
944
+
945
+ return shadow;
946
+ }
947
+
948
+ // PCF shadow mapping for smoother shadows
949
+ float shadowCalculationPCF(vec4 fragPosLightSpace, sampler2D shadowMap) {
950
+ // Check if PCF is disabled - fall back to basic shadow calculation
951
+ if (!uPCFEnabled) {
952
+ return shadowCalculation(fragPosLightSpace, shadowMap);
953
+ }
954
+
955
+ // Perform perspective divide to get NDC coordinates
956
+ vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
957
+
958
+ // Transform to [0,1] range for texture lookup
959
+ projCoords = projCoords * 0.5 + 0.5;
960
+
961
+ // Check if position is outside the shadow map bounds
962
+ if(projCoords.x < 0.0 || projCoords.x > 1.0 ||
963
+ projCoords.y < 0.0 || projCoords.y > 1.0 ||
964
+ projCoords.z < 0.0 || projCoords.z > 1.0) {
965
+ return 1.0; // No shadow outside shadow map
966
+ }
967
+
968
+ // Get current depth value
969
+ float currentDepth = projCoords.z;
970
+
971
+ // Apply bias from uniform - adjust using softness factor
972
+ float softnessFactor = max(0.1, uShadowSoftness); // Ensure minimum softness
973
+ float bias = uShadowBias * softnessFactor;
974
+
975
+ // Calculate PCF with explicit shadow map sampling
976
+ float shadow = 0.0;
977
+ vec2 texelSize = 1.0 / vec2(textureSize(shadowMap, 0));
978
+
979
+ // Determine PCF kernel radius based on uPCFSize
980
+ int pcfRadius = uPCFSize / 2;
981
+ float totalSamples = 0.0;
982
+
983
+ // Dynamic PCF sampling using the specified kernel size
984
+ for(int x = -pcfRadius; x <= pcfRadius; ++x) {
985
+ for(int y = -pcfRadius; y <= pcfRadius; ++y) {
986
+ // Skip samples outside the kernel radius
987
+ // (needed for non-square kernels like 3x3, 5x5, etc.)
988
+ if (abs(x) <= pcfRadius && abs(y) <= pcfRadius) {
989
+ // Apply softness factor to sampling coordinates
990
+ vec2 offset = vec2(x, y) * texelSize * mix(1.0, 2.0, uShadowSoftness);
991
+
992
+ // Explicitly sample shadow map with clear texture binding
993
+ float pcfDepth = texture(shadowMap, projCoords.xy + offset).r;
994
+ shadow += currentDepth - bias > pcfDepth ? 0.0 : 1.0;
995
+ totalSamples += 1.0;
996
+ }
997
+ }
998
+ }
999
+
1000
+ // Average samples
1001
+ shadow /= max(1.0, totalSamples);
1002
+
1003
+ return shadow;
1004
+ }
1005
+
1006
+ // Calculate shadow for omnidirectional point light with cubemap shadow
1007
+ float pointShadowCalculation(vec3 fragPos, vec3 lightPos, samplerCube shadowMap, float farPlane) {
1008
+ // Calculate fragment-to-light vector
1009
+ vec3 fragToLight = fragPos - lightPos;
1010
+
1011
+ // Get current distance from fragment to light
1012
+ float currentDepth = length(fragToLight);
1013
+
1014
+ // Normalize to [0,1] range using far plane
1015
+ currentDepth = currentDepth / farPlane;
1016
+
1017
+ // Apply bias
1018
+ float bias = uShadowBias;
1019
+
1020
+ // Sample from cubemap shadow map in the direction of fragToLight
1021
+ float closestDepth = texture(shadowMap, fragToLight).r;
1022
+
1023
+ // Check if fragment is in shadow
1024
+ float shadow = currentDepth - bias > closestDepth ? 0.0 : 1.0;
1025
+
1026
+ return shadow;
1027
+ }
1028
+
1029
+ // PCF shadow calculation for omnidirectional point light
1030
+ float pointShadowCalculationPCF(vec3 fragPos, vec3 lightPos, samplerCube shadowMap, float farPlane) {
1031
+ // Check if PCF is disabled - fall back to basic shadow calculation
1032
+ if (!uPCFEnabled) {
1033
+ return pointShadowCalculation(fragPos, lightPos, shadowMap, farPlane);
1034
+ }
1035
+
1036
+ // Calculate fragment-to-light vector (will be used as cubemap direction)
1037
+ vec3 fragToLight = fragPos - lightPos;
1038
+
1039
+ // Get current distance from fragment to light
1040
+ float currentDepth = length(fragToLight);
1041
+
1042
+ // Normalize to [0,1] range using far plane
1043
+ currentDepth = currentDepth / farPlane;
1044
+
1045
+ // Apply bias, adjusted by softness
1046
+ float softnessFactor = max(0.1, uShadowSoftness); // Ensure minimum softness
1047
+ float bias = uShadowBias * softnessFactor;
1048
+
1049
+ // Set up PCF sampling
1050
+ float shadow = 0.0;
1051
+ int samples = 0;
1052
+ float diskRadius = 0.01 * softnessFactor; // Adjust based on softness and distance
1053
+
1054
+ // Generate a tangent space TBN matrix for sampling in a cone
1055
+ vec3 absFragToLight = abs(fragToLight);
1056
+ vec3 tangent, bitangent;
1057
+
1058
+ // Find least used axis to avoid precision issues
1059
+ if (absFragToLight.x <= absFragToLight.y && absFragToLight.x <= absFragToLight.z) {
1060
+ tangent = vec3(0.0, fragToLight.z, -fragToLight.y);
1061
+ } else if (absFragToLight.y <= absFragToLight.x && absFragToLight.y <= absFragToLight.z) {
1062
+ tangent = vec3(fragToLight.z, 0.0, -fragToLight.x);
1063
+ } else {
1064
+ tangent = vec3(fragToLight.y, -fragToLight.x, 0.0);
1065
+ }
1066
+
1067
+ tangent = normalize(tangent);
1068
+ bitangent = normalize(cross(fragToLight, tangent));
1069
+
1070
+ // Determine sample count based on PCF size
1071
+ int pcfRadius = uPCFSize / 2;
1072
+ int maxSamples = (pcfRadius * 2 + 1) * (pcfRadius * 2 + 1);
1073
+
1074
+ for (int i = 0; i < maxSamples; i++) {
1075
+ // Skip if we exceed the requested PCF size
1076
+ int x = (i % 9) - 4;
1077
+ int y = (i / 9) - 4;
1078
+
1079
+ if (abs(x) <= pcfRadius && abs(y) <= pcfRadius) {
1080
+ // Generate offset direction based on x, y grid position
1081
+ float angle = float(x) * (3.14159265359 / float(pcfRadius + 1)); // Convert x to angle
1082
+ float distance = float(y) + 0.1; // Add small offset to avoid zero
1083
+
1084
+ // Calculate offset direction in tangent space
1085
+ vec3 offset = tangent * (cos(angle) * distance * diskRadius) +
1086
+ bitangent * (sin(angle) * distance * diskRadius);
1087
+
1088
+ // Sample from the cubemap with offset
1089
+ float closestDepth = texture(shadowMap, normalize(fragToLight + offset)).r;
1090
+
1091
+ // Check if fragment is in shadow with bias
1092
+ shadow += currentDepth - bias > closestDepth ? 0.0 : 1.0;
1093
+ samples++;
1094
+ }
1095
+ }
1096
+
1097
+ // Average all samples
1098
+ shadow /= float(max(samples, 1));
1099
+
1100
+ return shadow;
1101
+ }`
1102
+ : `
1103
+ // Unpack depth from RGBA color
1104
+ float unpackDepth(vec4 packedDepth) {
1105
+ const vec4 bitShift = vec4(1.0, 1.0/256.0, 1.0/(256.0*256.0), 1.0/(256.0*256.0*256.0));
1106
+ return dot(packedDepth, bitShift);
1107
+ }
1108
+
1109
+ // Shadow calculation for WebGL1
1110
+ float shadowCalculation(vec4 fragPosLightSpace, sampler2D shadowMap) {
1111
+ // Perform perspective divide to get NDC coordinates
1112
+ vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
1113
+
1114
+ // Transform to [0,1] range for texture lookup
1115
+ projCoords = projCoords * 0.5 + 0.5;
1116
+
1117
+ // Check if position is outside the shadow map bounds
1118
+ if(projCoords.x < 0.0 || projCoords.x > 1.0 ||
1119
+ projCoords.y < 0.0 || projCoords.y > 1.0 ||
1120
+ projCoords.z < 0.0 || projCoords.z > 1.0) {
1121
+ return 1.0; // No shadow outside shadow map
1122
+ }
1123
+
1124
+ // Get packed depth value
1125
+ vec4 packedDepth = texture2D(shadowMap, projCoords.xy);
1126
+
1127
+ // Unpack the depth value
1128
+ float closestDepth = unpackDepth(packedDepth);
1129
+
1130
+ // Get current depth value
1131
+ float currentDepth = projCoords.z;
1132
+
1133
+ // Apply bias from uniform to avoid shadow acne
1134
+ float bias = uShadowBias;
1135
+
1136
+ // Check if fragment is in shadow
1137
+ float shadow = currentDepth - bias > closestDepth ? 0.0 : 1.0;
1138
+
1139
+ return shadow;
1140
+ }
1141
+
1142
+ // PCF shadow calculation for WebGL1
1143
+ float shadowCalculationPCF(vec4 fragPosLightSpace, sampler2D shadowMap) {
1144
+ // Check if PCF is disabled - fall back to basic shadow calculation
1145
+ if (!uPCFEnabled) {
1146
+ return shadowCalculation(fragPosLightSpace, shadowMap);
1147
+ }
1148
+
1149
+ // Perform perspective divide to get NDC coordinates
1150
+ vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
1151
+
1152
+ // Transform to [0,1] range for texture lookup
1153
+ projCoords = projCoords * 0.5 + 0.5;
1154
+
1155
+ // Check if position is outside the shadow map bounds
1156
+ if(projCoords.x < 0.0 || projCoords.x > 1.0 ||
1157
+ projCoords.y < 0.0 || projCoords.y > 1.0 ||
1158
+ projCoords.z < 0.0 || projCoords.z > 1.0) {
1159
+ return 1.0; // No shadow outside shadow map
1160
+ }
1161
+
1162
+ // Get current depth value
1163
+ float currentDepth = projCoords.z;
1164
+
1165
+ // Apply bias from uniform - adjust using softness factor
1166
+ float softnessFactor = max(0.1, uShadowSoftness); // Ensure minimum softness
1167
+ float bias = uShadowBias * softnessFactor;
1168
+
1169
+ // Calculate PCF with explicit shadow map sampling
1170
+ float shadow = 0.0;
1171
+ float texelSize = 1.0 / uShadowMapSize;
1172
+
1173
+ // Determine PCF kernel radius based on uPCFSize
1174
+ int pcfRadius = int(uPCFSize) / 2;
1175
+ float totalSamples = 0.0;
1176
+
1177
+ // WebGL1 has more limited loop support, so limit to max 9x9 kernel
1178
+ // We need fixed loop bounds in WebGL1
1179
+ for(int x = -4; x <= 4; ++x) {
1180
+ for(int y = -4; y <= 4; ++y) {
1181
+ // Skip samples outside the requested kernel radius
1182
+ if (abs(x) <= pcfRadius && abs(y) <= pcfRadius) {
1183
+ // Apply softness factor to sampling coordinates
1184
+ vec2 offset = vec2(x, y) * texelSize * mix(1.0, 2.0, uShadowSoftness);
1185
+
1186
+ vec4 packedDepth = texture2D(shadowMap, projCoords.xy + offset);
1187
+ float pcfDepth = unpackDepth(packedDepth);
1188
+ shadow += currentDepth - bias > pcfDepth ? 0.0 : 1.0;
1189
+ totalSamples += 1.0;
1190
+ }
1191
+ }
1192
+ }
1193
+
1194
+ // Average samples
1195
+ shadow /= max(1.0, totalSamples);
1196
+
1197
+ return shadow;
1198
+ }
1199
+
1200
+ // For WebGL1 without cubemap support, calculate shadow from a single face
1201
+ float pointShadowCalculation(vec3 fragPos, vec3 lightPos, sampler2D shadowMap, float farPlane) {
1202
+ // We can't do proper cubemap in WebGL1, so this is just an approximation
1203
+ // using the first face of what would be a cubemap
1204
+ vec3 fragToLight = fragPos - lightPos;
1205
+
1206
+ // Get current distance from fragment to light
1207
+ float currentDepth = length(fragToLight);
1208
+
1209
+ // Normalize to [0,1] range using far plane
1210
+ currentDepth = currentDepth / farPlane;
1211
+
1212
+ // Simple planar mapping for the single shadow map face
1213
+ // This is just a fallback - won't look great but better than nothing
1214
+ vec2 shadowCoord = vec2(
1215
+ (fragToLight.x / abs(fragToLight.x + 0.0001) + 1.0) * 0.25,
1216
+ (fragToLight.y / abs(fragToLight.y + 0.0001) + 1.0) * 0.25
1217
+ );
1218
+
1219
+ // Apply bias
1220
+ float bias = uShadowBias;
1221
+
1222
+ // Sample from shadow map
1223
+ vec4 packedDepth = texture2D(shadowMap, shadowCoord);
1224
+ float closestDepth = unpackDepth(packedDepth);
1225
+
1226
+ // Check if fragment is in shadow
1227
+ float shadow = currentDepth - bias > closestDepth ? 0.0 : 1.0;
1228
+
1229
+ return shadow;
1230
+ }
1231
+
1232
+ // Simplified PCF for WebGL1 single-face approximation
1233
+ float pointShadowCalculationPCF(vec3 fragPos, vec3 lightPos, sampler2D shadowMap, float farPlane) {
1234
+ // Check if PCF is disabled - fall back to basic shadow calculation
1235
+ if (!uPCFEnabled) {
1236
+ return pointShadowCalculation(fragPos, lightPos, shadowMap, farPlane);
1237
+ }
1238
+
1239
+ // Calculate fragment-to-light vector
1240
+ vec3 fragToLight = fragPos - lightPos;
1241
+
1242
+ // Get current distance from fragment to light
1243
+ float currentDepth = length(fragToLight);
1244
+
1245
+ // Normalize to [0,1] range using far plane
1246
+ currentDepth = currentDepth / farPlane;
1247
+
1248
+ // Simple planar mapping for the single shadow map face
1249
+ vec2 shadowCoord = vec2(
1250
+ (fragToLight.x / abs(fragToLight.x + 0.0001) + 1.0) * 0.25,
1251
+ (fragToLight.y / abs(fragToLight.y + 0.0001) + 1.0) * 0.25
1252
+ );
1253
+
1254
+ // Apply bias
1255
+ float softnessFactor = max(0.1, uShadowSoftness); // Ensure minimum softness
1256
+ float bias = uShadowBias * softnessFactor;
1257
+
1258
+ // Set up PCF sampling
1259
+ float shadow = 0.0;
1260
+ float texelSize = 1.0 / uShadowMapSize;
1261
+
1262
+ // Determine PCF kernel radius based on uPCFSize
1263
+ int pcfRadius = int(uPCFSize) / 2;
1264
+ float totalSamples = 0.0;
1265
+
1266
+ // Limit loop size for WebGL1
1267
+ for(int x = -4; x <= 4; ++x) {
1268
+ for(int y = -4; y <= 4; ++y) {
1269
+ // Skip samples outside the requested kernel radius
1270
+ if (abs(x) <= pcfRadius && abs(y) <= pcfRadius) {
1271
+ // Apply softness factor to sampling coordinates
1272
+ vec2 offset = vec2(x, y) * texelSize * mix(1.0, 2.0, uShadowSoftness);
1273
+
1274
+ vec4 packedDepth = texture2D(shadowMap, shadowCoord + offset);
1275
+ float pcfDepth = unpackDepth(packedDepth);
1276
+ shadow += currentDepth - bias > pcfDepth ? 0.0 : 1.0;
1277
+ totalSamples += 1.0;
1278
+ }
1279
+ }
1280
+ }
1281
+
1282
+ // Average samples
1283
+ shadow /= max(1.0, totalSamples);
1284
+
1285
+ return shadow;
1286
+ }`;
1287
+
1288
+ return `${isWebGL2 ? "#version 300 es\n" : ""}
1289
+ precision highp float;
1290
+ ${isWebGL2 ? "precision mediump sampler2DArray;\n" : ""}
1291
+
1292
+ // Inputs from vertex shader
1293
+ ${isWebGL2 ? "in" : "varying"} vec3 vNormal;
1294
+ ${isWebGL2 ? "in" : "varying"} vec3 vWorldPos;
1295
+ ${isWebGL2 ? "in" : "varying"} vec4 vFragPosLightSpace; // Added for shadow mapping
1296
+ ${isWebGL2 ? "in" : "varying"} vec3 vFragPos; // THIS IS IMPORTANT - now we include vFragPos from the vertex shader
1297
+
1298
+ ${isWebGL2 ? "in" : "varying"} vec3 vColor;
1299
+ ${isWebGL2 ? "in" : "varying"} vec3 vViewDir;
1300
+ ${isWebGL2 ? "flat in" : "varying"} float vTextureIndex;
1301
+ ${isWebGL2 ? "in" : "varying"} vec2 vTexCoord;
1302
+ ${isWebGL2 ? "flat in" : "varying"} float vUseTexture;
1303
+
1304
+ // Material properties - global defaults
1305
+ uniform float uRoughness;
1306
+ uniform float uMetallic;
1307
+ uniform float uBaseReflectivity;
1308
+
1309
+ // Material properties texture (each texel contains roughness, metallic, baseReflectivity)
1310
+ uniform sampler2D uMaterialPropertiesTexture;
1311
+ uniform bool uUsePerTextureMaterials;
1312
+
1313
+ // Light properties
1314
+ uniform vec3 uLightPos; // Used for directional light position
1315
+ uniform vec3 uPointLightPos; // Position for point light #0
1316
+ uniform vec3 uLightDir;
1317
+ uniform vec3 uCameraPos;
1318
+ uniform float uLightIntensity;
1319
+ uniform float uPointLightIntensity; // Separate uniform for point light intensity
1320
+
1321
+ // Shadow mapping
1322
+ uniform sampler2D uShadowMap;
1323
+ ${isWebGL2 ? "uniform samplerCube uPointShadowMap;" : "uniform sampler2D uPointShadowMap;"}
1324
+ uniform bool uShadowsEnabled;
1325
+ uniform bool uPointShadowsEnabled; // Enable point light shadows
1326
+ uniform int uPointLightCount; // Number of point lights
1327
+ uniform float uLightRadius; // Point light radius
1328
+
1329
+ // Additional light textures and data
1330
+ uniform sampler2D uDirectionalLightData;
1331
+ uniform vec2 uDirectionalLightTextureSize;
1332
+ uniform sampler2D uPointLightData;
1333
+ uniform vec2 uPointLightTextureSize;
1334
+ uniform int uDirectionalLightCount;
1335
+
1336
+ // Additional point lights
1337
+ ${isWebGL2 ? "uniform samplerCube uPointShadowMap1;" : "uniform sampler2D uPointShadowMap1;"}
1338
+ ${isWebGL2 ? "uniform samplerCube uPointShadowMap2;" : "uniform sampler2D uPointShadowMap2;"}
1339
+ ${isWebGL2 ? "uniform samplerCube uPointShadowMap3;" : "uniform sampler2D uPointShadowMap3;"}
1340
+ uniform bool uPointShadowsEnabled1; // Second point light shadows
1341
+ uniform bool uPointShadowsEnabled2; // Third point light shadows
1342
+ uniform bool uPointShadowsEnabled3; // Fourth point light shadows
1343
+ uniform float uPointLightIntensity1; // Second point light intensity
1344
+ uniform vec3 uPointLightPos1; // Second point light position
1345
+
1346
+ uniform vec3 uPointLightColor1; // Second point light color
1347
+ uniform float uPointLightRadius1; // Second point light radius
1348
+ uniform float uShadowBias; // Shadow bias uniform for controlling shadow acne
1349
+ uniform float uShadowMapSize; // Shadow map size for texture calculations
1350
+ uniform float uShadowSoftness; // Controls shadow edge softness (0-1)
1351
+ uniform int uPCFSize; // Controls PCF kernel size (1, 3, 5, 7, 9)
1352
+ uniform bool uPCFEnabled; // Controls whether PCF filtering is enabled
1353
+ uniform float uFarPlane; // Far plane for point light shadows
1354
+ uniform vec3 uLightColor; // Directional light color
1355
+ uniform vec3 uPointLightColor; // Point light color
1356
+
1357
+
1358
+
1359
+ // Texture sampler
1360
+ uniform sampler2DArray uPBRTextureArray;
1361
+
1362
+ ${isWebGL2 ? "out vec4 fragColor;" : ""}
1363
+
1364
+ // Constants for performance
1365
+ #define PI 3.14159265359
1366
+ #define RECIPROCAL_PI 0.31830988618
1367
+
1368
+ // Structures to hold light data
1369
+ struct DirectionalLight {
1370
+ vec3 position;
1371
+ vec3 direction;
1372
+ vec3 color;
1373
+ float intensity;
1374
+ bool shadowsEnabled;
1375
+ };
1376
+
1377
+ struct PointLight {
1378
+ vec3 position;
1379
+ vec3 color;
1380
+ float intensity;
1381
+ float radius;
1382
+ bool shadowsEnabled;
1383
+ };
1384
+
1385
+ // Functions to extract light data from textures
1386
+ DirectionalLight getDirectionalLight(int index) {
1387
+ // Each light takes 3 pixels horizontally
1388
+ int basePixel = index * 3;
1389
+
1390
+ // Calculate UV coordinates for each pixel
1391
+ // First pixel: position + enabled
1392
+ float u1 = (float(basePixel) + 0.5) / uDirectionalLightTextureSize.x;
1393
+ // Second pixel: direction + shadowEnabled
1394
+ float u2 = (float(basePixel + 1) + 0.5) / uDirectionalLightTextureSize.x;
1395
+ // Third pixel: color + intensity
1396
+ float u3 = (float(basePixel + 2) + 0.5) / uDirectionalLightTextureSize.x;
1397
+
1398
+ // Use centered V coordinate (there's only one row)
1399
+ float v = 0.5 / uDirectionalLightTextureSize.y;
1400
+
1401
+ // Sample pixels from texture
1402
+ vec4 posData = texture(uDirectionalLightData, vec2(u1, v));
1403
+ vec4 dirData = texture(uDirectionalLightData, vec2(u2, v));
1404
+ vec4 colorData = texture(uDirectionalLightData, vec2(u3, v));
1405
+
1406
+ // Create and populate the light structure
1407
+ DirectionalLight light;
1408
+ light.position = posData.xyz;
1409
+ light.direction = dirData.xyz;
1410
+ light.color = colorData.rgb;
1411
+ light.intensity = colorData.a;
1412
+ light.shadowsEnabled = dirData.a > 0.5;
1413
+
1414
+ return light;
1415
+ }
1416
+
1417
+ PointLight getPointLight(int index) {
1418
+ // Each light takes 3 pixels horizontally
1419
+ int basePixel = index * 3;
1420
+
1421
+ // Calculate UV coordinates for each pixel
1422
+ // First pixel: position + enabled
1423
+ float u1 = (float(basePixel) + 0.5) / uPointLightTextureSize.x;
1424
+ // Second pixel: color + intensity
1425
+ float u2 = (float(basePixel + 1) + 0.5) / uPointLightTextureSize.x;
1426
+ // Third pixel: radius + shadowEnabled + padding
1427
+ float u3 = (float(basePixel + 2) + 0.5) / uPointLightTextureSize.x;
1428
+
1429
+ // Use centered V coordinate (there's only one row)
1430
+ float v = 0.5 / uPointLightTextureSize.y;
1431
+
1432
+ // Sample pixels from texture
1433
+ vec4 posData = texture(uPointLightData, vec2(u1, v));
1434
+ vec4 colorData = texture(uPointLightData, vec2(u2, v));
1435
+ vec4 radiusData = texture(uPointLightData, vec2(u3, v));
1436
+
1437
+ // Create and populate the light structure
1438
+ PointLight light;
1439
+ light.position = posData.xyz;
1440
+ light.color = colorData.rgb;
1441
+ light.intensity = colorData.a;
1442
+ light.radius = radiusData.r;
1443
+ light.shadowsEnabled = radiusData.g > 0.5;
1444
+
1445
+ return light;
1446
+ }
1447
+
1448
+ // Shadow mapping functions
1449
+ ${shadowFunctions}
1450
+
1451
+ // Optimized PBR function that combines GGX and Fresnel calculations
1452
+ // This is faster than separate function calls
1453
+ vec3 specularBRDF(vec3 N, vec3 L, vec3 V, vec3 F0, float roughness) {
1454
+
1455
+ vec3 H = normalize(V + L);
1456
+ float NdotH = max(dot(N, H), 0.0);
1457
+ float NdotV = max(dot(N, V), 0.0);
1458
+ float NdotL = max(dot(N, L), 0.0);
1459
+ float HdotV = max(dot(H, V), 0.0);
1460
+
1461
+ // Roughness terms
1462
+ float a = roughness * roughness;
1463
+ float a2 = a * a;
1464
+
1465
+ // Distribution
1466
+ float NdotH2 = NdotH * NdotH;
1467
+ float denom = (NdotH2 * (a2 - 1.0) + 1.0);
1468
+ float D = a2 / (PI * denom * denom);
1469
+
1470
+ // Geometry
1471
+ float k = ((roughness + 1.0) * (roughness + 1.0)) / 8.0;
1472
+ float G1_V = NdotV / (NdotV * (1.0 - k) + k);
1473
+ float G1_L = NdotL / (NdotL * (1.0 - k) + k);
1474
+ float G = G1_V * G1_L;
1475
+
1476
+ // Fresnel
1477
+ vec3 F = F0 + (1.0 - F0) * pow(1.0 - HdotV, 5.0);
1478
+
1479
+ // Combined specular term
1480
+ return (D * G * F) / (4.0 * NdotV * NdotL + 0.001);
1481
+ }
1482
+
1483
+ void main() {
1484
+ vec3 N = normalize(vNormal);
1485
+ vec3 V = normalize(vViewDir);
1486
+ // Negate light direction to be consistent with shadow mapping convention
1487
+ vec3 L = normalize(-uLightDir); // Light direction (negated for consistency)
1488
+ float NdotL = max(dot(N, L), 0.0);
1489
+
1490
+ // Fast path for distance attenuation calculation
1491
+ float distanceToLight = length(vWorldPos - uLightPos);
1492
+ float distanceAttenuation = 1.0 / (1.0 + 0.0001 * distanceToLight * distanceToLight);
1493
+
1494
+ // Efficient texture sampling with conditional
1495
+ vec3 albedo = (vUseTexture > 0.5) ?
1496
+ texture(uPBRTextureArray, vec3(vTexCoord, vTextureIndex)).rgb :
1497
+ vColor;
1498
+
1499
+ // Get material properties based on texture index if using textures
1500
+ float roughness = uRoughness;
1501
+ float metallic = uMetallic;
1502
+ float baseReflectivity = uBaseReflectivity;
1503
+
1504
+ if (uUsePerTextureMaterials && vUseTexture > 0.5) {
1505
+ // Sample material properties from the material texture
1506
+ // Convert texture index to texture coordinates (0-1 range)
1507
+ float textureCoord = (vTextureIndex + 0.5) / float(textureSize(uMaterialPropertiesTexture, 0).x);
1508
+ vec4 materialProps = texture(uMaterialPropertiesTexture, vec2(textureCoord, 0.5));
1509
+
1510
+ // Extract material properties
1511
+ roughness = materialProps.r;
1512
+ metallic = materialProps.g;
1513
+ baseReflectivity = materialProps.b;
1514
+ }
1515
+
1516
+ // Base reflectivity with per-texture material mix
1517
+ vec3 baseF0 = mix(vec3(baseReflectivity), albedo, metallic);
1518
+
1519
+ // Calculate specular using our optimized function with per-texture roughness
1520
+ // Note: L is already negated above for consistency with shadow mapping
1521
+ vec3 specular = specularBRDF(N, L, V, baseF0, roughness);
1522
+
1523
+ // Efficient diffuse calculation with per-texture metallic
1524
+ vec3 kD = (vec3(1.0) - specular) * (1.0 - metallic);
1525
+
1526
+ // Calculate shadow factor if shadows are enabled
1527
+ float shadow = 1.0;
1528
+ if (uShadowsEnabled) {
1529
+ // Use the PCF shadow calculation for soft shadows
1530
+ // The shadow map
1531
+ float shadowFactor = shadowCalculationPCF(vFragPosLightSpace, uShadowMap);
1532
+ // Adjust shadow intensity for PBR - not completely black shadows
1533
+ shadow = 1.0 - (1.0 - shadowFactor) * 0.8;
1534
+ }
1535
+
1536
+ // Calculate directional light contribution (if active)
1537
+ vec3 directionalColor = vec3(0.0);
1538
+
1539
+ // Process all directional lights from the data texture
1540
+ if (uDirectionalLightCount > 0) {
1541
+ for (int i = 0; i < uDirectionalLightCount; i++) {
1542
+ if (i >= 100) break; // Reasonable safety limit
1543
+
1544
+ // Extract light data from texture
1545
+ DirectionalLight light = getDirectionalLight(i);
1546
+
1547
+ // Calculate diffuse component
1548
+ float lightDiffuse = max(0.0, dot(normalize(N), normalize(-light.direction)));
1549
+
1550
+ // Calculate shadow - currently only first light gets shadow mapping
1551
+ float lightShadow = 1.0;
1552
+ if (light.shadowsEnabled && i == 0 && uShadowsEnabled) {
1553
+ lightShadow = shadow;
1554
+ }
1555
+
1556
+ // Calculate light-specific attenuation
1557
+ float lightDistance = length(vWorldPos - light.position);
1558
+ float lightAttenuation = 1.0 / (1.0 + 0.0001 * lightDistance * lightDistance);
1559
+
1560
+ // Add this light's contribution
1561
+ directionalColor += (kD * albedo * RECIPROCAL_PI + specular) * lightDiffuse *
1562
+ lightShadow * light.intensity * lightAttenuation * light.color;
1563
+ }
1564
+ } else if (uShadowsEnabled) {
1565
+ // Fallback to legacy directional light if no lights in texture
1566
+ // Apply intensity scaling for PBR
1567
+ float scaledLegacyIntensity = uLightIntensity * 0.00001;
1568
+ directionalColor = (kD * albedo * RECIPROCAL_PI + specular) * NdotL * scaledLegacyIntensity * distanceAttenuation * shadow * uLightColor;
1569
+ }
1570
+
1571
+ // Calculate point light contributions
1572
+ vec3 pointLightColors = vec3(0.0);
1573
+
1574
+ // Process all point lights from the data texture
1575
+ for (int i = 0; i < uPointLightCount; i++) {
1576
+ if (i >= 100) break; // Reasonable safety limit
1577
+
1578
+ // Extract light data from texture
1579
+ PointLight light = getPointLight(i);
1580
+
1581
+ // Direction from fragment to point light (we need to negate for consistent lighting)
1582
+ vec3 pointL = normalize(light.position - vWorldPos);
1583
+ float pointNdotL = max(dot(N, pointL), 0.0);
1584
+
1585
+ // Calculate distance attenuation for point light
1586
+ float pointDistance = length(vWorldPos - light.position);
1587
+ float pointAttenuation = 1.0 / (1.0 + (pointDistance * pointDistance) / (light.radius * light.radius));
1588
+
1589
+ // Calculate specular for point light
1590
+ vec3 pointSpecular = specularBRDF(N, pointL, V, baseF0, roughness);
1591
+ vec3 pointKD = (vec3(1.0) - pointSpecular) * (1.0 - metallic);
1592
+
1593
+ // Calculate shadow for point light
1594
+ float pointShadow = 1.0;
1595
+ if (light.shadowsEnabled) {
1596
+ // Handle first 4 shadow maps with a switch statement
1597
+ int lightIdx = i % 4; // Limit to 4 shadowed lights
1598
+ switch(lightIdx) {
1599
+ case 0:
1600
+ if (uPointShadowsEnabled) {
1601
+ float pointShadowFactor = pointShadowCalculationPCF(vFragPos, light.position, uPointShadowMap, uFarPlane);
1602
+ pointShadow = 1.0 - (1.0 - pointShadowFactor) * 0.8;
1603
+ }
1604
+ break;
1605
+ case 1:
1606
+ if (uPointShadowsEnabled1) {
1607
+ float pointShadowFactor = pointShadowCalculationPCF(vFragPos, light.position, uPointShadowMap1, uFarPlane);
1608
+ pointShadow = 1.0 - (1.0 - pointShadowFactor) * 0.8;
1609
+ }
1610
+ break;
1611
+ case 2:
1612
+ if (uPointShadowsEnabled2) {
1613
+ float pointShadowFactor = pointShadowCalculationPCF(vFragPos, light.position, uPointShadowMap2, uFarPlane);
1614
+ pointShadow = 1.0 - (1.0 - pointShadowFactor) * 0.8;
1615
+ }
1616
+ break;
1617
+ case 3:
1618
+ if (uPointShadowsEnabled3) {
1619
+ float pointShadowFactor = pointShadowCalculationPCF(vFragPos, light.position, uPointShadowMap3, uFarPlane);
1620
+ pointShadow = 1.0 - (1.0 - pointShadowFactor) * 0.8;
1621
+ }
1622
+ break;
1623
+ }
1624
+ }
1625
+
1626
+ // Calculate point light color with proper PBR contribution
1627
+ pointLightColors += (pointKD * albedo * RECIPROCAL_PI + pointSpecular) * pointNdotL *
1628
+ light.intensity * pointAttenuation * pointShadow * light.color;
1629
+ }
1630
+
1631
+ // Legacy point light handling - only use if no new lights are available
1632
+ if (uPointLightCount == 0) {
1633
+ // Backward compatibility for first point light
1634
+ vec3 pointL = normalize(uPointLightPos - vWorldPos);
1635
+ float pointNdotL = max(dot(N, pointL), 0.0);
1636
+
1637
+ float pointDistance = length(vWorldPos - uPointLightPos);
1638
+ float pointAttenuation = 1.0 / (1.0 + (pointDistance * pointDistance) / (uLightRadius * uLightRadius));
1639
+
1640
+ vec3 pointSpecular = specularBRDF(N, pointL, V, baseF0, roughness);
1641
+ vec3 pointKD = (vec3(1.0) - pointSpecular) * (1.0 - metallic);
1642
+
1643
+ float pointShadow = 1.0;
1644
+ if (uPointShadowsEnabled) {
1645
+ float pointShadowFactor = pointShadowCalculationPCF(vFragPos, uPointLightPos, uPointShadowMap, uFarPlane);
1646
+ pointShadow = 1.0 - (1.0 - pointShadowFactor) * 0.8;
1647
+ }
1648
+
1649
+ // First light contribution
1650
+ pointLightColors += (pointKD * albedo * RECIPROCAL_PI + pointSpecular) * pointNdotL *
1651
+ uPointLightIntensity * pointAttenuation * pointShadow * uPointLightColor;
1652
+
1653
+ // Second point light - only if more than one light is available
1654
+ if (uPointLightCount > 1) {
1655
+ pointL = normalize(uPointLightPos1 - vWorldPos);
1656
+ pointNdotL = max(dot(N, pointL), 0.0);
1657
+
1658
+ pointDistance = length(vWorldPos - uPointLightPos1);
1659
+ pointAttenuation = 1.0 / (1.0 + (pointDistance * pointDistance) / (uPointLightRadius1 * uPointLightRadius1));
1660
+
1661
+ pointSpecular = specularBRDF(N, pointL, V, baseF0, roughness);
1662
+ pointKD = (vec3(1.0) - pointSpecular) * (1.0 - metallic);
1663
+
1664
+ pointShadow = 1.0;
1665
+ if (uPointShadowsEnabled1) {
1666
+ float pointShadowFactor = pointShadowCalculationPCF(vFragPos, uPointLightPos1, uPointShadowMap1, uFarPlane);
1667
+ pointShadow = 1.0 - (1.0 - pointShadowFactor) * 0.8;
1668
+ }
1669
+
1670
+ // Add second light contribution
1671
+ pointLightColors += (pointKD * albedo * RECIPROCAL_PI + pointSpecular) * pointNdotL *
1672
+ uPointLightIntensity1 * pointAttenuation * pointShadow * uPointLightColor1;
1673
+ }
1674
+ }
1675
+
1676
+ // Combine lighting with physically correct blending
1677
+ vec3 color = directionalColor;
1678
+
1679
+ // Blend point light if it exists
1680
+ if (uPointLightCount > 0) {
1681
+ // Direct addition of light contributions is physically accurate
1682
+ color = directionalColor + pointLightColors;
1683
+ }
1684
+
1685
+ // Add ambient light (pre-computed constant)
1686
+ color += vec3(0.3) * albedo;
1687
+
1688
+ ${isWebGL2 ? "fragColor" : "gl_FragColor"} = vec4(color, 1.0);
1689
+ }`;
1690
+ }
1691
+
1692
+ //--------------------------------------------------------------------------
1693
+ // VIRTUALBOY SHADER VARIANT
1694
+ //--------------------------------------------------------------------------
1695
+
1696
+ /**
1697
+ * VirtualBoy vertex shader
1698
+ * @param {boolean} isWebGL2 - Whether WebGL2 is being used
1699
+ * @returns {string} - Vertex shader source code
1700
+ */
1701
+ getVirtualBoyVertexShader(isWebGL2) {
1702
+ return `${isWebGL2 ? "#version 300 es\n" : ""}
1703
+ ${isWebGL2 ? "in" : "attribute"} vec3 aPosition;
1704
+ ${isWebGL2 ? "in" : "attribute"} vec3 aNormal;
1705
+ ${isWebGL2 ? "in" : "attribute"} vec3 aColor;
1706
+
1707
+ uniform mat4 uProjectionMatrix;
1708
+ uniform mat4 uViewMatrix;
1709
+ uniform mat4 uModelMatrix;
1710
+ uniform vec3 uLightDir;
1711
+
1712
+ ${
1713
+ isWebGL2
1714
+ ? "flat out float vLighting;\nout vec3 vBarycentricCoord;"
1715
+ : "varying float vLighting;\nvarying vec3 vBarycentricCoord;"
1716
+ }
1717
+
1718
+ void main() {
1719
+ gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aPosition, 1.0);
1720
+ vec3 worldNormal = normalize(mat3(uModelMatrix) * aNormal);
1721
+ // Negate light direction to be consistent with other shaders
1722
+ vLighting = max(0.3, min(1.0, dot(worldNormal, normalize(-uLightDir))));
1723
+
1724
+ float id = float(gl_VertexID % 3);
1725
+ vBarycentricCoord = vec3(id == 0.0, id == 1.0, id == 2.0);
1726
+ }`;
1727
+ }
1728
+
1729
+ /**
1730
+ * VirtualBoy fragment shader
1731
+ * @param {boolean} isWebGL2 - Whether WebGL2 is being used
1732
+ * @returns {string} - Fragment shader source code
1733
+ */
1734
+ getVirtualBoyFragmentShader(isWebGL2) {
1735
+ return `${isWebGL2 ? "#version 300 es\n" : ""}
1736
+ precision mediump float;
1737
+ ${
1738
+ isWebGL2
1739
+ ? "flat in float vLighting;\nin vec3 vBarycentricCoord;\nout vec4 fragColor;"
1740
+ : "varying float vLighting;\nvarying vec3 vBarycentricCoord;"
1741
+ }
1742
+
1743
+ void main() {
1744
+ float edgeWidth = 1.0;
1745
+ vec3 d = fwidth(vBarycentricCoord);
1746
+ vec3 a3 = smoothstep(vec3(0.0), d * edgeWidth, vBarycentricCoord);
1747
+ float edge = min(min(a3.x, a3.y), a3.z);
1748
+
1749
+ if (edge < 0.9) {
1750
+ ${isWebGL2 ? "fragColor" : "gl_FragColor"} = vec4(1.0, 0.0, 0.0, 1.0) * vLighting;
1751
+ } else {
1752
+ ${isWebGL2 ? "fragColor" : "gl_FragColor"} = vec4(0.0, 0.0, 0.0, 1.0);
1753
+ }
1754
+ }`;
1755
+ }
1756
+ }