reze-engine 0.2.10 → 0.2.12

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.
package/src/pool.ts ADDED
@@ -0,0 +1,483 @@
1
+ import { Vec3 } from "./math"
2
+
3
+ export interface PoolOptions {
4
+ y?: number // Y position (default: 12)
5
+ size?: number // Plane size (default: 100)
6
+ segments?: number // Subdivision count (default: 50)
7
+ }
8
+
9
+ export class Pool {
10
+ private device!: GPUDevice
11
+ private vertexBuffer!: GPUBuffer
12
+ private indexBuffer!: GPUBuffer
13
+ private pipeline!: GPURenderPipeline
14
+ private bindGroup!: GPUBindGroup
15
+ private bindGroupLayout!: GPUBindGroupLayout
16
+ private uniformBuffer!: GPUBuffer
17
+ private cameraBindGroupLayout!: GPUBindGroupLayout
18
+ private cameraBindGroup!: GPUBindGroup
19
+ private cameraUniformBuffer!: GPUBuffer
20
+ private indexCount: number = 0
21
+ private y: number
22
+ private size: number
23
+ private segments: number
24
+ private seaColor: Vec3
25
+ private seaLight: Vec3
26
+ private startTime: number = performance.now()
27
+
28
+ constructor(
29
+ device: GPUDevice,
30
+ cameraBindGroupLayout: GPUBindGroupLayout,
31
+ cameraUniformBuffer: GPUBuffer,
32
+ options?: PoolOptions
33
+ ) {
34
+ this.device = device
35
+ this.cameraBindGroupLayout = cameraBindGroupLayout
36
+ this.cameraUniformBuffer = cameraUniformBuffer
37
+ this.y = options?.y ?? 15
38
+ this.size = options?.size ?? 100
39
+ this.segments = options?.segments ?? 50
40
+ // Hardcoded dark night pool colors (not used in shader, but kept for uniform buffer)
41
+ this.seaColor = new Vec3(0.02, 0.05, 0.12) // Dark night pool base
42
+ this.seaLight = new Vec3(0.1, 0.3, 0.5) // Light cyan for lit areas
43
+ }
44
+
45
+ public async init() {
46
+ this.createGeometry()
47
+ this.createShader()
48
+ this.createUniforms()
49
+ }
50
+
51
+ private createGeometry() {
52
+ const segments = this.segments
53
+ const size = this.size
54
+ const halfSize = size / 2
55
+ const step = size / segments
56
+
57
+ // Generate vertices
58
+ const vertices: number[] = []
59
+ for (let i = 0; i <= segments; i++) {
60
+ for (let j = 0; j <= segments; j++) {
61
+ const x = -halfSize + j * step
62
+ const z = -halfSize + i * step
63
+ const y = this.y
64
+ const u = j / segments
65
+ const v = i / segments
66
+
67
+ // Position (x, y, z) + UV (u, v)
68
+ vertices.push(x, y, z, u, v)
69
+ }
70
+ }
71
+
72
+ // Generate indices
73
+ const indices: number[] = []
74
+ for (let i = 0; i < segments; i++) {
75
+ for (let j = 0; j < segments; j++) {
76
+ const topLeft = i * (segments + 1) + j
77
+ const topRight = topLeft + 1
78
+ const bottomLeft = (i + 1) * (segments + 1) + j
79
+ const bottomRight = bottomLeft + 1
80
+
81
+ // Two triangles per quad
82
+ indices.push(topLeft, bottomLeft, topRight)
83
+ indices.push(topRight, bottomLeft, bottomRight)
84
+ }
85
+ }
86
+
87
+ this.indexCount = indices.length
88
+
89
+ // Create buffers
90
+ this.vertexBuffer = this.device.createBuffer({
91
+ label: "pool vertices",
92
+ size: vertices.length * 4,
93
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
94
+ })
95
+ this.device.queue.writeBuffer(this.vertexBuffer, 0, new Float32Array(vertices))
96
+
97
+ const indexBufferSize = indices.length * 4
98
+ this.indexBuffer = this.device.createBuffer({
99
+ label: "pool indices",
100
+ size: indexBufferSize,
101
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
102
+ })
103
+ this.device.queue.writeBuffer(this.indexBuffer, 0, new Uint32Array(indices))
104
+
105
+ // Verify: segments=50 should give 50*50*6 = 15000 indices = 60000 bytes
106
+ if (this.indexCount !== indices.length) {
107
+ console.warn(`Pool index count mismatch: expected ${indices.length}, got ${this.indexCount}`)
108
+ }
109
+ }
110
+
111
+ private createShader() {
112
+ const shaderModule = this.device.createShaderModule({
113
+ label: "pool shader",
114
+ code: /* wgsl */ `
115
+ struct CameraUniforms {
116
+ view: mat4x4f,
117
+ projection: mat4x4f,
118
+ viewPos: vec3f,
119
+ _padding: f32,
120
+ };
121
+
122
+ struct PoolUniforms {
123
+ time: f32,
124
+ poolY: f32,
125
+ seaColor: vec3f,
126
+ seaLight: vec3f,
127
+ };
128
+
129
+ struct VertexOutput {
130
+ @builtin(position) position: vec4f,
131
+ @location(0) worldPos: vec3f,
132
+ @location(1) uv: vec2f,
133
+ };
134
+
135
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
136
+ @group(1) @binding(0) var<uniform> pool: PoolUniforms;
137
+
138
+ // Procedural noise function (simplified)
139
+ fn hash(p: vec2f) -> f32 {
140
+ var p3 = fract(vec3f(p.xyx) * vec3f(443.8975, 397.2973, 491.1871));
141
+ p3 += dot(p3, p3.yzx + 19.19);
142
+ return fract((p3.x + p3.y) * p3.z);
143
+ }
144
+
145
+ fn noise(p: vec2f) -> f32 {
146
+ let i = floor(p);
147
+ var f = fract(p);
148
+ f = f * f * (3.0 - 2.0 * f);
149
+
150
+ let a = hash(i);
151
+ let b = hash(i + vec2f(1.0, 0.0));
152
+ let c = hash(i + vec2f(0.0, 1.0));
153
+ let d = hash(i + vec2f(1.0, 1.0));
154
+
155
+ return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
156
+ }
157
+
158
+ // Layered noise for water height (adapted from Shadertoy - matches reference exactly)
159
+ fn waterHeight(uv: vec2f, time: f32) -> f32 {
160
+ var e = 0.0;
161
+ // Match Shadertoy: time*mod(j,.789)*.1 - time*.05
162
+ for (var j = 1.0; j < 6.0; j += 1.0) {
163
+ let timeOffset = time * (j % 0.789) * 0.1 - time * 0.05;
164
+ let scaledUV = uv * (j * 1.789) + j * 159.45 + timeOffset;
165
+ e += noise(scaledUV) / j;
166
+ }
167
+ return e / 6.0;
168
+ }
169
+
170
+ // Calculate water normals from height gradients (matches Shadertoy reference)
171
+ fn waterNormals(uv: vec2f, time: f32) -> vec3f {
172
+ // Match Shadertoy: uv.x *= .25 (scale X differently for more wave detail)
173
+ let scaledUV = vec2f(uv.x * 0.25, uv.y);
174
+ let eps = 0.008; // Match Shadertoy epsilon
175
+ let h = waterHeight(scaledUV, time);
176
+ let hx = waterHeight(scaledUV + vec2f(eps, 0.0), time);
177
+ let hz = waterHeight(scaledUV + vec2f(0.0, eps), time);
178
+
179
+ // Match Shadertoy normal calculation exactly
180
+ let n = vec3f(h - hx, 1.0, h - hz);
181
+ return normalize(n);
182
+ }
183
+
184
+ @vertex fn vs(
185
+ @location(0) position: vec3f,
186
+ @location(1) uv: vec2f
187
+ ) -> VertexOutput {
188
+ var output: VertexOutput;
189
+
190
+ // Displace Y based on water height - much higher waves
191
+ let time = pool.time;
192
+ // More wave detail - smaller scale for more waves
193
+ // Wave direction: back-left to front-right (both U and V increase)
194
+ let waveUV = uv * 12.0 + vec2f(time * 0.3, time * 0.2); // Front-right direction (both positive)
195
+ let height = waterHeight(waveUV, time) * 2; // Much higher wave amplitude
196
+ let displacedY = position.y + height;
197
+
198
+ let worldPos = vec3f(position.x, displacedY, position.z);
199
+ output.worldPos = worldPos;
200
+ output.uv = uv;
201
+ output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
202
+
203
+ return output;
204
+ }
205
+
206
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
207
+ let time = pool.time;
208
+ // More wave detail - smaller scale for more waves (matches Shadertoy approach)
209
+ // Wave direction: back-left to front-right (both U and V increase)
210
+ let uv = input.uv * 12.0 + vec2f(time * 0.3, time * 0.2); // Front-right direction (both positive)
211
+
212
+ // Calculate water normals from height gradients (this creates the wave effect)
213
+ let n = waterNormals(uv, time);
214
+
215
+ // View direction
216
+ let viewDir = normalize(camera.viewPos - input.worldPos);
217
+
218
+ // Fresnel effect for reflection (stronger at glancing angles)
219
+ var fresnel = 1.0 - max(dot(n, viewDir), 0.0);
220
+ fresnel = fresnel * fresnel;
221
+
222
+ // Dark night pool - very dark base
223
+ let darkPoolColor = vec3f(0.01, 0.02, 0.05); // Very dark blue-black
224
+
225
+ // Center spotlight effect - reflection-like bright center
226
+ let centerUV = input.uv - vec2f(0.5, 0.5); // Center at (0.5, 0.5)
227
+ let distFromCenter = length(centerUV);
228
+ // Smaller spotlight area with very smooth, subtle gradient
229
+ let spotlightFalloff = 1.0 - smoothstep(0.0, 0.12, distFromCenter); // Smaller radius (0.12)
230
+
231
+ // Reflection-like bright center - brighter, balanced blue
232
+ let spotlightColor = vec3f(0.2, 0.4, 0.6); // Balanced blue
233
+ let spotlightCenter = vec3f(0.5, 0.65, 0.85); // More white center
234
+
235
+ // Very smooth, subtle gradient mix - multiple smoothstep layers for smoother transition
236
+ let falloff1 = smoothstep(0.0, 0.12, distFromCenter); // Outer edge
237
+ let falloff2 = smoothstep(0.0, 0.08, distFromCenter); // Inner edge
238
+ var color = mix(darkPoolColor, spotlightColor, (1.0 - falloff1) * 0.9); // Brighter outer gradient
239
+ color = mix(color, spotlightCenter, (1.0 - falloff2) * (1.0 - falloff2) * 1.0); // Very bright inner reflection
240
+
241
+ // Add reflection-like effect based on view angle and normals
242
+ let reflectionFactor = max(dot(n, vec3f(0.0, 1.0, 0.0)), 0.0); // More reflection when looking down
243
+ let reflectionBrightness = spotlightFalloff * reflectionFactor * 0.5;
244
+ color += spotlightCenter * reflectionBrightness; // Add reflection-like brightness
245
+
246
+ // Wave-based color variation (matches Shadertoy transparency approach)
247
+ // Match Shadertoy: transparency = dot(n, vec3(0.,.2,1.5)) * 12. + 1.5
248
+ var transparency = dot(n, vec3f(0.0, 0.2, 1.5));
249
+ transparency = (transparency * 12.0 + 1.5);
250
+
251
+ // Match Shadertoy color mixing: mix with seaColor and seaLight (brighter, balanced blue)
252
+ let seaColor = vec3f(0.08, 0.18, 0.3); // Balanced blue
253
+ let seaLight = vec3f(0.12, 0.25, 0.45); // Balanced blue
254
+ // Only apply this mixing subtly to avoid green tint
255
+ color = mix(color, seaColor, clamp(transparency, 0.0, 1.0) * 0.3);
256
+ color = mix(color, seaLight, max(0.0, transparency - 1.5) * 0.2);
257
+
258
+ // Enhanced wave-based color variation for more visible waves
259
+ let waveHeight = waterHeight(uv, time);
260
+ let waveContrast = (waveHeight - 0.5) * 0.3; // Amplify wave contrast
261
+ color += vec3f(0.05, 0.08, 0.15) * waveContrast * spotlightFalloff; // Balanced blue wave highlights
262
+
263
+ // Enhanced underwater glow - white/neutral glow with subtle blue tint, much brighter
264
+ let glowIntensity = spotlightFalloff * 0.6 + fresnel * 0.3 + waveHeight * 0.2; // Stronger glow
265
+ let glowColor = vec3f(0.35, 0.35, 0.45); // Brighter glow with subtle blue tint
266
+ color += glowColor * glowIntensity;
267
+
268
+ // Additional subtle white glow around the center, brighter and more white
269
+ let centerGlow = spotlightFalloff * spotlightFalloff * 0.4; // Soft glow falloff
270
+ let whiteGlow = vec3f(0.55, 0.55, 0.6); // Brighter white glow
271
+ color += whiteGlow * centerGlow * 0.7; // Brighter white center glow
272
+
273
+ // Reflection of dark night sky
274
+ let nightSkyColor = vec3f(0.02, 0.04, 0.08); // Very dark night sky
275
+ let reflection = mix(darkPoolColor, nightSkyColor, fresnel * 0.2);
276
+ color = mix(color, reflection, fresnel * 0.3 * (1.0 - spotlightFalloff)); // Less reflection in spotlight
277
+
278
+ // Specular highlights from underwater lights (bokeh-like bright spots)
279
+ let lightDir1 = normalize(vec3f(0.3, -0.4, 0.6));
280
+ let lightDir2 = normalize(vec3f(-0.3, -0.3, 0.7));
281
+ let reflDir1 = reflect(-lightDir1, n);
282
+ let reflDir2 = reflect(-lightDir2, n);
283
+ var specular1 = max(dot(viewDir, reflDir1), 0.0);
284
+ var specular2 = max(dot(viewDir, reflDir2), 0.0);
285
+ specular1 = pow(specular1, 150.0); // Tight, bright highlights
286
+ specular2 = pow(specular2, 180.0);
287
+ // Subtle white/blue highlights (bokeh effect) - darker, less blue
288
+ color += vec3f(0.8, 0.8, 0.9) * specular1 * 1.2 * spotlightFalloff; // Darker white
289
+ color += vec3f(0.4, 0.5, 0.7) * specular2 * 0.9 * spotlightFalloff; // Darker, less blue
290
+
291
+ return vec4f(color, 0.8); // Half transparent water
292
+ }
293
+ `,
294
+ })
295
+
296
+ // Create bind group layout for pool uniforms
297
+ this.bindGroupLayout = this.device.createBindGroupLayout({
298
+ label: "pool bind group layout",
299
+ entries: [
300
+ {
301
+ binding: 0,
302
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
303
+ buffer: {
304
+ type: "uniform",
305
+ },
306
+ },
307
+ ],
308
+ })
309
+
310
+ // Create render pipeline
311
+ this.pipeline = this.device.createRenderPipeline({
312
+ label: "pool pipeline",
313
+ layout: this.device.createPipelineLayout({
314
+ bindGroupLayouts: [this.cameraBindGroupLayout, this.bindGroupLayout],
315
+ }),
316
+ vertex: {
317
+ module: shaderModule,
318
+ entryPoint: "vs",
319
+ buffers: [
320
+ {
321
+ arrayStride: 5 * 4, // 3 floats (position) + 2 floats (uv)
322
+ attributes: [
323
+ {
324
+ shaderLocation: 0,
325
+ offset: 0,
326
+ format: "float32x3",
327
+ },
328
+ {
329
+ shaderLocation: 1,
330
+ offset: 3 * 4,
331
+ format: "float32x2",
332
+ },
333
+ ],
334
+ },
335
+ ],
336
+ },
337
+ fragment: {
338
+ module: shaderModule,
339
+ entryPoint: "fs",
340
+ targets: [
341
+ {
342
+ format: "bgra8unorm",
343
+ blend: {
344
+ color: {
345
+ srcFactor: "src-alpha",
346
+ dstFactor: "one-minus-src-alpha",
347
+ },
348
+ alpha: {
349
+ srcFactor: "one",
350
+ dstFactor: "one-minus-src-alpha",
351
+ },
352
+ },
353
+ },
354
+ ],
355
+ },
356
+ primitive: {
357
+ topology: "triangle-list",
358
+ cullMode: "none",
359
+ },
360
+ depthStencil: {
361
+ depthWriteEnabled: true,
362
+ depthCompare: "less-equal",
363
+ format: "depth24plus-stencil8",
364
+ },
365
+ multisample: {
366
+ count: 4,
367
+ },
368
+ })
369
+ }
370
+
371
+ private createUniforms() {
372
+ // Create uniform buffer
373
+ // WGSL uniform buffers require 16-byte alignment:
374
+ // time: f32 (4) + poolY (4) + padding (8) = 16 bytes
375
+ // seaColor: vec3f (12) + padding (4) = 16 bytes
376
+ // seaLight: vec3f (12) + padding (4) = 16 bytes
377
+ // Total: 48 bytes
378
+ this.uniformBuffer = this.device.createBuffer({
379
+ label: "pool uniforms",
380
+ size: 48,
381
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
382
+ })
383
+
384
+ // Create bind group
385
+ this.bindGroup = this.device.createBindGroup({
386
+ label: "pool bind group",
387
+ layout: this.bindGroupLayout,
388
+ entries: [
389
+ {
390
+ binding: 0,
391
+ resource: {
392
+ buffer: this.uniformBuffer,
393
+ },
394
+ },
395
+ ],
396
+ })
397
+
398
+ // Create camera bind group
399
+ this.cameraBindGroup = this.device.createBindGroup({
400
+ label: "pool camera bind group",
401
+ layout: this.cameraBindGroupLayout,
402
+ entries: [
403
+ {
404
+ binding: 0,
405
+ resource: {
406
+ buffer: this.cameraUniformBuffer,
407
+ },
408
+ },
409
+ ],
410
+ })
411
+ }
412
+
413
+ public updateUniforms() {
414
+ const time = (performance.now() - this.startTime) / 1000.0
415
+ // WGSL uniform buffer layout (16-byte aligned):
416
+ // offset 0: time (f32), poolY (f32), padding (8 bytes)
417
+ // offset 16: seaColor (vec3f), padding (4 bytes)
418
+ // offset 32: seaLight (vec3f), padding (4 bytes)
419
+ const data = new Float32Array(12)
420
+ data[0] = time
421
+ data[1] = this.y
422
+ // data[2-3] = padding (unused)
423
+ data[4] = this.seaColor.x
424
+ data[5] = this.seaColor.y
425
+ data[6] = this.seaColor.z
426
+ // data[7] = padding (unused)
427
+ data[8] = this.seaLight.x
428
+ data[9] = this.seaLight.y
429
+ data[10] = this.seaLight.z
430
+ // data[11] = padding (unused)
431
+
432
+ this.device.queue.writeBuffer(this.uniformBuffer, 0, data)
433
+ }
434
+
435
+ public render(
436
+ pass: GPURenderPassEncoder,
437
+ restoreBuffers?: {
438
+ vertexBuffer: GPUBuffer
439
+ jointsBuffer: GPUBuffer
440
+ weightsBuffer: GPUBuffer
441
+ indexBuffer: GPUBuffer
442
+ }
443
+ ) {
444
+ this.updateUniforms()
445
+
446
+ // Set pool's pipeline and bind groups FIRST
447
+ pass.setPipeline(this.pipeline)
448
+ pass.setBindGroup(0, this.cameraBindGroup)
449
+ pass.setBindGroup(1, this.bindGroup)
450
+
451
+ // IMPORTANT: Set pool's own buffers AFTER setting pipeline
452
+ // Pool only needs vertex buffer 0 (position + UV), but we must keep buffers 1 and 2 set
453
+ // for subsequent model rendering (eyes, hair, etc.)
454
+ pass.setVertexBuffer(0, this.vertexBuffer)
455
+ // Explicitly keep model's buffers 1 and 2 set - pool pipeline doesn't use them but they must stay
456
+ if (restoreBuffers) {
457
+ pass.setVertexBuffer(1, restoreBuffers.jointsBuffer)
458
+ pass.setVertexBuffer(2, restoreBuffers.weightsBuffer)
459
+ }
460
+
461
+ // Set pool's index buffer - this MUST be set to override model's index buffer
462
+ pass.setIndexBuffer(this.indexBuffer, "uint32")
463
+
464
+ // Draw all pool indices starting from 0
465
+ // Parameters: indexCount, instanceCount, firstIndex, baseVertex, firstInstance
466
+ // We always draw from index 0 with all indices
467
+ pass.drawIndexed(this.indexCount, 1, 0, 0, 0)
468
+
469
+ // Restore model's buffers for subsequent rendering (eyes, hair, etc.)
470
+ // This ensures vertex buffer 0 and index buffer are restored to model's buffers
471
+ if (restoreBuffers) {
472
+ pass.setVertexBuffer(0, restoreBuffers.vertexBuffer)
473
+ pass.setVertexBuffer(1, restoreBuffers.jointsBuffer)
474
+ pass.setVertexBuffer(2, restoreBuffers.weightsBuffer)
475
+ pass.setIndexBuffer(restoreBuffers.indexBuffer, "uint32")
476
+ }
477
+ }
478
+
479
+ public dispose() {
480
+ // Buffers will be cleaned up automatically when device is lost
481
+ // But we could explicitly destroy them if needed
482
+ }
483
+ }