reze-engine 0.3.6 → 0.3.8

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.
@@ -1,1122 +0,0 @@
1
- // Pool scene renderer - full raymarched pool scene from pool.html
2
- export class PoolScene {
3
- constructor(device, format, sampleCount, cameraUniformBuffer, config = {}) {
4
- this.startTime = performance.now();
5
- this.waterHeightTextureSize = 512; // Resolution of height map texture
6
- this.waterPlaneSize = 300.0; // Size of the water plane in world units
7
- this.waterPlaneSubdivisions = 128; // Number of subdivisions for the plane
8
- this.moonRadius = 0.05; // Moon angular radius for shader-based rendering
9
- this.device = device;
10
- this.format = format;
11
- this.sampleCount = sampleCount;
12
- this.cameraUniformBuffer = cameraUniformBuffer;
13
- this.config = {
14
- cloudSharpness: config.cloudSharpness ?? 0.001,
15
- windSpeed: config.windSpeed ?? [-43.0, 32.0],
16
- bumpFactor: config.bumpFactor ?? 0.05,
17
- bumpDistance: config.bumpDistance ?? 70.0,
18
- skyColor: config.skyColor ?? [0.08, 0.08, 0.12],
19
- moonlightColor: config.moonlightColor ?? [0.4, 0.4, 0.2],
20
- skyByMoonlightColor: config.skyByMoonlightColor ?? [0.4, 0.2, 0.87],
21
- waterColor: config.waterColor ?? [0.12, 0.13, 0.16],
22
- exposure: config.exposure ?? 0.9,
23
- epsilon: config.epsilon ?? 0.01,
24
- marchSteps: config.marchSteps ?? 100,
25
- moonPosition: config.moonPosition ?? [0, 1, 10],
26
- };
27
- this.moonPosition = this.config.moonPosition;
28
- this.createWaterHeightTexture();
29
- this.createPipeline();
30
- this.createComputePipeline();
31
- this.createUniformBuffer();
32
- this.createWaterPlane();
33
- this.createWaterPlanePipeline();
34
- }
35
- createWaterHeightTexture() {
36
- // Create texture for water height map (RGBA16Float is filterable, we use R channel for height)
37
- this.waterHeightTexture = this.device.createTexture({
38
- size: [this.waterHeightTextureSize, this.waterHeightTextureSize, 1],
39
- format: "rgba16float",
40
- usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING,
41
- });
42
- this.waterHeightTextureView = this.waterHeightTexture.createView();
43
- // Create sampler for reading the height map
44
- this.waterHeightSampler = this.device.createSampler({
45
- magFilter: "linear",
46
- minFilter: "linear",
47
- addressModeU: "repeat",
48
- addressModeV: "repeat",
49
- });
50
- }
51
- createComputePipeline() {
52
- const computeShaderCode = `
53
- struct Uniforms {
54
- time: f32,
55
- _padding1: f32,
56
- resolution: vec2<f32>,
57
- modelPos: vec3<f32>,
58
- _padding2: f32,
59
- worldBounds: vec4<f32>, // minX, minZ, maxX, maxZ
60
- };
61
-
62
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
63
- @group(0) @binding(1) var heightMap: texture_storage_2d<rgba16float, write>;
64
-
65
- // Math matrices (column-major)
66
- const m: mat3x3<f32> = mat3x3<f32>(
67
- vec3<f32>(0.00, -0.90, -0.60),
68
- vec3<f32>(0.90, 0.36, -0.48),
69
- vec3<f32>(-0.60, -0.48, 0.34)
70
- );
71
-
72
- const mr: mat2x2<f32> = mat2x2<f32>(
73
- 0.84147, 0.54030,
74
- 0.54030, -0.84147
75
- );
76
-
77
- // Hash function
78
- fn hash(n: f32) -> f32 {
79
- return fract(sin(n) * 43758.5453);
80
- }
81
-
82
- fn hash3(p: vec3<f32>) -> f32 {
83
- var p3 = fract(p * 0.1031);
84
- p3 += dot(p3, p3.yzx + 33.33);
85
- return fract((p3.x + p3.y) * p3.z);
86
- }
87
-
88
- fn noise3(x: vec3<f32>) -> f32 {
89
- let p = floor(x);
90
- var f = fract(x);
91
- f = f * f * (3.0 - 2.0 * f);
92
-
93
- let n = p.x + p.y * 57.0 + 113.0 * p.z;
94
- return mix(
95
- mix(mix(hash3(p), hash3(p + vec3<f32>(1.0, 0.0, 0.0)), f.x),
96
- mix(hash3(p + vec3<f32>(0.0, 1.0, 0.0)), hash3(p + vec3<f32>(1.0, 1.0, 0.0)), f.x), f.y),
97
- mix(mix(hash3(p + vec3<f32>(0.0, 0.0, 1.0)), hash3(p + vec3<f32>(1.0, 0.0, 1.0)), f.x),
98
- mix(hash3(p + vec3<f32>(0.0, 1.0, 1.0)), hash3(p + vec3<f32>(1.0, 1.0, 1.0)), f.x), f.y), f.z
99
- );
100
- }
101
-
102
- fn fbm3(p: vec3<f32>) -> f32 {
103
- var f: f32 = 0.0;
104
- var p_var = p;
105
- f = 0.5000 * noise3(p_var);
106
- p_var = m * p_var * 2.02;
107
- f += 0.2500 * noise3(p_var);
108
- p_var = m * p_var * 2.33;
109
- f += 0.1250 * noise3(p_var);
110
- p_var = m * p_var * 2.01;
111
- f += 0.0625 * noise3(p_var);
112
- return f / 0.9175;
113
- }
114
-
115
- fn waterHeightMap(pos: vec2<f32>, time: f32) -> f32 {
116
- var posm = pos;
117
- posm = mr * posm;
118
- posm.x += 0.25 * time;
119
- let f = fbm3(vec3<f32>(posm * 1.9, time * 0.27));
120
- var height = 0.5 + 0.1 * f;
121
- height += 0.13 * sin(posm.x * 6.0 + 10.0 * f);
122
-
123
- // Generate ripples from model position
124
- let modelPos2D = vec2<f32>(uniforms.modelPos.x, uniforms.modelPos.z);
125
- let d = length(pos - modelPos2D);
126
- let ripple1 = 0.15 * cos(d * 50.0 - time * 4.0) * (1.0 - smoothstep(0.0, 1.5, d));
127
- let ripple2 = 0.08 * cos(d * 35.0 - time * 3.0) * (1.0 - smoothstep(0.0, 2.0, d));
128
- let ripple3 = 0.05 * cos(d * 25.0 - time * 2.0) * (1.0 - smoothstep(0.0, 3.0, d));
129
- height += ripple1 + ripple2 + ripple3;
130
-
131
- return height;
132
- }
133
-
134
- @compute @workgroup_size(8, 8)
135
- fn cs_main(@builtin(global_invocation_id) globalId: vec3<u32>) {
136
- let time = uniforms.time + 23.0;
137
- let texelCoord = vec2<i32>(globalId.xy);
138
-
139
- // Map texture coordinates to world space
140
- let uv = vec2<f32>(globalId.xy) / vec2<f32>(${this.waterHeightTextureSize}.0);
141
- let worldX = mix(uniforms.worldBounds.x, uniforms.worldBounds.z, uv.x);
142
- let worldZ = mix(uniforms.worldBounds.y, uniforms.worldBounds.w, uv.y);
143
- let worldPos = vec2<f32>(worldX, worldZ);
144
-
145
- let height = waterHeightMap(worldPos, time);
146
- textureStore(heightMap, texelCoord, vec4<f32>(height, 0.0, 0.0, 1.0));
147
- }
148
- `;
149
- const module = this.device.createShaderModule({ code: computeShaderCode });
150
- this.computePipeline = this.device.createComputePipeline({
151
- layout: "auto",
152
- compute: {
153
- module,
154
- entryPoint: "cs_main",
155
- },
156
- });
157
- }
158
- createPipeline() {
159
- const shaderCode = `
160
- struct CameraUniforms {
161
- view: mat4x4<f32>,
162
- projection: mat4x4<f32>,
163
- viewPos: vec3<f32>,
164
- _padding: f32,
165
- };
166
-
167
- struct Uniforms {
168
- time: f32,
169
- _padding1: f32,
170
- resolution: vec2<f32>,
171
- modelPos: vec3<f32>,
172
- _padding2: f32,
173
- };
174
-
175
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
176
- @group(0) @binding(1) var<uniform> uniforms: Uniforms;
177
-
178
- struct VertexOutput {
179
- @builtin(position) Position: vec4<f32>,
180
- @location(0) uv: vec2<f32>,
181
- };
182
-
183
- @vertex
184
- fn vs_main(@builtin(vertex_index) VertexIndex: u32) -> VertexOutput {
185
- var pos = array<vec2<f32>, 6>(
186
- vec2<f32>(-1.0, -1.0), vec2<f32>(1.0, -1.0), vec2<f32>(-1.0, 1.0),
187
- vec2<f32>(-1.0, 1.0), vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0)
188
- );
189
-
190
- var output: VertexOutput;
191
- output.Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
192
- var uv = pos[VertexIndex] * 0.5 + 0.5;
193
- output.uv = vec2<f32>(uv.x, 1.0 - uv.y);
194
- return output;
195
- }
196
-
197
- // Constants
198
- const CLOUDSHARPNESS: f32 = ${this.config.cloudSharpness};
199
- const WINDSPEED: vec2<f32> = vec2<f32>(${this.config.windSpeed[0]}, ${this.config.windSpeed[1]});
200
- const BUMPFACTOR: f32 = ${this.config.bumpFactor};
201
- const BUMPDISTANCE: f32 = ${this.config.bumpDistance};
202
- const SKYCOLOR: vec3<f32> = vec3<f32>(${this.config.skyColor[0]}, ${this.config.skyColor[1]}, ${this.config.skyColor[2]});
203
- const MOONLIGHTCOLOR: vec3<f32> = vec3<f32>(${this.config.moonlightColor[0]}, ${this.config.moonlightColor[1]}, ${this.config.moonlightColor[2]});
204
- const SKYBYMOONLIGHTCOLOR: vec3<f32> = vec3<f32>(${this.config.skyByMoonlightColor[0]}, ${this.config.skyByMoonlightColor[1]}, ${this.config.skyByMoonlightColor[2]});
205
- const WATERCOLOR: vec3<f32> = vec3<f32>(${this.config.waterColor[0]}, ${this.config.waterColor[1]}, ${this.config.waterColor[2]});
206
- const EXPOSURE: f32 = ${this.config.exposure};
207
- const EPSILON: f32 = ${this.config.epsilon};
208
- const MARCHSTEPS: i32 = ${this.config.marchSteps};
209
-
210
- // Math matrices (column-major)
211
- const m: mat3x3<f32> = mat3x3<f32>(
212
- vec3<f32>(0.00, -0.90, -0.60),
213
- vec3<f32>(0.90, 0.36, -0.48),
214
- vec3<f32>(-0.60, -0.48, 0.34)
215
- );
216
-
217
- const mr: mat2x2<f32> = mat2x2<f32>(
218
- 0.84147, 0.54030,
219
- 0.54030, -0.84147
220
- );
221
-
222
- // Hash function
223
- fn hash(n: f32) -> f32 {
224
- return fract(sin(n) * 43758.5453);
225
- }
226
-
227
- // Procedural noise
228
- fn hash3(p: vec3<f32>) -> f32 {
229
- var p3 = fract(p * 0.1031);
230
- p3 += dot(p3, p3.yzx + 33.33);
231
- return fract((p3.x + p3.y) * p3.z);
232
- }
233
-
234
- fn noise3(x: vec3<f32>) -> f32 {
235
- let p = floor(x);
236
- var f = fract(x);
237
- f = f * f * (3.0 - 2.0 * f);
238
-
239
- let n = p.x + p.y * 57.0 + 113.0 * p.z;
240
- return mix(
241
- mix(mix(hash3(p), hash3(p + vec3<f32>(1.0, 0.0, 0.0)), f.x),
242
- mix(hash3(p + vec3<f32>(0.0, 1.0, 0.0)), hash3(p + vec3<f32>(1.0, 1.0, 0.0)), f.x), f.y),
243
- mix(mix(hash3(p + vec3<f32>(0.0, 0.0, 1.0)), hash3(p + vec3<f32>(1.0, 0.0, 1.0)), f.x),
244
- mix(hash3(p + vec3<f32>(0.0, 1.0, 1.0)), hash3(p + vec3<f32>(1.0, 1.0, 1.0)), f.x), f.y), f.z
245
- );
246
- }
247
-
248
- fn noise2(x: vec2<f32>) -> f32 {
249
- let p = floor(x);
250
- var f = fract(x);
251
- f = f * f * (3.0 - 2.0 * f);
252
-
253
- let n = p.x + p.y * 57.0;
254
- return mix(
255
- mix(hash(f32(n)), hash(f32(n + 1.0)), f.x),
256
- mix(hash(f32(n + 57.0)), hash(f32(n + 58.0)), f.x),
257
- f.y
258
- );
259
- }
260
-
261
- fn fbm3(p: vec3<f32>) -> f32 {
262
- var f: f32 = 0.0;
263
- var p_var = p;
264
- f = 0.5000 * noise3(p_var);
265
- p_var = m * p_var * 2.02;
266
- f += 0.2500 * noise3(p_var);
267
- p_var = m * p_var * 2.33;
268
- f += 0.1250 * noise3(p_var);
269
- p_var = m * p_var * 2.01;
270
- f += 0.0625 * noise3(p_var);
271
- return f / 0.9175;
272
- }
273
-
274
- fn fbm2(p: vec2<f32>) -> f32 {
275
- var f: f32 = 0.0;
276
- var p_var = p;
277
- f = 0.5000 * noise2(p_var);
278
- p_var = mr * p_var * 2.02;
279
- f += 0.2500 * noise2(p_var);
280
- p_var = mr * p_var * 2.33;
281
- f += 0.1250 * noise2(p_var);
282
- p_var = mr * p_var * 2.01;
283
- f += 0.0625 * noise2(p_var);
284
- return f / 0.9175;
285
- }
286
-
287
- // Height maps
288
- fn heightMap(pos: vec3<f32>) -> f32 {
289
- let n = noise2(vec2<f32>(0.0, 4.2) + pos.xz * 0.14);
290
- return 9.0 * (n - 0.7);
291
- }
292
-
293
- // Intersection functions
294
- struct IntersectResult {
295
- hit: bool,
296
- dist: f32,
297
- }
298
-
299
- fn intersectPlane(ro: vec3<f32>, rd: vec3<f32>, height: f32) -> IntersectResult {
300
- var result: IntersectResult;
301
- result.hit = false;
302
- result.dist = 0.0;
303
-
304
- if (rd.y == 0.0) {
305
- return result;
306
- }
307
-
308
- let d = -(ro.y - height) / rd.y;
309
- let d_clamped = min(100000.0, d);
310
- if (d_clamped > 0.0) {
311
- result.hit = true;
312
- result.dist = d_clamped;
313
- }
314
- return result;
315
- }
316
-
317
- fn intersectHeightMap(ro: vec3<f32>, rd: vec3<f32>, maxdist: f32) -> IntersectResult {
318
- var result: IntersectResult;
319
- result.hit = false;
320
- result.dist = 0.0;
321
-
322
- var dt: f32 = 0.3;
323
- var dist: f32 = 0.0;
324
-
325
- for (var i: i32 = 0; i < MARCHSTEPS; i++) {
326
- if (result.hit || dist > maxdist) {
327
- break;
328
- }
329
- dist += dt;
330
- dt = min(dt * 1.1, 0.5);
331
- let pos = ro + rd * dist;
332
- if (heightMap(pos) >= pos.y) {
333
- result.hit = true;
334
- result.dist = dist;
335
- }
336
- }
337
- return result;
338
- }
339
-
340
- struct SphereResult {
341
- hit: bool,
342
- dist: f32,
343
- normal: vec3<f32>,
344
- }
345
-
346
- fn intersectSphere(ro: vec3<f32>, rd: vec3<f32>, sph: vec4<f32>) -> SphereResult {
347
- var result: SphereResult;
348
- result.hit = false;
349
- result.dist = 0.0;
350
- result.normal = vec3<f32>(0.0);
351
-
352
- let ds = ro - sph.xyz;
353
- let bs = dot(rd, ds);
354
- let cs = dot(ds, ds) - sph.w * sph.w;
355
- let ts = bs * bs - cs;
356
-
357
- if (ts > 0.0) {
358
- let ts_val = -bs - sqrt(ts);
359
- if (ts_val > 0.0) {
360
- result.hit = true;
361
- result.dist = ts_val;
362
- result.normal = normalize(((ro + ts_val * rd) - sph.xyz) / sph.w);
363
- }
364
- }
365
- return result;
366
- }
367
-
368
- // Cloud density - simplified to always be very light
369
- fn cloudDensity(rd: vec3<f32>, time: f32) -> f32 {
370
- let planeResult = intersectPlane(vec3<f32>(0.0, 0.0, 0.0), rd, 500.0);
371
- let intersection = rd * planeResult.dist;
372
-
373
- // Very light, consistent cloud cover (no time variation)
374
- let cloudCover = 0.15;
375
- // Simplified cloud calculation - much lighter
376
- var cloud = 0.3 + 0.2 * fbm3(vec3<f32>((intersection.xz + WINDSPEED * time) * 0.001, time * 0.25));
377
-
378
- cloud += 0.01 * noise2(intersection.xz - WINDSPEED * time * 0.01);
379
-
380
- // Clamp to keep clouds very light
381
- cloud = clamp(cloud, 0.0, 0.25);
382
-
383
- // Simplified - always light clouds, no heavy blocking
384
- cloud = cloud * 0.3; // Scale down to keep very light
385
-
386
- return cloud;
387
- }
388
-
389
- // Sky color
390
- fn skyColorFunc(rd: vec3<f32>, time: f32, screenUV: vec2<f32>) -> vec3<f32> {
391
- // Moon position from config
392
- let moonPos = vec3<f32>(${this.moonPosition[0]}.0, ${this.moonPosition[1]}.0, ${this.moonPosition[2]}.0);
393
- let moondir = normalize(moonPos);
394
-
395
- // Glow follows moon position - strongest when looking directly at moon
396
- let moonAngle = dot(moondir, rd);
397
- let moonglow = clamp(moonAngle, 0.0, 1.0);
398
- var col = SKYCOLOR * moondir.y;
399
- col += 0.6 * SKYBYMOONLIGHTCOLOR * moonglow;
400
- col += 0.8 * MOONLIGHTCOLOR * pow(moonglow, 35.0);
401
-
402
- // Moon rendering - project moon to screen space for perfect circle (like original shader)
403
- // Project moon position to screen space
404
- let moonWorldPos = vec4<f32>(moonPos, 1.0);
405
- let moonViewPos = camera.view * moonWorldPos;
406
- let moonClipPos = camera.projection * moonViewPos;
407
-
408
- // Only render moon if it's in front of camera
409
- if (moonClipPos.w > 0.0) {
410
- // Convert to NDC then to screen UV (0-1)
411
- let moonNDC = moonClipPos.xy / moonClipPos.w;
412
- let moonScreenUV = (moonNDC + 1.0) * 0.5;
413
- // Flip Y for screen space (WebGPU has Y=0 at top)
414
- let moonScreenPos = vec2<f32>(moonScreenUV.x, 1.0 - moonScreenUV.y);
415
-
416
- // Calculate screen-space radius based on moon's angular size
417
- // moonRadius is in radians, convert to screen-space units
418
- let moonScreenRadius = ${this.moonRadius * 0.15}; // Adjusted for screen space
419
- let moonDist2D = length(screenUV - moonScreenPos);
420
-
421
- // Sharp circular moon disk (like original: length(uvInit-vec2(0.,.04))<.03)
422
- let moonDisk = 1.0 - smoothstep(moonScreenRadius * 0.95, moonScreenRadius, moonDist2D);
423
- let moonBrightness = 3.0;
424
- let moonColor = vec3<f32>(1.0, 0.98, 0.92) * moonBrightness;
425
-
426
- // Render moon disk
427
- col = mix(col, moonColor, moonDisk);
428
-
429
- // Add glow/bloom effect - extends beyond moon disk (like original sun flare)
430
- let glowRadius = moonScreenRadius * 3.0;
431
- let glowDist2D = moonDist2D / glowRadius;
432
- let glowFactor = pow(max(0.0, 1.0 - glowDist2D), 2.0) * 1.5;
433
- // Only add glow outside the moon disk
434
- let glowMask = 1.0 - moonDisk;
435
- col += moonColor * glowFactor * glowMask;
436
- }
437
-
438
- // Stars only when moon is far or behind camera
439
- let moonDist = acos(clamp(dot(moondir, rd), -1.0, 1.0));
440
- if (moonDist > ${this.moonRadius * 3.0}) {
441
- let rds = rd;
442
- let v = 1.0 / (2.0 * (1.0 + rds.z));
443
- let xy = vec2<f32>(rds.y * v, rds.x * v);
444
- var s = noise2(rds.xz * 134.0);
445
- s += noise2(rds.xz * 370.0);
446
- s += noise2(rds.xz * 870.0);
447
- s = pow(s, 19.0) * 0.00000001 * max(rd.y, 0.0);
448
- if (s > 0.1) {
449
- let backStars = vec3<f32>((1.0 - sin(xy.x * 20.0 + time * 13.0 * rds.x + xy.y * 30.0)) * 0.5 * s, s, s);
450
- col += backStars;
451
- }
452
- }
453
-
454
- // Apply clouds, but don't dim the moon if it's visible
455
- let cloudDens = cloudDensity(rd, time);
456
- if (moonDist > 1.0) {
457
- col *= (1.0 - cloudDens);
458
- } else {
459
- // Moon is visible - don't dim it at all, keep it bright
460
- // Moon stays fully visible through clouds
461
- }
462
-
463
- return col;
464
- }
465
-
466
- // Trace function (removed bottle intersection)
467
- struct TraceResult {
468
- material: i32, // 0=sky, 1=mountain, 2=water
469
- intersection: vec3<f32>,
470
- normal: vec3<f32>,
471
- dist: f32,
472
- color: vec3<f32>,
473
- }
474
-
475
- fn trace(ro: vec3<f32>, rd: vec3<f32>, currentDistance: f32, reflection: bool, time: f32) -> TraceResult {
476
- var result: TraceResult;
477
- result.material = 0;
478
- result.dist = 100000.0;
479
- result.intersection = vec3<f32>(0.0);
480
- result.normal = vec3<f32>(0.0, 1.0, 0.0);
481
- result.color = vec3<f32>(0.0);
482
-
483
- // Skip mountains and water - only render sky
484
- // Water is now handled by the 3D water plane mesh
485
-
486
- // Always return sky color (no water intersection)
487
- // Calculate screen UV for moon projection
488
- let q = input.uv;
489
- result.color = skyColorFunc(rd, time, q);
490
-
491
- return result;
492
- }
493
-
494
- @fragment
495
- fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
496
- let time = uniforms.time + 23.0;
497
- let q = input.uv;
498
- var p = -1.0 + 2.0 * q;
499
- p.y = -p.y; // Flip Y: WebGPU (0,0) is top-left, ShaderToy (0,0) is bottom-left
500
- p.x *= uniforms.resolution.x / uniforms.resolution.y;
501
-
502
- // Use engine's camera to reconstruct ray direction
503
- // Extract camera basis vectors from view matrix
504
- let viewRight = vec3<f32>(camera.view[0].x, camera.view[1].x, camera.view[2].x);
505
- let viewUp = vec3<f32>(camera.view[0].y, camera.view[1].y, camera.view[2].y);
506
- let viewForward = vec3<f32>(camera.view[0].z, camera.view[1].z, camera.view[2].z);
507
-
508
- // Extract FOV and aspect from projection matrix
509
- let f = camera.projection[1].y;
510
- let aspect = f / camera.projection[0].x;
511
- let fovY = 2.0 * atan(1.0 / f);
512
-
513
- // Compute ray direction in view space
514
- let tanHalfFov = tan(fovY * 0.5);
515
- let viewDir = vec3<f32>(
516
- p.x * aspect * tanHalfFov,
517
- p.y * tanHalfFov,
518
- -1.0 // In view space, forward is -Z
519
- );
520
-
521
- // Transform to world space
522
- let rd = normalize(
523
- viewDir.x * viewRight +
524
- viewDir.y * viewUp +
525
- (-viewDir.z) * viewForward
526
- );
527
-
528
- // Ray origin is camera position
529
- let ro = camera.viewPos;
530
-
531
- var traceResult = trace(ro, rd, 0.0, false, time);
532
-
533
- // No water reflections in background - water plane handles its own reflections
534
-
535
- var col = pow(traceResult.color, vec3<f32>(EXPOSURE));
536
- col = clamp(col, vec3<f32>(0.0), vec3<f32>(1.0));
537
-
538
- col *= 0.25 + 0.75 * pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.15);
539
-
540
- return vec4<f32>(col, 1.0);
541
- }
542
- `;
543
- const module = this.device.createShaderModule({ code: shaderCode });
544
- this.pipeline = this.device.createRenderPipeline({
545
- layout: "auto",
546
- vertex: {
547
- module,
548
- entryPoint: "vs_main",
549
- },
550
- fragment: {
551
- module,
552
- entryPoint: "fs_main",
553
- targets: [{ format: this.format }],
554
- },
555
- primitive: {
556
- topology: "triangle-list",
557
- },
558
- multisample: {
559
- count: this.sampleCount,
560
- },
561
- depthStencil: {
562
- depthWriteEnabled: false, // Background doesn't write depth - model will render on top
563
- depthCompare: "always", // Always pass depth test
564
- format: "depth24plus-stencil8",
565
- },
566
- });
567
- }
568
- createUniformBuffer() {
569
- // Uniforms: time, padding, resX, resY, modelPos.x, modelPos.y, modelPos.z, padding
570
- const uniformBufferSize = 32;
571
- this.uniformBuffer = this.device.createBuffer({
572
- size: uniformBufferSize,
573
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
574
- });
575
- this.bindGroup = this.device.createBindGroup({
576
- layout: this.pipeline.getBindGroupLayout(0),
577
- entries: [
578
- { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
579
- { binding: 1, resource: { buffer: this.uniformBuffer } },
580
- ],
581
- });
582
- // Create compute bind group
583
- const computeUniformBufferSize = 48; // time, padding, resX, resY, modelPos.xyz, padding, worldBounds.xyzw
584
- this.computeUniformBuffer = this.device.createBuffer({
585
- size: computeUniformBufferSize,
586
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
587
- });
588
- this.computeBindGroup = this.device.createBindGroup({
589
- layout: this.computePipeline.getBindGroupLayout(0),
590
- entries: [
591
- { binding: 0, resource: { buffer: this.computeUniformBuffer } },
592
- { binding: 1, resource: this.waterHeightTextureView },
593
- ],
594
- });
595
- }
596
- dispatchCompute(encoder, width, height, modelPos = [0, 0, 0]) {
597
- const now = performance.now();
598
- const time = (now - this.startTime) / 1000;
599
- // Calculate world bounds for height map (centered around model position)
600
- const worldSize = 50.0;
601
- const worldBounds = [
602
- modelPos[0] - worldSize, // minX
603
- modelPos[2] - worldSize, // minZ
604
- modelPos[0] + worldSize, // maxX
605
- modelPos[2] + worldSize, // maxZ
606
- ];
607
- // Write compute uniforms: time, padding, resX, resY, modelPos.xyz, padding, worldBounds.xyzw
608
- const computeUniforms = new Float32Array([
609
- time,
610
- 0,
611
- width,
612
- height,
613
- modelPos[0],
614
- modelPos[1],
615
- modelPos[2],
616
- 0,
617
- worldBounds[0],
618
- worldBounds[1],
619
- worldBounds[2],
620
- worldBounds[3],
621
- ]);
622
- this.device.queue.writeBuffer(this.computeUniformBuffer, 0, computeUniforms);
623
- // Dispatch compute shader to generate water height map
624
- const computePass = encoder.beginComputePass();
625
- computePass.setPipeline(this.computePipeline);
626
- computePass.setBindGroup(0, this.computeBindGroup);
627
- computePass.dispatchWorkgroups(Math.ceil(this.waterHeightTextureSize / 8), Math.ceil(this.waterHeightTextureSize / 8), 1);
628
- computePass.end();
629
- }
630
- createWaterPlane() {
631
- const subdivisions = this.waterPlaneSubdivisions;
632
- const size = this.waterPlaneSize;
633
- const halfSize = size / 2.0;
634
- // Generate vertices
635
- const vertices = [];
636
- const indices = [];
637
- const step = size / subdivisions;
638
- // Create grid of vertices
639
- for (let z = 0; z <= subdivisions; z++) {
640
- for (let x = 0; x <= subdivisions; x++) {
641
- const posX = -halfSize + x * step;
642
- const posZ = -halfSize + z * step;
643
- const u = x / subdivisions;
644
- const v = z / subdivisions;
645
- // Position (x, y, z) - y will be displaced by height map in shader
646
- vertices.push(posX, 15.0, posZ);
647
- // UV coordinates for sampling height map
648
- vertices.push(u, v);
649
- }
650
- }
651
- // Create indices for triangles
652
- for (let z = 0; z < subdivisions; z++) {
653
- for (let x = 0; x < subdivisions; x++) {
654
- const i = z * (subdivisions + 1) + x;
655
- // First triangle
656
- indices.push(i);
657
- indices.push(i + 1);
658
- indices.push(i + subdivisions + 1);
659
- // Second triangle
660
- indices.push(i + 1);
661
- indices.push(i + subdivisions + 2);
662
- indices.push(i + subdivisions + 1);
663
- }
664
- }
665
- // Create vertex buffer
666
- const vertexData = new Float32Array(vertices);
667
- this.waterPlaneVertexBuffer = this.device.createBuffer({
668
- size: vertexData.byteLength,
669
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
670
- });
671
- this.device.queue.writeBuffer(this.waterPlaneVertexBuffer, 0, vertexData);
672
- // Create index buffer
673
- const indexData = new Uint32Array(indices);
674
- this.waterPlaneIndexBuffer = this.device.createBuffer({
675
- size: indexData.byteLength,
676
- usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
677
- });
678
- this.device.queue.writeBuffer(this.waterPlaneIndexBuffer, 0, indexData);
679
- }
680
- createWaterPlanePipeline() {
681
- const shaderCode = `
682
- struct CameraUniforms {
683
- view: mat4x4<f32>,
684
- projection: mat4x4<f32>,
685
- viewPos: vec3<f32>,
686
- _padding: f32,
687
- };
688
-
689
- struct Uniforms {
690
- time: f32,
691
- _padding1: f32,
692
- resolution: vec2<f32>,
693
- modelPos: vec3<f32>,
694
- _padding2: f32,
695
- };
696
-
697
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
698
- @group(0) @binding(1) var<uniform> uniforms: Uniforms;
699
- @group(0) @binding(2) var waterHeightSampler: sampler;
700
- @group(0) @binding(3) var waterHeightTexture: texture_2d<f32>;
701
-
702
- struct VertexInput {
703
- @location(0) position: vec3<f32>,
704
- @location(1) uv: vec2<f32>,
705
- };
706
-
707
- struct VertexOutput {
708
- @builtin(position) Position: vec4<f32>,
709
- @location(0) worldPos: vec3<f32>,
710
- @location(1) uv: vec2<f32>,
711
- @location(2) viewPos: vec3<f32>,
712
- };
713
-
714
- const WATER_HEIGHT_WORLD_SIZE: f32 = 50.0;
715
- const WATER_HEIGHT_SCALE: f32 = 0.1; // Scale factor for height displacement
716
-
717
- @vertex
718
- fn vs_main(input: VertexInput) -> VertexOutput {
719
- // Sample height map to displace vertex
720
- let modelPos2D = vec2<f32>(uniforms.modelPos.x, uniforms.modelPos.z);
721
- let worldPos2D = vec2<f32>(input.position.x, input.position.z);
722
- let offset = worldPos2D - modelPos2D;
723
- let texCoord = (offset / WATER_HEIGHT_WORLD_SIZE) * 0.5 + 0.5;
724
-
725
- let height = textureSampleLevel(waterHeightTexture, waterHeightSampler, texCoord, 0.0).r;
726
-
727
- // Displace vertex position based on height map
728
- let displacedPos = vec3<f32>(
729
- input.position.x,
730
- input.position.y + height * WATER_HEIGHT_SCALE,
731
- input.position.z
732
- );
733
-
734
- // Transform to clip space
735
- let worldPos = displacedPos;
736
- let worldPos4 = vec4<f32>(worldPos, 1.0);
737
- let viewPos4 = camera.view * worldPos4;
738
- let clipPos = camera.projection * viewPos4;
739
-
740
- var output: VertexOutput;
741
- output.worldPos = worldPos;
742
- output.uv = input.uv;
743
- output.viewPos = viewPos4.xyz;
744
- output.Position = clipPos;
745
- return output;
746
- }
747
-
748
- const WATERCOLOR: vec3<f32> = vec3<f32>(${this.config.waterColor[0]}, ${this.config.waterColor[1]}, ${this.config.waterColor[2]});
749
- const MOONLIGHTCOLOR: vec3<f32> = vec3<f32>(${this.config.moonlightColor[0]}, ${this.config.moonlightColor[1]}, ${this.config.moonlightColor[2]});
750
- const SKYCOLOR: vec3<f32> = vec3<f32>(${this.config.skyColor[0]}, ${this.config.skyColor[1]}, ${this.config.skyColor[2]});
751
- const SKYBYMOONLIGHTCOLOR: vec3<f32> = vec3<f32>(${this.config.skyByMoonlightColor[0]}, ${this.config.skyByMoonlightColor[1]}, ${this.config.skyByMoonlightColor[2]});
752
- const CLOUDSHARPNESS: f32 = ${this.config.cloudSharpness};
753
- const WINDSPEED: vec2<f32> = vec2<f32>(${this.config.windSpeed[0]}, ${this.config.windSpeed[1]});
754
- const EXPOSURE: f32 = ${this.config.exposure};
755
- const EPSILON: f32 = ${this.config.epsilon};
756
-
757
- // Math matrices (column-major)
758
- const m: mat3x3<f32> = mat3x3<f32>(
759
- vec3<f32>(0.00, -0.90, -0.60),
760
- vec3<f32>(0.90, 0.36, -0.48),
761
- vec3<f32>(-0.60, -0.48, 0.34)
762
- );
763
-
764
- // Hash function
765
- fn hash(n: f32) -> f32 {
766
- return fract(sin(n) * 43758.5453);
767
- }
768
-
769
- fn hash3(p: vec3<f32>) -> f32 {
770
- var p3 = fract(p * 0.1031);
771
- p3 += dot(p3, p3.yzx + 33.33);
772
- return fract((p3.x + p3.y) * p3.z);
773
- }
774
-
775
- fn noise3(x: vec3<f32>) -> f32 {
776
- let p = floor(x);
777
- var f = fract(x);
778
- f = f * f * (3.0 - 2.0 * f);
779
-
780
- let n = p.x + p.y * 57.0 + 113.0 * p.z;
781
- return mix(
782
- mix(mix(hash3(p), hash3(p + vec3<f32>(1.0, 0.0, 0.0)), f.x),
783
- mix(hash3(p + vec3<f32>(0.0, 1.0, 0.0)), hash3(p + vec3<f32>(1.0, 1.0, 0.0)), f.x), f.y),
784
- mix(mix(hash3(p + vec3<f32>(0.0, 0.0, 1.0)), hash3(p + vec3<f32>(1.0, 0.0, 1.0)), f.x),
785
- mix(hash3(p + vec3<f32>(0.0, 1.0, 1.0)), hash3(p + vec3<f32>(1.0, 1.0, 1.0)), f.x), f.y), f.z
786
- );
787
- }
788
-
789
- fn noise2(x: vec2<f32>) -> f32 {
790
- let p = floor(x);
791
- var f = fract(x);
792
- f = f * f * (3.0 - 2.0 * f);
793
-
794
- let n = p.x + p.y * 57.0;
795
- return mix(
796
- mix(hash(f32(n)), hash(f32(n + 1.0)), f.x),
797
- mix(hash(f32(n + 57.0)), hash(f32(n + 58.0)), f.x),
798
- f.y
799
- );
800
- }
801
-
802
- fn fbm3(p: vec3<f32>) -> f32 {
803
- var f: f32 = 0.0;
804
- var p_var = p;
805
- f = 0.5000 * noise3(p_var);
806
- p_var = m * p_var * 2.02;
807
- f += 0.2500 * noise3(p_var);
808
- p_var = m * p_var * 2.33;
809
- f += 0.1250 * noise3(p_var);
810
- p_var = m * p_var * 2.01;
811
- f += 0.0625 * noise3(p_var);
812
- return f / 0.9175;
813
- }
814
-
815
- // Intersection functions for cloud density
816
- struct IntersectResult {
817
- hit: bool,
818
- dist: f32,
819
- }
820
-
821
- fn intersectPlane(ro: vec3<f32>, rd: vec3<f32>, height: f32) -> IntersectResult {
822
- var result: IntersectResult;
823
- result.hit = false;
824
- result.dist = 0.0;
825
-
826
- if (rd.y == 0.0) {
827
- return result;
828
- }
829
-
830
- let d = -(ro.y - height) / rd.y;
831
- let d_clamped = min(100000.0, d);
832
- if (d_clamped > 0.0) {
833
- result.hit = true;
834
- result.dist = d_clamped;
835
- }
836
- return result;
837
- }
838
-
839
- // Cloud density - simplified to always be very light
840
- fn cloudDensity(rd: vec3<f32>, time: f32) -> f32 {
841
- let planeResult = intersectPlane(vec3<f32>(0.0, 0.0, 0.0), rd, 500.0);
842
- let intersection = rd * planeResult.dist;
843
-
844
- // Very light, consistent cloud cover (no time variation)
845
- let cloudCover = 0.15;
846
- // Simplified cloud calculation - much lighter
847
- var cloud = 0.3 + 0.2 * fbm3(vec3<f32>((intersection.xz + WINDSPEED * time) * 0.001, time * 0.25));
848
-
849
- cloud += 0.01 * noise2(intersection.xz - WINDSPEED * time * 0.01);
850
-
851
- // Clamp to keep clouds very light
852
- cloud = clamp(cloud, 0.0, 0.25);
853
-
854
- // Simplified - always light clouds, no heavy blocking
855
- cloud = cloud * 0.3; // Scale down to keep very light
856
-
857
- return cloud;
858
- }
859
-
860
- struct SphereResult {
861
- hit: bool,
862
- dist: f32,
863
- normal: vec3<f32>,
864
- }
865
-
866
- fn intersectSphere(ro: vec3<f32>, rd: vec3<f32>, sph: vec4<f32>) -> SphereResult {
867
- var result: SphereResult;
868
- result.hit = false;
869
- result.dist = 0.0;
870
- result.normal = vec3<f32>(0.0);
871
-
872
- let ds = ro - sph.xyz;
873
- let bs = dot(rd, ds);
874
- let cs = dot(ds, ds) - sph.w * sph.w;
875
- let ts = bs * bs - cs;
876
-
877
- if (ts > 0.0) {
878
- let ts_val = -bs - sqrt(ts);
879
- if (ts_val > 0.0) {
880
- result.hit = true;
881
- result.dist = ts_val;
882
- result.normal = normalize(((ro + ts_val * rd) - sph.xyz) / sph.w);
883
- }
884
- }
885
- return result;
886
- }
887
-
888
- // Sky color function (for reflections - uses angular distance since reflections are already distorted)
889
- fn skyColorFunc(rd: vec3<f32>, time: f32) -> vec3<f32> {
890
- // Moon position from config
891
- let moonPos = vec3<f32>(${this.moonPosition[0]}.0, ${this.moonPosition[1]}.0, ${this.moonPosition[2]}.0);
892
- let moondir = normalize(moonPos);
893
-
894
- // Glow follows moon position - strongest when looking directly at moon
895
- let moonAngle = dot(moondir, rd);
896
- let moonglow = clamp(moonAngle, 0.0, 1.0);
897
- var col = SKYCOLOR * moondir.y;
898
- col += 0.6 * SKYBYMOONLIGHTCOLOR * moonglow;
899
- col += 0.8 * MOONLIGHTCOLOR * pow(moonglow, 35.0);
900
-
901
- // Moon rendering - use angular distance (OK for reflections which are already distorted)
902
- let moonAngularRadius = ${this.moonRadius};
903
- let angleToMoon = acos(clamp(dot(moondir, rd), -1.0, 1.0));
904
- let moonDist = angleToMoon / moonAngularRadius;
905
-
906
- // Sharp circular moon disk
907
- let moonDisk = 1.0 - smoothstep(0.95, 1.0, moonDist);
908
- let moonBrightness = 3.0;
909
- let moonColor = vec3<f32>(1.0, 0.98, 0.92) * moonBrightness;
910
-
911
- // Render moon disk
912
- col = mix(col, moonColor, moonDisk);
913
-
914
- // Add glow/bloom effect
915
- let glowDist = angleToMoon / (moonAngularRadius * 2.0);
916
- let glowFactor = pow(max(0.0, 1.0 - glowDist), 2.5) * 1.2;
917
- let glowMask = 1.0 - moonDisk;
918
- col += moonColor * glowFactor * glowMask;
919
-
920
- // Stars only when not looking at moon
921
- if (moonDist > 1.5) {
922
- let rds = rd;
923
- let v = 1.0 / (2.0 * (1.0 + rds.z));
924
- let xy = vec2<f32>(rds.y * v, rds.x * v);
925
- var s = noise2(rds.xz * 134.0);
926
- s += noise2(rds.xz * 370.0);
927
- s += noise2(rds.xz * 870.0);
928
- s = pow(s, 19.0) * 0.00000001 * max(rd.y, 0.0);
929
- if (s > 0.1) {
930
- let backStars = vec3<f32>((1.0 - sin(xy.x * 20.0 + time * 13.0 * rds.x + xy.y * 30.0)) * 0.5 * s, s, s);
931
- col += backStars;
932
- }
933
- }
934
-
935
- // Apply clouds, but don't dim the moon if it's visible
936
- let cloudDens = cloudDensity(rd, time);
937
- if (moonDist > 1.0) {
938
- col *= (1.0 - cloudDens);
939
- } else {
940
- // Moon is visible - don't dim it at all, keep it bright
941
- // Moon stays fully visible through clouds
942
- }
943
-
944
- return col;
945
- }
946
-
947
- fn calculateNormal(pos: vec2<f32>, time: f32) -> vec3<f32> {
948
- let modelPos2D = vec2<f32>(uniforms.modelPos.x, uniforms.modelPos.z);
949
- let offset = pos - modelPos2D;
950
- let texCoord = (offset / WATER_HEIGHT_WORLD_SIZE) * 0.5 + 0.5;
951
-
952
- let dx = vec2<f32>(EPSILON, 0.0);
953
- let dz = vec2<f32>(0.0, EPSILON);
954
-
955
- let heightL = textureSampleLevel(waterHeightTexture, waterHeightSampler, texCoord - dx * 0.5, 0.0).r;
956
- let heightR = textureSampleLevel(waterHeightTexture, waterHeightSampler, texCoord + dx * 0.5, 0.0).r;
957
- let heightD = textureSampleLevel(waterHeightTexture, waterHeightSampler, texCoord - dz * 0.5, 0.0).r;
958
- let heightU = textureSampleLevel(waterHeightTexture, waterHeightSampler, texCoord + dz * 0.5, 0.0).r;
959
-
960
- let normal = vec3<f32>(
961
- (heightL - heightR) * WATER_HEIGHT_SCALE / EPSILON,
962
- 1.0,
963
- (heightD - heightU) * WATER_HEIGHT_SCALE / EPSILON
964
- );
965
- return normalize(normal);
966
- }
967
-
968
- @fragment
969
- fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
970
- let time = uniforms.time + 23.0;
971
-
972
- // Calculate distance from camera
973
- let dist = length(input.viewPos);
974
-
975
- // Calculate normal from height map
976
- let normal = calculateNormal(input.worldPos.xz, time);
977
-
978
- // Calculate view direction (from water surface to camera)
979
- let viewDir = normalize(camera.viewPos - input.worldPos);
980
-
981
- // Calculate reflection direction
982
- let reflectDir = reflect(-viewDir, normal);
983
-
984
- // Sample sky color with clouds in reflection direction
985
- let skyReflection = skyColorFunc(reflectDir, time);
986
-
987
- // Also sample sky color directly above (for atmospheric blending)
988
- let skyColorAbove = skyColorFunc(vec3<f32>(0.0, 1.0, 0.0), time);
989
-
990
- // Moon position from config
991
- let moonPos = vec3<f32>(${this.moonPosition[0]}.0, ${this.moonPosition[1]}.0, ${this.moonPosition[2]}.0);
992
- let moondir = normalize(moonPos);
993
-
994
- // Calculate lighting
995
- let diff = clamp(dot(normal, moondir), 0.0, 1.0);
996
-
997
- // Base water color with lighting - blend more with sky color
998
- var col = mix(WATERCOLOR, SKYCOLOR * 1.2, 0.3) * MOONLIGHTCOLOR * diff * 1.5;
999
-
1000
- // Add reflection (fresnel effect - more reflection at glancing angles)
1001
- let fresnel = pow(1.0 - max(dot(viewDir, normal), 0.0), 2.0);
1002
- let reflectStrength = 0.9 * fresnel;
1003
- col = mix(col, skyReflection, reflectStrength);
1004
-
1005
- // Distance fog - blend with sky color at distance (updated for larger plane)
1006
- let fogStart = 60.0;
1007
- let fogEnd = 200.0;
1008
- let fogFactor = smoothstep(fogStart, fogEnd, dist);
1009
- col = mix(col, skyColorAbove, fogFactor * 0.8);
1010
-
1011
- // Edge fading - fade out at the edges of the plane (updated for larger plane)
1012
- let edgeDistance = length(input.worldPos.xz - vec2<f32>(uniforms.modelPos.x, uniforms.modelPos.z));
1013
- let edgeFadeStart = 100.0;
1014
- let edgeFadeEnd = 180.0;
1015
- // Smoother, more gradual edge fade
1016
- let edgeFade = 1.0 - smoothstep(edgeFadeStart, edgeFadeEnd, edgeDistance);
1017
-
1018
- // Atmospheric perspective - blend more with sky at distance and edges for better integration
1019
- // More gradual blending for smoother transition
1020
- let atmosphericBlend = fogFactor * 0.7 + (1.0 - edgeFade) * 0.8;
1021
- col = mix(col, skyColorAbove, atmosphericBlend);
1022
-
1023
- // Alpha based on distance and edge fade (more transparent to see model through water)
1024
- var alpha = 0.8;
1025
- alpha *= edgeFade;
1026
- // Fade out at very far distances (updated for larger plane)
1027
- alpha *= (1.0 - smoothstep(180.0, 300.0, dist) * 0.5);
1028
-
1029
- // Apply exposure
1030
- col = pow(col, vec3<f32>(EXPOSURE));
1031
- col = clamp(col, vec3<f32>(0.0), vec3<f32>(1.0));
1032
-
1033
- return vec4<f32>(col, alpha);
1034
- }
1035
- `;
1036
- const module = this.device.createShaderModule({ code: shaderCode });
1037
- this.waterPlanePipeline = this.device.createRenderPipeline({
1038
- layout: "auto",
1039
- vertex: {
1040
- module,
1041
- entryPoint: "vs_main",
1042
- buffers: [
1043
- {
1044
- arrayStride: 5 * 4, // 3 floats (position) + 2 floats (uv)
1045
- attributes: [
1046
- { shaderLocation: 0, offset: 0, format: "float32x3" }, // position
1047
- { shaderLocation: 1, offset: 12, format: "float32x2" }, // uv
1048
- ],
1049
- },
1050
- ],
1051
- },
1052
- fragment: {
1053
- module,
1054
- entryPoint: "fs_main",
1055
- targets: [
1056
- {
1057
- format: this.format,
1058
- blend: {
1059
- color: {
1060
- srcFactor: "src-alpha",
1061
- dstFactor: "one-minus-src-alpha",
1062
- },
1063
- alpha: {
1064
- srcFactor: "one",
1065
- dstFactor: "one-minus-src-alpha",
1066
- },
1067
- },
1068
- },
1069
- ],
1070
- },
1071
- primitive: {
1072
- topology: "triangle-list",
1073
- cullMode: "none", // Water plane is visible from both sides
1074
- },
1075
- multisample: {
1076
- count: this.sampleCount,
1077
- },
1078
- depthStencil: {
1079
- depthWriteEnabled: false, // Don't write depth so model can show through water
1080
- depthCompare: "less", // Test depth to avoid rendering behind background and model
1081
- format: "depth24plus-stencil8",
1082
- },
1083
- });
1084
- // Create bind group for water plane (same as background, uses same resources)
1085
- this.waterPlaneBindGroup = this.device.createBindGroup({
1086
- layout: this.waterPlanePipeline.getBindGroupLayout(0),
1087
- entries: [
1088
- { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1089
- { binding: 1, resource: { buffer: this.uniformBuffer } },
1090
- { binding: 2, resource: this.waterHeightSampler },
1091
- { binding: 3, resource: this.waterHeightTextureView },
1092
- ],
1093
- });
1094
- }
1095
- renderWaterPlane(pass, width, height, modelPos = [0, 0, 0]) {
1096
- const now = performance.now();
1097
- const time = (now - this.startTime) / 1000;
1098
- // Write uniforms: time, padding, resX, resY, modelPos.x, modelPos.y, modelPos.z, padding
1099
- const uniforms = new Float32Array([time, 0, width, height, modelPos[0], modelPos[1], modelPos[2], 0]);
1100
- this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
1101
- // Render water plane
1102
- pass.setPipeline(this.waterPlanePipeline);
1103
- pass.setBindGroup(0, this.waterPlaneBindGroup);
1104
- pass.setVertexBuffer(0, this.waterPlaneVertexBuffer);
1105
- pass.setIndexBuffer(this.waterPlaneIndexBuffer, "uint32");
1106
- pass.drawIndexed(this.waterPlaneSubdivisions * this.waterPlaneSubdivisions * 6, 1, 0, 0);
1107
- }
1108
- render(pass, width, height, modelPos = [0, 0, 0]) {
1109
- const now = performance.now();
1110
- const time = (now - this.startTime) / 1000;
1111
- // Write render uniforms: time, padding, resX, resY, modelPos.x, modelPos.y, modelPos.z, padding
1112
- const uniforms = new Float32Array([time, 0, width, height, modelPos[0], modelPos[1], modelPos[2], 0]);
1113
- this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
1114
- // Render pool scene
1115
- pass.setPipeline(this.pipeline);
1116
- pass.setBindGroup(0, this.bindGroup);
1117
- pass.draw(6, 1, 0, 0);
1118
- }
1119
- getSkyColor() {
1120
- return this.config.skyColor;
1121
- }
1122
- }