reze-engine 0.1.3 → 0.1.4

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 CHANGED
@@ -1,99 +1,99 @@
1
- # Reze Engine
2
-
3
- A lightweight engine built with WebGPU and TypeScript for real-time 3D anime character MMD model rendering.
4
-
5
- ## Usage
6
-
7
- ```typescript
8
- export default function Home() {
9
- const canvasRef = useRef<HTMLCanvasElement>(null)
10
- const engineRef = useRef<Engine | null>(null)
11
- const [engineError, setEngineError] = useState<string | null>(null)
12
- const [loading, setLoading] = useState(true)
13
- const [stats, setStats] = useState<EngineStats>({
14
- fps: 0,
15
- frameTime: 0,
16
- memoryUsed: 0,
17
- drawCalls: 0,
18
- vertices: 0,
19
- triangles: 0,
20
- materials: 0,
21
- textures: 0,
22
- textureMemory: 0,
23
- bufferMemory: 0,
24
- gpuMemory: 0,
25
- })
26
- const [progress, setProgress] = useState(0)
27
-
28
- const initEngine = useCallback(async () => {
29
- if (canvasRef.current) {
30
- // Initialize engine
31
- try {
32
- const engine = new Engine(canvasRef.current)
33
- engineRef.current = engine
34
- await engine.init()
35
- await engine.loadModel("/models/塞尔凯特/塞尔凯特.pmx")
36
- setLoading(false)
37
-
38
- engine.runRenderLoop(() => {
39
- setStats(engine.getStats())
40
- })
41
- } catch (error) {
42
- setEngineError(error instanceof Error ? error.message : "Unknown error")
43
- }
44
- }
45
- }, [])
46
-
47
- useEffect(() => {
48
- void (async () => {
49
- initEngine()
50
- })()
51
-
52
- // Cleanup on unmount
53
- return () => {
54
- if (engineRef.current) {
55
- engineRef.current.dispose()
56
- }
57
- }
58
- }, [initEngine])
59
-
60
- useEffect(() => {
61
- if (loading) {
62
- const interval = setInterval(() => {
63
- setProgress((prev) => {
64
- if (prev >= 100) {
65
- return 0
66
- }
67
- return prev + 1
68
- })
69
- }, 50)
70
-
71
- return () => clearInterval(interval)
72
- }
73
- }, [loading])
74
-
75
- return (
76
- <div
77
- className="fixed inset-0 w-full h-full overflow-hidden touch-none"
78
- style={{
79
- background:
80
- "radial-gradient(ellipse at center, rgba(35, 35, 45, 0.8) 0%, rgba(35, 35, 45, 0.8) 8%, rgba(8, 8, 12, 0.95) 65%, rgba(0, 0, 0, 1) 100%)",
81
- }}
82
- >
83
- <Header stats={stats} />
84
-
85
- {engineError && (
86
- <div className="absolute inset-0 w-full h-full flex items-center justify-center text-white p-6">
87
- Engine Error: {engineError}
88
- </div>
89
- )}
90
- {loading && !engineError && (
91
- <div className="absolute inset-0 max-w-xs mx-auto w-full h-full flex items-center justify-center text-white p-6">
92
- <Progress value={progress} className="rounded-none" />
93
- </div>
94
- )}
95
- <canvas ref={canvasRef} className="absolute inset-0 w-full h-full touch-none z-1" />
96
- </div>
97
- )
98
- }
99
- ```
1
+ # Reze Engine
2
+
3
+ A lightweight engine built with WebGPU and TypeScript for real-time 3D anime character MMD model rendering.
4
+
5
+ ## Usage
6
+
7
+ ```typescript
8
+ export default function Home() {
9
+ const canvasRef = useRef<HTMLCanvasElement>(null)
10
+ const engineRef = useRef<Engine | null>(null)
11
+ const [engineError, setEngineError] = useState<string | null>(null)
12
+ const [loading, setLoading] = useState(true)
13
+ const [stats, setStats] = useState<EngineStats>({
14
+ fps: 0,
15
+ frameTime: 0,
16
+ memoryUsed: 0,
17
+ drawCalls: 0,
18
+ vertices: 0,
19
+ triangles: 0,
20
+ materials: 0,
21
+ textures: 0,
22
+ textureMemory: 0,
23
+ bufferMemory: 0,
24
+ gpuMemory: 0,
25
+ })
26
+ const [progress, setProgress] = useState(0)
27
+
28
+ const initEngine = useCallback(async () => {
29
+ if (canvasRef.current) {
30
+ // Initialize engine
31
+ try {
32
+ const engine = new Engine(canvasRef.current)
33
+ engineRef.current = engine
34
+ await engine.init()
35
+ await engine.loadModel("/models/塞尔凯特/塞尔凯特.pmx")
36
+ setLoading(false)
37
+
38
+ engine.runRenderLoop(() => {
39
+ setStats(engine.getStats())
40
+ })
41
+ } catch (error) {
42
+ setEngineError(error instanceof Error ? error.message : "Unknown error")
43
+ }
44
+ }
45
+ }, [])
46
+
47
+ useEffect(() => {
48
+ void (async () => {
49
+ initEngine()
50
+ })()
51
+
52
+ // Cleanup on unmount
53
+ return () => {
54
+ if (engineRef.current) {
55
+ engineRef.current.dispose()
56
+ }
57
+ }
58
+ }, [initEngine])
59
+
60
+ useEffect(() => {
61
+ if (loading) {
62
+ const interval = setInterval(() => {
63
+ setProgress((prev) => {
64
+ if (prev >= 100) {
65
+ return 0
66
+ }
67
+ return prev + 1
68
+ })
69
+ }, 50)
70
+
71
+ return () => clearInterval(interval)
72
+ }
73
+ }, [loading])
74
+
75
+ return (
76
+ <div
77
+ className="fixed inset-0 w-full h-full overflow-hidden touch-none"
78
+ style={{
79
+ background:
80
+ "radial-gradient(ellipse at center, rgba(35, 35, 45, 0.8) 0%, rgba(35, 35, 45, 0.8) 8%, rgba(8, 8, 12, 0.95) 65%, rgba(0, 0, 0, 1) 100%)",
81
+ }}
82
+ >
83
+ <Header stats={stats} />
84
+
85
+ {engineError && (
86
+ <div className="absolute inset-0 w-full h-full flex items-center justify-center text-white p-6">
87
+ Engine Error: {engineError}
88
+ </div>
89
+ )}
90
+ {loading && !engineError && (
91
+ <div className="absolute inset-0 max-w-xs mx-auto w-full h-full flex items-center justify-center text-white p-6">
92
+ <Progress value={progress} className="rounded-none" />
93
+ </div>
94
+ )}
95
+ <canvas ref={canvasRef} className="absolute inset-0 w-full h-full touch-none z-1" />
96
+ </div>
97
+ )
98
+ }
99
+ ```
package/dist/engine.d.ts CHANGED
@@ -71,7 +71,7 @@ export declare class Engine {
71
71
  stopRenderLoop(): void;
72
72
  dispose(): void;
73
73
  loadModel(path: string): Promise<void>;
74
- rotateBones(bones: string[], rotations: Quat[], duration: number): void;
74
+ rotateBones(bones: string[], rotations: Quat[], durationMs?: number): void;
75
75
  private setupModelBuffers;
76
76
  private materialDraws;
77
77
  private outlineDraws;
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
package/dist/engine.js CHANGED
@@ -74,112 +74,112 @@ export class Engine {
74
74
  });
