reze-engine 0.4.0 → 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 +32 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +488 -14
- package/package.json +2 -3
- package/src/engine.ts +567 -14
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,8 +21,12 @@ 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;
|
|
27
|
+
// Double-tap detection
|
|
28
|
+
this.lastTouchTime = 0;
|
|
29
|
+
this.DOUBLE_TAP_DELAY = 300; // ms
|
|
26
30
|
this.currentModel = null;
|
|
27
31
|
this.modelDir = "";
|
|
28
32
|
this.textureCache = new Map();
|
|
@@ -42,7 +46,7 @@ export class Engine {
|
|
|
42
46
|
};
|
|
43
47
|
this.animationFrameId = null;
|
|
44
48
|
this.renderLoopCallback = null;
|
|
45
|
-
this.
|
|
49
|
+
this.handleCanvasDoubleClick = (event) => {
|
|
46
50
|
if (!this.onRaycast || !this.currentModel)
|
|
47
51
|
return;
|
|
48
52
|
const rect = this.canvas.getBoundingClientRect();
|
|
@@ -50,6 +54,31 @@ export class Engine {
|
|
|
50
54
|
const y = event.clientY - rect.top;
|
|
51
55
|
this.performRaycast(x, y);
|
|
52
56
|
};
|
|
57
|
+
this.handleCanvasTouch = (event) => {
|
|
58
|
+
if (!this.onRaycast || !this.currentModel)
|
|
59
|
+
return;
|
|
60
|
+
// Prevent default to avoid triggering mouse events
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
// Get the first touch
|
|
63
|
+
const touch = event.changedTouches[0];
|
|
64
|
+
if (!touch)
|
|
65
|
+
return;
|
|
66
|
+
const currentTime = Date.now();
|
|
67
|
+
const timeDiff = currentTime - this.lastTouchTime;
|
|
68
|
+
// Check for double-tap (within delay threshold)
|
|
69
|
+
if (timeDiff < this.DOUBLE_TAP_DELAY) {
|
|
70
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
71
|
+
const x = touch.clientX - rect.left;
|
|
72
|
+
const y = touch.clientY - rect.top;
|
|
73
|
+
this.performRaycast(x, y);
|
|
74
|
+
// Reset last touch time to prevent triple-tap triggering double-tap
|
|
75
|
+
this.lastTouchTime = 0;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Single tap - update last touch time for potential double-tap
|
|
79
|
+
this.lastTouchTime = currentTime;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
53
82
|
this.canvas = canvas;
|
|
54
83
|
if (options) {
|
|
55
84
|
this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor;
|
|
@@ -324,6 +353,154 @@ export class Engine {
|
|
|
324
353
|
depthCompare: "less-equal",
|
|
325
354
|
},
|
|
326
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
|
+
});
|
|
327
504
|
// Create bind group layout for outline pipelines
|
|
328
505
|
this.outlineBindGroupLayout = this.device.createBindGroupLayout({
|
|
329
506
|
label: "outline bind group layout",
|
|
@@ -849,9 +1026,10 @@ export class Engine {
|
|
|
849
1026
|
this.resizeObserver = new ResizeObserver(() => this.handleResize());
|
|
850
1027
|
this.resizeObserver.observe(this.canvas);
|
|
851
1028
|
this.handleResize();
|
|
852
|
-
// Setup raycasting click handler
|
|
1029
|
+
// Setup raycasting double-click handler for desktop
|
|
853
1030
|
if (this.onRaycast) {
|
|
854
|
-
this.canvas.addEventListener("
|
|
1031
|
+
this.canvas.addEventListener("dblclick", this.handleCanvasDoubleClick);
|
|
1032
|
+
this.canvas.addEventListener("touchend", this.handleCanvasTouch);
|
|
855
1033
|
}
|
|
856
1034
|
}
|
|
857
1035
|
handleResize() {
|
|
@@ -980,6 +1158,30 @@ export class Engine {
|
|
|
980
1158
|
this.updateLightBuffer();
|
|
981
1159
|
return true;
|
|
982
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
|
+
}
|
|
983
1185
|
updateLightBuffer() {
|
|
984
1186
|
this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData);
|
|
985
1187
|
}
|
|
@@ -1029,6 +1231,11 @@ export class Engine {
|
|
|
1029
1231
|
this.stopAnimation();
|
|
1030
1232
|
if (this.camera)
|
|
1031
1233
|
this.camera.detachControl();
|
|
1234
|
+
// Remove raycasting event listeners
|
|
1235
|
+
if (this.onRaycast) {
|
|
1236
|
+
this.canvas.removeEventListener("dblclick", this.handleCanvasDoubleClick);
|
|
1237
|
+
this.canvas.removeEventListener("touchend", this.handleCanvasTouch);
|
|
1238
|
+
}
|
|
1032
1239
|
if (this.resizeObserver) {
|
|
1033
1240
|
this.resizeObserver.disconnect();
|
|
1034
1241
|
this.resizeObserver = null;
|
|
@@ -1142,6 +1349,124 @@ export class Engine {
|
|
|
1142
1349
|
}
|
|
1143
1350
|
await this.setupMaterials(model);
|
|
1144
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
|
+
}
|
|
1145
1470
|
async setupMaterials(model) {
|
|
1146
1471
|
const materials = model.getMaterials();
|
|
1147
1472
|
if (materials.length === 0) {
|
|
@@ -1335,18 +1660,119 @@ export class Engine {
|
|
|
1335
1660
|
}
|
|
1336
1661
|
}
|
|
1337
1662
|
// Helper: Render eyes with stencil writing (for post-alpha-eye effect)
|
|
1338
|
-
renderEyes(pass) {
|
|
1339
|
-
|
|
1340
|
-
|
|
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");
|
|
1341
1695
|
for (const draw of this.drawCalls) {
|
|
1342
|
-
if (draw.type === "
|
|
1696
|
+
if (draw.type === "ground" && this.shouldRenderDrawCall(draw)) {
|
|
1343
1697
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1344
1698
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1345
1699
|
}
|
|
1346
1700
|
}
|
|
1347
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
|
+
}
|
|
1348
1763
|
// Helper: Render hair with post-alpha-eye effect (depth pre-pass + stencil-based shading + outlines)
|
|
1349
|
-
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
|
+
}
|
|
1350
1776
|
// Hair depth pre-pass (reduces overdraw via early depth rejection)
|
|
1351
1777
|
const hasHair = this.drawCalls.some((d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(d));
|
|
1352
1778
|
if (hasHair) {
|
|
@@ -1396,7 +1822,7 @@ export class Engine {
|
|
|
1396
1822
|
// Get camera matrices
|
|
1397
1823
|
const viewMatrix = this.camera.getViewMatrix();
|
|
1398
1824
|
const projectionMatrix = this.camera.getProjectionMatrix();
|
|
1399
|
-
// Convert screen coordinates to world space ray
|
|
1825
|
+
// Convert screen coordinates to world space ray
|
|
1400
1826
|
const canvas = this.canvas;
|
|
1401
1827
|
const rect = canvas.getBoundingClientRect();
|
|
1402
1828
|
// Convert to clip space (-1 to 1)
|
|
@@ -1430,7 +1856,7 @@ export class Engine {
|
|
|
1430
1856
|
const skinning = this.currentModel.getSkinning();
|
|
1431
1857
|
if (!baseVertices || !indices || !skinning) {
|
|
1432
1858
|
if (this.onRaycast) {
|
|
1433
|
-
this.onRaycast(null);
|
|
1859
|
+
this.onRaycast(null, screenX, screenY);
|
|
1434
1860
|
}
|
|
1435
1861
|
return;
|
|
1436
1862
|
}
|
|
@@ -1556,7 +1982,7 @@ export class Engine {
|
|
|
1556
1982
|
}
|
|
1557
1983
|
// Call the callback with the result
|
|
1558
1984
|
if (this.onRaycast) {
|
|
1559
|
-
this.onRaycast(closestHit?.materialName || null);
|
|
1985
|
+
this.onRaycast(closestHit?.materialName || null, screenX, screenY);
|
|
1560
1986
|
}
|
|
1561
1987
|
}
|
|
1562
1988
|
// Render strategy: 1) Opaque non-eye/hair 2) Eyes (stencil=1) 3) Hair (depth pre-pass + split by stencil) 4) Transparent 5) Bloom
|
|
@@ -1602,7 +2028,11 @@ export class Engine {
|
|
|
1602
2028
|
this.drawOutlines(pass, false);
|
|
1603
2029
|
// Pass 3: Hair rendering (depth pre-pass + shading + outlines)
|
|
1604
2030
|
this.renderHair(pass);
|
|
1605
|
-
// Pass 4:
|
|
2031
|
+
// Pass 4: Ground (with reflections)
|
|
2032
|
+
if (this.groundHasReflections) {
|
|
2033
|
+
this.renderGround(pass);
|
|
2034
|
+
}
|
|
2035
|
+
// Pass 5: Transparent
|
|
1606
2036
|
pass.setPipeline(this.modelPipeline);
|
|
1607
2037
|
for (const draw of this.drawCalls) {
|
|
1608
2038
|
if (draw.type === "transparent" && this.shouldRenderDrawCall(draw)) {
|
|
@@ -1733,7 +2163,11 @@ export class Engine {
|
|
|
1733
2163
|
// Increment version to invalidate cached skinned vertices
|
|
1734
2164
|
this.skinMatricesVersion++;
|
|
1735
2165
|
}
|
|
1736
|
-
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
|
+
}
|
|
1737
2171
|
pass.setPipeline(this.outlinePipeline);
|
|
1738
2172
|
const outlineType = transparent ? "transparent-outline" : "opaque-outline";
|
|
1739
2173
|
for (const draw of this.drawCalls) {
|
|
@@ -1765,4 +2199,44 @@ export class Engine {
|
|
|
1765
2199
|
this.lastFpsUpdate = now;
|
|
1766
2200
|
}
|
|
1767
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
|
+
}
|
|
1768
2242
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reze-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "A WebGPU-based MMD model renderer",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -41,6 +41,5 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^20",
|
|
43
43
|
"typescript": "^5"
|
|
44
|
-
}
|
|
45
|
-
"peerDependencies": {}
|
|
44
|
+
}
|
|
46
45
|
}
|