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/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
- private handleResize() {
1707
- const displayWidth = this.canvas.clientWidth
1708
- const displayHeight = this.canvas.clientHeight
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(displayWidth * dpr)
1712
- const height = Math.floor(displayHeight * dpr)
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)
@@ -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"}
@@ -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"}
@@ -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();