75
75
  const shaderModule = this.device.createShaderModule({
76
76
  label: "model shaders",
77
- code: /* wgsl */ `
78
- struct CameraUniforms {
79
- view: mat4x4f,
80
- projection: mat4x4f,
81
- viewPos: vec3f,
82
- _padding: f32,
83
- };
84
-
85
- struct Light {
86
- direction: vec3f,
87
- _padding1: f32,
88
- color: vec3f,
89
- intensity: f32,
90
- };
91
-
92
- struct LightUniforms {
93
- ambient: f32,
94
- lightCount: f32,
95
- _padding1: f32,
96
- _padding2: f32,
97
- lights: array<Light, 4>,
98
- };
99
-
100
- struct MaterialUniforms {
101
- alpha: f32,
102
- _padding1: f32,
103
- _padding2: f32,
104
- _padding3: f32,
105
- };
106
-
107
- struct VertexOutput {
108
- @builtin(position) position: vec4f,
109
- @location(0) normal: vec3f,
110
- @location(1) uv: vec2f,
111
- @location(2) worldPos: vec3f,
112
- };
113
-
114
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
115
- @group(0) @binding(1) var<uniform> light: LightUniforms;
116
- @group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
117
- @group(0) @binding(3) var diffuseSampler: sampler;
118
- @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
119
- @group(0) @binding(5) var toonTexture: texture_2d<f32>;
120
- @group(0) @binding(6) var toonSampler: sampler;
121
- @group(0) @binding(7) var<uniform> material: MaterialUniforms;
122
-
123
- @vertex fn vs(
124
- @location(0) position: vec3f,
125
- @location(1) normal: vec3f,
126
- @location(2) uv: vec2f,
127
- @location(3) joints0: vec4<u32>,
128
- @location(4) weights0: vec4<f32>
129
- ) -> VertexOutput {
130
- var output: VertexOutput;
131
- let pos4 = vec4f(position, 1.0);
132
-
133
- // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
134
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
135
- var normalizedWeights: vec4f;
136
- if (weightSum > 0.0001) {
137
- normalizedWeights = weights0 / weightSum;
138
- } else {
139
- normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
140
- }
141
-
142
- var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
143
- var skinnedNrm = vec3f(0.0, 0.0, 0.0);
144
- for (var i = 0u; i < 4u; i++) {
145
- let j = joints0[i];
146
- let w = normalizedWeights[i];
147
- let m = skinMats[j];
148
- skinnedPos += (m * pos4) * w;
149
- let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
150
- skinnedNrm += (r3 * normal) * w;
151
- }
152
- let worldPos = skinnedPos.xyz;
153
- output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
154
- output.normal = normalize(skinnedNrm);
155
- output.uv = uv;
156
- output.worldPos = worldPos;
157
- return output;
158
- }
159
-
160
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
161
- let n = normalize(input.normal);
162
- let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
163
-
164
- var lightAccum = vec3f(light.ambient);
165
- let numLights = u32(light.lightCount);
166
- for (var i = 0u; i < numLights; i++) {
167
- let l = -light.lights[i].direction;
168
- let nDotL = max(dot(n, l), 0.0);
169
- let toonUV = vec2f(nDotL, 0.5);
170
- let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
171
- let radiance = light.lights[i].color * light.lights[i].intensity;
172
- lightAccum += toonFactor * radiance * nDotL;
173
- }
174
-
175
- let color = albedo * lightAccum;
176
- let finalAlpha = material.alpha;
177
- if (finalAlpha < 0.001) {
178
- discard;
179
- }
180
-
181
- return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
182
- }
77
+ code: /* wgsl */ `
78
+ struct CameraUniforms {
79
+ view: mat4x4f,
80
+ projection: mat4x4f,
81
+ viewPos: vec3f,
82
+ _padding: f32,
83
+ };
84
+
85
+ struct Light {
86
+ direction: vec3f,
87
+ _padding1: f32,
88
+ color: vec3f,
89
+ intensity: f32,
90
+ };
91
+
92
+ struct LightUniforms {
93
+ ambient: f32,
94
+ lightCount: f32,
95
+ _padding1: f32,
96
+ _padding2: f32,
97
+ lights: array<Light, 4>,
98
+ };
99
+
100
+ struct MaterialUniforms {
101
+ alpha: f32,
102
+ _padding1: f32,
103
+ _padding2: f32,
104
+ _padding3: f32,
105
+ };
106
+
107
+ struct VertexOutput {
108
+ @builtin(position) position: vec4f,
109
+ @location(0) normal: vec3f,
110
+ @location(1) uv: vec2f,
111
+ @location(2) worldPos: vec3f,
112
+ };
113
+
114
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
115
+ @group(0) @binding(1) var<uniform> light: LightUniforms;
116
+ @group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
117
+ @group(0) @binding(3) var diffuseSampler: sampler;
118
+ @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
119
+ @group(0) @binding(5) var toonTexture: texture_2d<f32>;
120
+ @group(0) @binding(6) var toonSampler: sampler;
121
+ @group(0) @binding(7) var<uniform> material: MaterialUniforms;
122
+
123
+ @vertex fn vs(
124
+ @location(0) position: vec3f,
125
+ @location(1) normal: vec3f,
126
+ @location(2) uv: vec2f,
127
+ @location(3) joints0: vec4<u32>,
128
+ @location(4) weights0: vec4<f32>
129
+ ) -> VertexOutput {
130
+ var output: VertexOutput;
131
+ let pos4 = vec4f(position, 1.0);
132
+
133
+ // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
134
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
135
+ var normalizedWeights: vec4f;
136
+ if (weightSum > 0.0001) {
137
+ normalizedWeights = weights0 / weightSum;
138
+ } else {
139
+ normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
140
+ }
141
+
142
+ var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
143
+ var skinnedNrm = vec3f(0.0, 0.0, 0.0);
144
+ for (var i = 0u; i < 4u; i++) {
145
+ let j = joints0[i];
146
+ let w = normalizedWeights[i];
147
+ let m = skinMats[j];
148
+ skinnedPos += (m * pos4) * w;
149
+ let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
150
+ skinnedNrm += (r3 * normal) * w;
151
+ }
152
+ let worldPos = skinnedPos.xyz;
153
+ output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
154
+ output.normal = normalize(skinnedNrm);
155
+ output.uv = uv;
156
+ output.worldPos = worldPos;
157
+ return output;
158
+ }
159
+
160
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
161
+ let n = normalize(input.normal);
162
+ let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
163
+
164
+ var lightAccum = vec3f(light.ambient);
165
+ let numLights = u32(light.lightCount);
166
+ for (var i = 0u; i < numLights; i++) {
167
+ let l = -light.lights[i].direction;
168
+ let nDotL = max(dot(n, l), 0.0);
169
+ let toonUV = vec2f(nDotL, 0.5);
170
+ let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
171
+ let radiance = light.lights[i].color * light.lights[i].intensity;
172
+ lightAccum += toonFactor * radiance * nDotL;
173
+ }
174
+
175
+ let color = albedo * lightAccum;
176
+ let finalAlpha = material.alpha;
177
+ if (finalAlpha < 0.001) {
178
+ discard;
179
+ }
180
+
181
+ return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
182
+ }
183
183
  `,
184
184
  });
185
185
  // Single pipeline for all materials with alpha blending
@@ -239,72 +239,72 @@ export class Engine {
239
239
  });
240
240
  const outlineShaderModule = this.device.createShaderModule({
241
241
  label: "outline shaders",
242
- code: /* wgsl */ `
243
- struct CameraUniforms {
244
- view: mat4x4f,
245
- projection: mat4x4f,
246
- viewPos: vec3f,
247
- _padding: f32,
248
- };
249
-
250
- struct MaterialUniforms {
251
- edgeColor: vec4f,
252
- edgeSize: f32,
253
- _padding1: f32,
254
- _padding2: f32,
255
- _padding3: f32,
256
- };
257
-
258
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
259
- @group(0) @binding(1) var<uniform> material: MaterialUniforms;
260
- @group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
261
-
262
- struct VertexOutput {
263
- @builtin(position) position: vec4f,
264
- };
265
-
266
- @vertex fn vs(
267
- @location(0) position: vec3f,
268
- @location(1) normal: vec3f,
269
- @location(2) uv: vec2f,
270
- @location(3) joints0: vec4<u32>,
271
- @location(4) weights0: vec4<f32>
272
- ) -> VertexOutput {
273
- var output: VertexOutput;
274
- let pos4 = vec4f(position, 1.0);
275
-
276
- // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
277
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
278
- var normalizedWeights: vec4f;
279
- if (weightSum > 0.0001) {
280
- normalizedWeights = weights0 / weightSum;
281
- } else {
282
- normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
283
- }
284
-
285
- var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
286
- var skinnedNrm = vec3f(0.0, 0.0, 0.0);
287
- for (var i = 0u; i < 4u; i++) {
288
- let j = joints0[i];
289
- let w = normalizedWeights[i];
290
- let m = skinMats[j];
291
- skinnedPos += (m * pos4) * w;
292
- let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
293
- skinnedNrm += (r3 * normal) * w;
294
- }
295
- let worldPos = skinnedPos.xyz;
296
- let worldNormal = normalize(skinnedNrm);
297
-
298
- // MMD invert hull: expand vertices outward along normals
299
- let scaleFactor = 0.01;
300
- let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
301
- output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
302
- return output;
303
- }
304
-
305
- @fragment fn fs() -> @location(0) vec4f {
306
- return material.edgeColor;
307
- }
242
+ code: /* wgsl */ `
243
+ struct CameraUniforms {
244
+ view: mat4x4f,
245
+ projection: mat4x4f,
246
+ viewPos: vec3f,
247
+ _padding: f32,
248
+ };
249
+
250
+ struct MaterialUniforms {
251
+ edgeColor: vec4f,
252
+ edgeSize: f32,
253
+ _padding1: f32,
254
+ _padding2: f32,
255
+ _padding3: f32,
256
+ };
257
+
258
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
259
+ @group(0) @binding(1) var<uniform> material: MaterialUniforms;
260
+ @group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
261
+
262
+ struct VertexOutput {
263
+ @builtin(position) position: vec4f,
264
+ };
265
+
266
+ @vertex fn vs(
267
+ @location(0) position: vec3f,
268
+ @location(1) normal: vec3f,
269
+ @location(2) uv: vec2f,
270
+ @location(3) joints0: vec4<u32>,
271
+ @location(4) weights0: vec4<f32>
272
+ ) -> VertexOutput {
273
+ var output: VertexOutput;
274
+ let pos4 = vec4f(position, 1.0);
275
+
276
+ // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
277
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
278
+ var normalizedWeights: vec4f;
279
+ if (weightSum > 0.0001) {
280
+ normalizedWeights = weights0 / weightSum;
281
+ } else {
282
+ normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
283
+ }
284
+
285
+ var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
286
+ var skinnedNrm = vec3f(0.0, 0.0, 0.0);
287
+ for (var i = 0u; i < 4u; i++) {
288
+ let j = joints0[i];
289
+ let w = normalizedWeights[i];
290
+ let m = skinMats[j];
291
+ skinnedPos += (m * pos4) * w;
292
+ let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
293
+ skinnedNrm += (r3 * normal) * w;
294
+ }
295
+ let worldPos = skinnedPos.xyz;
296
+ let worldNormal = normalize(skinnedNrm);
297
+
298
+ // MMD invert hull: expand vertices outward along normals
299
+ let scaleFactor = 0.01;
300
+ let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
301
+ output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
302
+ return output;
303
+ }
304
+
305
+ @fragment fn fs() -> @location(0) vec4f {
306
+ return material.edgeColor;
307
+ }
308
308
  `,
309
309
  });
