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 +66 -66
- package/dist/audio.d.ts +29 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +116 -0
- package/dist/engine.js +4 -4
- package/dist/particles.d.ts +67 -0
- package/dist/particles.d.ts.map +1 -0
- package/dist/particles.js +266 -0
- package/package.json +1 -1
- package/src/camera.ts +358 -358
- package/src/engine.ts +4 -4
- package/src/ik-solver.ts +411 -411
- package/src/math.ts +584 -584
- package/src/physics.ts +742 -742
- package/src/vmd-loader.ts +276 -276
- package/dist/engine_ts.d.ts +0 -143
- package/dist/engine_ts.d.ts.map +0 -1
- package/dist/engine_ts.js +0 -1575
- package/dist/player.d.ts +0 -64
- package/dist/player.d.ts.map +0 -1
- package/dist/player.js +0 -220
- package/src/player.ts +0 -290
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)
|
package/dist/audio.d.ts
ADDED
|
@@ -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
|
-
|
|
256
|
-
rimFactor =
|
|
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
|
+
}
|