reze-engine 0.3.11 → 0.3.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/src/engine.ts CHANGED
@@ -9,6 +9,16 @@ export type EngineOptions = {
9
9
  rimLightIntensity?: number
10
10
  cameraDistance?: number
11
11
  cameraTarget?: Vec3
12
+ cameraFov?: number
13
+ }
14
+
15
+ export const DEFAULT_ENGINE_OPTIONS: Required<EngineOptions> = {
16
+ ambientColor: new Vec3(1.0, 1.0, 1.0),
17
+ bloomIntensity: 0.12,
18
+ rimLightIntensity: 0.45,
19
+ cameraDistance: 26.6,
20
+ cameraTarget: new Vec3(0, 12.5, 0),
21
+ cameraFov: Math.PI / 4,
12
22
  }
13
23
 
14
24
  export interface EngineStats {
@@ -42,8 +52,9 @@ export class Engine {
42
52
  private camera!: Camera
43
53
  private cameraUniformBuffer!: GPUBuffer
44
54
  private cameraMatrixData = new Float32Array(36)
45
- private cameraDistance: number = 26.6
46
- private cameraTarget: Vec3 = new Vec3(0, 12.5, 0)
55
+ private cameraDistance!: number
56
+ private cameraTarget!: Vec3
57
+ private cameraFov!: number
47
58
  private lightUniformBuffer!: GPUBuffer
48
59
  private lightData = new Float32Array(4)
49
60
  private vertexBuffer!: GPUBuffer
@@ -72,18 +83,8 @@ export class Engine {
72
83
  private readonly STENCIL_EYE_VALUE = 1
73
84
  private readonly BLOOM_DOWNSCALE_FACTOR = 2
74
85
 
75
- // Default values
76
- private static readonly DEFAULT_BLOOM_THRESHOLD = 0.01
77
- private static readonly DEFAULT_BLOOM_INTENSITY = 0.12
78
- private static readonly DEFAULT_RIM_LIGHT_INTENSITY = 0.45
79
- private static readonly DEFAULT_CAMERA_DISTANCE = 26.6
80
- private static readonly DEFAULT_CAMERA_TARGET = new Vec3(0, 12.5, 0)
81
- private static readonly TRANSPARENCY_EPSILON = 0.001
82
- private static readonly STATS_FPS_UPDATE_INTERVAL_MS = 1000
83
- private static readonly STATS_FRAME_TIME_ROUNDING = 100
84
-
85
86
  // Ambient light settings
86
- private ambientColor: Vec3 = new Vec3(1.0, 1.0, 1.0)
87
+ private ambientColor!: Vec3
87
88
  // Bloom post-processing textures
88
89
  private sceneRenderTexture!: GPUTexture
89
90
  private sceneRenderTextureView!: GPUTextureView // Cached view (recreated on resize)
@@ -104,10 +105,10 @@ export class Engine {
104
105
  private bloomBlurVBindGroup?: GPUBindGroup
105
106
  private bloomComposeBindGroup?: GPUBindGroup
106
107
  // Bloom settings
107
- private bloomThreshold: number = Engine.DEFAULT_BLOOM_THRESHOLD
108
- private bloomIntensity: number = Engine.DEFAULT_BLOOM_INTENSITY
108
+ private bloomThreshold!: number
109
+ private bloomIntensity!: number
109
110
  // Rim light settings
110
- private rimLightIntensity: number = Engine.DEFAULT_RIM_LIGHT_INTENSITY
111
+ private rimLightIntensity!: number
111
112
 
112
113
  private currentModel: Model | null = null
113
114
  private modelDir: string = ""
@@ -132,11 +133,12 @@ export class Engine {
132
133
  constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
133
134
  this.canvas = canvas
134
135
  if (options) {
135
- this.ambientColor = options.ambientColor ?? new Vec3(1.0, 1.0, 1.0)
136
- this.bloomIntensity = options.bloomIntensity ?? Engine.DEFAULT_BLOOM_INTENSITY
137
- this.rimLightIntensity = options.rimLightIntensity ?? Engine.DEFAULT_RIM_LIGHT_INTENSITY
138
- this.cameraDistance = options.cameraDistance ?? Engine.DEFAULT_CAMERA_DISTANCE
139
- this.cameraTarget = options.cameraTarget ?? Engine.DEFAULT_CAMERA_TARGET
136
+ this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor!
137
+ this.bloomIntensity = options.bloomIntensity ?? DEFAULT_ENGINE_OPTIONS.bloomIntensity
138
+ this.rimLightIntensity = options.rimLightIntensity ?? DEFAULT_ENGINE_OPTIONS.rimLightIntensity
139
+ this.cameraDistance = options.cameraDistance ?? DEFAULT_ENGINE_OPTIONS.cameraDistance
140
+ this.cameraTarget = options.cameraTarget ?? DEFAULT_ENGINE_OPTIONS.cameraTarget
141
+ this.cameraFov = options.cameraFov ?? DEFAULT_ENGINE_OPTIONS.cameraFov
140
142
  }
141
143
  }
142
144
 
@@ -366,12 +368,12 @@ export class Engine {
366
368
 
367
369
  let lightAccum = light.ambientColor;
368
370
 
369
- // Rim light calculation
371
+ // Rim light calculation - proper Fresnel for edge-only highlights
370
372
  let viewDir = normalize(camera.viewPos - input.worldPos);
371
- var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
372
- rimFactor = rimFactor * rimFactor; // Optimized: direct multiply instead of pow(x, 2.0)
373
+ let fresnel = 1.0 - abs(dot(n, viewDir));
374
+ let rimFactor = pow(fresnel, 4.0); // Higher power for sharper edge-only effect
373
375
  let rimLight = material.rimColor * material.rimIntensity * rimFactor;
374
-
376
+
375
377
  let color = albedo * lightAccum + rimLight;
376
378
 
377
379
  return vec4f(color, finalAlpha);
@@ -1052,7 +1054,7 @@ export class Engine {
1052
1054
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1053
1055
  })
1054
1056
 
1055
- this.camera = new Camera(Math.PI, Math.PI / 2.5, this.cameraDistance, this.cameraTarget)
1057
+ this.camera = new Camera(Math.PI, Math.PI / 2.5, this.cameraDistance, this.cameraTarget, this.cameraFov)
1056
1058
 
1057
1059
  this.camera.aspect = this.canvas.width / this.canvas.height
1058
1060
  this.camera.attachControl(this.canvas)
@@ -1285,7 +1287,7 @@ export class Engine {
1285
1287
  if (!diffuseTexture) throw new Error(`Material "${mat.name}" has no diffuse texture`)
1286
1288
 
1287
1289
  const materialAlpha = mat.diffuse[3]
1288
- const isTransparent = materialAlpha < 1.0 - Engine.TRANSPARENCY_EPSILON
1290
+ const isTransparent = materialAlpha < 1.0 - 0.001
1289
1291
 
1290
1292
  const materialUniformBuffer = this.createMaterialUniformBuffer(mat.name, materialAlpha, 0.0)
1291
1293
 
@@ -1739,17 +1741,15 @@ export class Engine {
1739
1741
  this.frameTimeSum -= avg
1740
1742
  this.frameTimeCount = maxSamples
1741
1743
  }
1742
- this.stats.frameTime =
1743
- Math.round((this.frameTimeSum / this.frameTimeCount) * Engine.STATS_FRAME_TIME_ROUNDING) /
1744
- Engine.STATS_FRAME_TIME_ROUNDING
1744
+ this.stats.frameTime = Math.round((this.frameTimeSum / this.frameTimeCount) * 100) / 100
1745
1745
 
1746
1746
  // FPS tracking
1747
1747
  const now = performance.now()
1748
1748
  this.framesSinceLastUpdate++
1749
1749
  const elapsed = now - this.lastFpsUpdate
1750
1750
 
1751
- if (elapsed >= Engine.STATS_FPS_UPDATE_INTERVAL_MS) {
1752
- this.stats.fps = Math.round((this.framesSinceLastUpdate / elapsed) * Engine.STATS_FPS_UPDATE_INTERVAL_MS)
1751
+ if (elapsed >= 1000) {
1752
+ this.stats.fps = Math.round((this.framesSinceLastUpdate / elapsed) * 1000)
1753
1753
  this.framesSinceLastUpdate = 0
1754
1754
  this.lastFpsUpdate = now
1755
1755
  }
package/src/player.ts DELETED
@@ -1,290 +0,0 @@
1
- import { Quat, Vec3, bezierInterpolate } from "./math"
2
- import { BoneFrame, MorphFrame, VMDKeyFrame, VMDLoader } from "./vmd-loader"
3
-
4
- export interface AnimationPose {
5
- boneRotations: Map<string, Quat>
6
- boneTranslations: Map<string, Vec3>
7
- morphWeights: Map<string, number>
8
- }
9
-
10
- export interface AnimationProgress {
11
- current: number
12
- duration: number
13
- percentage: number
14
- }
15
-
16
- export class Player {
17
- // Animation data
18
- private frames: VMDKeyFrame[] = []
19
- private boneTracks: Map<string, Array<{ boneFrame: BoneFrame; time: number }>> = new Map()
20
- private morphTracks: Map<string, Array<{ morphFrame: MorphFrame; time: number }>> = new Map()
21
- private _duration: number = 0
22
-
23
- // Playback state
24
- private isPlaying: boolean = false
25
- private isPaused: boolean = false
26
- private _currentTime: number = 0
27
-
28
- // Timing
29
- private startTime: number = 0 // Real-time when playback started
30
-
31
- /**
32
- * Load VMD animation file
33
- */
34
- async loadVmd(vmdUrl: string): Promise<void> {
35
- // Load animation
36
- this.frames = await VMDLoader.load(vmdUrl)
37
- this.processFrames()
38
- }
39
-
40
- /**
41
- * Process frames into tracks
42
- */
43
- private processFrames(): void {
44
- // Helper to group frames by name and sort by time
45
- const groupFrames = <T>(
46
- items: Array<{ item: T; name: string; time: number }>
47
- ): Map<string, Array<{ item: T; time: number }>> => {
48
- const tracks = new Map<string, Array<{ item: T; time: number }>>()
49
- for (const { item, name, time } of items) {
50
- if (!tracks.has(name)) tracks.set(name, [])
51
- tracks.get(name)!.push({ item, time })
52
- }
53
- for (const keyFrames of tracks.values()) {
54
- keyFrames.sort((a, b) => a.time - b.time)
55
- }
56
- return tracks
57
- }
58
-
59
- // Collect all bone and morph frames
60
- const boneItems: Array<{ item: BoneFrame; name: string; time: number }> = []
61
- const morphItems: Array<{ item: MorphFrame; name: string; time: number }> = []
62
-
63
- for (const keyFrame of this.frames) {
64
- for (const boneFrame of keyFrame.boneFrames) {
65
- boneItems.push({ item: boneFrame, name: boneFrame.boneName, time: keyFrame.time })
66
- }
67
- for (const morphFrame of keyFrame.morphFrames) {
68
- morphItems.push({ item: morphFrame, name: morphFrame.morphName, time: keyFrame.time })
69
- }
70
- }
71
-
72
- // Transform to expected format
73
- this.boneTracks = new Map()
74
- for (const [name, frames] of groupFrames(boneItems).entries()) {
75
- this.boneTracks.set(
76
- name,
77
- frames.map((f) => ({ boneFrame: f.item, time: f.time }))
78
- )
79
- }
80
-
81
- this.morphTracks = new Map()
82
- for (const [name, frames] of groupFrames(morphItems).entries()) {
83
- this.morphTracks.set(
84
- name,
85
- frames.map((f) => ({ morphFrame: f.item, time: f.time }))
86
- )
87
- }
88
-
89
- // Calculate duration from all tracks
90
- const allTracks = [...this.boneTracks.values(), ...this.morphTracks.values()]
91
- this._duration = allTracks.reduce((max, keyFrames) => {
92
- const lastTime = keyFrames[keyFrames.length - 1]?.time ?? 0
93
- return Math.max(max, lastTime)
94
- }, 0)
95
- }
96
-
97
- /**
98
- * Start or resume playback
99
- * Note: For iOS, this should be called synchronously from a user interaction event
100
- */
101
- play(): void {
102
- if (this.frames.length === 0) return
103
-
104
- this.isPaused = false
105
- this.startTime = performance.now() - this._currentTime * 1000
106
-
107
- this.isPlaying = true
108
- }
109
-
110
- /**
111
- * Pause playback
112
- */
113
- pause(): void {
114
- if (!this.isPlaying || this.isPaused) return
115
- this.isPaused = true
116
- }
117
-
118
- /**
119
- * Stop playback and reset to beginning
120
- */
121
- stop(): void {
122
- this.isPlaying = false
123
- this.isPaused = false
124
- this._currentTime = 0
125
- this.startTime = 0
126
- }
127
-
128
- /**
129
- * Seek to specific time
130
- */
131
- seek(time: number): void {
132
- const clampedTime = Math.max(0, Math.min(time, this._duration))
133
- this._currentTime = clampedTime
134
-
135
- if (this.isPlaying && !this.isPaused) {
136
- this.startTime = performance.now() - clampedTime * 1000
137
- }
138
- }
139
-
140
- /**
141
- * Update playback and return current pose
142
- * Returns null if not playing, but returns current pose if paused
143
- */
144
- update(currentRealTime: number): AnimationPose | null {
145
- if (!this.isPlaying || this.frames.length === 0) {
146
- return null
147
- }
148
-
149
- // If paused, return current pose at paused time (no time update)
150
- if (this.isPaused) {
151
- return this.getPoseAtTime(this._currentTime)
152
- }
153
-
154
- // Calculate current animation time
155
- const elapsedSeconds = (currentRealTime - this.startTime) / 1000
156
- this._currentTime = elapsedSeconds
157
-
158
- // Check if animation ended
159
- if (this._currentTime >= this._duration) {
160
- this._currentTime = this._duration
161
- this.pause() // Auto-pause at end
162
- return this.getPoseAtTime(this._currentTime)
163
- }
164
-
165
- return this.getPoseAtTime(this._currentTime)
166
- }
167
-
168
- /**
169
- * Get pose at specific time (pure function)
170
- */
171
- getPoseAtTime(time: number): AnimationPose {
172
- const pose: AnimationPose = {
173
- boneRotations: new Map(),
174
- boneTranslations: new Map(),
175
- morphWeights: new Map(),
176
- }
177
-
178
- // Generic binary search for upper bound
179
- const upperBound = <T extends { time: number }>(time: number, keyFrames: T[]): number => {
180
- let left = 0,
181
- right = keyFrames.length
182
- while (left < right) {
183
- const mid = Math.floor((left + right) / 2)
184
- if (keyFrames[mid].time <= time) left = mid + 1
185
- else right = mid
186
- }
187
- return left
188
- }
189
-
190
- // Process bone tracks
191
- for (const [boneName, keyFrames] of this.boneTracks.entries()) {
192
- if (keyFrames.length === 0) continue
193
-
194
- const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time))
195
- const idx = upperBound(clampedTime, keyFrames) - 1
196
- if (idx < 0) continue
197
-
198
- const frameA = keyFrames[idx].boneFrame
199
- const frameB = keyFrames[idx + 1]?.boneFrame
200
-
201
- if (!frameB) {
202
- pose.boneRotations.set(boneName, frameA.rotation)
203
- pose.boneTranslations.set(boneName, frameA.translation)
204
- } else {
205
- const timeA = keyFrames[idx].time
206
- const timeB = keyFrames[idx + 1].time
207
- const gradient = (clampedTime - timeA) / (timeB - timeA)
208
- const interp = frameB.interpolation
209
-
210
- pose.boneRotations.set(
211
- boneName,
212
- Quat.slerp(
213
- frameA.rotation,
214
- frameB.rotation,
215
- bezierInterpolate(interp[0] / 127, interp[1] / 127, interp[2] / 127, interp[3] / 127, gradient)
216
- )
217
- )
218
-
219
- const lerp = (a: number, b: number, w: number) => a + (b - a) * w
220
- const getWeight = (offset: number) =>
221
- bezierInterpolate(
222
- interp[offset] / 127,
223
- interp[offset + 8] / 127,
224
- interp[offset + 4] / 127,
225
- interp[offset + 12] / 127,
226
- gradient
227
- )
228
-
229
- pose.boneTranslations.set(
230
- boneName,
231
- new Vec3(
232
- lerp(frameA.translation.x, frameB.translation.x, getWeight(0)),
233
- lerp(frameA.translation.y, frameB.translation.y, getWeight(16)),
234
- lerp(frameA.translation.z, frameB.translation.z, getWeight(32))
235
- )
236
- )
237
- }
238
- }
239
-
240
- // Process morph tracks
241
- for (const [morphName, keyFrames] of this.morphTracks.entries()) {
242
- if (keyFrames.length === 0) continue
243
-
244
- const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time))
245
- const idx = upperBound(clampedTime, keyFrames) - 1
246
- if (idx < 0) continue
247
-
248
- const frameA = keyFrames[idx].morphFrame
249
- const frameB = keyFrames[idx + 1]?.morphFrame
250
-
251
- if (!frameB) {
252
- pose.morphWeights.set(morphName, frameA.weight)
253
- } else {
254
- const timeA = keyFrames[idx].time
255
- const timeB = keyFrames[idx + 1].time
256
- const gradient = (clampedTime - timeA) / (timeB - timeA)
257
- pose.morphWeights.set(morphName, frameA.weight + (frameB.weight - frameA.weight) * gradient)
258
- }
259
- }
260
-
261
- return pose
262
- }
263
-
264
- /**
265
- * Get current playback progress
266
- */
267
- getProgress(): AnimationProgress {
268
- return {
269
- current: this._currentTime,
270
- duration: this._duration,
271
- percentage: this._duration > 0 ? (this._currentTime / this._duration) * 100 : 0,
272
- }
273
- }
274
-
275
- get currentTime(): number {
276
- return this._currentTime
277
- }
278
-
279
- get duration(): number {
280
- return this._duration
281
- }
282
-
283
- get isPlayingState(): boolean {
284
- return this.isPlaying && !this.isPaused
285
- }
286
-
287
- get isPausedState(): boolean {
288
- return this.isPaused
289
- }
290
- }