@multiplekex/shallot 0.2.4 → 0.3.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 (62) hide show
  1. package/package.json +1 -1
  2. package/src/core/component.ts +1 -1
  3. package/src/core/index.ts +1 -13
  4. package/src/core/math.ts +186 -0
  5. package/src/core/state.ts +1 -1
  6. package/src/core/xml.ts +56 -41
  7. package/src/extras/arrows/index.ts +3 -3
  8. package/src/extras/caustic.ts +37 -0
  9. package/src/extras/gradient/index.ts +63 -69
  10. package/src/extras/index.ts +3 -0
  11. package/src/extras/lines/index.ts +3 -3
  12. package/src/extras/orbit/index.ts +1 -1
  13. package/src/extras/skylab/index.ts +314 -0
  14. package/src/extras/text/font.ts +69 -14
  15. package/src/extras/text/index.ts +17 -69
  16. package/src/extras/text/sdf.ts +13 -2
  17. package/src/extras/water/index.ts +119 -0
  18. package/src/standard/defaults.ts +2 -0
  19. package/src/standard/index.ts +2 -0
  20. package/src/standard/raster/batch.ts +149 -0
  21. package/src/standard/raster/forward.ts +832 -0
  22. package/src/standard/raster/index.ts +191 -0
  23. package/src/standard/raster/shadow.ts +408 -0
  24. package/src/standard/{render → raytracing}/bvh/blas.ts +336 -88
  25. package/src/standard/raytracing/bvh/radix.ts +473 -0
  26. package/src/standard/raytracing/bvh/refit.ts +711 -0
  27. package/src/standard/{render → raytracing}/bvh/structs.ts +0 -55
  28. package/src/standard/{render → raytracing}/bvh/tlas.ts +155 -140
  29. package/src/standard/{render → raytracing}/bvh/traverse.ts +72 -64
  30. package/src/standard/{render → raytracing}/depth.ts +9 -9
  31. package/src/standard/raytracing/index.ts +409 -0
  32. package/src/standard/{render → raytracing}/instance.ts +31 -16
  33. package/src/standard/{render → raytracing}/ray.ts +1 -1
  34. package/src/standard/raytracing/shaders.ts +798 -0
  35. package/src/standard/{render → raytracing}/triangle.ts +1 -1
  36. package/src/standard/render/camera.ts +96 -106
  37. package/src/standard/render/data.ts +1 -1
  38. package/src/standard/render/index.ts +136 -220
  39. package/src/standard/render/indirect.ts +9 -10
  40. package/src/standard/render/light.ts +2 -2
  41. package/src/standard/render/mesh.ts +404 -0
  42. package/src/standard/render/overlay.ts +8 -5
  43. package/src/standard/render/pass.ts +1 -1
  44. package/src/standard/render/postprocess.ts +263 -242
  45. package/src/standard/render/scene.ts +28 -16
  46. package/src/standard/render/surface/index.ts +81 -12
  47. package/src/standard/render/surface/shaders.ts +511 -0
  48. package/src/standard/render/surface/structs.ts +23 -6
  49. package/src/standard/tween/tween.ts +44 -115
  50. package/src/standard/render/bvh/radix.ts +0 -476
  51. package/src/standard/render/forward/index.ts +0 -259
  52. package/src/standard/render/forward/raster.ts +0 -228
  53. package/src/standard/render/mesh/box.ts +0 -20
  54. package/src/standard/render/mesh/index.ts +0 -446
  55. package/src/standard/render/mesh/plane.ts +0 -11
  56. package/src/standard/render/mesh/sphere.ts +0 -40
  57. package/src/standard/render/mesh/unified.ts +0 -96
  58. package/src/standard/render/shaders.ts +0 -484
  59. package/src/standard/render/surface/compile.ts +0 -67
  60. package/src/standard/render/surface/noise.ts +0 -45
  61. package/src/standard/render/surface/wgsl.ts +0 -573
  62. /package/src/standard/{render → raytracing}/intersection.ts +0 -0
@@ -1,10 +1,11 @@
1
1
  import { MAX_ENTITIES } from "../../../core";
2
2
  import { setTraits } from "../../../core/component";
3
+ import { WGSL_STRUCTS } from "./structs";
4
+ import { compileVertexBody, WGSL_LIGHTING_CALC, SPECULAR_WGSL } from "./shaders";
3
5
 
4
6
  export interface SurfaceData {
5
7
  vertex?: string;
6
8
  fragment?: string;
7
- lit?: boolean;
8
9
  }
9
10
 
