reze-engine 0.4.1 → 0.4.2
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 +27 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +449 -9
- package/package.json +1 -1
- package/src/engine.ts +522 -8
package/dist/engine.d.ts
CHANGED
|
@@ -39,6 +39,9 @@ export declare class Engine {
|
|
|
39
39
|
private hairPipelineOverEyes;
|
|
40
40
|
private hairPipelineOverNonEyes;
|
|
41
41
|
private hairDepthPipeline;
|
|
42
|
+
private groundPipeline;
|
|
43
|
+
private groundBindGroupLayout;
|
|
44
|
+
private reflectionPipeline;
|
|
42
45
|
private outlinePipeline;
|
|
43
46
|
private hairOutlinePipeline;
|
|
44
47
|
private mainBindGroupLayout;
|
|
@@ -72,6 +75,14 @@ export declare class Engine {
|
|
|
72
75
|
private bloomThreshold;
|
|
73
76
|
private bloomIntensity;
|
|
74
77
|
private rimLightIntensity;
|
|
78
|
+
private groundVertexBuffer?;
|
|
79
|
+
private groundIndexBuffer?;
|
|
80
|
+
private groundReflectionTexture?;
|
|
81
|
+
private groundReflectionResolveTexture?;
|
|
82
|
+
private groundReflectionDepthTexture?;
|
|
83
|
+
private groundReflectionBindGroup?;
|
|
84
|
+
private groundMaterialUniformBuffer?;
|
|
85
|
+
private groundHasReflections;
|
|
75
86
|
private onRaycast?;
|
|
76
87
|
private cachedSkinnedVertices?;
|
|
77
88
|
private cachedSkinMatricesVersion;
|
|
@@ -106,6 +117,15 @@ export declare class Engine {
|
|
|
106
117
|
private setAmbientColor;
|
|
107
118
|
clearLights(): void;
|
|
108
119
|
addLight(direction: Vec3, color: Vec3, intensity?: number): boolean;
|
|
120
|
+
addGround(options?: {
|
|
121
|
+
width?: number;
|
|
122
|
+
height?: number;
|
|
123
|
+
diffuseColor?: Vec3;
|
|
124
|
+
reflectionLevel?: number;
|
|
125
|
+
reflectionTextureSize?: number;
|
|
126
|
+
fadeStart?: number;
|
|
127
|
+
fadeEnd?: number;
|
|
128
|
+
}): void;
|
|
109
129
|
private updateLightBuffer;
|
|
110
130
|
loadAnimation(url: string): Promise<void>;
|
|
111
131
|
playAnimation(): void;
|
|
@@ -132,12 +152,17 @@ export declare class Engine {
|
|
|
132
152
|
getMaterials(): string[];
|
|
133
153
|
private updateVertexBuffer;
|
|
134
154
|
private setupModelBuffers;
|
|
155
|
+
private createGroundGeometry;
|
|
156
|
+
private createGroundMaterialBuffer;
|
|
157
|
+
private createReflectionTexture;
|
|
135
158
|
private setupMaterials;
|
|
136
159
|
private createMaterialUniformBuffer;
|
|
137
160
|
private createUniformBuffer;
|
|
138
161
|
private shouldRenderDrawCall;
|
|
139
162
|
private createTextureFromPath;
|
|
140
163
|
private renderEyes;
|
|
164
|
+
private renderGround;
|
|
165
|
+
private renderReflectionTexture;
|
|
141
166
|
private renderHair;
|
|
142
167
|
private handleCanvasDoubleClick;
|
|
143
168
|
private handleCanvasTouch;
|
|
@@ -149,5 +174,7 @@ export declare class Engine {
|
|
|
149
174
|
private updateSkinMatrices;
|
|
150
175
|
private drawOutlines;
|
|
151
176
|
private updateStats;
|
|
177
|
+
private createMirrorMatrix;
|
|
178
|
+
private writeMirrorTransformedSkinMatrices;
|
|
152
179
|
}
|
|
153
180
|
//# sourceMappingURL=engine.d.ts.map
|
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,EAAQ,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAIzC,MAAM,MAAM,eAAe,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;AAEjG,MAAM,MAAM,aAAa,GAAG;IAC1B,YAAY,CAAC,EAAE,IAAI,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,IAAI,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,eAAe,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC,GAAG,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAA;AAEjH,eAAO,MAAM,sBAAsB,EAAE,qBASpC,CAAA;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;CAClB;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Camera } from "./camera";
|
|
2
|
-
import { Vec3 } from "./math";
|
|
2
|
+
import { Mat4, Vec3 } from "./math";
|
|
3
3
|
import { PmxLoader } from "./pmx-loader";
|
|
4
4
|
export const DEFAULT_ENGINE_OPTIONS = {
|
|
5
5
|
ambientColor: new Vec3(0.85, 0.85, 0.85),
|
|
@@ -21,6 +21,7 @@ export class Engine {
|
|
|
21
21
|
// Constants
|
|
22
22
|
this.STENCIL_EYE_VALUE = 1;
|
|
23
23
|
this.BLOOM_DOWNSCALE_FACTOR = 2;
|
|
24
|
+
this.groundHasReflections = false;
|
|
24
25
|
this.cachedSkinMatricesVersion = -1;
|
|
25
26
|
this.skinMatricesVersion = 0;
|
|
26
27
|
// Double-tap detection
|
|
@@ -352,6 +353,154 @@ export class Engine {
|
|
|
352
353
|
depthCompare: "less-equal",
|
|
353
354
|
},
|
|
354
355
|
});
|
|
356
|
+
// Create ground/reflection pipeline with reflection texture support
|
|
357
|
+
this.groundBindGroupLayout = this.device.createBindGroupLayout({
|
|
358
|
+
label: "ground bind group layout",
|
|
359
|
+
entries: [
|
|
360
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // camera
|
|
361
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // light
|
|
362
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // reflectionTexture
|
|
363
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // reflectionSampler
|
|
364
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // groundMaterial
|
|
365
|
+
],
|
|
366
|
+
});
|
|
367
|
+
const groundPipelineLayout = this.device.createPipelineLayout({
|
|
368
|
+
label: "ground pipeline layout",
|
|
369
|
+
bindGroupLayouts: [this.groundBindGroupLayout],
|
|
370
|
+
});
|
|
371
|
+
const groundShaderModule = this.device.createShaderModule({
|
|
372
|
+
label: "ground shaders",
|
|
373
|
+
code: /* wgsl */ `
|
|
374
|
+
struct CameraUniforms {
|
|
375
|
+
view: mat4x4f,
|
|
376
|
+
projection: mat4x4f,
|
|
377
|
+
viewPos: vec3f,
|
|
378
|
+
_padding: f32,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
struct LightUniforms {
|
|
382
|
+
ambientColor: vec4f,
|
|
383
|
+
lights: array<Light, 4>,
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
struct Light {
|
|
387
|
+
direction: vec4f,
|
|
388
|
+
color: vec4f,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
struct GroundMaterialUniforms {
|
|
392
|
+
diffuseColor: vec3f,
|
|
393
|
+
reflectionLevel: f32,
|
|
394
|
+
fadeStart: f32,
|
|
395
|
+
fadeEnd: f32,
|
|
396
|
+
_padding1: f32,
|
|
397
|
+
_padding2: f32,
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
401
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
402
|
+
@group(0) @binding(2) var reflectionTexture: texture_2d<f32>;
|
|
403
|
+
@group(0) @binding(3) var reflectionSampler: sampler;
|
|
404
|
+
@group(0) @binding(4) var<uniform> material: GroundMaterialUniforms;
|
|
405
|
+
|
|
406
|
+
struct VertexOutput {
|
|
407
|
+
@builtin(position) position: vec4f,
|
|
408
|
+
@location(0) normal: vec3f,
|
|
409
|
+
@location(1) uv: vec2f,
|
|
410
|
+
@location(2) worldPos: vec3f,
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
@vertex fn vs(
|
|
414
|
+
@location(0) position: vec3f,
|
|
415
|
+
@location(1) normal: vec3f,
|
|
416
|
+
@location(2) uv: vec2f,
|
|
417
|
+
) -> VertexOutput {
|
|
418
|
+
var output: VertexOutput;
|
|
419
|
+
let worldPos = position;
|
|
420
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
421
|
+
output.normal = normal;
|
|
422
|
+
output.uv = uv;
|
|
423
|
+
output.worldPos = worldPos;
|
|
424
|
+
return output;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
428
|
+
let n = normalize(input.normal);
|
|
429
|
+
|
|
430
|
+
let clipPos = camera.projection * camera.view * vec4f(input.worldPos, 1.0);
|
|
431
|
+
let ndcPos = clipPos.xyz / clipPos.w;
|
|
432
|
+
var reflectionUV = vec2f(ndcPos.x * 0.5 + 0.5, 0.5 - ndcPos.y * 0.5);
|
|
433
|
+
|
|
434
|
+
let sampledReflectionColor = textureSample(reflectionTexture, reflectionSampler, reflectionUV).rgb;
|
|
435
|
+
let isValidReflection = clipPos.w > 0.0 &&
|
|
436
|
+
all(reflectionUV >= vec2f(0.0)) && all(reflectionUV <= vec2f(1.0));
|
|
437
|
+
var reflectionColor = select(vec3f(1.0, 1.0, 1.0), sampledReflectionColor, isValidReflection);
|
|
438
|
+
|
|
439
|
+
let distanceFromCamera = length(input.worldPos - camera.viewPos);
|
|
440
|
+
let fadeFactor = clamp((distanceFromCamera - 15.0) / 20.0, 0.0, 1.0);
|
|
441
|
+
reflectionColor *= (1.0 - fadeFactor * 0.3);
|
|
442
|
+
|
|
443
|
+
let diffuseColor = material.diffuseColor;
|
|
444
|
+
var finalColor = mix(diffuseColor, reflectionColor, material.reflectionLevel);
|
|
445
|
+
|
|
446
|
+
// Ground edge fade effect - smooth fade out at edges based on distance from center
|
|
447
|
+
let centerDist = length(input.worldPos.xz); // Distance from ground center in XZ plane
|
|
448
|
+
|
|
449
|
+
// Smoothstep for much smoother gradient transition
|
|
450
|
+
let t = clamp((centerDist - material.fadeStart) / (material.fadeEnd - material.fadeStart), 0.0, 1.0);
|
|
451
|
+
let edgeFade = 1.0 - smoothstep(0.0, 1.0, t);
|
|
452
|
+
finalColor *= edgeFade;
|
|
453
|
+
|
|
454
|
+
// Accumulate light contribution
|
|
455
|
+
var lightAccum = light.ambientColor.xyz;
|
|
456
|
+
for (var i = 0u; i < 4u; i++) {
|
|
457
|
+
let intensity = light.lights[i].color.w;
|
|
458
|
+
if (intensity > 0.0) {
|
|
459
|
+
let l = -light.lights[i].direction.xyz;
|
|
460
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
461
|
+
let radiance = light.lights[i].color.xyz * intensity;
|
|
462
|
+
lightAccum += radiance * nDotL;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Apply lighting to the blended color
|
|
467
|
+
let litColor = finalColor * lightAccum;
|
|
468
|
+
|
|
469
|
+
return vec4f(litColor, edgeFade);
|
|
470
|
+
}
|
|
471
|
+
`,
|
|
472
|
+
});
|
|
473
|
+
this.groundPipeline = this.createRenderPipeline({
|
|
474
|
+
label: "ground pipeline",
|
|
475
|
+
layout: groundPipelineLayout,
|
|
476
|
+
shaderModule: groundShaderModule,
|
|
477
|
+
vertexBuffers: fullVertexBuffers,
|
|
478
|
+
fragmentTarget: standardBlend,
|
|
479
|
+
cullMode: "back",
|
|
480
|
+
depthStencil: {
|
|
481
|
+
format: "depth24plus-stencil8",
|
|
482
|
+
depthWriteEnabled: true,
|
|
483
|
+
depthCompare: "less-equal",
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
// Create reflection pipeline (multisampled version for higher quality)
|
|
487
|
+
this.reflectionPipeline = this.createRenderPipeline({
|
|
488
|
+
label: "reflection pipeline",
|
|
489
|
+
layout: mainPipelineLayout,
|
|
490
|
+
shaderModule,
|
|
491
|
+
vertexBuffers: fullVertexBuffers,
|
|
492
|
+
fragmentTarget: {
|
|
493
|
+
format: this.presentationFormat,
|
|
494
|
+
blend: standardBlend.blend,
|
|
495
|
+
},
|
|
496
|
+
multisample: { count: this.sampleCount }, // Use same multisampling as main render
|
|
497
|
+
cullMode: "none",
|
|
498
|
+
depthStencil: {
|
|
499
|
+
format: "depth24plus-stencil8",
|
|
500
|
+
depthWriteEnabled: true,
|
|
501
|
+
depthCompare: "less-equal",
|
|
502
|
+
},
|
|
503
|
+
});
|
|
355
504
|
// Create bind group layout for outline pipelines
|
|
356
505
|
this.outlineBindGroupLayout = this.device.createBindGroupLayout({
|
|
357
506
|
label: "outline bind group layout",
|
|
@@ -1009,6 +1158,30 @@ export class Engine {
|
|
|
1009
1158
|
this.updateLightBuffer();
|
|
1010
1159
|
return true;
|
|
1011
1160
|
}
|
|
1161
|
+
addGround(options) {
|
|
1162
|
+
const opts = {
|
|
1163
|
+
width: 100,
|
|
1164
|
+
height: 100,
|
|
1165
|
+
diffuseColor: new Vec3(1, 1, 1),
|
|
1166
|
+
reflectionLevel: 0.5,
|
|
1167
|
+
reflectionTextureSize: 1024,
|
|
1168
|
+
fadeStart: 5.0,
|
|
1169
|
+
fadeEnd: 60.0,
|
|
1170
|
+
...options,
|
|
1171
|
+
};
|
|
1172
|
+
// Create ground geometry
|
|
1173
|
+
this.createGroundGeometry(opts.width, opts.height);
|
|
1174
|
+
this.createGroundMaterialBuffer(opts.diffuseColor, opts.reflectionLevel, opts.fadeStart, opts.fadeEnd);
|
|
1175
|
+
this.createReflectionTexture(opts.reflectionTextureSize);
|
|
1176
|
+
this.groundHasReflections = true;
|
|
1177
|
+
this.drawCalls.push({
|
|
1178
|
+
type: "ground",
|
|
1179
|
+
count: 6, // 2 triangles, 3 indices each
|
|
1180
|
+
firstIndex: 0,
|
|
1181
|
+
bindGroup: this.groundReflectionBindGroup,
|
|
1182
|
+
materialName: "Ground",
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1012
1185
|
updateLightBuffer() {
|
|
1013
1186
|
this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData);
|
|
1014
1187
|
}
|
|
@@ -1176,6 +1349,124 @@ export class Engine {
|
|
|
1176
1349
|
}
|
|
1177
1350
|
await this.setupMaterials(model);
|
|
1178
1351
|
}
|
|
1352
|
+
createGroundGeometry(width = 100, height = 100) {
|
|
1353
|
+
const halfWidth = width / 2;
|
|
1354
|
+
const halfHeight = height / 2;
|
|
1355
|
+
const vertices = new Float32Array([
|
|
1356
|
+
// Bottom-left
|
|
1357
|
+
-halfWidth,
|
|
1358
|
+
0,
|
|
1359
|
+
-halfHeight, // position
|
|
1360
|
+
0,
|
|
1361
|
+
1,
|
|
1362
|
+
0, // normal (up)
|
|
1363
|
+
0,
|
|
1364
|
+
0, // uv
|
|
1365
|
+
// Bottom-right
|
|
1366
|
+
halfWidth,
|
|
1367
|
+
0,
|
|
1368
|
+
-halfHeight, // position
|
|
1369
|
+
0,
|
|
1370
|
+
1,
|
|
1371
|
+
0, // normal (up)
|
|
1372
|
+
1,
|
|
1373
|
+
0, // uv
|
|
1374
|
+
// Top-right
|
|
1375
|
+
halfWidth,
|
|
1376
|
+
0,
|
|
1377
|
+
halfHeight, // position
|
|
1378
|
+
0,
|
|
1379
|
+
1,
|
|
1380
|
+
0, // normal (up)
|
|
1381
|
+
1,
|
|
1382
|
+
1, // uv
|
|
1383
|
+
// Top-left
|
|
1384
|
+
-halfWidth,
|
|
1385
|
+
0,
|
|
1386
|
+
halfHeight, // position
|
|
1387
|
+
0,
|
|
1388
|
+
1,
|
|
1389
|
+
0, // normal (up)
|
|
1390
|
+
0,
|
|
1391
|
+
1, // uv
|
|
1392
|
+
]);
|
|
1393
|
+
// Create indices for two triangles
|
|
1394
|
+
const indices = new Uint16Array([
|
|
1395
|
+
0,
|
|
1396
|
+
1,
|
|
1397
|
+
2, // First triangle
|
|
1398
|
+
0,
|
|
1399
|
+
2,
|
|
1400
|
+
3, // Second triangle
|
|
1401
|
+
]);
|
|
1402
|
+
// Create vertex buffer
|
|
1403
|
+
this.groundVertexBuffer = this.device.createBuffer({
|
|
1404
|
+
label: "ground vertex buffer",
|
|
1405
|
+
size: vertices.byteLength,
|
|
1406
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1407
|
+
});
|
|
1408
|
+
this.device.queue.writeBuffer(this.groundVertexBuffer, 0, vertices);
|
|
1409
|
+
this.groundIndexBuffer = this.device.createBuffer({
|
|
1410
|
+
label: "ground index buffer",
|
|
1411
|
+
size: indices.byteLength,
|
|
1412
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
1413
|
+
});
|
|
1414
|
+
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices);
|
|
1415
|
+
}
|
|
1416
|
+
createGroundMaterialBuffer(diffuseColor = new Vec3(1, 1, 1), reflectionLevel = 0.5, fadeStart = 5.0, fadeEnd = 60.0) {
|
|
1417
|
+
const materialData = new Float32Array([
|
|
1418
|
+
diffuseColor.x,
|
|
1419
|
+
diffuseColor.y,
|
|
1420
|
+
diffuseColor.z, // diffuseColor (12 bytes)
|
|
1421
|
+
reflectionLevel, // reflectionLevel (4 bytes)
|
|
1422
|
+
fadeStart, // fadeStart (4 bytes)
|
|
1423
|
+
fadeEnd, // fadeEnd (4 bytes)
|
|
1424
|
+
0, // padding (4 bytes)
|
|
1425
|
+
0, // padding (4 bytes)
|
|
1426
|
+
0, // padding (4 bytes)
|
|
1427
|
+
0, // padding (4 bytes)
|
|
1428
|
+
]);
|
|
1429
|
+
this.groundMaterialUniformBuffer = this.device.createBuffer({
|
|
1430
|
+
label: "ground material uniform buffer",
|
|
1431
|
+
size: materialData.byteLength,
|
|
1432
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1433
|
+
});
|
|
1434
|
+
this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, materialData);
|
|
1435
|
+
}
|
|
1436
|
+
createReflectionTexture(size = 1024) {
|
|
1437
|
+
this.groundReflectionTexture = this.device.createTexture({
|
|
1438
|
+
label: "ground reflection texture",
|
|
1439
|
+
size: [size, size],
|
|
1440
|
+
sampleCount: this.sampleCount,
|
|
1441
|
+
format: this.presentationFormat,
|
|
1442
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1443
|
+
});
|
|
1444
|
+
this.groundReflectionResolveTexture = this.device.createTexture({
|
|
1445
|
+
label: "ground reflection resolve texture",
|
|
1446
|
+
size: [size, size],
|
|
1447
|
+
format: this.presentationFormat,
|
|
1448
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1449
|
+
});
|
|
1450
|
+
this.groundReflectionDepthTexture = this.device.createTexture({
|
|
1451
|
+
label: "ground reflection depth texture",
|
|
1452
|
+
size: [size, size],
|
|
1453
|
+
sampleCount: this.sampleCount,
|
|
1454
|
+
format: "depth24plus-stencil8",
|
|
1455
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1456
|
+
});
|
|
1457
|
+
// Create a bind group for the reflection texture that can be used in the ground material
|
|
1458
|
+
this.groundReflectionBindGroup = this.device.createBindGroup({
|
|
1459
|
+
label: "ground reflection bind group",
|
|
1460
|
+
layout: this.groundBindGroupLayout,
|
|
1461
|
+
entries: [
|
|
1462
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1463
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1464
|
+
{ binding: 2, resource: this.groundReflectionResolveTexture.createView() }, // Use resolve texture for sampling
|
|
1465
|
+
{ binding: 3, resource: this.materialSampler },
|
|
1466
|
+
{ binding: 4, resource: { buffer: this.groundMaterialUniformBuffer } },
|
|
1467
|
+
],
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1179
1470
|
async setupMaterials(model) {
|
|
1180
1471
|
const materials = model.getMaterials();
|
|
1181
1472
|
if (materials.length === 0) {
|
|
@@ -1369,18 +1660,119 @@ export class Engine {
|
|
|
1369
1660
|
}
|
|
1370
1661
|
}
|
|
1371
1662
|
// Helper: Render eyes with stencil writing (for post-alpha-eye effect)
|
|
1372
|
-
renderEyes(pass) {
|
|
1373
|
-
|
|
1374
|
-
|
|
1663
|
+
renderEyes(pass, useReflectionPipeline = false) {
|
|
1664
|
+
if (useReflectionPipeline) {
|
|
1665
|
+
// For reflections, use the basic reflection pipeline instead of specialized eye pipeline
|
|
1666
|
+
pass.setPipeline(this.reflectionPipeline);
|
|
1667
|
+
for (const draw of this.drawCalls) {
|
|
1668
|
+
if (draw.type === "eye") {
|
|
1669
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1670
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
else {
|
|
1675
|
+
pass.setPipeline(this.eyePipeline);
|
|
1676
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE);
|
|
1677
|
+
for (const draw of this.drawCalls) {
|
|
1678
|
+
if (draw.type === "eye" && this.shouldRenderDrawCall(draw)) {
|
|
1679
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1680
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
renderGround(pass) {
|
|
1686
|
+
if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer) {
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
if (this.groundReflectionTexture) {
|
|
1690
|
+
this.renderReflectionTexture();
|
|
1691
|
+
}
|
|
1692
|
+
pass.setPipeline(this.groundPipeline);
|
|
1693
|
+
pass.setVertexBuffer(0, this.groundVertexBuffer);
|
|
1694
|
+
pass.setIndexBuffer(this.groundIndexBuffer, "uint16");
|
|
1375
1695
|
for (const draw of this.drawCalls) {
|
|
1376
|
-
if (draw.type === "
|
|
1696
|
+
if (draw.type === "ground" && this.shouldRenderDrawCall(draw)) {
|
|
1377
1697
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1378
1698
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1379
1699
|
}
|
|
1380
1700
|
}
|
|
1381
1701
|
}
|
|
1702
|
+
renderReflectionTexture() {
|
|
1703
|
+
if (!this.groundReflectionTexture)
|
|
1704
|
+
return;
|
|
1705
|
+
const mirrorMatrix = this.createMirrorMatrix(new Vec3(0, 1, 0), 0);
|
|
1706
|
+
this.updateCameraUniforms();
|
|
1707
|
+
const reflectionEncoder = this.device.createCommandEncoder();
|
|
1708
|
+
const reflectionPassDescriptor = {
|
|
1709
|
+
label: "reflection render pass",
|
|
1710
|
+
colorAttachments: [
|
|
1711
|
+
{
|
|
1712
|
+
view: this.groundReflectionTexture.createView(),
|
|
1713
|
+
resolveTarget: this.groundReflectionResolveTexture.createView(),
|
|
1714
|
+
clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, // White
|
|
1715
|
+
loadOp: "clear",
|
|
1716
|
+
storeOp: "store",
|
|
1717
|
+
},
|
|
1718
|
+
],
|
|
1719
|
+
depthStencilAttachment: {
|
|
1720
|
+
view: this.groundReflectionDepthTexture.createView(),
|
|
1721
|
+
depthClearValue: 1.0,
|
|
1722
|
+
depthLoadOp: "clear",
|
|
1723
|
+
depthStoreOp: "store",
|
|
1724
|
+
stencilClearValue: 0,
|
|
1725
|
+
stencilLoadOp: "clear",
|
|
1726
|
+
stencilStoreOp: "discard",
|
|
1727
|
+
},
|
|
1728
|
+
};
|
|
1729
|
+
const reflectionPass = reflectionEncoder.beginRenderPass(reflectionPassDescriptor);
|
|
1730
|
+
if (this.currentModel) {
|
|
1731
|
+
reflectionPass.setVertexBuffer(0, this.vertexBuffer);
|
|
1732
|
+
reflectionPass.setVertexBuffer(1, this.jointsBuffer);
|
|
1733
|
+
reflectionPass.setVertexBuffer(2, this.weightsBuffer);
|
|
1734
|
+
reflectionPass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
1735
|
+
this.writeMirrorTransformedSkinMatrices(mirrorMatrix);
|
|
1736
|
+
reflectionPass.setPipeline(this.reflectionPipeline);
|
|
1737
|
+
for (const draw of this.drawCalls) {
|
|
1738
|
+
if (draw.type === "opaque" && this.shouldRenderDrawCall(draw)) {
|
|
1739
|
+
reflectionPass.setBindGroup(0, draw.bindGroup);
|
|
1740
|
+
reflectionPass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
// Render eyes (using reflection pipeline)
|
|
1744
|
+
this.renderEyes(reflectionPass, true);
|
|
1745
|
+
// Render hair (using reflection pipeline)
|
|
1746
|
+
this.renderHair(reflectionPass, true);
|
|
1747
|
+
// Render transparent objects
|
|
1748
|
+
for (const draw of this.drawCalls) {
|
|
1749
|
+
if (draw.type === "transparent" && this.shouldRenderDrawCall(draw)) {
|
|
1750
|
+
reflectionPass.setBindGroup(0, draw.bindGroup);
|
|
1751
|
+
reflectionPass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
this.drawOutlines(reflectionPass, true, true);
|
|
1755
|
+
}
|
|
1756
|
+
reflectionPass.end();
|
|
1757
|
+
// Submit reflection rendering commands
|
|
1758
|
+
const reflectionCommandBuffer = reflectionEncoder.finish();
|
|
1759
|
+
this.device.queue.submit([reflectionCommandBuffer]);
|
|
1760
|
+
// Restore original skin matrices
|
|
1761
|
+
this.updateSkinMatrices();
|
|
1762
|
+
}
|
|
1382
1763
|
// Helper: Render hair with post-alpha-eye effect (depth pre-pass + stencil-based shading + outlines)
|
|
1383
|
-
renderHair(pass) {
|
|
1764
|
+
renderHair(pass, useReflectionPipeline = false) {
|
|
1765
|
+
if (useReflectionPipeline) {
|
|
1766
|
+
// For reflections, use the basic reflection pipeline for all hair
|
|
1767
|
+
pass.setPipeline(this.reflectionPipeline);
|
|
1768
|
+
for (const draw of this.drawCalls) {
|
|
1769
|
+
if (draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") {
|
|
1770
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1771
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1384
1776
|
// Hair depth pre-pass (reduces overdraw via early depth rejection)
|
|
1385
1777
|
const hasHair = this.drawCalls.some((d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(d));
|
|
1386
1778
|
if (hasHair) {
|
|
@@ -1430,7 +1822,7 @@ export class Engine {
|
|
|
1430
1822
|
// Get camera matrices
|
|
1431
1823
|
const viewMatrix = this.camera.getViewMatrix();
|
|
1432
1824
|
const projectionMatrix = this.camera.getProjectionMatrix();
|
|
1433
|
-
// Convert screen coordinates to world space ray
|
|
1825
|
+
// Convert screen coordinates to world space ray
|
|
1434
1826
|
const canvas = this.canvas;
|
|
1435
1827
|
const rect = canvas.getBoundingClientRect();
|
|
1436
1828
|
// Convert to clip space (-1 to 1)
|
|
@@ -1636,7 +2028,11 @@ export class Engine {
|
|
|
1636
2028
|
this.drawOutlines(pass, false);
|
|
1637
2029
|
// Pass 3: Hair rendering (depth pre-pass + shading + outlines)
|
|
1638
2030
|
this.renderHair(pass);
|
|
1639
|
-
// Pass 4:
|
|
2031
|
+
// Pass 4: Ground (with reflections)
|
|
2032
|
+
if (this.groundHasReflections) {
|
|
2033
|
+
this.renderGround(pass);
|
|
2034
|
+
}
|
|
2035
|
+
// Pass 5: Transparent
|
|
1640
2036
|
pass.setPipeline(this.modelPipeline);
|
|
1641
2037
|
for (const draw of this.drawCalls) {
|
|
1642
2038
|
if (draw.type === "transparent" && this.shouldRenderDrawCall(draw)) {
|
|
@@ -1767,7 +2163,11 @@ export class Engine {
|
|
|
1767
2163
|
// Increment version to invalidate cached skinned vertices
|
|
1768
2164
|
this.skinMatricesVersion++;
|
|
1769
2165
|
}
|
|
1770
|
-
drawOutlines(pass, transparent) {
|
|
2166
|
+
drawOutlines(pass, transparent, useReflectionPipeline = false) {
|
|
2167
|
+
if (useReflectionPipeline) {
|
|
2168
|
+
// Skip outlines for reflections - not critical for the effect
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
1771
2171
|
pass.setPipeline(this.outlinePipeline);
|
|
1772
2172
|
const outlineType = transparent ? "transparent-outline" : "opaque-outline";
|
|
1773
2173
|
for (const draw of this.drawCalls) {
|
|
@@ -1799,4 +2199,44 @@ export class Engine {
|
|
|
1799
2199
|
this.lastFpsUpdate = now;
|
|
1800
2200
|
}
|
|
1801
2201
|
}
|
|
2202
|
+
createMirrorMatrix(planeNormal, planeDistance) {
|
|
2203
|
+
// Create reflection matrix across a plane
|
|
2204
|
+
const n = planeNormal.normalize();
|
|
2205
|
+
return new Mat4(new Float32Array([
|
|
2206
|
+
1 - 2 * n.x * n.x,
|
|
2207
|
+
-2 * n.x * n.y,
|
|
2208
|
+
-2 * n.x * n.z,
|
|
2209
|
+
0,
|
|
2210
|
+
-2 * n.y * n.x,
|
|
2211
|
+
1 - 2 * n.y * n.y,
|
|
2212
|
+
-2 * n.y * n.z,
|
|
2213
|
+
0,
|
|
2214
|
+
-2 * n.z * n.x,
|
|
2215
|
+
-2 * n.z * n.y,
|
|
2216
|
+
1 - 2 * n.z * n.z,
|
|
2217
|
+
0,
|
|
2218
|
+
-2 * planeDistance * n.x,
|
|
2219
|
+
-2 * planeDistance * n.y,
|
|
2220
|
+
-2 * planeDistance * n.z,
|
|
2221
|
+
1,
|
|
2222
|
+
]));
|
|
2223
|
+
}
|
|
2224
|
+
writeMirrorTransformedSkinMatrices(mirrorMatrix) {
|
|
2225
|
+
if (!this.currentModel || !this.skinMatrixBuffer)
|
|
2226
|
+
return;
|
|
2227
|
+
const originalMatrices = this.currentModel.getSkinMatrices();
|
|
2228
|
+
const transformedMatrices = new Float32Array(originalMatrices.length);
|
|
2229
|
+
for (let i = 0; i < originalMatrices.length; i += 16) {
|
|
2230
|
+
const boneMatrixValues = new Float32Array(16);
|
|
2231
|
+
for (let j = 0; j < 16; j++) {
|
|
2232
|
+
boneMatrixValues[j] = originalMatrices[i + j];
|
|
2233
|
+
}
|
|
2234
|
+
const boneMatrix = new Mat4(boneMatrixValues);
|
|
2235
|
+
const transformed = mirrorMatrix.multiply(boneMatrix);
|
|
2236
|
+
for (let j = 0; j < 16; j++) {
|
|
2237
|
+
transformedMatrices[i + j] = transformed.values[j];
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
this.device.queue.writeBuffer(this.skinMatrixBuffer, 0, transformedMatrices);
|
|
2241
|
+
}
|
|
1802
2242
|
}
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -40,6 +40,7 @@ type DrawCallType =
|
|
|
40
40
|
| "hair-over-eyes"
|
|
41
41
|
| "hair-over-non-eyes"
|
|
42
42
|
| "transparent"
|
|
43
|
+
| "ground"
|
|
43
44
|
| "opaque-outline"
|
|
44
45
|
| "eye-outline"
|
|
45
46
|
| "hair-outline"
|
|
@@ -77,6 +78,10 @@ export class Engine {
|
|
|
77
78
|
private hairPipelineOverEyes!: GPURenderPipeline
|
|
78
79
|
private hairPipelineOverNonEyes!: GPURenderPipeline
|
|
79
80
|
private hairDepthPipeline!: GPURenderPipeline
|
|
81
|
+
// Ground/reflection pipeline
|
|
82
|
+
private groundPipeline!: GPURenderPipeline
|
|
83
|
+
private groundBindGroupLayout!: GPUBindGroupLayout
|
|
84
|
+
private reflectionPipeline!: GPURenderPipeline
|
|
80
85
|
// Outline pipelines
|
|
81
86
|
private outlinePipeline!: GPURenderPipeline
|
|
82
87
|
private hairOutlinePipeline!: GPURenderPipeline
|
|
@@ -120,6 +125,16 @@ export class Engine {
|
|
|
120
125
|
// Rim light settings
|
|
121
126
|
private rimLightIntensity!: number
|
|
122
127
|
|
|
128
|
+
// Ground/reflection properties
|
|
129
|
+
private groundVertexBuffer?: GPUBuffer
|
|
130
|
+
private groundIndexBuffer?: GPUBuffer
|
|
131
|
+
private groundReflectionTexture?: GPUTexture
|
|
132
|
+
private groundReflectionResolveTexture?: GPUTexture // Resolve target for multisampled texture
|
|
133
|
+
private groundReflectionDepthTexture?: GPUTexture
|
|
134
|
+
private groundReflectionBindGroup?: GPUBindGroup
|
|
135
|
+
private groundMaterialUniformBuffer?: GPUBuffer
|
|
136
|
+
private groundHasReflections = false
|
|
137
|
+
|
|
123
138
|
// Raycasting
|
|
124
139
|
private onRaycast?: RaycastCallback
|
|
125
140
|
private cachedSkinnedVertices?: Float32Array
|
|
@@ -452,6 +467,159 @@ export class Engine {
|
|
|
452
467
|
},
|
|
453
468
|
})
|
|
454
469
|
|
|
470
|
+
// Create ground/reflection pipeline with reflection texture support
|
|
471
|
+
this.groundBindGroupLayout = this.device.createBindGroupLayout({
|
|
472
|
+
label: "ground bind group layout",
|
|
473
|
+
entries: [
|
|
474
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // camera
|
|
475
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // light
|
|
476
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // reflectionTexture
|
|
477
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // reflectionSampler
|
|
478
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // groundMaterial
|
|
479
|
+
],
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
const groundPipelineLayout = this.device.createPipelineLayout({
|
|
483
|
+
label: "ground pipeline layout",
|
|
484
|
+
bindGroupLayouts: [this.groundBindGroupLayout],
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
const groundShaderModule = this.device.createShaderModule({
|
|
488
|
+
label: "ground shaders",
|
|
489
|
+
code: /* wgsl */ `
|
|
490
|
+
struct CameraUniforms {
|
|
491
|
+
view: mat4x4f,
|
|
492
|
+
projection: mat4x4f,
|
|
493
|
+
viewPos: vec3f,
|
|
494
|
+
_padding: f32,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
struct LightUniforms {
|
|
498
|
+
ambientColor: vec4f,
|
|
499
|
+
lights: array<Light, 4>,
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
struct Light {
|
|
503
|
+
direction: vec4f,
|
|
504
|
+
color: vec4f,
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
struct GroundMaterialUniforms {
|
|
508
|
+
diffuseColor: vec3f,
|
|
509
|
+
reflectionLevel: f32,
|
|
510
|
+
fadeStart: f32,
|
|
511
|
+
fadeEnd: f32,
|
|
512
|
+
_padding1: f32,
|
|
513
|
+
_padding2: f32,
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
517
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
518
|
+
@group(0) @binding(2) var reflectionTexture: texture_2d<f32>;
|
|
519
|
+
@group(0) @binding(3) var reflectionSampler: sampler;
|
|
520
|
+
@group(0) @binding(4) var<uniform> material: GroundMaterialUniforms;
|
|
521
|
+
|
|
522
|
+
struct VertexOutput {
|
|
523
|
+
@builtin(position) position: vec4f,
|
|
524
|
+
@location(0) normal: vec3f,
|
|
525
|
+
@location(1) uv: vec2f,
|
|
526
|
+
@location(2) worldPos: vec3f,
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
@vertex fn vs(
|
|
530
|
+
@location(0) position: vec3f,
|
|
531
|
+
@location(1) normal: vec3f,
|
|
532
|
+
@location(2) uv: vec2f,
|
|
533
|
+
) -> VertexOutput {
|
|
534
|
+
var output: VertexOutput;
|
|
535
|
+
let worldPos = position;
|
|
536
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
537
|
+
output.normal = normal;
|
|
538
|
+
output.uv = uv;
|
|
539
|
+
output.worldPos = worldPos;
|
|
540
|
+
return output;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
544
|
+
let n = normalize(input.normal);
|
|
545
|
+
|
|
546
|
+
let clipPos = camera.projection * camera.view * vec4f(input.worldPos, 1.0);
|
|
547
|
+
let ndcPos = clipPos.xyz / clipPos.w;
|
|
548
|
+
var reflectionUV = vec2f(ndcPos.x * 0.5 + 0.5, 0.5 - ndcPos.y * 0.5);
|
|
549
|
+
|
|
550
|
+
let sampledReflectionColor = textureSample(reflectionTexture, reflectionSampler, reflectionUV).rgb;
|
|
551
|
+
let isValidReflection = clipPos.w > 0.0 &&
|
|
552
|
+
all(reflectionUV >= vec2f(0.0)) && all(reflectionUV <= vec2f(1.0));
|
|
553
|
+
var reflectionColor = select(vec3f(1.0, 1.0, 1.0), sampledReflectionColor, isValidReflection);
|
|
554
|
+
|
|
555
|
+
let distanceFromCamera = length(input.worldPos - camera.viewPos);
|
|
556
|
+
let fadeFactor = clamp((distanceFromCamera - 15.0) / 20.0, 0.0, 1.0);
|
|
557
|
+
reflectionColor *= (1.0 - fadeFactor * 0.3);
|
|
558
|
+
|
|
559
|
+
let diffuseColor = material.diffuseColor;
|
|
560
|
+
var finalColor = mix(diffuseColor, reflectionColor, material.reflectionLevel);
|
|
561
|
+
|
|
562
|
+
// Ground edge fade effect - smooth fade out at edges based on distance from center
|
|
563
|
+
let centerDist = length(input.worldPos.xz); // Distance from ground center in XZ plane
|
|
564
|
+
|
|
565
|
+
// Smoothstep for much smoother gradient transition
|
|
566
|
+
let t = clamp((centerDist - material.fadeStart) / (material.fadeEnd - material.fadeStart), 0.0, 1.0);
|
|
567
|
+
let edgeFade = 1.0 - smoothstep(0.0, 1.0, t);
|
|
568
|
+
finalColor *= edgeFade;
|
|
569
|
+
|
|
570
|
+
// Accumulate light contribution
|
|
571
|
+
var lightAccum = light.ambientColor.xyz;
|
|
572
|
+
for (var i = 0u; i < 4u; i++) {
|
|
573
|
+
let intensity = light.lights[i].color.w;
|
|
574
|
+
if (intensity > 0.0) {
|
|
575
|
+
let l = -light.lights[i].direction.xyz;
|
|
576
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
577
|
+
let radiance = light.lights[i].color.xyz * intensity;
|
|
578
|
+
lightAccum += radiance * nDotL;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Apply lighting to the blended color
|
|
583
|
+
let litColor = finalColor * lightAccum;
|
|
584
|
+
|
|
585
|
+
return vec4f(litColor, edgeFade);
|
|
586
|
+
}
|
|
587
|
+
`,
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
this.groundPipeline = this.createRenderPipeline({
|
|
591
|
+
label: "ground pipeline",
|
|
592
|
+
layout: groundPipelineLayout,
|
|
593
|
+
shaderModule: groundShaderModule,
|
|
594
|
+
vertexBuffers: fullVertexBuffers,
|
|
595
|
+
fragmentTarget: standardBlend,
|
|
596
|
+
cullMode: "back",
|
|
597
|
+
depthStencil: {
|
|
598
|
+
format: "depth24plus-stencil8",
|
|
599
|
+
depthWriteEnabled: true,
|
|
600
|
+
depthCompare: "less-equal",
|
|
601
|
+
},
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
// Create reflection pipeline (multisampled version for higher quality)
|
|
605
|
+
this.reflectionPipeline = this.createRenderPipeline({
|
|
606
|
+
label: "reflection pipeline",
|
|
607
|
+
layout: mainPipelineLayout,
|
|
608
|
+
shaderModule,
|
|
609
|
+
vertexBuffers: fullVertexBuffers,
|
|
610
|
+
fragmentTarget: {
|
|
611
|
+
format: this.presentationFormat,
|
|
612
|
+
blend: standardBlend.blend,
|
|
613
|
+
},
|
|
614
|
+
multisample: { count: this.sampleCount }, // Use same multisampling as main render
|
|
615
|
+
cullMode: "none",
|
|
616
|
+
depthStencil: {
|
|
617
|
+
format: "depth24plus-stencil8",
|
|
618
|
+
depthWriteEnabled: true,
|
|
619
|
+
depthCompare: "less-equal",
|
|
620
|
+
},
|
|
621
|
+
})
|
|
622
|
+
|
|
455
623
|
// Create bind group layout for outline pipelines
|
|
456
624
|
this.outlineBindGroupLayout = this.device.createBindGroupLayout({
|
|
457
625
|
label: "outline bind group layout",
|
|
@@ -1161,6 +1329,42 @@ export class Engine {
|
|
|
1161
1329
|
return true
|
|
1162
1330
|
}
|
|
1163
1331
|
|
|
1332
|
+
public addGround(options?: {
|
|
1333
|
+
width?: number
|
|
1334
|
+
height?: number
|
|
1335
|
+
diffuseColor?: Vec3
|
|
1336
|
+
reflectionLevel?: number
|
|
1337
|
+
reflectionTextureSize?: number
|
|
1338
|
+
fadeStart?: number
|
|
1339
|
+
fadeEnd?: number
|
|
1340
|
+
}): void {
|
|
1341
|
+
const opts = {
|
|
1342
|
+
width: 100,
|
|
1343
|
+
height: 100,
|
|
1344
|
+
diffuseColor: new Vec3(1, 1, 1),
|
|
1345
|
+
reflectionLevel: 0.5,
|
|
1346
|
+
reflectionTextureSize: 1024,
|
|
1347
|
+
fadeStart: 5.0,
|
|
1348
|
+
fadeEnd: 60.0,
|
|
1349
|
+
...options,
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// Create ground geometry
|
|
1353
|
+
this.createGroundGeometry(opts.width, opts.height)
|
|
1354
|
+
|
|
1355
|
+
this.createGroundMaterialBuffer(opts.diffuseColor, opts.reflectionLevel, opts.fadeStart, opts.fadeEnd)
|
|
1356
|
+
this.createReflectionTexture(opts.reflectionTextureSize)
|
|
1357
|
+
this.groundHasReflections = true
|
|
1358
|
+
|
|
1359
|
+
this.drawCalls.push({
|
|
1360
|
+
type: "ground",
|
|
1361
|
+
count: 6, // 2 triangles, 3 indices each
|
|
1362
|
+
firstIndex: 0,
|
|
1363
|
+
bindGroup: this.groundReflectionBindGroup!,
|
|
1364
|
+
materialName: "Ground",
|
|
1365
|
+
})
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1164
1368
|
private updateLightBuffer() {
|
|
1165
1369
|
this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData)
|
|
1166
1370
|
}
|
|
@@ -1379,6 +1583,143 @@ export class Engine {
|
|
|
1379
1583
|
await this.setupMaterials(model)
|
|
1380
1584
|
}
|
|
1381
1585
|
|
|
1586
|
+
private createGroundGeometry(width: number = 100, height: number = 100) {
|
|
1587
|
+
const halfWidth = width / 2
|
|
1588
|
+
const halfHeight = height / 2
|
|
1589
|
+
|
|
1590
|
+
const vertices = new Float32Array([
|
|
1591
|
+
// Bottom-left
|
|
1592
|
+
-halfWidth,
|
|
1593
|
+
0,
|
|
1594
|
+
-halfHeight, // position
|
|
1595
|
+
0,
|
|
1596
|
+
1,
|
|
1597
|
+
0, // normal (up)
|
|
1598
|
+
0,
|
|
1599
|
+
0, // uv
|
|
1600
|
+
|
|
1601
|
+
// Bottom-right
|
|
1602
|
+
halfWidth,
|
|
1603
|
+
0,
|
|
1604
|
+
-halfHeight, // position
|
|
1605
|
+
0,
|
|
1606
|
+
1,
|
|
1607
|
+
0, // normal (up)
|
|
1608
|
+
1,
|
|
1609
|
+
0, // uv
|
|
1610
|
+
|
|
1611
|
+
// Top-right
|
|
1612
|
+
halfWidth,
|
|
1613
|
+
0,
|
|
1614
|
+
halfHeight, // position
|
|
1615
|
+
0,
|
|
1616
|
+
1,
|
|
1617
|
+
0, // normal (up)
|
|
1618
|
+
1,
|
|
1619
|
+
1, // uv
|
|
1620
|
+
|
|
1621
|
+
// Top-left
|
|
1622
|
+
-halfWidth,
|
|
1623
|
+
0,
|
|
1624
|
+
halfHeight, // position
|
|
1625
|
+
0,
|
|
1626
|
+
1,
|
|
1627
|
+
0, // normal (up)
|
|
1628
|
+
0,
|
|
1629
|
+
1, // uv
|
|
1630
|
+
])
|
|
1631
|
+
|
|
1632
|
+
// Create indices for two triangles
|
|
1633
|
+
const indices = new Uint16Array([
|
|
1634
|
+
0,
|
|
1635
|
+
1,
|
|
1636
|
+
2, // First triangle
|
|
1637
|
+
0,
|
|
1638
|
+
2,
|
|
1639
|
+
3, // Second triangle
|
|
1640
|
+
])
|
|
1641
|
+
|
|
1642
|
+
// Create vertex buffer
|
|
1643
|
+
this.groundVertexBuffer = this.device.createBuffer({
|
|
1644
|
+
label: "ground vertex buffer",
|
|
1645
|
+
size: vertices.byteLength,
|
|
1646
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1647
|
+
})
|
|
1648
|
+
this.device.queue.writeBuffer(this.groundVertexBuffer, 0, vertices)
|
|
1649
|
+
|
|
1650
|
+
this.groundIndexBuffer = this.device.createBuffer({
|
|
1651
|
+
label: "ground index buffer",
|
|
1652
|
+
size: indices.byteLength,
|
|
1653
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
1654
|
+
})
|
|
1655
|
+
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices)
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
private createGroundMaterialBuffer(
|
|
1659
|
+
diffuseColor: Vec3 = new Vec3(1, 1, 1),
|
|
1660
|
+
reflectionLevel: number = 0.5,
|
|
1661
|
+
fadeStart: number = 5.0,
|
|
1662
|
+
fadeEnd: number = 60.0
|
|
1663
|
+
) {
|
|
1664
|
+
const materialData = new Float32Array([
|
|
1665
|
+
diffuseColor.x,
|
|
1666
|
+
diffuseColor.y,
|
|
1667
|
+
diffuseColor.z, // diffuseColor (12 bytes)
|
|
1668
|
+
reflectionLevel, // reflectionLevel (4 bytes)
|
|
1669
|
+
fadeStart, // fadeStart (4 bytes)
|
|
1670
|
+
fadeEnd, // fadeEnd (4 bytes)
|
|
1671
|
+
0, // padding (4 bytes)
|
|
1672
|
+
0, // padding (4 bytes)
|
|
1673
|
+
0, // padding (4 bytes)
|
|
1674
|
+
0, // padding (4 bytes)
|
|
1675
|
+
])
|
|
1676
|
+
|
|
1677
|
+
this.groundMaterialUniformBuffer = this.device.createBuffer({
|
|
1678
|
+
label: "ground material uniform buffer",
|
|
1679
|
+
size: materialData.byteLength,
|
|
1680
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1681
|
+
})
|
|
1682
|
+
this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, materialData)
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
private createReflectionTexture(size: number = 1024) {
|
|
1686
|
+
this.groundReflectionTexture = this.device.createTexture({
|
|
1687
|
+
label: "ground reflection texture",
|
|
1688
|
+
size: [size, size],
|
|
1689
|
+
sampleCount: this.sampleCount,
|
|
1690
|
+
format: this.presentationFormat,
|
|
1691
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1692
|
+
})
|
|
1693
|
+
|
|
1694
|
+
this.groundReflectionResolveTexture = this.device.createTexture({
|
|
1695
|
+
label: "ground reflection resolve texture",
|
|
1696
|
+
size: [size, size],
|
|
1697
|
+
format: this.presentationFormat,
|
|
1698
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1699
|
+
})
|
|
1700
|
+
|
|
1701
|
+
this.groundReflectionDepthTexture = this.device.createTexture({
|
|
1702
|
+
label: "ground reflection depth texture",
|
|
1703
|
+
size: [size, size],
|
|
1704
|
+
sampleCount: this.sampleCount,
|
|
1705
|
+
format: "depth24plus-stencil8",
|
|
1706
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1707
|
+
})
|
|
1708
|
+
|
|
1709
|
+
// Create a bind group for the reflection texture that can be used in the ground material
|
|
1710
|
+
this.groundReflectionBindGroup = this.device.createBindGroup({
|
|
1711
|
+
label: "ground reflection bind group",
|
|
1712
|
+
layout: this.groundBindGroupLayout,
|
|
1713
|
+
entries: [
|
|
1714
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1715
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1716
|
+
{ binding: 2, resource: this.groundReflectionResolveTexture!.createView() }, // Use resolve texture for sampling
|
|
1717
|
+
{ binding: 3, resource: this.materialSampler },
|
|
1718
|
+
{ binding: 4, resource: { buffer: this.groundMaterialUniformBuffer! } },
|
|
1719
|
+
],
|
|
1720
|
+
})
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1382
1723
|
private async setupMaterials(model: Model) {
|
|
1383
1724
|
const materials = model.getMaterials()
|
|
1384
1725
|
if (materials.length === 0) {
|
|
@@ -1598,19 +1939,135 @@ export class Engine {
|
|
|
1598
1939
|
}
|
|
1599
1940
|
|
|
1600
1941
|
// Helper: Render eyes with stencil writing (for post-alpha-eye effect)
|
|
1601
|
-
private renderEyes(pass: GPURenderPassEncoder) {
|
|
1602
|
-
|
|
1603
|
-
|
|
1942
|
+
private renderEyes(pass: GPURenderPassEncoder, useReflectionPipeline: boolean = false) {
|
|
1943
|
+
if (useReflectionPipeline) {
|
|
1944
|
+
// For reflections, use the basic reflection pipeline instead of specialized eye pipeline
|
|
1945
|
+
pass.setPipeline(this.reflectionPipeline)
|
|
1946
|
+
for (const draw of this.drawCalls) {
|
|
1947
|
+
if (draw.type === "eye") {
|
|
1948
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1949
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
} else {
|
|
1953
|
+
pass.setPipeline(this.eyePipeline)
|
|
1954
|
+
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
1955
|
+
for (const draw of this.drawCalls) {
|
|
1956
|
+
if (draw.type === "eye" && this.shouldRenderDrawCall(draw)) {
|
|
1957
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1958
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
private renderGround(pass: GPURenderPassEncoder) {
|
|
1965
|
+
if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer) {
|
|
1966
|
+
return
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
if (this.groundReflectionTexture) {
|
|
1970
|
+
this.renderReflectionTexture()
|
|
1971
|
+
}
|
|
1972
|
+
pass.setPipeline(this.groundPipeline)
|
|
1973
|
+
pass.setVertexBuffer(0, this.groundVertexBuffer)
|
|
1974
|
+
pass.setIndexBuffer(this.groundIndexBuffer, "uint16")
|
|
1975
|
+
|
|
1604
1976
|
for (const draw of this.drawCalls) {
|
|
1605
|
-
if (draw.type === "
|
|
1977
|
+
if (draw.type === "ground" && this.shouldRenderDrawCall(draw)) {
|
|
1606
1978
|
pass.setBindGroup(0, draw.bindGroup)
|
|
1607
1979
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1608
1980
|
}
|
|
1609
1981
|
}
|
|
1610
1982
|
}
|
|
1611
1983
|
|
|
1984
|
+
private renderReflectionTexture() {
|
|
1985
|
+
if (!this.groundReflectionTexture) return
|
|
1986
|
+
|
|
1987
|
+
const mirrorMatrix = this.createMirrorMatrix(new Vec3(0, 1, 0), 0)
|
|
1988
|
+
this.updateCameraUniforms()
|
|
1989
|
+
|
|
1990
|
+
const reflectionEncoder = this.device.createCommandEncoder()
|
|
1991
|
+
const reflectionPassDescriptor: GPURenderPassDescriptor = {
|
|
1992
|
+
label: "reflection render pass",
|
|
1993
|
+
colorAttachments: [
|
|
1994
|
+
{
|
|
1995
|
+
view: this.groundReflectionTexture!.createView(),
|
|
1996
|
+
resolveTarget: this.groundReflectionResolveTexture!.createView(),
|
|
1997
|
+
clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, // White
|
|
1998
|
+
loadOp: "clear",
|
|
1999
|
+
storeOp: "store",
|
|
2000
|
+
},
|
|
2001
|
+
],
|
|
2002
|
+
depthStencilAttachment: {
|
|
2003
|
+
view: this.groundReflectionDepthTexture!.createView(),
|
|
2004
|
+
depthClearValue: 1.0,
|
|
2005
|
+
depthLoadOp: "clear",
|
|
2006
|
+
depthStoreOp: "store",
|
|
2007
|
+
stencilClearValue: 0,
|
|
2008
|
+
stencilLoadOp: "clear",
|
|
2009
|
+
stencilStoreOp: "discard",
|
|
2010
|
+
},
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
const reflectionPass = reflectionEncoder.beginRenderPass(reflectionPassDescriptor)
|
|
2014
|
+
|
|
2015
|
+
if (this.currentModel) {
|
|
2016
|
+
reflectionPass.setVertexBuffer(0, this.vertexBuffer)
|
|
2017
|
+
reflectionPass.setVertexBuffer(1, this.jointsBuffer)
|
|
2018
|
+
reflectionPass.setVertexBuffer(2, this.weightsBuffer)
|
|
2019
|
+
reflectionPass.setIndexBuffer(this.indexBuffer!, "uint32")
|
|
2020
|
+
|
|
2021
|
+
this.writeMirrorTransformedSkinMatrices(mirrorMatrix)
|
|
2022
|
+
reflectionPass.setPipeline(this.reflectionPipeline)
|
|
2023
|
+
for (const draw of this.drawCalls) {
|
|
2024
|
+
if (draw.type === "opaque" && this.shouldRenderDrawCall(draw)) {
|
|
2025
|
+
reflectionPass.setBindGroup(0, draw.bindGroup)
|
|
2026
|
+
reflectionPass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// Render eyes (using reflection pipeline)
|
|
2031
|
+
this.renderEyes(reflectionPass, true)
|
|
2032
|
+
|
|
2033
|
+
// Render hair (using reflection pipeline)
|
|
2034
|
+
this.renderHair(reflectionPass, true)
|
|
2035
|
+
|
|
2036
|
+
// Render transparent objects
|
|
2037
|
+
for (const draw of this.drawCalls) {
|
|
2038
|
+
if (draw.type === "transparent" && this.shouldRenderDrawCall(draw)) {
|
|
2039
|
+
reflectionPass.setBindGroup(0, draw.bindGroup)
|
|
2040
|
+
reflectionPass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
this.drawOutlines(reflectionPass, true, true)
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
reflectionPass.end()
|
|
2048
|
+
|
|
2049
|
+
// Submit reflection rendering commands
|
|
2050
|
+
const reflectionCommandBuffer = reflectionEncoder.finish()
|
|
2051
|
+
this.device.queue.submit([reflectionCommandBuffer])
|
|
2052
|
+
|
|
2053
|
+
// Restore original skin matrices
|
|
2054
|
+
this.updateSkinMatrices()
|
|
2055
|
+
}
|
|
2056
|
+
|
|
1612
2057
|
// Helper: Render hair with post-alpha-eye effect (depth pre-pass + stencil-based shading + outlines)
|
|
1613
|
-
private renderHair(pass: GPURenderPassEncoder) {
|
|
2058
|
+
private renderHair(pass: GPURenderPassEncoder, useReflectionPipeline: boolean = false) {
|
|
2059
|
+
if (useReflectionPipeline) {
|
|
2060
|
+
// For reflections, use the basic reflection pipeline for all hair
|
|
2061
|
+
pass.setPipeline(this.reflectionPipeline)
|
|
2062
|
+
for (const draw of this.drawCalls) {
|
|
2063
|
+
if (draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") {
|
|
2064
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
2065
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
return
|
|
2069
|
+
}
|
|
2070
|
+
|
|
1614
2071
|
// Hair depth pre-pass (reduces overdraw via early depth rejection)
|
|
1615
2072
|
const hasHair = this.drawCalls.some(
|
|
1616
2073
|
(d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(d)
|
|
@@ -1707,7 +2164,7 @@ export class Engine {
|
|
|
1707
2164
|
const viewMatrix = this.camera.getViewMatrix()
|
|
1708
2165
|
const projectionMatrix = this.camera.getProjectionMatrix()
|
|
1709
2166
|
|
|
1710
|
-
// Convert screen coordinates to world space ray
|
|
2167
|
+
// Convert screen coordinates to world space ray
|
|
1711
2168
|
const canvas = this.canvas
|
|
1712
2169
|
const rect = canvas.getBoundingClientRect()
|
|
1713
2170
|
|
|
@@ -1969,7 +2426,12 @@ export class Engine {
|
|
|
1969
2426
|
// Pass 3: Hair rendering (depth pre-pass + shading + outlines)
|
|
1970
2427
|
this.renderHair(pass)
|
|
1971
2428
|
|
|
1972
|
-
// Pass 4:
|
|
2429
|
+
// Pass 4: Ground (with reflections)
|
|
2430
|
+
if (this.groundHasReflections) {
|
|
2431
|
+
this.renderGround(pass)
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
// Pass 5: Transparent
|
|
1973
2435
|
pass.setPipeline(this.modelPipeline)
|
|
1974
2436
|
for (const draw of this.drawCalls) {
|
|
1975
2437
|
if (draw.type === "transparent" && this.shouldRenderDrawCall(draw)) {
|
|
@@ -2127,7 +2589,12 @@ export class Engine {
|
|
|
2127
2589
|
this.skinMatricesVersion++
|
|
2128
2590
|
}
|
|
2129
2591
|
|
|
2130
|
-
private drawOutlines(pass: GPURenderPassEncoder, transparent: boolean) {
|
|
2592
|
+
private drawOutlines(pass: GPURenderPassEncoder, transparent: boolean, useReflectionPipeline: boolean = false) {
|
|
2593
|
+
if (useReflectionPipeline) {
|
|
2594
|
+
// Skip outlines for reflections - not critical for the effect
|
|
2595
|
+
return
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2131
2598
|
pass.setPipeline(this.outlinePipeline)
|
|
2132
2599
|
const outlineType: DrawCallType = transparent ? "transparent-outline" : "opaque-outline"
|
|
2133
2600
|
for (const draw of this.drawCalls) {
|
|
@@ -2162,4 +2629,51 @@ export class Engine {
|
|
|
2162
2629
|
this.lastFpsUpdate = now
|
|
2163
2630
|
}
|
|
2164
2631
|
}
|
|
2632
|
+
|
|
2633
|
+
private createMirrorMatrix(planeNormal: Vec3, planeDistance: number): Mat4 {
|
|
2634
|
+
// Create reflection matrix across a plane
|
|
2635
|
+
const n = planeNormal.normalize()
|
|
2636
|
+
|
|
2637
|
+
return new Mat4(
|
|
2638
|
+
new Float32Array([
|
|
2639
|
+
1 - 2 * n.x * n.x,
|
|
2640
|
+
-2 * n.x * n.y,
|
|
2641
|
+
-2 * n.x * n.z,
|
|
2642
|
+
0,
|
|
2643
|
+
-2 * n.y * n.x,
|
|
2644
|
+
1 - 2 * n.y * n.y,
|
|
2645
|
+
-2 * n.y * n.z,
|
|
2646
|
+
0,
|
|
2647
|
+
-2 * n.z * n.x,
|
|
2648
|
+
-2 * n.z * n.y,
|
|
2649
|
+
1 - 2 * n.z * n.z,
|
|
2650
|
+
0,
|
|
2651
|
+
-2 * planeDistance * n.x,
|
|
2652
|
+
-2 * planeDistance * n.y,
|
|
2653
|
+
-2 * planeDistance * n.z,
|
|
2654
|
+
1,
|
|
2655
|
+
])
|
|
2656
|
+
)
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
private writeMirrorTransformedSkinMatrices(mirrorMatrix: Mat4) {
|
|
2660
|
+
if (!this.currentModel || !this.skinMatrixBuffer) return
|
|
2661
|
+
|
|
2662
|
+
const originalMatrices = this.currentModel.getSkinMatrices()
|
|
2663
|
+
const transformedMatrices = new Float32Array(originalMatrices.length)
|
|
2664
|
+
|
|
2665
|
+
for (let i = 0; i < originalMatrices.length; i += 16) {
|
|
2666
|
+
const boneMatrixValues = new Float32Array(16)
|
|
2667
|
+
for (let j = 0; j < 16; j++) {
|
|
2668
|
+
boneMatrixValues[j] = originalMatrices[i + j]
|
|
2669
|
+
}
|
|
2670
|
+
const boneMatrix = new Mat4(boneMatrixValues)
|
|
2671
|
+
const transformed = mirrorMatrix.multiply(boneMatrix)
|
|
2672
|
+
for (let j = 0; j < 16; j++) {
|
|
2673
|
+
transformedMatrices[i + j] = transformed.values[j]
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
this.device.queue.writeBuffer(this.skinMatrixBuffer, 0, transformedMatrices)
|
|
2678
|
+
}
|
|
2165
2679
|
}
|