310
310
  this.outlinePipeline = this.device.createRenderPipeline({
@@ -380,31 +380,31 @@ export class Engine {
380
380
  createSkinMatrixComputePipeline() {
381
381
  const computeShader = this.device.createShaderModule({
382
382
  label: "skin matrix compute",
383
- code: /* wgsl */ `
384
- struct BoneCountUniform {
385
- count: u32,
386
- _padding1: u32,
387
- _padding2: u32,
388
- _padding3: u32,
389
- _padding4: vec4<u32>,
390
- };
391
-
392
- @group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
393
- @group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
394
- @group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
395
- @group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
396
-
397
- @compute @workgroup_size(64)
398
- fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
399
- let boneIndex = globalId.x;
400
- // Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
401
- if (boneIndex >= boneCount.count) {
402
- return;
403
- }
404
- let worldMat = worldMatrices[boneIndex];
405
- let invBindMat = inverseBindMatrices[boneIndex];
406
- skinMatrices[boneIndex] = worldMat * invBindMat;
407
- }
383
+ code: /* wgsl */ `
384
+ struct BoneCountUniform {
385
+ count: u32,
386
+ _padding1: u32,
387
+ _padding2: u32,
388
+ _padding3: u32,
389
+ _padding4: vec4<u32>,
390
+ };
391
+
392
+ @group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
393
+ @group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
394
+ @group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
395
+ @group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
396
+
397
+ @compute @workgroup_size(64)
398
+ fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
399
+ let boneIndex = globalId.x;
400
+ // Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
401
+ if (boneIndex >= boneCount.count) {
402
+ return;
403
+ }
404
+ let worldMat = worldMatrices[boneIndex];
405
+ let invBindMat = inverseBindMatrices[boneIndex];
406
+ skinMatrices[boneIndex] = worldMat * invBindMat;
407
+ }
408
408
  `,
409
409
  });
410
410
  this.skinMatrixComputePipeline = this.device.createComputePipeline({
@@ -557,8 +557,8 @@ export class Engine {
557
557
  this.physics = new Physics(model.getRigidbodies(), model.getJoints());
558
558
  await this.setupModelBuffers(model);
559
559
  }
560
- rotateBones(bones, rotations, duration) {
561
- this.currentModel?.rotateBones(bones, rotations, duration);
560
+ rotateBones(bones, rotations, durationMs) {
561
+ this.currentModel?.rotateBones(bones, rotations, durationMs);
562
562
  }
563
563
  // Step 7: Create vertex, index, and joint buffers
564
564
  async setupModelBuffers(model) {
package/dist/math.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export declare function easeInOut(t: number): number;
1
2
  export declare class Vec3 {
2
3
  x: number;
3
4
  y: number;
@@ -28,6 +29,7 @@ export declare class Quat {
28
29
  rotate(v: Vec3): Vec3;
29
30
  static fromTo(from: Vec3, to: Vec3): Quat;
30
31
  toArray(): [number, number, number, number];
32
+ static slerp(a: Quat, b: Quat, t: number): Quat;
31
33
  static fromEuler(rotX: number, rotY: number, rotZ: number): Quat;
32
34
  toEuler(): Vec3;
33
35
  }