reze-engine 0.22.0 → 0.23.0
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/README.md +50 -44
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +10 -5
- package/dist/engine.d.ts +24 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +40 -8
- package/dist/physics-debug.d.ts +30 -0
- package/dist/physics-debug.d.ts.map +1 -0
- package/dist/physics-debug.js +526 -0
- package/dist/shaders/passes/physics-debug.d.ts +2 -0
- package/dist/shaders/passes/physics-debug.d.ts.map +1 -0
- package/dist/shaders/passes/physics-debug.js +69 -0
- package/package.json +3 -3
- package/src/camera.ts +7 -4
- package/src/engine.ts +54 -10
- package/dist/gpu-profile.d.ts +0 -19
- package/dist/gpu-profile.d.ts.map +0 -1
- package/dist/gpu-profile.js +0 -120
- package/dist/physics/profile.d.ts +0 -18
- package/dist/physics/profile.d.ts.map +0 -1
- package/dist/physics/profile.js +0 -44
package/src/engine.ts
CHANGED
|
@@ -84,7 +84,7 @@ const PRESET_NAME_HINTS: Array<[MaterialPreset, string[]]> = [
|
|
|
84
84
|
],
|
|
85
85
|
["hair", ["前髪", "後髪", "髪", "髮", "头发", "頭髪", "もみあげ", "アホ毛", "ヘア", "hair", "ahoge", "bang"]],
|
|
86
86
|
["body", ["肌", "皮肤", "skin"]],
|
|
87
|
-
["metal", ["金属", "メタル", "metal"]],
|
|
87
|
+
["metal", ["金属", "メタル", "metal", "earring", "耳环", "耳環"]],
|
|
88
88
|
[
|
|
89
89
|
"cloth_smooth",
|
|
90
90
|
[
|
|
@@ -103,15 +103,20 @@ const PRESET_NAME_HINTS: Array<[MaterialPreset, string[]]> = [
|
|
|
103
103
|
"飾",
|
|
104
104
|
"饰",
|
|
105
105
|
"尾",
|
|
106
|
+
"套", // 外套 (coat), 手套 (gloves)
|
|
107
|
+
"腿", // 腿环 (leg ring/garter) and other leg-wear accessories
|
|
106
108
|
"skirt",
|
|
107
109
|
"dress",
|
|
108
110
|
"ribbon",
|
|
109
111
|
"sleeve",
|
|
110
112
|
"shoes",
|
|
113
|
+
"shirt",
|
|
114
|
+
"short", // shorts
|
|
111
115
|
"boot",
|
|
112
116
|
"hat",
|
|
113
117
|
"cloth",
|
|
114
118
|
"accessor",
|
|
119
|
+
"trigger",
|
|
115
120
|
],
|
|
116
121
|
],
|
|
117
122
|
]
|
|
@@ -1703,13 +1708,35 @@ export class Engine {
|
|
|
1703
1708
|
window.addEventListener("mouseup", this.handleGizmoMouseUp)
|
|
1704
1709
|
}
|
|
1705
1710
|
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1711
|
+
/** When set, render resolution is pinned to this size instead of tracking the
|
|
1712
|
+
* canvas's CSS size × devicePixelRatio (see setRenderSize). */
|
|
1713
|
+
private fixedRenderSize: { width: number; height: number } | null = null
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* Pin the render resolution (canvas backing store + every render target) to an
|
|
1717
|
+
* explicit size, decoupled from the canvas's CSS layout size — for offline
|
|
1718
|
+
* rendering at arbitrary resolution (video export). On screen the browser scales
|
|
1719
|
+
* the buffer to the layout box, so the canvas may display letterboxed/stretched
|
|
1720
|
+
* while pinned; hosts typically cover it with an export overlay. Pass null to
|
|
1721
|
+
* return to CSS-size × devicePixelRatio tracking. Applies immediately (targets
|
|
1722
|
+
* rebuild before this returns), so the next render() is at the new size.
|
|
1723
|
+
*/
|
|
1724
|
+
setRenderSize(width: number, height: number): void
|
|
1725
|
+
setRenderSize(size: null): void
|
|
1726
|
+
setRenderSize(widthOrNull: number | null, height?: number): void {
|
|
1727
|
+
this.fixedRenderSize =
|
|
1728
|
+
widthOrNull === null
|
|
1729
|
+
? null
|
|
1730
|
+
: { width: Math.max(1, Math.floor(widthOrNull)), height: Math.max(1, Math.floor(height ?? 1)) }
|
|
1731
|
+
this.resizePending = false
|
|
1732
|
+
this.handleResize()
|
|
1733
|
+
}
|
|
1709
1734
|
|
|
1735
|
+
private handleResize() {
|
|
1736
|
+
// Fixed override (offline/video rendering) wins; otherwise track CSS size × dpr.
|
|
1710
1737
|
const dpr = window.devicePixelRatio || 1
|
|
1711
|
-
const width = Math.floor(
|
|
1712
|
-
const height = Math.floor(
|
|
1738
|
+
const width = this.fixedRenderSize ? this.fixedRenderSize.width : Math.floor(this.canvas.clientWidth * dpr)
|
|
1739
|
+
const height = this.fixedRenderSize ? this.fixedRenderSize.height : Math.floor(this.canvas.clientHeight * dpr)
|
|
1713
1740
|
|
|
1714
1741
|
if (!this.multisampleTexture || this.canvas.width !== width || this.canvas.height !== height) {
|
|
1715
1742
|
this.canvas.width = width
|
|
@@ -3902,15 +3929,32 @@ export class Engine {
|
|
|
3902
3929
|
render() {
|
|
3903
3930
|
if (!this.multisampleTexture || !this.camera || !this.device) return
|
|
3904
3931
|
|
|
3932
|
+
const currentTime = performance.now()
|
|
3933
|
+
const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016
|
|
3934
|
+
this.lastFrameTime = currentTime
|
|
3935
|
+
this.renderWithDelta(deltaTime)
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
/**
|
|
3939
|
+
* Render one frame advancing every clock — animation, physics, tweens, and the
|
|
3940
|
+
* camera VMD — by exactly `deltaSeconds`, independent of wall time. This is the
|
|
3941
|
+
* offline-rendering primitive (video export): call it N times with 1/fps and the
|
|
3942
|
+
* result is deterministic whether the machine renders faster or slower than
|
|
3943
|
+
* realtime. Also resets the realtime clock so a later render() (returning to the
|
|
3944
|
+
* live loop) doesn't see the export's wall-clock gap as one giant delta.
|
|
3945
|
+
*/
|
|
3946
|
+
renderFrame(deltaSeconds: number) {
|
|
3947
|
+
if (!this.multisampleTexture || !this.camera || !this.device) return
|
|
3948
|
+
this.lastFrameTime = performance.now()
|
|
3949
|
+
this.renderWithDelta(deltaSeconds)
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
private renderWithDelta(deltaTime: number) {
|
|
3905
3953
|
if (this.resizePending) {
|
|
3906
3954
|
this.resizePending = false
|
|
3907
3955
|
this.handleResize()
|
|
3908
3956
|
}
|
|
3909
3957
|
|
|
3910
|
-
const currentTime = performance.now()
|
|
3911
|
-
const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016
|
|
3912
|
-
this.lastFrameTime = currentTime
|
|
3913
|
-
|
|
3914
3958
|
const hasModels = this.modelInstances.size > 0
|
|
3915
3959
|
if (hasModels) {
|
|
3916
3960
|
this.updateInstances(deltaTime)
|
package/dist/gpu-profile.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export declare class GpuTimestamp {
|
|
2
|
-
enabled: boolean;
|
|
3
|
-
readonly supported: boolean;
|
|
4
|
-
private device;
|
|
5
|
-
private querySet;
|
|
6
|
-
private resolveBuffer;
|
|
7
|
-
private readbackBuffer;
|
|
8
|
-
private nextSlot;
|
|
9
|
-
private frameLabels;
|
|
10
|
-
private inFlight;
|
|
11
|
-
private pendingLabels;
|
|
12
|
-
constructor(device: GPUDevice);
|
|
13
|
-
private ensureCreated;
|
|
14
|
-
beginFrame(): void;
|
|
15
|
-
wrap(desc: GPURenderPassDescriptor, label: string): GPURenderPassDescriptor;
|
|
16
|
-
endFrame(encoder: GPUCommandEncoder): void;
|
|
17
|
-
afterSubmit(): void;
|
|
18
|
-
}
|
|
19
|
-
//# sourceMappingURL=gpu-profile.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"gpu-profile.d.ts","sourceRoot":"","sources":["../src/gpu-profile.ts"],"names":[],"mappings":"AAoBA,qBAAa,YAAY;IACvB,OAAO,UAAQ;IACf,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,QAAQ,CAAQ;IAKxB,OAAO,CAAC,aAAa,CAAwB;gBAEjC,MAAM,EAAE,SAAS;IAK7B,OAAO,CAAC,aAAa;IAiBrB,UAAU,IAAI,IAAI;IAWlB,IAAI,CAAC,IAAI,EAAE,uBAAuB,EAAE,KAAK,EAAE,MAAM,GAAG,uBAAuB;IAqB3E,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAW1C,WAAW,IAAI,IAAI;CAwBpB"}
|
package/dist/gpu-profile.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
// WebGPU timestamp-query profiling. Records pass durations into the same
|
|
2
|
-
// profiler module as CPU phases (so the snapshot table mixes both with a
|
|
3
|
-
// "gpu." prefix). Read-only — no effect on rendering output.
|
|
4
|
-
//
|
|
5
|
-
// Pattern (per spec):
|
|
6
|
-
// 1. Each render pass declares timestampWrites { begin, end } slot indices.
|
|
7
|
-
// 2. After all passes, encoder.resolveQuerySet writes uint64 ns timestamps
|
|
8
|
-
// into a QUERY_RESOLVE buffer.
|
|
9
|
-
// 3. We copy the resolve buffer into a MAP_READ readback buffer.
|
|
10
|
-
// 4. Submit. After the GPU finishes, mapAsync resolves; we read durations
|
|
11
|
-
// and record them.
|
|
12
|
-
//
|
|
13
|
-
// One readback buffer with an in-flight gate: if mapAsync hasn't resolved
|
|
14
|
-
// from a previous frame, we skip the copy this frame (no contention, just
|
|
15
|
-
// one frame of dropped sampling).
|
|
16
|
-
import { profiler } from "./physics/profile";
|
|
17
|
-
const MAX_PASSES_PER_FRAME = 64;
|
|
18
|
-
export class GpuTimestamp {
|
|
19
|
-
constructor(device) {
|
|
20
|
-
this.enabled = false;
|
|
21
|
-
this.querySet = null;
|
|
22
|
-
this.resolveBuffer = null;
|
|
23
|
-
this.readbackBuffer = null;
|
|
24
|
-
this.nextSlot = 0;
|
|
25
|
-
this.frameLabels = [];
|
|
26
|
-
this.inFlight = false;
|
|
27
|
-
// Set inside endFrame() when a copy was queued this frame; consumed by
|
|
28
|
-
// afterSubmit() to kick mapAsync. mapAsync must run AFTER queue.submit —
|
|
29
|
-
// calling it before submit puts the buffer into "mapping pending" state,
|
|
30
|
-
// which makes the copy in the encoder illegal.
|
|
31
|
-
this.pendingLabels = null;
|
|
32
|
-
this.device = device;
|
|
33
|
-
this.supported = device.features.has("timestamp-query");
|
|
34
|
-
}
|
|
35
|
-
ensureCreated() {
|
|
36
|
-
if (this.querySet || !this.supported)
|
|
37
|
-
return;
|
|
38
|
-
const slots = MAX_PASSES_PER_FRAME * 2;
|
|
39
|
-
this.querySet = this.device.createQuerySet({ type: "timestamp", count: slots });
|
|
40
|
-
this.resolveBuffer = this.device.createBuffer({
|
|
41
|
-
label: "gpu-timestamp-resolve",
|
|
42
|
-
size: slots * 8,
|
|
43
|
-
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
|
|
44
|
-
});
|
|
45
|
-
this.readbackBuffer = this.device.createBuffer({
|
|
46
|
-
label: "gpu-timestamp-readback",
|
|
47
|
-
size: slots * 8,
|
|
48
|
-
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
// Called once at the top of each render() before any pass wrap.
|
|
52
|
-
beginFrame() {
|
|
53
|
-
if (!this.enabled || !this.supported)
|
|
54
|
-
return;
|
|
55
|
-
this.ensureCreated();
|
|
56
|
-
this.nextSlot = 0;
|
|
57
|
-
this.frameLabels = [];
|
|
58
|
-
}
|
|
59
|
-
// Wrap a render-pass descriptor with timestampWrites for `label`. If
|
|
60
|
-
// profiling is off / unsupported / out of slots, returns desc unchanged.
|
|
61
|
-
// The same label called multiple times (e.g. each bloom mip) is fine —
|
|
62
|
-
// the profiler accumulates by label.
|
|
63
|
-
wrap(desc, label) {
|
|
64
|
-
if (!this.enabled || !this.querySet)
|
|
65
|
-
return desc;
|
|
66
|
-
if (this.nextSlot + 2 > MAX_PASSES_PER_FRAME * 2)
|
|
67
|
-
return desc;
|
|
68
|
-
const begin = this.nextSlot;
|
|
69
|
-
const end = this.nextSlot + 1;
|
|
70
|
-
this.nextSlot += 2;
|
|
71
|
-
this.frameLabels.push(label);
|
|
72
|
-
return {
|
|
73
|
-
...desc,
|
|
74
|
-
timestampWrites: {
|
|
75
|
-
querySet: this.querySet,
|
|
76
|
-
beginningOfPassWriteIndex: begin,
|
|
77
|
-
endOfPassWriteIndex: end,
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
// Call on the encoder after all passes, BEFORE encoder.finish(). Adds
|
|
82
|
-
// resolveQuerySet + copyBufferToBuffer commands. mapAsync is deferred to
|
|
83
|
-
// afterSubmit() — calling it before submit transitions the readback buffer
|
|
84
|
-
// into "mapping pending" state, which makes the queued copy illegal.
|
|
85
|
-
endFrame(encoder) {
|
|
86
|
-
if (!this.enabled || !this.querySet || this.frameLabels.length === 0)
|
|
87
|
-
return;
|
|
88
|
-
const count = this.frameLabels.length * 2;
|
|
89
|
-
encoder.resolveQuerySet(this.querySet, 0, count, this.resolveBuffer, 0);
|
|
90
|
-
if (this.inFlight)
|
|
91
|
-
return;
|
|
92
|
-
encoder.copyBufferToBuffer(this.resolveBuffer, 0, this.readbackBuffer, 0, count * 8);
|
|
93
|
-
this.pendingLabels = this.frameLabels.slice();
|
|
94
|
-
}
|
|
95
|
-
// Call AFTER device.queue.submit(). Kicks mapAsync if a copy was queued
|
|
96
|
-
// this frame; the .then() reads durations and unmaps.
|
|
97
|
-
afterSubmit() {
|
|
98
|
-
const labels = this.pendingLabels;
|
|
99
|
-
if (!labels)
|
|
100
|
-
return;
|
|
101
|
-
this.pendingLabels = null;
|
|
102
|
-
this.inFlight = true;
|
|
103
|
-
const readback = this.readbackBuffer;
|
|
104
|
-
void readback.mapAsync(GPUMapMode.READ).then(() => {
|
|
105
|
-
const arr = new BigInt64Array(readback.getMappedRange());
|
|
106
|
-
for (let i = 0; i < labels.length; i++) {
|
|
107
|
-
const start = arr[i * 2];
|
|
108
|
-
const end = arr[i * 2 + 1];
|
|
109
|
-
// Per spec, timestamps are nanoseconds.
|
|
110
|
-
const ms = Number(end - start) / 1e6;
|
|
111
|
-
if (ms >= 0 && ms < 1000)
|
|
112
|
-
profiler.record("gpu." + labels[i], ms);
|
|
113
|
-
}
|
|
114
|
-
readback.unmap();
|
|
115
|
-
this.inFlight = false;
|
|
116
|
-
}, () => {
|
|
117
|
-
this.inFlight = false;
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
declare class PhysicsProfiler {
|
|
2
|
-
enabled: boolean;
|
|
3
|
-
private sums;
|
|
4
|
-
private counts;
|
|
5
|
-
begin(): number;
|
|
6
|
-
end(label: string, t0: number): void;
|
|
7
|
-
record(label: string, ms: number): void;
|
|
8
|
-
snapshot(): Record<string, {
|
|
9
|
-
avgMs: number;
|
|
10
|
-
calls: number;
|
|
11
|
-
totalMs: number;
|
|
12
|
-
}>;
|
|
13
|
-
reset(): void;
|
|
14
|
-
}
|
|
15
|
-
export declare const profiler: PhysicsProfiler;
|
|
16
|
-
export type ProfileSnapshot = ReturnType<PhysicsProfiler["snapshot"]>;
|
|
17
|
-
export {};
|
|
18
|
-
//# sourceMappingURL=profile.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/physics/profile.ts"],"names":[],"mappings":"AAIA,cAAM,eAAe;IACnB,OAAO,UAAQ;IACf,OAAO,CAAC,IAAI,CAA8C;IAC1D,OAAO,CAAC,MAAM,CAA8C;IAE5D,KAAK,IAAI,MAAM;IAIf,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IASpC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAQvC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAU7E,KAAK,IAAI,IAAI;CAId;AAED,eAAO,MAAM,QAAQ,iBAAwB,CAAA;AAC7C,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAA"}
|
package/dist/physics/profile.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// Per-phase wall-clock accumulator. Off by default — when `enabled` is false,
|
|
2
|
-
// begin()/end() short-circuit so production has zero overhead. Toggle via
|
|
3
|
-
// `RezePhysics.setProfileEnabled(true)` and read with `getProfile()`.
|
|
4
|
-
class PhysicsProfiler {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.enabled = false;
|
|
7
|
-
this.sums = Object.create(null);
|
|
8
|
-
this.counts = Object.create(null);
|
|
9
|
-
}
|
|
10
|
-
begin() {
|
|
11
|
-
return this.enabled ? performance.now() : 0;
|
|
12
|
-
}
|
|
13
|
-
end(label, t0) {
|
|
14
|
-
if (!this.enabled)
|
|
15
|
-
return;
|
|
16
|
-
const dt = performance.now() - t0;
|
|
17
|
-
this.sums[label] = (this.sums[label] ?? 0) + dt;
|
|
18
|
-
this.counts[label] = (this.counts[label] ?? 0) + 1;
|
|
19
|
-
}
|
|
20
|
-
// Direct accumulator for durations not measured via begin()/end() — used by
|
|
21
|
-
// GPU timestamp readback, which arrives async after the frame has ended.
|
|
22
|
-
record(label, ms) {
|
|
23
|
-
if (!this.enabled)
|
|
24
|
-
return;
|
|
25
|
-
this.sums[label] = (this.sums[label] ?? 0) + ms;
|
|
26
|
-
this.counts[label] = (this.counts[label] ?? 0) + 1;
|
|
27
|
-
}
|
|
28
|
-
// Snapshot since last reset(). avgMs is per call; totalMs is the cumulative
|
|
29
|
-
// window cost — divide by elapsed wall time to get a "% of frame budget".
|
|
30
|
-
snapshot() {
|
|
31
|
-
const out = {};
|
|
32
|
-
for (const k in this.sums) {
|
|
33
|
-
const total = this.sums[k];
|
|
34
|
-
const calls = this.counts[k];
|
|
35
|
-
out[k] = { avgMs: calls > 0 ? total / calls : 0, calls, totalMs: total };
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
reset() {
|
|
40
|
-
this.sums = Object.create(null);
|
|
41
|
-
this.counts = Object.create(null);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
export const profiler = new PhysicsProfiler();
|