reze-engine 0.3.11 → 0.3.12

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,66 +1,66 @@
1
- # Reze Engine
2
-
3
- A lightweight engine built with WebGPU and TypeScript for real-time 3D anime character MMD model rendering.
4
-
5
- ## Features
6
-
7
- - Physics
8
- - Alpha blending
9
- - Post alpha eye rendering
10
- - Rim lighting
11
- - Bloom
12
- - Outlines
13
- - MSAA 4x anti-aliasing
14
- - Bone and morph api
15
- - VMD animation
16
- - Ik solver
17
-
18
- ## Usage
19
-
20
- ```javascript
21
- export default function Scene() {
22
- const canvasRef = useRef < HTMLCanvasElement > null
23
- const engineRef = useRef < Engine > null
24
-
25
- const initEngine = useCallback(async () => {
26
- if (canvasRef.current) {
27
- try {
28
- const engine = new Engine(canvasRef.current)
29
- engineRef.current = engine
30
- await engine.init()
31
- await engine.loadModel("/models/塞尔凯特/塞尔凯特.pmx")
32
-
33
- engine.runRenderLoop(() => {})
34
- } catch (error) {
35
- console.error(error)
36
- }
37
- }
38
- }, [])
39
-
40
- useEffect(() => {
41
- void (async () => {
42
- initEngine()
43
- })()
44
-
45
- return () => {
46
- if (engineRef.current) {
47
- engineRef.current.dispose()
48
- }
49
- }
50
- }, [initEngine])
51
-
52
- return <canvas ref={canvasRef} className="w-full h-full" />
53
- }
54
- ```
55
-
56
- ## Projects Using This Engine
57
-
58
- - **[MiKaPo](https://mikapo.vercel.app)** - Online real-time motion capture for MMD using webcam and MediaPipe
59
- - **[Popo](https://popo.love)** - Fine-tuned LLM that generates MMD poses from natural language descriptions
60
- - **[MPL](https://mmd-mpl.vercel.app)** - Semantic motion programming language for scripting MMD animations with intuitive syntax
61
-
62
- ## Tutorial
63
-
64
- Learn WebGPU from scratch by building an anime character renderer in incremental steps. The tutorial covers the complete rendering pipeline from a simple triangle to fully textured, skeletal-animated characters.
65
-
66
- [How to Render an Anime Character with WebGPU](https://reze.one/tutorial)
1
+ # Reze Engine
2
+
3
+ A lightweight engine built with WebGPU and TypeScript for real-time 3D anime character MMD model rendering.
4
+
5
+ ## Features
6
+
7
+ - Physics
8
+ - Alpha blending
9
+ - Post alpha eye rendering
10
+ - Rim lighting
11
+ - Bloom
12
+ - Outlines
13
+ - MSAA 4x anti-aliasing
14
+ - Bone and morph api
15
+ - VMD animation
16
+ - Ik solver
17
+
18
+ ## Usage
19
+
20
+ ```javascript
21
+ export default function Scene() {
22
+ const canvasRef = useRef < HTMLCanvasElement > null
23
+ const engineRef = useRef < Engine > null
24
+
25
+ const initEngine = useCallback(async () => {
26
+ if (canvasRef.current) {
27
+ try {
28
+ const engine = new Engine(canvasRef.current)
29
+ engineRef.current = engine
30
+ await engine.init()
31
+ await engine.loadModel("/models/塞尔凯特/塞尔凯特.pmx")
32
+
33
+ engine.runRenderLoop(() => {})
34
+ } catch (error) {
35
+ console.error(error)
36
+ }
37
+ }
38
+ }, [])
39
+
40
+ useEffect(() => {
41
+ void (async () => {
42
+ initEngine()
43
+ })()
44
+
45
+ return () => {
46
+ if (engineRef.current) {
47
+ engineRef.current.dispose()
48
+ }
49
+ }
50
+ }, [initEngine])
51
+
52
+ return <canvas ref={canvasRef} className="w-full h-full" />
53
+ }
54
+ ```
55
+
56
+ ## Projects Using This Engine
57
+
58
+ - **[MiKaPo](https://mikapo.vercel.app)** - Online real-time motion capture for MMD using webcam and MediaPipe
59
+ - **[Popo](https://popo.love)** - Fine-tuned LLM that generates MMD poses from natural language descriptions
60
+ - **[MPL](https://mmd-mpl.vercel.app)** - Semantic motion programming language for scripting MMD animations with intuitive syntax
61
+
62
+ ## Tutorial
63
+
64
+ Learn WebGPU from scratch by building an anime character renderer in incremental steps. The tutorial covers the complete rendering pipeline from a simple triangle to fully textured, skeletal-animated characters.
65
+
66
+ [How to Render an Anime Character with WebGPU](https://reze.one/tutorial)
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Audio system for generating procedural explosion sounds
3
+ * Based on Web Audio API synthesis
4
+ */
5
+ export declare class AudioSystem {
6
+ private ctx;
7
+ private enabled;
8
+ private volume;
9
+ private limiter;
10
+ /**
11
+ * Initialize audio context and compressor
12
+ * Must be called after user interaction (browser requirement)
13
+ */
14
+ init(): void;
15
+ /**
16
+ * Play deep bass explosion sound (firework burst)
17
+ * Combines sub-bass, noise, and crack sounds for realistic explosion
18
+ */
19
+ playExplosion(): void;
20
+ /**
21
+ * Play lighter "whoosh" sound for rocket launch
22
+ */
23
+ playRocketLaunch(): void;
24
+ setVolume(volume: number): void;
25
+ getVolume(): number;
26
+ isEnabled(): boolean;
27
+ setEnabled(enabled: boolean): void;
28
+ }
29
+ //# sourceMappingURL=audio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,GAAG,CAA4B;IACvC,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAsC;IAErD;;;OAGG;IACH,IAAI,IAAI,IAAI;IAiBZ;;;OAGG;IACH,aAAa,IAAI,IAAI;IA8DrB;;OAEG;IACH,gBAAgB,IAAI,IAAI;IAsBxB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/B,SAAS,IAAI,MAAM;IAInB,SAAS,IAAI,OAAO;IAIpB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;CAGnC"}
package/dist/audio.js ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Audio system for generating procedural explosion sounds
3
+ * Based on Web Audio API synthesis
4
+ */
5
+ export class AudioSystem {
6
+ constructor() {
7
+ this.ctx = null;
8
+ this.enabled = false;
9
+ this.volume = 1;
10
+ this.limiter = null;
11
+ }
12
+ /**
13
+ * Initialize audio context and compressor
14
+ * Must be called after user interaction (browser requirement)
15
+ */
16
+ init() {
17
+ if (!this.ctx) {
18
+ this.ctx = new (window.AudioContext || window.webkitAudioContext)();
19
+ this.limiter = this.ctx.createDynamicsCompressor();
20
+ this.limiter.threshold.value = -10;
21
+ this.limiter.knee.value = 40;
22
+ this.limiter.ratio.value = 12;
23
+ this.limiter.connect(this.ctx.destination);
24
+ this.enabled = true;
25
+ }
26
+ // Resume if suspended (iOS/Safari requirement)
27
+ if (this.ctx.state === "suspended") {
28
+ this.ctx.resume();
29
+ }
30
+ }
31
+ /**
32
+ * Play deep bass explosion sound (firework burst)
33
+ * Combines sub-bass, noise, and crack sounds for realistic explosion
34
+ */
35
+ playExplosion() {
36
+ if (!this.enabled || !this.ctx || !this.limiter)
37
+ return;
38
+ const t = this.ctx.currentTime;
39
+ // --- 1. SUB-BASS KICK (deep rumble) ---
40
+ const osc = this.ctx.createOscillator();
41
+ const oscGain = this.ctx.createGain();
42
+ osc.type = "sine";
43
+ osc.frequency.setValueAtTime(50, t); // Start at 50Hz
44
+ osc.frequency.exponentialRampToValueAtTime(20, t + 2.5); // Drop to 20Hz
45
+ oscGain.gain.setValueAtTime(this.volume * 1.5, t);
46
+ oscGain.gain.exponentialRampToValueAtTime(0.01, t + 5.0); // Long decay
47
+ osc.connect(oscGain);
48
+ oscGain.connect(this.limiter);
49
+ osc.start(t);
50
+ osc.stop(t + 5.0);
51
+ // --- 2. NOISE (crackling/rumble texture) ---
52
+ const bufferSize = this.ctx.sampleRate * 5.0;
53
+ const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
54
+ const data = buffer.getChannelData(0);
55
+ for (let i = 0; i < bufferSize; i++) {
56
+ data[i] = Math.random() * 2 - 1;
57
+ }
58
+ const noise = this.ctx.createBufferSource();
59
+ noise.buffer = buffer;
60
+ const noiseFilter = this.ctx.createBiquadFilter();
61
+ noiseFilter.type = "lowpass";
62
+ noiseFilter.frequency.setValueAtTime(150, t);
63
+ noiseFilter.frequency.exponentialRampToValueAtTime(30, t + 4.0);
64
+ const noiseGain = this.ctx.createGain();
65
+ noiseGain.gain.setValueAtTime(this.volume * 1.0, t);
66
+ noiseGain.gain.exponentialRampToValueAtTime(0.01, t + 4.5);
67
+ noise.connect(noiseFilter);
68
+ noiseFilter.connect(noiseGain);
69
+ noiseGain.connect(this.limiter);
70
+ noise.start(t);
71
+ // --- 3. CRACK (sharp attack) ---
72
+ const crack = this.ctx.createOscillator();
73
+ crack.type = "triangle";
74
+ const crackGain = this.ctx.createGain();
75
+ crack.frequency.setValueAtTime(200, t);
76
+ crack.frequency.exponentialRampToValueAtTime(50, t + 0.1);
77
+ crackGain.gain.setValueAtTime(this.volume * 0.3, t);
78
+ crackGain.gain.exponentialRampToValueAtTime(0.01, t + 0.1);
79
+ crack.connect(crackGain);
80
+ crackGain.connect(this.limiter);
81
+ crack.start(t);
82
+ crack.stop(t + 0.1);
83
+ }
84
+ /**
85
+ * Play lighter "whoosh" sound for rocket launch
86
+ */
87
+ playRocketLaunch() {
88
+ if (!this.enabled || !this.ctx || !this.limiter)
89
+ return;
90
+ const t = this.ctx.currentTime;
91
+ // Rising tone
92
+ const osc = this.ctx.createOscillator();
93
+ const oscGain = this.ctx.createGain();
94
+ osc.type = "sawtooth";
95
+ osc.frequency.setValueAtTime(100, t);
96
+ osc.frequency.exponentialRampToValueAtTime(400, t + 0.5);
97
+ oscGain.gain.setValueAtTime(this.volume * 0.3, t);
98
+ oscGain.gain.exponentialRampToValueAtTime(0.01, t + 0.5);
99
+ osc.connect(oscGain);
100
+ oscGain.connect(this.limiter);
101
+ osc.start(t);
102
+ osc.stop(t + 0.5);
103
+ }
104
+ setVolume(volume) {
105
+ this.volume = Math.max(0, Math.min(1, volume));
106
+ }
107
+ getVolume() {
108
+ return this.volume;
109
+ }
110
+ isEnabled() {
111
+ return this.enabled;
112
+ }
113
+ setEnabled(enabled) {
114
+ this.enabled = enabled;
115
+ }
116
+ }
package/dist/engine.js CHANGED
@@ -250,12 +250,12 @@ export class Engine {
250
250
 
251
251
  let lightAccum = light.ambientColor;
252
252
 
253
- // Rim light calculation
253
+ // Rim light calculation - proper Fresnel for edge-only highlights
254
254
  let viewDir = normalize(camera.viewPos - input.worldPos);
255
- var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
256
- rimFactor = rimFactor * rimFactor; // Optimized: direct multiply instead of pow(x, 2.0)
255
+ let fresnel = 1.0 - abs(dot(n, viewDir));
256
+ let rimFactor = pow(fresnel, 4.0); // Higher power for sharper edge-only effect
257
257
  let rimLight = material.rimColor * material.rimIntensity * rimFactor;
258
-
258
+
259
259
  let color = albedo * lightAccum + rimLight;
260
260
 
261
261
  return vec4f(color, finalAlpha);
@@ -0,0 +1,67 @@
1
+ import { Vec3 } from "./math";
2
+ export interface ParticleSystemConfig {
3
+ particleCount: number;
4
+ particleSize: number;
5
+ fadeSpeed: number;
6
+ explosionForce: number;
7
+ hoverDuration: number;
8
+ gravity: number;
9
+ friction: number;
10
+ rocketSpeed: number;
11
+ rocketSize: number;
12
+ }
13
+ export declare const DEFAULT_PARTICLE_CONFIG: ParticleSystemConfig;
14
+ interface ParticleData {
15
+ positions: Float32Array;
16
+ velocities: Float32Array;
17
+ colors: Float32Array;
18
+ lifetimes: Float32Array;
19
+ count: number;
20
+ }
21
+ export declare class Firework {
22
+ private startX;
23
+ private config;
24
+ private phase;
25
+ private timer;
26
+ private pos;
27
+ private vel;
28
+ private targetY;
29
+ private colors;
30
+ private rocketAlive;
31
+ private rocketLifetime;
32
+ private justExploded;
33
+ private particles;
34
+ private baseColors;
35
+ constructor(startX: number, config: ParticleSystemConfig);
36
+ private hslToRgb;
37
+ update(deltaTime: number): void;
38
+ private smoothstep;
39
+ private explode;
40
+ isDead(): boolean;
41
+ isRocket(): boolean;
42
+ getRocketPosition(): Vec3;
43
+ getRocketColor(): Vec3;
44
+ getParticleData(): ParticleData | null;
45
+ hasJustExploded(): boolean;
46
+ clearExplosionFlag(): void;
47
+ }
48
+ export declare class ParticleSystem {
49
+ private fireworks;
50
+ private config;
51
+ private maxConcurrentFireworks;
52
+ private autoLaunch;
53
+ private launchInterval;
54
+ private lastLaunchTime;
55
+ private nextLaunchDelay;
56
+ constructor(config?: Partial<ParticleSystemConfig>);
57
+ update(deltaTime: number): void;
58
+ spawnFirework(x?: number): void;
59
+ getFireworks(): Firework[];
60
+ setAutoLaunch(enabled: boolean): void;
61
+ setConfig(config: Partial<ParticleSystemConfig>): void;
62
+ getConfig(): ParticleSystemConfig;
63
+ setMaxConcurrentFireworks(max: number): void;
64
+ getMaxConcurrentFireworks(): number;
65
+ }
66
+ export {};
67
+ //# sourceMappingURL=particles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../src/particles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAI7B,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,eAAO,MAAM,uBAAuB,EAAE,oBAUrC,CAAA;AAED,UAAU,YAAY;IACpB,SAAS,EAAE,YAAY,CAAA;IACvB,UAAU,EAAE,YAAY,CAAA;IACxB,MAAM,EAAE,YAAY,CAAA;IACpB,SAAS,EAAE,YAAY,CAAA;IACvB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,qBAAa,QAAQ;IAeP,OAAO,CAAC,MAAM;IAAU,OAAO,CAAC,MAAM;IAdlD,OAAO,CAAC,KAAK,CAA0C;IACvD,OAAO,CAAC,KAAK,CAAY;IACzB,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,YAAY,CAAiB;IAGrC,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,UAAU,CAA4B;gBAE1B,MAAM,EAAE,MAAM,EAAU,MAAM,EAAE,oBAAoB;IA4BxE,OAAO,CAAC,QAAQ;IAsBhB,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAgE/B,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,OAAO;IAwDf,MAAM,IAAI,OAAO;IAIjB,QAAQ,IAAI,OAAO;IAInB,iBAAiB,IAAI,IAAI;IAIzB,cAAc,IAAI,IAAI;IAItB,eAAe,IAAI,YAAY,GAAG,IAAI;IAItC,eAAe,IAAI,OAAO;IAI1B,kBAAkB,IAAI,IAAI;CAG3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,MAAM,CAAuD;IACrE,OAAO,CAAC,sBAAsB,CAAY;IAG1C,OAAO,CAAC,UAAU,CAAiB;IACnC,OAAO,CAAC,cAAc,CAAe;IACrC,OAAO,CAAC,cAAc,CAAY;IAClC,OAAO,CAAC,eAAe,CAAY;gBAEvB,MAAM,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC;IAMlD,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAoB/B,aAAa,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAS/B,YAAY,IAAI,QAAQ,EAAE;IAI1B,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAQrC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI;IAItD,SAAS,IAAI,oBAAoB;IAIjC,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAI5C,yBAAyB,IAAI,MAAM;CAGpC"}
@@ -0,0 +1,266 @@
1
+ import { Vec3 } from "./math";
2
+ const MAX_PARTICLES = 30000;
3
+ export const DEFAULT_PARTICLE_CONFIG = {
4
+ particleCount: 5000, // Reduced from 23000 for better performance
5
+ particleSize: 0.1, // Smaller particles
6
+ fadeSpeed: 0.008, // Faster fade = particles die sooner
7
+ explosionForce: 0.6, // Smaller explosion radius
8
+ hoverDuration: 1.2, // Shorter hover
9
+ gravity: 0.00265,
10
+ friction: 0.95494,
11
+ rocketSpeed: 1.0,
12
+ rocketSize: 0.4, // Smaller rocket trail
13
+ };
14
+ export class Firework {
15
+ constructor(startX, config) {
16
+ this.startX = startX;
17
+ this.config = config;
18
+ this.phase = "rocket";
19
+ this.timer = 0;
20
+ this.colors = [];
21
+ this.rocketAlive = true;
22
+ this.rocketLifetime = 1.0;
23
+ this.justExploded = false; // Flag for triggering audio
24
+ // Explosion particle data
25
+ this.particles = null;
26
+ this.baseColors = null;
27
+ // Generate color palette (mono, dual, or tri-color)
28
+ const rand = Math.random();
29
+ const baseHue = Math.random();
30
+ if (rand < 0.33) {
31
+ // MONO (1 Color)
32
+ this.colors.push(this.hslToRgb(baseHue, 1.0, 0.6));
33
+ }
34
+ else if (rand < 0.66) {
35
+ // DUAL (2 Colors - Complementary)
36
+ this.colors.push(this.hslToRgb(baseHue, 1.0, 0.6));
37
+ this.colors.push(this.hslToRgb((baseHue + 0.5) % 1.0, 1.0, 0.5));
38
+ }
39
+ else {
40
+ // TRI (3 Colors - Triad)
41
+ this.colors.push(this.hslToRgb(baseHue, 1.0, 0.6));
42
+ this.colors.push(this.hslToRgb((baseHue + 0.33) % 1.0, 1.0, 0.6));
43
+ this.colors.push(this.hslToRgb((baseHue + 0.66) % 1.0, 1.0, 0.6));
44
+ }
45
+ this.pos = new Vec3(startX, -50, (Math.random() - 0.5) * 30);
46
+ this.vel = new Vec3((Math.random() - 0.5) * 0.5, config.rocketSpeed * (0.9 + Math.random() * 0.3), (Math.random() - 0.5) * 0.5);
47
+ this.targetY = 10 + Math.random() * 20; // Higher explosion point
48
+ }
49
+ hslToRgb(h, s, l) {
50
+ let r, g, b;
51
+ if (s === 0) {
52
+ r = g = b = l;
53
+ }
54
+ else {
55
+ const hue2rgb = (p, q, t) => {
56
+ if (t < 0)
57
+ t += 1;
58
+ if (t > 1)
59
+ t -= 1;
60
+ if (t < 1 / 6)
61
+ return p + (q - p) * 6 * t;
62
+ if (t < 1 / 2)
63
+ return q;
64
+ if (t < 2 / 3)
65
+ return p + (q - p) * (2 / 3 - t) * 6;
66
+ return p;
67
+ };
68
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
69
+ const p = 2 * l - q;
70
+ r = hue2rgb(p, q, h + 1 / 3);
71
+ g = hue2rgb(p, q, h);
72
+ b = hue2rgb(p, q, h - 1 / 3);
73
+ }
74
+ return new Vec3(r, g, b);
75
+ }
76
+ update(deltaTime) {
77
+ if (this.phase === "rocket") {
78
+ this.pos.x += this.vel.x;
79
+ this.pos.y += this.vel.y;
80
+ this.pos.z += this.vel.z;
81
+ this.vel.y *= 0.99;
82
+ this.rocketLifetime -= deltaTime * 0.5;
83
+ if (this.vel.y < 0.2 || this.pos.y >= this.targetY || this.rocketLifetime <= 0) {
84
+ this.explode();
85
+ }
86
+ }
87
+ else if (this.phase === "explode") {
88
+ this.timer += deltaTime;
89
+ if (!this.particles)
90
+ return;
91
+ const isHovering = this.timer < this.config.hoverDuration;
92
+ const gravityFactor = this.smoothstep(this.timer, this.config.hoverDuration, this.config.hoverDuration + 0.5);
93
+ let aliveCount = 0;
94
+ const positions = this.particles.positions;
95
+ const velocities = this.particles.velocities;
96
+ const colors = this.particles.colors;
97
+ const lifetimes = this.particles.lifetimes;
98
+ for (let i = 0; i < this.particles.count; i++) {
99
+ if (lifetimes[i] > 0) {
100
+ aliveCount++;
101
+ const i3 = i * 3;
102
+ // Update position
103
+ positions[i3] += velocities[i3];
104
+ positions[i3 + 1] += velocities[i3 + 1];
105
+ positions[i3 + 2] += velocities[i3 + 2];
106
+ if (isHovering) {
107
+ // Hovering phase - apply friction
108
+ velocities[i3] *= this.config.friction;
109
+ velocities[i3 + 1] *= this.config.friction;
110
+ velocities[i3 + 2] *= this.config.friction;
111
+ }
112
+ else {
113
+ // Falling phase - apply gravity and fade
114
+ velocities[i3 + 1] -= this.config.gravity * gravityFactor;
115
+ velocities[i3] *= 0.98;
116
+ velocities[i3 + 1] *= 0.98;
117
+ velocities[i3 + 2] *= 0.98;
118
+ lifetimes[i] -= this.config.fadeSpeed;
119
+ }
120
+ // Update color alpha
121
+ const alpha = Math.max(0, lifetimes[i]);
122
+ if (this.baseColors) {
123
+ colors[i3] = this.baseColors[i3] * alpha * 1.5;
124
+ colors[i3 + 1] = this.baseColors[i3 + 1] * alpha * 1.5;
125
+ colors[i3 + 2] = this.baseColors[i3 + 2] * alpha * 1.5;
126
+ }
127
+ }
128
+ }
129
+ if (aliveCount === 0) {
130
+ this.phase = "dead";
131
+ }
132
+ }
133
+ }
134
+ smoothstep(x, edge0, edge1) {
135
+ const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
136
+ return t * t * (3 - 2 * t);
137
+ }
138
+ explode() {
139
+ this.phase = "explode";
140
+ this.timer = 0;
141
+ this.rocketAlive = false;
142
+ this.justExploded = true; // Mark for audio trigger
143
+ const particleCount = Math.min(this.config.particleCount, MAX_PARTICLES);
144
+ const positions = new Float32Array(particleCount * 3);
145
+ const velocities = new Float32Array(particleCount * 3);
146
+ const colors = new Float32Array(particleCount * 3);
147
+ const lifetimes = new Float32Array(particleCount);
148
+ this.baseColors = new Float32Array(particleCount * 3);
149
+ const baseSpeed = this.config.explosionForce * (0.8 + Math.random() * 0.4);
150
+ for (let i = 0; i < particleCount; i++) {
151
+ const i3 = i * 3;
152
+ // Set initial position to explosion center
153
+ positions[i3] = this.pos.x;
154
+ positions[i3 + 1] = this.pos.y;
155
+ positions[i3 + 2] = this.pos.z;
156
+ // Generate spherical velocity distribution
157
+ const theta = Math.random() * Math.PI * 2;
158
+ const phi = Math.acos(2 * Math.random() - 1);
159
+ const speed = baseSpeed * (0.8 + Math.random() * 0.4);
160
+ velocities[i3] = speed * Math.sin(phi) * Math.cos(theta);
161
+ velocities[i3 + 1] = speed * Math.sin(phi) * Math.sin(theta);
162
+ velocities[i3 + 2] = speed * Math.cos(phi);
163
+ // Pick color from palette and vary brightness
164
+ const targetColor = this.colors[Math.floor(Math.random() * this.colors.length)];
165
+ const brightness = 0.5 + Math.random() * 0.8;
166
+ this.baseColors[i3] = targetColor.x * brightness;
167
+ this.baseColors[i3 + 1] = targetColor.y * brightness;
168
+ this.baseColors[i3 + 2] = targetColor.z * brightness;
169
+ colors[i3] = this.baseColors[i3];
170
+ colors[i3 + 1] = this.baseColors[i3 + 1];
171
+ colors[i3 + 2] = this.baseColors[i3 + 2];
172
+ lifetimes[i] = 1.0;
173
+ }
174
+ this.particles = {
175
+ positions,
176
+ velocities,
177
+ colors,
178
+ lifetimes,
179
+ count: particleCount,
180
+ };
181
+ }
182
+ isDead() {
183
+ return this.phase === "dead";
184
+ }
185
+ isRocket() {
186
+ return this.phase === "rocket" && this.rocketAlive;
187
+ }
188
+ getRocketPosition() {
189
+ return this.pos;
190
+ }
191
+ getRocketColor() {
192
+ return this.colors[0];
193
+ }
194
+ getParticleData() {
195
+ return this.particles;
196
+ }
197
+ hasJustExploded() {
198
+ return this.justExploded;
199
+ }
200
+ clearExplosionFlag() {
201
+ this.justExploded = false;
202
+ }
203
+ }
204
+ export class ParticleSystem {
205
+ constructor(config) {
206
+ this.fireworks = [];
207
+ this.config = { ...DEFAULT_PARTICLE_CONFIG };
208
+ this.maxConcurrentFireworks = 5; // Limit concurrent fireworks for performance
209
+ // Auto-launch settings
210
+ this.autoLaunch = false;
211
+ this.launchInterval = 3000; // ms
212
+ this.lastLaunchTime = 0;
213
+ this.nextLaunchDelay = 0;
214
+ if (config) {
215
+ this.config = { ...this.config, ...config };
216
+ }
217
+ }
218
+ update(deltaTime) {
219
+ // Update existing fireworks
220
+ for (let i = this.fireworks.length - 1; i >= 0; i--) {
221
+ this.fireworks[i].update(deltaTime);
222
+ if (this.fireworks[i].isDead()) {
223
+ this.fireworks.splice(i, 1);
224
+ }
225
+ }
226
+ // Auto-launch
227
+ if (this.autoLaunch) {
228
+ const now = performance.now();
229
+ if (now - this.lastLaunchTime > this.nextLaunchDelay) {
230
+ this.lastLaunchTime = now;
231
+ this.nextLaunchDelay = this.launchInterval + Math.random() * 1000;
232
+ this.spawnFirework();
233
+ }
234
+ }
235
+ }
236
+ spawnFirework(x) {
237
+ // Don't spawn if we're at the limit (for performance)
238
+ if (this.fireworks.length >= this.maxConcurrentFireworks) {
239
+ return;
240
+ }
241
+ const startX = x !== undefined ? x : (Math.random() - 0.5) * 5;
242
+ this.fireworks.push(new Firework(startX, this.config));
243
+ }
244
+ getFireworks() {
245
+ return this.fireworks;
246
+ }
247
+ setAutoLaunch(enabled) {
248
+ this.autoLaunch = enabled;
249
+ if (enabled) {
250
+ this.lastLaunchTime = performance.now();
251
+ this.nextLaunchDelay = 0;
252
+ }
253
+ }
254
+ setConfig(config) {
255
+ this.config = { ...this.config, ...config };
256
+ }
257
+ getConfig() {
258
+ return { ...this.config };
259
+ }
260
+ setMaxConcurrentFireworks(max) {
261
+ this.maxConcurrentFireworks = Math.max(1, max);
262
+ }
263
+ getMaxConcurrentFireworks() {
264
+ return this.maxConcurrentFireworks;
265
+ }
266
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",