reze-engine 0.2.12 → 0.2.14
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/dist/engine.d.ts +8 -6
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +101 -56
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/package.json +1 -1
- package/src/engine.ts +124 -61
- package/src/index.ts +0 -1
- package/src/pool.ts +0 -483
package/dist/engine.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Quat, Vec3 } from "./math";
|
|
2
|
-
import { PoolOptions } from "./pool";
|
|
3
2
|
export type EngineOptions = {
|
|
4
3
|
ambient?: number;
|
|
5
4
|
bloomIntensity?: number;
|
|
@@ -76,7 +75,6 @@ export declare class Engine {
|
|
|
76
75
|
private currentModel;
|
|
77
76
|
private modelDir;
|
|
78
77
|
private physics;
|
|
79
|
-
private pool;
|
|
80
78
|
private materialSampler;
|
|
81
79
|
private textureCache;
|
|
82
80
|
private opaqueDraws;
|
|
@@ -102,6 +100,8 @@ export declare class Engine {
|
|
|
102
100
|
private gpuMemoryMB;
|
|
103
101
|
private hasAnimation;
|
|
104
102
|
private playingAnimation;
|
|
103
|
+
private breathingTimeout;
|
|
104
|
+
private breathingBaseRotations;
|
|
105
105
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions);
|
|
106
106
|
init(): Promise<void>;
|
|
107
107
|
private createPipelines;
|
|
@@ -112,20 +112,22 @@ export declare class Engine {
|
|
|
112
112
|
private setupResize;
|
|
113
113
|
private handleResize;
|
|
114
114
|
private setupCamera;
|
|
115
|
-
private createCameraBindGroupLayout;
|
|
116
|
-
private createCameraBindGroup;
|
|
117
115
|
private setupLighting;
|
|
118
116
|
private addLight;
|
|
119
117
|
private setAmbient;
|
|
120
118
|
loadAnimation(url: string): Promise<void>;
|
|
121
|
-
playAnimation(
|
|
119
|
+
playAnimation(options?: {
|
|
120
|
+
breathBones?: string[] | Record<string, number>;
|
|
121
|
+
breathDuration?: number;
|
|
122
|
+
}): void;
|
|
122
123
|
stopAnimation(): void;
|
|
124
|
+
private stopBreathing;
|
|
125
|
+
private startBreathing;
|
|
123
126
|
getStats(): EngineStats;
|
|
124
127
|
runRenderLoop(callback?: () => void): void;
|
|
125
128
|
stopRenderLoop(): void;
|
|
126
129
|
dispose(): void;
|
|
127
130
|
loadModel(path: string): Promise<void>;
|
|
128
|
-
addPool(options?: PoolOptions): Promise<void>;
|
|
129
131
|
rotateBones(bones: string[], rotations: Quat[], durationMs?: number): void;
|
|
130
132
|
private setupModelBuffers;
|
|
131
133
|
private setupMaterials;
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Camera } from "./camera";
|
|
2
2
|
import { Quat, Vec3 } from "./math";
|
|
3
|
-
import { Pool } from "./pool";
|
|
4
3
|
import { PmxLoader } from "./pmx-loader";
|
|
5
4
|
import { Physics } from "./physics";
|
|
6
5
|
import { VMDLoader } from "./vmd-loader";
|
|
@@ -27,7 +26,6 @@ export class Engine {
|
|
|
27
26
|
this.currentModel = null;
|
|
28
27
|
this.modelDir = "";
|
|
29
28
|
this.physics = null;
|
|
30
|
-
this.pool = null;
|
|
31
29
|
this.textureCache = new Map();
|
|
32
30
|
// Draw lists
|
|
33
31
|
this.opaqueDraws = [];
|
|
@@ -57,6 +55,8 @@ export class Engine {
|
|
|
57
55
|
this.gpuMemoryMB = 0;
|
|
58
56
|
this.hasAnimation = false; // Set to true when loadAnimation is called
|
|
59
57
|
this.playingAnimation = false; // Set to true when playAnimation is called
|
|
58
|
+
this.breathingTimeout = null;
|
|
59
|
+
this.breathingBaseRotations = new Map();
|
|
60
60
|
this.canvas = canvas;
|
|
61
61
|
if (options) {
|
|
62
62
|
this.ambient = options.ambient ?? 1.0;
|
|
@@ -1231,36 +1231,6 @@ export class Engine {
|
|
|
1231
1231
|
this.camera.aspect = this.canvas.width / this.canvas.height;
|
|
1232
1232
|
this.camera.attachControl(this.canvas);
|
|
1233
1233
|
}
|
|
1234
|
-
// Create camera bind group layout for pool (camera-only)
|
|
1235
|
-
createCameraBindGroupLayout() {
|
|
1236
|
-
return this.device.createBindGroupLayout({
|
|
1237
|
-
label: "camera bind group layout",
|
|
1238
|
-
entries: [
|
|
1239
|
-
{
|
|
1240
|
-
binding: 0,
|
|
1241
|
-
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
1242
|
-
buffer: {
|
|
1243
|
-
type: "uniform",
|
|
1244
|
-
},
|
|
1245
|
-
},
|
|
1246
|
-
],
|
|
1247
|
-
});
|
|
1248
|
-
}
|
|
1249
|
-
// Create camera bind group for pool
|
|
1250
|
-
createCameraBindGroup(layout) {
|
|
1251
|
-
return this.device.createBindGroup({
|
|
1252
|
-
label: "camera bind group for pool",
|
|
1253
|
-
layout: layout,
|
|
1254
|
-
entries: [
|
|
1255
|
-
{
|
|
1256
|
-
binding: 0,
|
|
1257
|
-
resource: {
|
|
1258
|
-
buffer: this.cameraUniformBuffer,
|
|
1259
|
-
},
|
|
1260
|
-
},
|
|
1261
|
-
],
|
|
1262
|
-
});
|
|
1263
|
-
}
|
|
1264
1234
|
// Step 5: Create lighting buffers
|
|
1265
1235
|
setupLighting() {
|
|
1266
1236
|
this.lightUniformBuffer = this.device.createBuffer({
|
|
@@ -1300,11 +1270,26 @@ export class Engine {
|
|
|
1300
1270
|
this.animationFrames = frames;
|
|
1301
1271
|
this.hasAnimation = true;
|
|
1302
1272
|
}
|
|
1303
|
-
playAnimation() {
|
|
1273
|
+
playAnimation(options) {
|
|
1304
1274
|
if (this.animationFrames.length === 0)
|
|
1305
1275
|
return;
|
|
1306
1276
|
this.stopAnimation();
|
|
1277
|
+
this.stopBreathing();
|
|
1307
1278
|
this.playingAnimation = true;
|
|
1279
|
+
// Enable breathing if breathBones is provided
|
|
1280
|
+
const enableBreath = options?.breathBones !== undefined && options.breathBones !== null;
|
|
1281
|
+
let breathBones = [];
|
|
1282
|
+
let breathRotationRanges = undefined;
|
|
1283
|
+
if (enableBreath && options.breathBones) {
|
|
1284
|
+
if (Array.isArray(options.breathBones)) {
|
|
1285
|
+
breathBones = options.breathBones;
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
breathBones = Object.keys(options.breathBones);
|
|
1289
|
+
breathRotationRanges = options.breathBones;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
const breathDuration = options?.breathDuration ?? 4000;
|
|
1308
1293
|
const allBoneKeyFrames = [];
|
|
1309
1294
|
for (const keyFrame of this.animationFrames) {
|
|
1310
1295
|
for (const boneFrame of keyFrame.boneFrames) {
|
|
@@ -1392,6 +1377,40 @@ export class Engine {
|
|
|
1392
1377
|
}
|
|
1393
1378
|
}
|
|
1394
1379
|
}
|
|
1380
|
+
// Setup breathing animation if enabled
|
|
1381
|
+
if (enableBreath && this.currentModel) {
|
|
1382
|
+
// Find the last frame time
|
|
1383
|
+
let maxTime = 0;
|
|
1384
|
+
for (const keyFrame of this.animationFrames) {
|
|
1385
|
+
if (keyFrame.time > maxTime) {
|
|
1386
|
+
maxTime = keyFrame.time;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
// Get last frame rotations directly from animation data for breathing bones
|
|
1390
|
+
const lastFrameRotations = new Map();
|
|
1391
|
+
for (const bone of breathBones) {
|
|
1392
|
+
const keyFrames = boneKeyFramesByBone.get(bone);
|
|
1393
|
+
if (keyFrames && keyFrames.length > 0) {
|
|
1394
|
+
// Find the rotation at the last frame time (closest keyframe <= maxTime)
|
|
1395
|
+
let lastRotation = null;
|
|
1396
|
+
for (let i = keyFrames.length - 1; i >= 0; i--) {
|
|
1397
|
+
if (keyFrames[i].time <= maxTime) {
|
|
1398
|
+
lastRotation = keyFrames[i].rotation;
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
if (lastRotation) {
|
|
1403
|
+
lastFrameRotations.set(bone, lastRotation);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
// Start breathing after animation completes
|
|
1408
|
+
// Use the last frame rotations directly from animation data (no need to capture from model)
|
|
1409
|
+
const animationEndTime = maxTime * 1000 + 200; // Small buffer for final tweens to complete
|
|
1410
|
+
this.breathingTimeout = window.setTimeout(() => {
|
|
1411
|
+
this.startBreathing(breathBones, lastFrameRotations, breathRotationRanges, breathDuration);
|
|
1412
|
+
}, animationEndTime);
|
|
1413
|
+
}
|
|
1395
1414
|
}
|
|
1396
1415
|
stopAnimation() {
|
|
1397
1416
|
for (const timeoutId of this.animationTimeouts) {
|
|
@@ -1400,6 +1419,54 @@ export class Engine {
|
|
|
1400
1419
|
this.animationTimeouts = [];
|
|
1401
1420
|
this.playingAnimation = false;
|
|
1402
1421
|
}
|
|
1422
|
+
stopBreathing() {
|
|
1423
|
+
if (this.breathingTimeout !== null) {
|
|
1424
|
+
clearTimeout(this.breathingTimeout);
|
|
1425
|
+
this.breathingTimeout = null;
|
|
1426
|
+
}
|
|
1427
|
+
this.breathingBaseRotations.clear();
|
|
1428
|
+
}
|
|
1429
|
+
startBreathing(bones, baseRotations, rotationRanges, durationMs = 4000) {
|
|
1430
|
+
if (!this.currentModel)
|
|
1431
|
+
return;
|
|
1432
|
+
// Store base rotations directly from last frame of animation data
|
|
1433
|
+
// These are the exact rotations from the animation - use them as-is
|
|
1434
|
+
for (const bone of bones) {
|
|
1435
|
+
const baseRot = baseRotations.get(bone);
|
|
1436
|
+
if (baseRot) {
|
|
1437
|
+
this.breathingBaseRotations.set(bone, baseRot);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const halfCycleMs = durationMs / 2;
|
|
1441
|
+
const defaultRotation = 0.02; // Default rotation range if not specified per bone
|
|
1442
|
+
// Start breathing cycle - oscillate around exact base rotation (final pose)
|
|
1443
|
+
// Each bone can have its own rotation range, or use default
|
|
1444
|
+
const animate = (isInhale) => {
|
|
1445
|
+
if (!this.currentModel)
|
|
1446
|
+
return;
|
|
1447
|
+
const breathingBoneNames = [];
|
|
1448
|
+
const breathingQuats = [];
|
|
1449
|
+
for (const bone of bones) {
|
|
1450
|
+
const baseRot = this.breathingBaseRotations.get(bone);
|
|
1451
|
+
if (!baseRot)
|
|
1452
|
+
continue;
|
|
1453
|
+
// Get rotation range for this bone (per-bone or default)
|
|
1454
|
+
const rotation = rotationRanges?.[bone] ?? defaultRotation;
|
|
1455
|
+
// Oscillate around base rotation with the bone's rotation range
|
|
1456
|
+
// isInhale: base * rotation, exhale: base * (-rotation)
|
|
1457
|
+
const oscillationRot = Quat.fromEuler(isInhale ? rotation : -rotation, 0, 0);
|
|
1458
|
+
const finalRot = baseRot.multiply(oscillationRot);
|
|
1459
|
+
breathingBoneNames.push(bone);
|
|
1460
|
+
breathingQuats.push(finalRot);
|
|
1461
|
+
}
|
|
1462
|
+
if (breathingBoneNames.length > 0) {
|
|
1463
|
+
this.rotateBones(breathingBoneNames, breathingQuats, halfCycleMs);
|
|
1464
|
+
}
|
|
1465
|
+
this.breathingTimeout = window.setTimeout(() => animate(!isInhale), halfCycleMs);
|
|
1466
|
+
};
|
|
1467
|
+
// Start breathing from exhale position (closer to base) to minimize initial movement
|
|
1468
|
+
animate(false);
|
|
1469
|
+
}
|
|
1403
1470
|
getStats() {
|
|
1404
1471
|
return { ...this.stats };
|
|
1405
1472
|
}
|
|
@@ -1424,6 +1491,7 @@ export class Engine {
|
|
|
1424
1491
|
dispose() {
|
|
1425
1492
|
this.stopRenderLoop();
|
|
1426
1493
|
this.stopAnimation();
|
|
1494
|
+
this.stopBreathing();
|
|
1427
1495
|
if (this.camera)
|
|
1428
1496
|
this.camera.detachControl();
|
|
1429
1497
|
if (this.resizeObserver) {
|
|
@@ -1449,14 +1517,6 @@ export class Engine {
|
|
|
1449
1517
|
this.physics = new Physics(model.getRigidbodies(), model.getJoints());
|
|
1450
1518
|
await this.setupModelBuffers(model);
|
|
1451
1519
|
}
|
|
1452
|
-
async addPool(options) {
|
|
1453
|
-
if (!this.device) {
|
|
1454
|
-
throw new Error("Engine must be initialized before adding pool");
|
|
1455
|
-
}
|
|
1456
|
-
const cameraLayout = this.createCameraBindGroupLayout();
|
|
1457
|
-
this.pool = new Pool(this.device, cameraLayout, this.cameraUniformBuffer, options);
|
|
1458
|
-
await this.pool.init();
|
|
1459
|
-
}
|
|
1460
1520
|
rotateBones(bones, rotations, durationMs) {
|
|
1461
1521
|
this.currentModel?.rotateBones(bones, rotations, durationMs);
|
|
1462
1522
|
}
|
|
@@ -1820,11 +1880,6 @@ export class Engine {
|
|
|
1820
1880
|
}
|
|
1821
1881
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
1822
1882
|
this.drawCallCount = 0;
|
|
1823
|
-
// Render pool first if no model
|
|
1824
|
-
if (this.pool && !this.currentModel) {
|
|
1825
|
-
this.pool.render(pass);
|
|
1826
|
-
this.drawCallCount++;
|
|
1827
|
-
}
|
|
1828
1883
|
if (this.currentModel) {
|
|
1829
1884
|
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
1830
1885
|
pass.setVertexBuffer(1, this.jointsBuffer);
|
|
@@ -1839,16 +1894,6 @@ export class Engine {
|
|
|
1839
1894
|
this.drawCallCount++;
|
|
1840
1895
|
}
|
|
1841
1896
|
}
|
|
1842
|
-
// Pass 1.5: Pool (water plane) - render after opaque, before eyes
|
|
1843
|
-
if (this.pool) {
|
|
1844
|
-
this.pool.render(pass, {
|
|
1845
|
-
vertexBuffer: this.vertexBuffer,
|
|
1846
|
-
jointsBuffer: this.jointsBuffer,
|
|
1847
|
-
weightsBuffer: this.weightsBuffer,
|
|
1848
|
-
indexBuffer: this.indexBuffer,
|
|
1849
|
-
});
|
|
1850
|
-
this.drawCallCount++;
|
|
1851
|
-
}
|
|
1852
1897
|
// Pass 2: Eyes (writes stencil value for hair to test against)
|
|
1853
1898
|
pass.setPipeline(this.eyePipeline);
|
|
1854
1899
|
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA"}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Camera } from "./camera"
|
|
2
2
|
import { Quat, Vec3 } from "./math"
|
|
3
3
|
import { Model } from "./model"
|
|
4
|
-
import { Pool, PoolOptions } from "./pool"
|
|
5
4
|
import { PmxLoader } from "./pmx-loader"
|
|
6
5
|
import { Physics } from "./physics"
|
|
7
6
|
import { VMDKeyFrame, VMDLoader } from "./vmd-loader"
|
|
@@ -108,7 +107,6 @@ export class Engine {
|
|
|
108
107
|
private currentModel: Model | null = null
|
|
109
108
|
private modelDir: string = ""
|
|
110
109
|
private physics: Physics | null = null
|
|
111
|
-
private pool: Pool | null = null
|
|
112
110
|
private materialSampler!: GPUSampler
|
|
113
111
|
private textureCache = new Map<string, GPUTexture>()
|
|
114
112
|
// Draw lists
|
|
@@ -141,6 +139,8 @@ export class Engine {
|
|
|
141
139
|
private gpuMemoryMB: number = 0
|
|
142
140
|
private hasAnimation = false // Set to true when loadAnimation is called
|
|
143
141
|
private playingAnimation = false // Set to true when playAnimation is called
|
|
142
|
+
private breathingTimeout: number | null = null
|
|
143
|
+
private breathingBaseRotations: Map<string, Quat> = new Map()
|
|
144
144
|
|
|
145
145
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
|
|
146
146
|
this.canvas = canvas
|
|
@@ -1376,38 +1376,6 @@ export class Engine {
|
|
|
1376
1376
|
this.camera.attachControl(this.canvas)
|
|
1377
1377
|
}
|
|
1378
1378
|
|
|
1379
|
-
// Create camera bind group layout for pool (camera-only)
|
|
1380
|
-
private createCameraBindGroupLayout(): GPUBindGroupLayout {
|
|
1381
|
-
return this.device.createBindGroupLayout({
|
|
1382
|
-
label: "camera bind group layout",
|
|
1383
|
-
entries: [
|
|
1384
|
-
{
|
|
1385
|
-
binding: 0,
|
|
1386
|
-
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
1387
|
-
buffer: {
|
|
1388
|
-
type: "uniform",
|
|
1389
|
-
},
|
|
1390
|
-
},
|
|
1391
|
-
],
|
|
1392
|
-
})
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// Create camera bind group for pool
|
|
1396
|
-
private createCameraBindGroup(layout: GPUBindGroupLayout): GPUBindGroup {
|
|
1397
|
-
return this.device.createBindGroup({
|
|
1398
|
-
label: "camera bind group for pool",
|
|
1399
|
-
layout: layout,
|
|
1400
|
-
entries: [
|
|
1401
|
-
{
|
|
1402
|
-
binding: 0,
|
|
1403
|
-
resource: {
|
|
1404
|
-
buffer: this.cameraUniformBuffer,
|
|
1405
|
-
},
|
|
1406
|
-
},
|
|
1407
|
-
],
|
|
1408
|
-
})
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
1379
|
// Step 5: Create lighting buffers
|
|
1412
1380
|
private setupLighting() {
|
|
1413
1381
|
this.lightUniformBuffer = this.device.createBuffer({
|
|
@@ -1454,12 +1422,32 @@ export class Engine {
|
|
|
1454
1422
|
this.hasAnimation = true
|
|
1455
1423
|
}
|
|
1456
1424
|
|
|
1457
|
-
public playAnimation(
|
|
1425
|
+
public playAnimation(options?: {
|
|
1426
|
+
breathBones?: string[] | Record<string, number> // Array of bone names or map of bone name -> rotation range
|
|
1427
|
+
breathDuration?: number // Breathing cycle duration in milliseconds
|
|
1428
|
+
}) {
|
|
1458
1429
|
if (this.animationFrames.length === 0) return
|
|
1459
1430
|
|
|
1460
1431
|
this.stopAnimation()
|
|
1432
|
+
this.stopBreathing()
|
|
1461
1433
|
this.playingAnimation = true
|
|
1462
1434
|
|
|
1435
|
+
// Enable breathing if breathBones is provided
|
|
1436
|
+
const enableBreath = options?.breathBones !== undefined && options.breathBones !== null
|
|
1437
|
+
let breathBones: string[] = []
|
|
1438
|
+
let breathRotationRanges: Record<string, number> | undefined = undefined
|
|
1439
|
+
|
|
1440
|
+
if (enableBreath && options.breathBones) {
|
|
1441
|
+
if (Array.isArray(options.breathBones)) {
|
|
1442
|
+
breathBones = options.breathBones
|
|
1443
|
+
} else {
|
|
1444
|
+
breathBones = Object.keys(options.breathBones)
|
|
1445
|
+
breathRotationRanges = options.breathBones
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const breathDuration = options?.breathDuration ?? 4000
|
|
1450
|
+
|
|
1463
1451
|
const allBoneKeyFrames: BoneKeyFrame[] = []
|
|
1464
1452
|
for (const keyFrame of this.animationFrames) {
|
|
1465
1453
|
for (const boneFrame of keyFrame.boneFrames) {
|
|
@@ -1563,6 +1551,43 @@ export class Engine {
|
|
|
1563
1551
|
}
|
|
1564
1552
|
}
|
|
1565
1553
|
}
|
|
1554
|
+
|
|
1555
|
+
// Setup breathing animation if enabled
|
|
1556
|
+
if (enableBreath && this.currentModel) {
|
|
1557
|
+
// Find the last frame time
|
|
1558
|
+
let maxTime = 0
|
|
1559
|
+
for (const keyFrame of this.animationFrames) {
|
|
1560
|
+
if (keyFrame.time > maxTime) {
|
|
1561
|
+
maxTime = keyFrame.time
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Get last frame rotations directly from animation data for breathing bones
|
|
1566
|
+
const lastFrameRotations = new Map<string, Quat>()
|
|
1567
|
+
for (const bone of breathBones) {
|
|
1568
|
+
const keyFrames = boneKeyFramesByBone.get(bone)
|
|
1569
|
+
if (keyFrames && keyFrames.length > 0) {
|
|
1570
|
+
// Find the rotation at the last frame time (closest keyframe <= maxTime)
|
|
1571
|
+
let lastRotation: Quat | null = null
|
|
1572
|
+
for (let i = keyFrames.length - 1; i >= 0; i--) {
|
|
1573
|
+
if (keyFrames[i].time <= maxTime) {
|
|
1574
|
+
lastRotation = keyFrames[i].rotation
|
|
1575
|
+
break
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (lastRotation) {
|
|
1579
|
+
lastFrameRotations.set(bone, lastRotation)
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Start breathing after animation completes
|
|
1585
|
+
// Use the last frame rotations directly from animation data (no need to capture from model)
|
|
1586
|
+
const animationEndTime = maxTime * 1000 + 200 // Small buffer for final tweens to complete
|
|
1587
|
+
this.breathingTimeout = window.setTimeout(() => {
|
|
1588
|
+
this.startBreathing(breathBones, lastFrameRotations, breathRotationRanges, breathDuration)
|
|
1589
|
+
}, animationEndTime)
|
|
1590
|
+
}
|
|
1566
1591
|
}
|
|
1567
1592
|
|
|
1568
1593
|
public stopAnimation() {
|
|
@@ -1573,6 +1598,69 @@ export class Engine {
|
|
|
1573
1598
|
this.playingAnimation = false
|
|
1574
1599
|
}
|
|
1575
1600
|
|
|
1601
|
+
private stopBreathing() {
|
|
1602
|
+
if (this.breathingTimeout !== null) {
|
|
1603
|
+
clearTimeout(this.breathingTimeout)
|
|
1604
|
+
this.breathingTimeout = null
|
|
1605
|
+
}
|
|
1606
|
+
this.breathingBaseRotations.clear()
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
private startBreathing(
|
|
1610
|
+
bones: string[],
|
|
1611
|
+
baseRotations: Map<string, Quat>,
|
|
1612
|
+
rotationRanges?: Record<string, number>,
|
|
1613
|
+
durationMs: number = 4000
|
|
1614
|
+
) {
|
|
1615
|
+
if (!this.currentModel) return
|
|
1616
|
+
|
|
1617
|
+
// Store base rotations directly from last frame of animation data
|
|
1618
|
+
// These are the exact rotations from the animation - use them as-is
|
|
1619
|
+
for (const bone of bones) {
|
|
1620
|
+
const baseRot = baseRotations.get(bone)
|
|
1621
|
+
if (baseRot) {
|
|
1622
|
+
this.breathingBaseRotations.set(bone, baseRot)
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
const halfCycleMs = durationMs / 2
|
|
1627
|
+
const defaultRotation = 0.02 // Default rotation range if not specified per bone
|
|
1628
|
+
|
|
1629
|
+
// Start breathing cycle - oscillate around exact base rotation (final pose)
|
|
1630
|
+
// Each bone can have its own rotation range, or use default
|
|
1631
|
+
const animate = (isInhale: boolean) => {
|
|
1632
|
+
if (!this.currentModel) return
|
|
1633
|
+
|
|
1634
|
+
const breathingBoneNames: string[] = []
|
|
1635
|
+
const breathingQuats: Quat[] = []
|
|
1636
|
+
|
|
1637
|
+
for (const bone of bones) {
|
|
1638
|
+
const baseRot = this.breathingBaseRotations.get(bone)
|
|
1639
|
+
if (!baseRot) continue
|
|
1640
|
+
|
|
1641
|
+
// Get rotation range for this bone (per-bone or default)
|
|
1642
|
+
const rotation = rotationRanges?.[bone] ?? defaultRotation
|
|
1643
|
+
|
|
1644
|
+
// Oscillate around base rotation with the bone's rotation range
|
|
1645
|
+
// isInhale: base * rotation, exhale: base * (-rotation)
|
|
1646
|
+
const oscillationRot = Quat.fromEuler(isInhale ? rotation : -rotation, 0, 0)
|
|
1647
|
+
const finalRot = baseRot.multiply(oscillationRot)
|
|
1648
|
+
|
|
1649
|
+
breathingBoneNames.push(bone)
|
|
1650
|
+
breathingQuats.push(finalRot)
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
if (breathingBoneNames.length > 0) {
|
|
1654
|
+
this.rotateBones(breathingBoneNames, breathingQuats, halfCycleMs)
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
this.breathingTimeout = window.setTimeout(() => animate(!isInhale), halfCycleMs)
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Start breathing from exhale position (closer to base) to minimize initial movement
|
|
1661
|
+
animate(false)
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1576
1664
|
public getStats(): EngineStats {
|
|
1577
1665
|
return { ...this.stats }
|
|
1578
1666
|
}
|
|
@@ -1604,6 +1692,7 @@ export class Engine {
|
|
|
1604
1692
|
public dispose() {
|
|
1605
1693
|
this.stopRenderLoop()
|
|
1606
1694
|
this.stopAnimation()
|
|
1695
|
+
this.stopBreathing()
|
|
1607
1696
|
if (this.camera) this.camera.detachControl()
|
|
1608
1697
|
if (this.resizeObserver) {
|
|
1609
1698
|
this.resizeObserver.disconnect()
|
|
@@ -1631,15 +1720,6 @@ export class Engine {
|
|
|
1631
1720
|
await this.setupModelBuffers(model)
|
|
1632
1721
|
}
|
|
1633
1722
|
|
|
1634
|
-
public async addPool(options?: PoolOptions) {
|
|
1635
|
-
if (!this.device) {
|
|
1636
|
-
throw new Error("Engine must be initialized before adding pool")
|
|
1637
|
-
}
|
|
1638
|
-
const cameraLayout = this.createCameraBindGroupLayout()
|
|
1639
|
-
this.pool = new Pool(this.device, cameraLayout, this.cameraUniformBuffer, options)
|
|
1640
|
-
await this.pool.init()
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
1723
|
public rotateBones(bones: string[], rotations: Quat[], durationMs?: number) {
|
|
1644
1724
|
this.currentModel?.rotateBones(bones, rotations, durationMs)
|
|
1645
1725
|
}
|
|
@@ -2067,12 +2147,6 @@ export class Engine {
|
|
|
2067
2147
|
|
|
2068
2148
|
this.drawCallCount = 0
|
|
2069
2149
|
|
|
2070
|
-
// Render pool first if no model
|
|
2071
|
-
if (this.pool && !this.currentModel) {
|
|
2072
|
-
this.pool.render(pass)
|
|
2073
|
-
this.drawCallCount++
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
2150
|
if (this.currentModel) {
|
|
2077
2151
|
pass.setVertexBuffer(0, this.vertexBuffer)
|
|
2078
2152
|
pass.setVertexBuffer(1, this.jointsBuffer)
|
|
@@ -2089,17 +2163,6 @@ export class Engine {
|
|
|
2089
2163
|
}
|
|
2090
2164
|
}
|
|
2091
2165
|
|
|
2092
|
-
// Pass 1.5: Pool (water plane) - render after opaque, before eyes
|
|
2093
|
-
if (this.pool) {
|
|
2094
|
-
this.pool.render(pass, {
|
|
2095
|
-
vertexBuffer: this.vertexBuffer,
|
|
2096
|
-
jointsBuffer: this.jointsBuffer,
|
|
2097
|
-
weightsBuffer: this.weightsBuffer,
|
|
2098
|
-
indexBuffer: this.indexBuffer!,
|
|
2099
|
-
})
|
|
2100
|
-
this.drawCallCount++
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
2166
|
// Pass 2: Eyes (writes stencil value for hair to test against)
|
|
2104
2167
|
pass.setPipeline(this.eyePipeline)
|
|
2105
2168
|
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
package/src/index.ts
CHANGED
package/src/pool.ts
DELETED
|
@@ -1,483 +0,0 @@
|
|
|
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
|
-
}
|