10
11
  interface ComposedSurface extends SurfaceData {
@@ -21,23 +22,28 @@ export function createSurfaceRegistry(): SurfaceRegistry {
21
22
  return registry;
22
23
  }
23
24
 
25
+ const surfaceNames = new Map<string, number>();
26
+
24
27
  function initBuiltIns(registry: SurfaceRegistry): void {
25
- registry.surfaces.push({ lit: true });
28
+ registry.surfaces.push({});
26
29
 
27
30
  registry.surfaces.push({
28
- lit: false,
29
31
  fragment: `(*surface).baseColor = (*surface).normal * 0.5 + 0.5;`,
30
32
  });
31
33
 
32
34
  registry.surfaces.push({
33
- lit: false,
34
35
  fragment: `
35
36
  let depth = position.z;
36
37
  let remapped = pow(1.0 - depth, 0.1);
37
38
  (*surface).baseColor = vec3(remapped);`,
38
39
  });
39
40
 
40
- registry.surfaces.push({ lit: false });
41
+ registry.surfaces.push({});
42
+
43
+ surfaceNames.set("default", 0);
44
+ surfaceNames.set("normals", 1);
45
+ surfaceNames.set("depth", 2);
46
+ surfaceNames.set("albedo", 3);
41
47
  }
42
48
 
43
49
  export const SurfaceType = {
@@ -73,14 +79,11 @@ export function composeSurfaces(registry: SurfaceRegistry, ...ids: number[]): nu
73
79
 
74
80
  const vertexParts: string[] = [];
75
81
  const fragmentParts: string[] = [];
76
- let lit = true;
77
82
 
78
83
  for (const id of validIds) {
79
84
  const s = registry.surfaces[id];
80
85
  if (!s) continue;
81
86
 
82
- if (s.lit === false) lit = false;
83
-
84
87
  if ("composed" in s) {
85
88
  for (const composedId of s.composed) {
86
89
  const inner = registry.surfaces[composedId];
@@ -96,7 +99,6 @@ export function composeSurfaces(registry: SurfaceRegistry, ...ids: number[]): nu
96
99
  const composedData: ComposedSurface = {
97
100
  vertex: vertexParts.length > 0 ? vertexParts.join("\n ") : undefined,
98
101
  fragment: fragmentParts.length > 0 ? fragmentParts.join("\n ") : undefined,
99
- lit,
100
102
  composed: validIds,
101
103
  };
102
104
 
@@ -107,8 +109,14 @@ export function composeSurfaces(registry: SurfaceRegistry, ...ids: number[]): nu
107
109
 
108
110
  const defaultRegistry = createSurfaceRegistry();
109
111
 
110
- export function surface(data: SurfaceData): number {
111
- return registerSurface(defaultRegistry, data);
112
+ export function surface(data: SurfaceData, name?: string): number {
113
+ const id = registerSurface(defaultRegistry, data);
114
+ if (name) surfaceNames.set(name, id);
115
+ return id;
116
+ }
117
+
118
+ export function getSurfaceByName(name: string): number | undefined {
119
+ return surfaceNames.get(name);
112
120
  }
113
121
 
114
122
  export function getDefaultSurface(id: number): SurfaceData | undefined {
@@ -120,6 +128,7 @@ export function getDefaultAllSurfaces(): SurfaceData[] {
120
128
  }
121
129
 
122
130
  export function clearDefaultSurfaces(): void {
131
+ surfaceNames.clear();
123
132
  clearSurfaces(defaultRegistry);
124
133
  }
125
134
 
@@ -141,6 +150,66 @@ setTraits(Surface, {
141
150
  defaults: () => ({
142
151
  type: SurfaceType.Default,
143
152
  }),
153
+ parse: { type: getSurfaceByName },
144
154
  });
145
155
 
146
- export { compileSurface } from "./compile";
156
+ export function compileSurface(data: SurfaceData): string {
157
+ const vertexTransform = compileVertexBody(data.vertex);
158
+
159
+ const fragmentBody = data.fragment ?? "";
160
+
161
+ return /* wgsl */ `
162
+ ${WGSL_STRUCTS}
163
+ ${SPECULAR_WGSL}
164
+
165
+ fn userVertexTransform(worldPos: vec3<f32>, normal: vec3<f32>, eid: u32) -> vec3<f32> {
166
+ ${vertexTransform}
167
+ }
168
+
169
+ fn userFragment(surface: ptr<function, SurfaceData>, position: vec4<f32>) {
170
+ ${fragmentBody}
171
+ }
172
+
173
+ @vertex
174
+ fn vs(input: VertexInput) -> VertexOutput {
175
+ let eid = entityIds[input.instance];
176
+ let world = matrices[eid];
177
+ let scaledPos = input.position * sizes[eid].xyz;
178
+ let baseWorldPos = (world * vec4<f32>(scaledPos, 1.0)).xyz;
179
+ let worldNormal = normalize((world * vec4<f32>(input.normal, 0.0)).xyz);
180
+ let finalWorldPos = userVertexTransform(baseWorldPos, worldNormal, eid);
181
+
182
+ var output: VertexOutput;
183
+ output.position = scene.viewProj * vec4<f32>(finalWorldPos, 1.0);
184
+ output.color = data[eid].baseColor;
185
+ output.worldNormal = worldNormal;
186
+ output.entityId = eid;
187
+ output.worldPos = finalWorldPos;
188
+ return output;
189
+ }
190
+
191
+ @fragment
192
+ fn fs(input: VertexOutput) -> FragmentOutput {
193
+ let eid = input.entityId;
194
+ let d = data[eid];
195
+
196
+ var surface: SurfaceData;
197
+ surface.baseColor = input.color.rgb;
198
+ surface.roughness = d.pbr.x;
199
+ surface.metallic = d.pbr.y;
200
+ surface.emission = d.emission.rgb * d.emission.a;
201
+ surface.normal = normalize(input.worldNormal);
202
+ surface.worldPos = input.worldPos;
203
+
204
+ userFragment(&surface, input.position);
205
+
206
+ let shadowFactor = 1.0;
207
+ ${WGSL_LIGHTING_CALC}
208
+
209
+ var output: FragmentOutput;
210
+ output.color = vec4<f32>(litColor, input.color.a);
211
+ output.entityId = input.entityId;
212
+ return output;
213
+ }
214
+ `;
215
+ }
@@ -0,0 +1,511 @@
1
+ export function compileVertexBody(vertex?: string): string {
2
+ return vertex
3
+ ? `var pos = worldPos;
4
+ ${vertex}
5
+ return pos;`
6
+ : "return worldPos;";
7
+ }
8
+
9
+ const STARS_WGSL = /* wgsl */ `
10
+ fn hashStar(p: vec2<f32>) -> f32 {
11
+ var p3 = fract(vec3(p.x, p.y, p.x) * 0.1031);
12
+ p3 += dot(p3, p3.yzx + 33.33);
13
+ return fract((p3.x + p3.y) * p3.z);
14
+ }
15
+
16
+ fn hash2Star(p: vec2<f32>) -> vec2<f32> {
17
+ var p3 = fract(vec3(p.x, p.y, p.x) * vec3(0.1031, 0.1030, 0.0973));
18
+ p3 += dot(p3, p3.yzx + 33.33);
19
+ return fract((p3.xx + p3.yz) * p3.zy);
20
+ }
21
+
22
+ fn sampleStars(dir: vec3<f32>) -> vec3<f32> {
23
+ if (sky.starParams.z <= 0.0 || dir.y < 0.0) {
24
+ return vec3(0.0);
25
+ }
26
+
27
+ let theta = atan2(dir.z, dir.x);
28
+ let phi = asin(clamp(dir.y, -1.0, 1.0));
29
+
30
+ let gridSize = mix(20.0, 100.0, sky.starParams.y);
31
+ let cell = vec2(theta * gridSize / 3.14159, phi * gridSize / 1.5708);
32
+ let cellId = floor(cell);
33
+ let cellFract = fract(cell);
34
+
35
+ var starColor = vec3(0.0);
36
+
37
+ for (var dy = -1; dy <= 1; dy++) {
38
+ for (var dx = -1; dx <= 1; dx++) {
39
+ let neighbor = cellId + vec2(f32(dx), f32(dy));
40
+ let starHash = hashStar(neighbor);
41
+
42
+ if (starHash > sky.starParams.y * 0.7) {
43
+ continue;
44
+ }
45
+
46
+ let starPos = hash2Star(neighbor);
47
+ let starCenter = neighbor + starPos;
48
+ let dist = length(cell - starCenter);
49
+
50
+ let brightness = hashStar(neighbor + vec2(100.0, 100.0));
51
+ let radius = 0.02 + brightness * 0.03;
52
+
53
+ if (dist < radius) {
54
+ let twinkle = 0.8 + 0.2 * sin(brightness * 100.0);
55
+ let intensity = sky.starParams.x * brightness * twinkle;
56
+ let falloff = 1.0 - smoothstep(0.0, radius, dist);
57
+
58
+ let temp = hashStar(neighbor + vec2(200.0, 200.0));
59
+ let tint = mix(vec3(1.0, 0.9, 0.8), vec3(0.8, 0.9, 1.0), temp);
60
+
61
+ starColor = max(starColor, tint * intensity * falloff);
62
+ }
63
+ }
64
+ }
65
+
66
+ return starColor;
67
+ }
68
+ `;
69
+
70
+ export const NOISE_WGSL = /* wgsl */ `
71
+ fn hash2(p: vec2<f32>) -> f32 {
72
+ var p3 = fract(vec3(p.x, p.y, p.x) * 0.1031);
73
+ p3 += dot(p3, p3.yzx + 33.33);
74
+ return fract((p3.x + p3.y) * p3.z);
75
+ }
76
+
77
+ fn value2d(p: vec2f, seed: vec2f) -> f32 {
78
+ let i = floor(p);
79
+ let f = fract(p);
80
+ let u = f * f * (3.0 - 2.0 * f);
81
+ return mix(
82
+ mix(fract(sin(dot(i, seed)) * 43758.5) * 2.0 - 1.0,
83
+ fract(sin(dot(i + vec2(1.0, 0.0), seed)) * 43758.5) * 2.0 - 1.0, u.x),
84
+ mix(fract(sin(dot(i + vec2(0.0, 1.0), seed)) * 43758.5) * 2.0 - 1.0,
85
+ fract(sin(dot(i + vec2(1.0, 1.0), seed)) * 43758.5) * 2.0 - 1.0, u.x), u.y);
86
+ }
87
+
88
+ fn simplex2(p: vec2<f32>) -> f32 {
89
+ let K1 = 0.366025404;
90
+ let K2 = 0.211324865;
91
+
92
+ let i = floor(p + (p.x + p.y) * K1);
93
+ let a = p - i + (i.x + i.y) * K2;
94
+
95
+ let o = select(vec2(0.0, 1.0), vec2(1.0, 0.0), a.x > a.y);
96
+ let b = a - o + K2;
97
+ let c = a - 1.0 + 2.0 * K2;
98
+
99
+ let h = max(0.5 - vec3(dot(a, a), dot(b, b), dot(c, c)), vec3(0.0));
100
+ let h4 = h * h * h * h;
101
+
102
+ let n = vec3(
103
+ dot(a, vec2(hash2(i) * 2.0 - 1.0, hash2(i + vec2(0.0, 1.0)) * 2.0 - 1.0)),
104
+ dot(b, vec2(hash2(i + o) * 2.0 - 1.0, hash2(i + o + vec2(0.0, 1.0)) * 2.0 - 1.0)),
105
+ dot(c, vec2(hash2(i + 1.0) * 2.0 - 1.0, hash2(i + vec2(1.0, 2.0)) * 2.0 - 1.0))
106
+ );
107
+
108
+ return dot(h4, n) * 70.0;
109
+ }
110
+
111
+ const FBM2_OCTAVES = 5;
112
+
113
+ fn fbm2(p: vec2<f32>) -> f32 {
114
+ var value = 0.0;
115
+ var amplitude = 0.5;
116
+ var frequency = 1.0;
117
+ var pos = p;
118
+
119
+ for (var i = 0; i < FBM2_OCTAVES; i++) {
120
+ value += amplitude * simplex2(pos * frequency);
121
+ amplitude *= 0.5;
122
+ frequency *= 2.0;
123
+ }
124
+
125
+ return value;
126
+ }
127
+ `;
128
+
129
+ const MOON_WGSL = /* wgsl */ `
130
+ fn sampleMoon(dir: vec3<f32>) -> vec3<f32> {
131
+ if (sky.moonParams.z <= 0.0) {
132
+ return vec3(0.0);
133
+ }
134
+
135
+ let moonDir = sky.moonDirection.xyz;
136
+ let moonDot = dot(dir, moonDir);
137
+
138
+ let moonSize = 0.9995;
139
+ let moonColor = vec3(0.9, 0.9, 0.85);
140
+ let edgeWidth = 0.0003;
141
+ let glowStart = 0.999;
142
+
143
+ if (moonDot > moonSize - edgeWidth) {
144
+ let toCenter = dir - moonDir * moonDot;
145
+ let diskRight = normalize(cross(moonDir, vec3(0.0, 1.0, 0.0)));
146
+ let diskUp = cross(diskRight, moonDir);
147
+
148
+ let diskRadius = sqrt(1.0 - moonSize * moonSize);
149
+ let u = dot(toCenter, diskRight) / diskRadius;
150
+ let v = dot(toCenter, diskUp) / diskRadius;
151
+
152
+ let r2 = u * u + v * v;
153
+ let z = sqrt(max(0.0, 1.0 - r2));
154
+
155
+ let diskEdge = smoothstep(1.0 + edgeWidth / diskRadius, 1.0 - edgeWidth / diskRadius, sqrt(r2));
156
+
157
+ let limb = pow(z, 0.6);
158
+
159
+ let cellU = u * 8.0;
160
+ let cellV = v * 8.0;
161
+ let craterNoise = hashStar(floor(vec2(cellU, cellV)) + vec2(50.0, 50.0));
162
+ let surfaceVariation = 0.85 + 0.15 * craterNoise;
163
+
164
+ let phase = sky.moonParams.x;
165
+ let sunAngle = phase * 6.28318;
166
+ let sunLocalX = sin(sunAngle);
167
+ let sunLocalZ = -cos(sunAngle);
168
+
169
+ let illumination = u * sunLocalX + z * sunLocalZ;
170
+ let lit = smoothstep(-0.05, 0.05, illumination);
171
+
172
+ let earthshine = vec3(0.06, 0.07, 0.1);
173
+ let dayColor = moonColor * surfaceVariation * limb;
174
+ let surfaceColor = mix(earthshine * limb, dayColor, lit);
175
+
176
+ return surfaceColor * diskEdge * smoothstep(0.15, 0.3, sky.moonParams.y);
177
+ } else {
178
+ let glowFalloff = max(0.0, moonDot - glowStart) / (moonSize - edgeWidth - glowStart);
179
+ let glow = pow(glowFalloff, 2.0) * sky.moonParams.y;
180
+ return moonColor * glow * 0.3;
181
+ }
182
+ }
183
+ `;
184
+
185
+ const CLOUDS_WGSL = /* wgsl */ `
186
+ fn sampleClouds(dir: vec3<f32>) -> vec4<f32> {
187
+ if (sky.cloudParams.w <= 0.0 || dir.y < 0.01) {
188
+ return vec4(0.0);
189
+ }
190
+
191
+ let t = sky.cloudParams.z / max(dir.y, 0.001);
192
+ let uv = dir.xz * t;
193
+
194
+ var n = fbm2(uv);
195
+
196
+ let coverage = sky.cloudParams.x;
197
+ let density = sky.cloudParams.y;
198
+ n = smoothstep(1.0 - coverage, 1.0, n * 0.5 + 0.5) * density;
199
+
200
+ n *= smoothstep(0.0, 0.15, dir.y);
201
+
202
+ return vec4(sky.cloudColor.rgb, n);
203
+ }
204
+ `;
205
+
206
+ export const SKY_DIR_WGSL = /* wgsl */ `
207
+ const DEG_TO_RAD: f32 = 0.017453292;
208
+
209
+ fn computeSkyDir(screenX: f32, screenY: f32) -> vec3<f32> {
210
+ let width = scene.viewport.x;
211
+ let height = scene.viewport.y;
212
+
213
+ let ndcX = screenX * 2.0 - 1.0;
214
+ let ndcY = 1.0 - screenY * 2.0;
215
+
216
+ let aspect = width / height;
217
+
218
+ let cameraWorld = scene.cameraWorld;
219
+ let r00 = cameraWorld[0][0]; let r10 = cameraWorld[0][1]; let r20 = cameraWorld[0][2];
220
+ let r01 = cameraWorld[1][0]; let r11 = cameraWorld[1][1]; let r21 = cameraWorld[1][2];
221
+ let r02 = cameraWorld[2][0]; let r12 = cameraWorld[2][1]; let r22 = cameraWorld[2][2];
222
+
223
+ let skyFov = select(scene.fov, 60.0, scene.cameraMode > 0.5);
224
+ let tanHalfFov = tan((skyFov * DEG_TO_RAD) / 2.0);
225
+ let camDirX = ndcX * aspect * tanHalfFov;
226
+ let camDirY = ndcY * tanHalfFov;
227
+ let camDirZ = -1.0;
228
+ var dirX = r00 * camDirX + r01 * camDirY + r02 * camDirZ;
229
+ var dirY = r10 * camDirX + r11 * camDirY + r12 * camDirZ;
230
+ var dirZ = r20 * camDirX + r21 * camDirY + r22 * camDirZ;
231
+ let len = sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ);
232
+ dirX /= len; dirY /= len; dirZ /= len;
233
+ return vec3(dirX, dirY, dirZ);
234
+ }
235
+ `;
236
+
237
+ export const SKY_WGSL = /* wgsl */ `
238
+ ${STARS_WGSL}
239
+ ${MOON_WGSL}
240
+ ${CLOUDS_WGSL}
241
+
242
+ fn sampleSky(dir: vec3<f32>) -> vec3<f32> {
243
+ if (sky.skyZenith.a <= 0.0) {
244
+ return scene.clearColor.rgb;
245
+ }
246
+
247
+ let t = pow(clamp(dir.y, 0.0, 1.0), 0.25);
248
+ var color = mix(sky.skyHorizon.rgb, sky.skyZenith.rgb, t);
249
+
250
+ let horizonBand = 1.0 - abs(dir.y);
251
+ let horizonBlend = pow(horizonBand, 32.0);
252
+ color = mix(color, vec3(1.09), horizonBlend);
253
+
254
+ color += sampleStars(dir);
255
+
256
+ let clouds = sampleClouds(dir);
257
+ color = mix(color, clouds.rgb, clouds.a);
258
+
259
+ let moonContrib = sampleMoon(dir);
260
+ color += moonContrib * (1.0 - clouds.a * 0.7);
261
+
262
+ let sunDir = -scene.sunDirection.xyz;
263
+ let sunDot = dot(dir, sunDir);
264
+
265
+ let sunVisualColor = select(scene.sunColor.rgb, sky.sunVisualColor.rgb, sky.sunParams.z > 0.5);
266
+
267
+ let glowParam = sky.sunParams.y;
268
+
269
+ let baseSunSize = 0.9995;
270
+ let sunSizeParam = sky.sunParams.x;
271
+ let sunThreshold = 1.0 - (1.0 - baseSunSize) * sunSizeParam;
272
+ let sunEdgeWidth = (1.0 - sunThreshold) * 0.15;
273
+
274
+ let diskBlend = smoothstep(sunThreshold - sunEdgeWidth, sunThreshold + sunEdgeWidth, sunDot);
275
+ if (diskBlend > 0.0 && glowParam > 0.0) {
276
+ let radial = saturate((sunDot - sunThreshold) / (1.0 - sunThreshold));
277
+ let limbDarken = mix(0.7, 1.0, pow(radial, 0.5));
278
+ color += sunVisualColor * limbDarken * diskBlend * smoothstep(0.0, 0.3, glowParam);
279
+ }
280
+
281
+ let glow = max(0.0, sunDot);
282
+ let innerGlow = pow(glow, 32.0) * glowParam * 1.5;
283
+ let outerGlow = pow(glow, 3.0) * glowParam * 0.15;
284
+ color += sunVisualColor * (innerGlow + outerGlow);
285
+
286
+ if (sky.hazeDensity > 0.0) {
287
+ let horizonFactor = 1.0 - clamp(dir.y, 0.0, 1.0);
288
+ let hazeAmount = pow(horizonFactor, 2.0) * saturate(sky.hazeDensity * 5.0);
289
+ color = mix(color, sky.hazeColor.rgb, hazeAmount);
290
+ }
291
+
292
+ return color;
293
+ }
294
+ `;
295
+
296
+ export const HAZE_WGSL = /* wgsl */ `
297
+ fn applyHaze(color: vec3<f32>, dist: f32) -> vec3<f32> {
298
+ if (sky.hazeDensity <= 0.0) {
299
+ return color;
300
+ }
301
+ let haze = 1.0 - exp(-sky.hazeDensity * dist);
302
+ return mix(color, sky.hazeColor.rgb, haze);
303
+ }
304
+ `;
305
+
306
+ export const SHADOW_SAMPLE_WGSL = /* wgsl */ `
307
+ const GOLDEN_ANGLE: f32 = 2.399963229728653;
308
+ const CASCADE_BLEND_RANGE: f32 = 0.1;
309
+
310
+ fn selectCascade(viewZ: f32) -> u32 {
311
+ if (viewZ < shadow.cascadeSplits.x) { return 0u; }
312
+ if (viewZ < shadow.cascadeSplits.y) { return 1u; }
313
+ if (viewZ < shadow.cascadeSplits.z) { return 2u; }
314
+ return 3u;
315
+ }
316
+
317
+ fn getCascadeViewProj(cascade: u32) -> mat4x4<f32> {
318
+ switch cascade {
319
+ case 0u: { return shadow.cascade0ViewProj; }
320
+ case 1u: { return shadow.cascade1ViewProj; }
321
+ case 2u: { return shadow.cascade2ViewProj; }
322
+ default: { return shadow.cascade3ViewProj; }
323
+ }
324
+ }
325
+
326
+ fn getCascadeTexelSize(cascade: u32) -> f32 {
327
+ switch cascade {
328
+ case 0u: { return shadow.cascadeTexelSizes.x; }
329
+ case 1u: { return shadow.cascadeTexelSizes.y; }
330
+ case 2u: { return shadow.cascadeTexelSizes.z; }
331
+ default: { return shadow.cascadeTexelSizes.w; }
332
+ }
333
+ }
334
+
335
+ fn getCascadeSplit(cascade: u32) -> f32 {
336
+ switch cascade {
337
+ case 0u: { return shadow.cascadeSplits.x; }
338
+ case 1u: { return shadow.cascadeSplits.y; }
339
+ case 2u: { return shadow.cascadeSplits.z; }
340
+ default: { return shadow.cascadeSplits.w; }
341
+ }
342
+ }
343
+
344
+ fn computeShadowBias(cascade: u32, NdotL: f32) -> f32 {
345
+ let baseBias = 0.0001 + f32(cascade) * 0.00005;
346
+ let clampedNdotL = max(NdotL, 0.01);
347
+ let slopeBias = baseBias * sqrt(1.0 - clampedNdotL * clampedNdotL) / clampedNdotL;
348
+ return baseBias + min(slopeBias, baseBias * 5.0);
349
+ }
350
+
351
+ fn computeNormalOffset(normal: vec3<f32>, lightDir: vec3<f32>, cascade: u32) -> vec3<f32> {
352
+ let NdotL = abs(dot(normal, lightDir));
353
+ let texelSize = getCascadeTexelSize(cascade);
354
+ let slopeScale = saturate(1.0 - NdotL);
355
+ return normal * texelSize * (1.0 + slopeScale * 2.0);
356
+ }
357
+
358
+ fn sampleShadowAtCascadeWithBias(worldPos: vec3<f32>, cascade: u32, bias: f32) -> f32 {
359
+ let lightPos = getCascadeViewProj(cascade) * vec4(worldPos, 1.0);
360
+ let ndc = lightPos.xyz / lightPos.w;
361
+
362
+ let inBounds = abs(ndc.x) <= 1.0 && abs(ndc.y) <= 1.0 && ndc.z >= 0.0 && ndc.z <= 1.0;
363
+
364
+ let safeNdc = clamp(ndc, vec3(-1.0, -1.0, 0.0), vec3(1.0, 1.0, 1.0));
365
+ var uv = safeNdc.xy * 0.5 + 0.5;
366
+ uv.y = 1.0 - uv.y;
367
+
368
+ let offset = vec2(f32(cascade % 2u) * 0.5, f32(cascade / 2u) * 0.5);
369
+ uv = uv * 0.5 + offset;
370
+
371
+ let sampled = textureSampleCompare(shadowMap, shadowSampler, uv, safeNdc.z - bias);
372
+ return select(1.0, sampled, inBounds);
373
+ }
374
+
375
+ fn sampleShadowOffsetWithBias(worldPos: vec3<f32>, cascade: u32, uvOffset: vec2<f32>, bias: f32) -> f32 {
376
+ let lightPos = getCascadeViewProj(cascade) * vec4(worldPos, 1.0);
377
+ let ndc = lightPos.xyz / lightPos.w;
378
+
379
+ let inBounds = abs(ndc.x) <= 1.0 && abs(ndc.y) <= 1.0 && ndc.z >= 0.0 && ndc.z <= 1.0;
380
+
381
+ let safeNdc = clamp(ndc, vec3(-1.0, -1.0, 0.0), vec3(1.0, 1.0, 1.0));
382
+ var uv = safeNdc.xy * 0.5 + 0.5;
383
+ uv.y = 1.0 - uv.y;
384
+
385
+ let cascadeOffset = vec2(f32(cascade % 2u) * 0.5, f32(cascade / 2u) * 0.5);
386
+ uv = uv * 0.5 + cascadeOffset + uvOffset;
387
+
388
+ let sampled = textureSampleCompare(shadowMap, shadowSampler, uv, safeNdc.z - bias);
389
+ return select(1.0, sampled, inBounds);
390
+ }
391
+
392
+ fn vogelDiskSample(sampleIndex: u32, samplesCount: u32, rotation: f32) -> vec2<f32> {
393
+ let angle = GOLDEN_ANGLE * f32(sampleIndex) + rotation;
394
+ let radius = sqrt((f32(sampleIndex) + 0.5) / f32(samplesCount));
395
+ return vec2(cos(angle), sin(angle)) * radius;
396
+ }
397
+
398
+ fn sampleShadowPCFAtCascade(worldPos: vec3<f32>, cascade: u32, softness: f32, samples: u32, bias: f32) -> f32 {
399
+ let atlasTexelSize = 1.0 / 2048.0;
400
+ var total = 0.0;
401
+
402
+ for (var i = 0u; i < samples; i++) {
403
+ let disk = vogelDiskSample(i, samples, 0.0);
404
+ let uvOffset = disk * softness * atlasTexelSize * 4.0;
405
+ total += sampleShadowOffsetWithBias(worldPos, cascade, uvOffset, bias);
406
+ }
407
+ return total / f32(samples);
408
+ }
409
+
410
+ fn computeCascadeBlend(viewZ: f32, cascade: u32) -> f32 {
411
+ if (cascade >= 3u) { return 0.0; }
412
+
413
+ let splitEnd = getCascadeSplit(cascade);
414
+ let blendStart = splitEnd * (1.0 - CASCADE_BLEND_RANGE);
415
+
416
+ if (viewZ < blendStart) { return 0.0; }
417
+ return saturate((viewZ - blendStart) / (splitEnd - blendStart));
418
+ }
419
+
420
+ fn distanceFade(viewZ: f32, maxDist: f32) -> f32 {
421
+ let fadeStart = maxDist * 0.9;
422
+ let fade = saturate((maxDist - viewZ) / (maxDist - fadeStart));
423
+ return select(fade, 1.0, viewZ <= fadeStart);
424
+ }
425
+
426
+ fn sampleShadow(worldPos: vec3<f32>, normal: vec3<f32>, viewZ: f32) -> f32 {
427
+ let lightDir = -scene.sunDirection.xyz;
428
+ let NdotL = max(dot(normal, lightDir), 0.0);
429
+
430
+ let cascade = selectCascade(viewZ);
431
+ let offset = computeNormalOffset(normal, lightDir, cascade);
432
+ let offsetPos = worldPos + offset;
433
+ let bias = computeShadowBias(cascade, NdotL);
434
+ let shadowCurrent = sampleShadowAtCascadeWithBias(offsetPos, cascade, bias);
435
+
436
+ let nextCascade = min(cascade + 1u, 3u);
437
+ let nextOffset = computeNormalOffset(normal, lightDir, nextCascade);
438
+ let nextOffsetPos = worldPos + nextOffset;
439
+ let nextBias = computeShadowBias(nextCascade, NdotL);
440
+ let shadowNext = sampleShadowAtCascadeWithBias(nextOffsetPos, nextCascade, nextBias);
441
+
442
+ let blendFactor = computeCascadeBlend(viewZ, cascade) * f32(cascade < 3u);
443
+ let cascadeShadow = mix(shadowCurrent, shadowNext, blendFactor);
444
+
445
+ let fade = distanceFade(viewZ, shadow.cascadeSplits.w);
446
+ return mix(1.0, cascadeShadow, fade);
447
+ }
448
+
449
+ fn sampleShadowPCF(worldPos: vec3<f32>, normal: vec3<f32>, viewZ: f32, softness: f32, samples: u32) -> f32 {
450
+ if (softness <= 0.0) {
451
+ return sampleShadow(worldPos, normal, viewZ);
452
+ }
453
+
454
+ let lightDir = -scene.sunDirection.xyz;
455
+ let NdotL = max(dot(normal, lightDir), 0.0);
456
+
457
+ let cascade = selectCascade(viewZ);
458
+ let offset = computeNormalOffset(normal, lightDir, cascade);
459
+ let offsetPos = worldPos + offset;
460
+ let bias = computeShadowBias(cascade, NdotL);
461
+ let shadowCurrent = sampleShadowPCFAtCascade(offsetPos, cascade, softness, samples, bias);
462
+
463
+ let nextCascade = min(cascade + 1u, 3u);
464
+ let nextOffset = computeNormalOffset(normal, lightDir, nextCascade);
465
+ let nextOffsetPos = worldPos + nextOffset;
466
+ let nextBias = computeShadowBias(nextCascade, NdotL);
467
+ let shadowNext = sampleShadowPCFAtCascade(nextOffsetPos, nextCascade, softness, samples, nextBias);
468
+
469
+ let blendFactor = computeCascadeBlend(viewZ, cascade) * f32(cascade < 3u);
470
+ let cascadeShadow = mix(shadowCurrent, shadowNext, blendFactor);
471
+
472
+ let fade = distanceFade(viewZ, shadow.cascadeSplits.w);
473
+ return mix(1.0, cascadeShadow, fade);
474
+ }
475
+ `;
476
+
477
+ export const SPECULAR_WGSL = /* wgsl */ `
478
+ const DIELECTRIC_F0: f32 = 0.04;
479
+
480
+ fn blinnPhongSpecular(N: vec3<f32>, L: vec3<f32>, V: vec3<f32>, roughness: f32) -> f32 {
481
+ let H = normalize(L + V);
482
+ let NdotH = max(dot(N, H), 0.0);
483
+ let shininess = pow(2.0, (1.0 - roughness) * 10.0);
484
+ let intensity = (1.0 - roughness) * (1.0 - roughness);
485
+ return pow(NdotH, shininess) * intensity;
486
+ }
487
+
488
+ fn schlickFresnel(cosTheta: f32, F0: vec3<f32>) -> vec3<f32> {
489
+ return F0 + (vec3(1.0) - F0) * pow(1.0 - cosTheta, 5.0);
490
+ }
491
+
492
+ fn computeF0Vec(baseColor: vec3<f32>, metallic: f32) -> vec3<f32> {
493
+ return mix(vec3(DIELECTRIC_F0), baseColor, metallic);
494
+ }
495
+ `;
496
+
497
+ export const WGSL_LIGHTING_CALC = /* wgsl */ `
498
+ let V = normalize(scene.cameraWorld[3].xyz - surface.worldPos);
499
+ let L = -scene.sunDirection.xyz;
500
+ let NdotL = max(dot(surface.normal, L), 0.0);
501
+ let NdotV = max(dot(surface.normal, V), 0.0);
502
+ let F0 = computeF0Vec(surface.baseColor, surface.metallic);
503
+ let F = schlickFresnel(NdotV, F0);
504
+ let diffuseWeight = 1.0 - surface.metallic;
505
+ let ambient = scene.ambientColor.rgb * scene.ambientColor.a;
506
+ let sunDiffuse = scene.sunColor.rgb * NdotL * shadowFactor;
507
+ let diffuseColor = surface.baseColor * (ambient + sunDiffuse) * diffuseWeight + surface.emission;
508
+ let specTerm = blinnPhongSpecular(surface.normal, L, V, surface.roughness);
509
+ let specular = scene.sunColor.rgb * specTerm * F * NdotL * shadowFactor;
510
+ let litColor = diffuseColor + specular;
511
+ `;