@bornengine/engine 0.4.16
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/LICENSE +21 -0
- package/README.md +231 -0
- package/native/android/Cargo.lock +1848 -0
- package/native/android/Cargo.toml +24 -0
- package/native/android/src/lib.rs +702 -0
- package/native/ios/Cargo.lock +1690 -0
- package/native/ios/Cargo.toml +32 -0
- package/native/ios/src/lib.rs +1267 -0
- package/native/linux/Cargo.lock +3279 -0
- package/native/linux/Cargo.toml +29 -0
- package/native/linux/src/lib.rs +1331 -0
- package/native/macos/Cargo.lock +3310 -0
- package/native/macos/Cargo.toml +46 -0
- package/native/macos/src/lib.rs +1302 -0
- package/native/shared/Cargo.lock +1899 -0
- package/native/shared/Cargo.toml +62 -0
- package/native/shared/assets/default_font.ttf +0 -0
- package/native/shared/build.rs +270 -0
- package/native/shared/shaders/common/clouds.wgsl +122 -0
- package/native/shared/shaders/common/fog.wgsl +16 -0
- package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
- package/native/shared/shaders/common/imposter.wgsl +112 -0
- package/native/shared/shaders/common/pbr.wgsl +186 -0
- package/native/shared/shaders/common/shadows.wgsl +186 -0
- package/native/shared/shaders/common/sky.wgsl +8 -0
- package/native/shared/shaders/common/tonemap.wgsl +25 -0
- package/native/shared/shaders/impulse_field.wgsl +57 -0
- package/native/shared/shaders/material_abi.wgsl +383 -0
- package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
- package/native/shared/src/anim_mixer.rs +61 -0
- package/native/shared/src/attach.rs +263 -0
- package/native/shared/src/audio/decode.rs +123 -0
- package/native/shared/src/audio/mod.rs +863 -0
- package/native/shared/src/audio/render.rs +892 -0
- package/native/shared/src/audio/spsc.rs +156 -0
- package/native/shared/src/audio/stream.rs +226 -0
- package/native/shared/src/custom_shaders.rs +104 -0
- package/native/shared/src/decals.rs +245 -0
- package/native/shared/src/drs.rs +211 -0
- package/native/shared/src/engine.rs +261 -0
- package/native/shared/src/ffi.rs +116 -0
- package/native/shared/src/ffi_core/assets.rs +388 -0
- package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
- package/native/shared/src/ffi_core/draw.rs +334 -0
- package/native/shared/src/ffi_core/game_loop.rs +577 -0
- package/native/shared/src/ffi_core/input.rs +234 -0
- package/native/shared/src/ffi_core/mod.rs +127 -0
- package/native/shared/src/ffi_core/models.rs +1154 -0
- package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
- package/native/shared/src/ffi_core/scene.rs +626 -0
- package/native/shared/src/ffi_core/vfx.rs +212 -0
- package/native/shared/src/ffi_core/visual.rs +691 -0
- package/native/shared/src/frame_callbacks.rs +122 -0
- package/native/shared/src/geometry.rs +236 -0
- package/native/shared/src/handles.rs +182 -0
- package/native/shared/src/input.rs +448 -0
- package/native/shared/src/jolt_sys.rs +822 -0
- package/native/shared/src/lib.rs +55 -0
- package/native/shared/src/models.rs +1093 -0
- package/native/shared/src/models_gltf.rs +1280 -0
- package/native/shared/src/particles.rs +391 -0
- package/native/shared/src/physics_jolt.rs +1908 -0
- package/native/shared/src/picking.rs +298 -0
- package/native/shared/src/postfx.rs +345 -0
- package/native/shared/src/profiler.rs +492 -0
- package/native/shared/src/ragdoll.rs +474 -0
- package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
- package/native/shared/src/renderer/brdf_lut.rs +154 -0
- package/native/shared/src/renderer/draw2d.rs +143 -0
- package/native/shared/src/renderer/formats.rs +822 -0
- package/native/shared/src/renderer/froxel.rs +421 -0
- package/native/shared/src/renderer/gi_bake.rs +653 -0
- package/native/shared/src/renderer/graph.rs +462 -0
- package/native/shared/src/renderer/hiz.rs +269 -0
- package/native/shared/src/renderer/hot_reload.rs +390 -0
- package/native/shared/src/renderer/impulse_field.rs +456 -0
- package/native/shared/src/renderer/lighting.rs +154 -0
- package/native/shared/src/renderer/material_instancing.rs +171 -0
- package/native/shared/src/renderer/material_pipeline.rs +700 -0
- package/native/shared/src/renderer/material_system.rs +1996 -0
- package/native/shared/src/renderer/material_system_tests.rs +601 -0
- package/native/shared/src/renderer/material_system_wasm.rs +41 -0
- package/native/shared/src/renderer/mod.rs +12556 -0
- package/native/shared/src/renderer/model_draw.rs +641 -0
- package/native/shared/src/renderer/occlusion.rs +429 -0
- package/native/shared/src/renderer/planar_pass.rs +593 -0
- package/native/shared/src/renderer/planar_reflection.rs +499 -0
- package/native/shared/src/renderer/post_pass.rs +249 -0
- package/native/shared/src/renderer/postfx_chain.rs +728 -0
- package/native/shared/src/renderer/pt_pass.rs +577 -0
- package/native/shared/src/renderer/scene_pass.rs +607 -0
- package/native/shared/src/renderer/shader_include.rs +205 -0
- package/native/shared/src/renderer/shader_library.rs +135 -0
- package/native/shared/src/renderer/shaders/ao.rs +570 -0
- package/native/shared/src/renderer/shaders/core.rs +1243 -0
- package/native/shared/src/renderer/shaders/env.rs +907 -0
- package/native/shared/src/renderer/shaders/gi.rs +810 -0
- package/native/shared/src/renderer/shaders/mod.rs +19 -0
- package/native/shared/src/renderer/shaders/post.rs +1558 -0
- package/native/shared/src/renderer/shaders/pt.rs +1859 -0
- package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
- package/native/shared/src/renderer/shadow_pass.rs +731 -0
- package/native/shared/src/renderer/ssgi_pass.rs +392 -0
- package/native/shared/src/renderer/ssr_pass.rs +188 -0
- package/native/shared/src/renderer/texture_store.rs +473 -0
- package/native/shared/src/renderer/transient.rs +591 -0
- package/native/shared/src/renderer/types.rs +941 -0
- package/native/shared/src/renderer/util.rs +152 -0
- package/native/shared/src/scene.rs +1362 -0
- package/native/shared/src/sdf_cache.rs +274 -0
- package/native/shared/src/shadows.rs +1036 -0
- package/native/shared/src/staging.rs +102 -0
- package/native/shared/src/string_header.rs +266 -0
- package/native/shared/src/text_renderer.rs +502 -0
- package/native/shared/src/textures.rs +197 -0
- package/native/tvos/Cargo.lock +1693 -0
- package/native/tvos/Cargo.toml +36 -0
- package/native/tvos/metal-patched/Cargo.toml +178 -0
- package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
- package/native/tvos/metal-patched/LICENSE-MIT +25 -0
- package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
- package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
- package/native/tvos/metal-patched/src/argument.rs +366 -0
- package/native/tvos/metal-patched/src/blitpass.rs +102 -0
- package/native/tvos/metal-patched/src/buffer.rs +71 -0
- package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
- package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
- package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
- package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
- package/native/tvos/metal-patched/src/computepass.rs +107 -0
- package/native/tvos/metal-patched/src/constants.rs +152 -0
- package/native/tvos/metal-patched/src/counters.rs +119 -0
- package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
- package/native/tvos/metal-patched/src/device.rs +2134 -0
- package/native/tvos/metal-patched/src/drawable.rs +39 -0
- package/native/tvos/metal-patched/src/encoder.rs +2041 -0
- package/native/tvos/metal-patched/src/heap.rs +281 -0
- package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
- package/native/tvos/metal-patched/src/lib.rs +657 -0
- package/native/tvos/metal-patched/src/library.rs +902 -0
- package/native/tvos/metal-patched/src/mps.rs +575 -0
- package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
- package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
- package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
- package/native/tvos/metal-patched/src/renderpass.rs +443 -0
- package/native/tvos/metal-patched/src/resource.rs +182 -0
- package/native/tvos/metal-patched/src/sampler.rs +165 -0
- package/native/tvos/metal-patched/src/sync.rs +178 -0
- package/native/tvos/metal-patched/src/texture.rs +352 -0
- package/native/tvos/metal-patched/src/types.rs +90 -0
- package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
- package/native/tvos/src/audio_backend.rs +197 -0
- package/native/tvos/src/lib.rs +1891 -0
- package/native/visionos/Cargo.lock +1693 -0
- package/native/visionos/Cargo.toml +40 -0
- package/native/visionos/src/audio_backend.rs +197 -0
- package/native/visionos/src/lib.rs +1887 -0
- package/native/watchos/Cargo.lock +16 -0
- package/native/watchos/Cargo.toml +19 -0
- package/native/watchos/shaders/bloom_postfx.metal +99 -0
- package/native/watchos/src/BloomWatchApp.swift +1267 -0
- package/native/watchos/src/BloomWatchAudio.swift +179 -0
- package/native/watchos/src/audio.rs +55 -0
- package/native/watchos/src/draw_list.rs +229 -0
- package/native/watchos/src/ffi_stubs.rs +915 -0
- package/native/watchos/src/ffi_stubs_manual.rs +35 -0
- package/native/watchos/src/lib.rs +1124 -0
- package/native/watchos/src/models.rs +746 -0
- package/native/watchos/src/postfx.rs +95 -0
- package/native/watchos/src/scene.rs +534 -0
- package/native/watchos/src/textures.rs +184 -0
- package/native/web/Cargo.lock +1657 -0
- package/native/web/Cargo.toml +43 -0
- package/native/web/bloom_glue.js +695 -0
- package/native/web/build.sh +131 -0
- package/native/web/index.html +35 -0
- package/native/web/jolt_bridge.js +1519 -0
- package/native/web/src/input_ffi.rs +286 -0
- package/native/web/src/lib.rs +1796 -0
- package/native/web/src/material_ffi.rs +710 -0
- package/native/web/src/parity_ffi.rs +343 -0
- package/native/web/src/physics_ffi.rs +643 -0
- package/native/web/src/ragdoll_ffi.rs +250 -0
- package/native/web/src/render_settings.rs +98 -0
- package/native/windows/Cargo.lock +1815 -0
- package/native/windows/Cargo.toml +68 -0
- package/native/windows/src/lib.rs +1486 -0
- package/package.json +4279 -0
- package/src/audio/index.ts +315 -0
- package/src/core/colors.ts +63 -0
- package/src/core/index.ts +1206 -0
- package/src/core/keys.ts +63 -0
- package/src/core/types.ts +104 -0
- package/src/index.ts +171 -0
- package/src/math/index.ts +516 -0
- package/src/mobile/index.ts +294 -0
- package/src/models/index.ts +1258 -0
- package/src/physics/index.ts +1134 -0
- package/src/scene/index.ts +698 -0
- package/src/shapes/index.ts +120 -0
- package/src/text/index.ts +48 -0
- package/src/textures/index.ts +187 -0
- package/src/vfx/index.ts +191 -0
- package/src/world/index.ts +24 -0
- package/src/world/loader.ts +423 -0
- package/src/world/prefab.ts +217 -0
- package/src/world/render.ts +172 -0
- package/src/world/saver.ts +108 -0
- package/src/world/serialize.ts +301 -0
- package/src/world/terrain.ts +355 -0
- package/src/world/types.ts +160 -0
- package/src/world/validate.ts +319 -0
- package/src/world/version.ts +114 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
//! EN-026 — CPU-simulated, GPU-instanced particle system.
|
|
2
|
+
//!
|
|
3
|
+
//! Why this lives in the engine rather than in game code: the game can already
|
|
4
|
+
//! build an instance buffer and draw it, but only by pushing every float
|
|
5
|
+
//! across the FFI one call at a time (`bloom_mesh_scratch_push_f32`). At 2 000
|
|
6
|
+
//! live particles that is ~24 000 FFI calls per frame — the shooter's entire
|
|
7
|
+
//! current per-frame FFI budget is ~240. So the *simulation* has to sit on the
|
|
8
|
+
//! native side, and the game/engine traffic becomes O(spawn events) instead of
|
|
9
|
+
//! O(particles): a burst is one call.
|
|
10
|
+
//!
|
|
11
|
+
//! What the engine owns: the pool, the integrator, and the packed instance
|
|
12
|
+
//! buffer. What the game owns: the material (so the look, the atlas and the
|
|
13
|
+
//! blend mode stay authorable in WGSL) and the draw call. A system is created
|
|
14
|
+
//! per *look* — smoke, sparks, blood, shells — because each wants its own
|
|
15
|
+
//! texture and blend bucket anyway, and one draw per look is cheap.
|
|
16
|
+
//!
|
|
17
|
+
//! Sim is deliberately simple and closed-form-ish: position, velocity,
|
|
18
|
+
//! gravity, linear drag, and a lifetime; size and colour are curves over
|
|
19
|
+
//! normalized age evaluated in the shader from `extra.x`. No collision, no
|
|
20
|
+
//! sorting (additive needs none, and the cutout bucket is depth-tested).
|
|
21
|
+
|
|
22
|
+
/// Everything about how one system's particles are born, move, and look.
|
|
23
|
+
/// Uploaded once from TS as a flat float array — see `configure_from_slice`.
|
|
24
|
+
#[derive(Clone, Copy)]
|
|
25
|
+
pub struct ParticleConfig {
|
|
26
|
+
pub life: f32,
|
|
27
|
+
pub life_var: f32,
|
|
28
|
+
/// Initial speed along the emit direction.
|
|
29
|
+
pub speed: f32,
|
|
30
|
+
pub speed_var: f32,
|
|
31
|
+
/// Half-angle of the emission cone, radians. `PI` = fully spherical.
|
|
32
|
+
pub spread: f32,
|
|
33
|
+
/// Constant acceleration (usually negative Y).
|
|
34
|
+
pub gravity: f32,
|
|
35
|
+
/// Linear drag coefficient (per second). 0 = vacuum.
|
|
36
|
+
pub drag: f32,
|
|
37
|
+
pub size0: f32,
|
|
38
|
+
pub size1: f32,
|
|
39
|
+
pub size_var: f32,
|
|
40
|
+
pub color0: [f32; 4],
|
|
41
|
+
pub color1: [f32; 4],
|
|
42
|
+
/// Roll speed, radians/sec (billboard spin).
|
|
43
|
+
pub spin: f32,
|
|
44
|
+
pub spin_var: f32,
|
|
45
|
+
/// Spawn positions are jittered inside a sphere of this radius.
|
|
46
|
+
pub pos_jitter: f32,
|
|
47
|
+
/// > 0 stretches the billboard along its velocity by this many seconds of
|
|
48
|
+
/// travel — the difference between a round spark and a tracer streak.
|
|
49
|
+
pub stretch: f32,
|
|
50
|
+
/// Fraction of the emitter's own velocity the particle inherits.
|
|
51
|
+
pub inherit: f32,
|
|
52
|
+
/// Number of atlas frames; the shader gets a frame index in `extra.y`.
|
|
53
|
+
pub frames: f32,
|
|
54
|
+
/// Bounce off the y = `floor_y` plane instead of passing through it.
|
|
55
|
+
/// `restitution` <= 0 disables (the default).
|
|
56
|
+
pub floor_y: f32,
|
|
57
|
+
pub restitution: f32,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
impl Default for ParticleConfig {
|
|
61
|
+
fn default() -> Self {
|
|
62
|
+
Self {
|
|
63
|
+
life: 1.0, life_var: 0.0,
|
|
64
|
+
speed: 1.0, speed_var: 0.0,
|
|
65
|
+
spread: 0.3,
|
|
66
|
+
gravity: -9.81,
|
|
67
|
+
drag: 0.0,
|
|
68
|
+
size0: 0.2, size1: 0.2, size_var: 0.0,
|
|
69
|
+
color0: [1.0; 4], color1: [1.0, 1.0, 1.0, 0.0],
|
|
70
|
+
spin: 0.0, spin_var: 0.0,
|
|
71
|
+
pos_jitter: 0.0,
|
|
72
|
+
stretch: 0.0,
|
|
73
|
+
inherit: 0.0,
|
|
74
|
+
frames: 1.0,
|
|
75
|
+
floor_y: 0.0, restitution: 0.0,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Structure-of-arrays pool. Dead particles are swap-removed from the live
|
|
81
|
+
/// prefix, so the live set is always `[0, live)` and the instance write is one
|
|
82
|
+
/// contiguous memcpy with no compaction pass.
|
|
83
|
+
pub struct ParticleSystem {
|
|
84
|
+
pub capacity: usize,
|
|
85
|
+
pub live: usize,
|
|
86
|
+
pub cfg: ParticleConfig,
|
|
87
|
+
/// GPU instance buffer handle (dynamic, capacity-sized).
|
|
88
|
+
pub instance_buffer: u32,
|
|
89
|
+
|
|
90
|
+
px: Vec<f32>, py: Vec<f32>, pz: Vec<f32>,
|
|
91
|
+
vx: Vec<f32>, vy: Vec<f32>, vz: Vec<f32>,
|
|
92
|
+
age: Vec<f32>, life: Vec<f32>,
|
|
93
|
+
rot: Vec<f32>, spin: Vec<f32>,
|
|
94
|
+
size: Vec<f32>, seed: Vec<f32>,
|
|
95
|
+
|
|
96
|
+
/// Packed 12-float-per-instance staging buffer, reused every frame.
|
|
97
|
+
packed: Vec<f32>,
|
|
98
|
+
rng: u32,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl ParticleSystem {
|
|
102
|
+
pub fn new(capacity: usize, instance_buffer: u32) -> Self {
|
|
103
|
+
let z = || vec![0.0f32; capacity];
|
|
104
|
+
Self {
|
|
105
|
+
capacity,
|
|
106
|
+
live: 0,
|
|
107
|
+
cfg: ParticleConfig::default(),
|
|
108
|
+
instance_buffer,
|
|
109
|
+
px: z(), py: z(), pz: z(),
|
|
110
|
+
vx: z(), vy: z(), vz: z(),
|
|
111
|
+
age: z(), life: z(),
|
|
112
|
+
rot: z(), spin: z(),
|
|
113
|
+
size: z(), seed: z(),
|
|
114
|
+
packed: vec![0.0; capacity * 12],
|
|
115
|
+
rng: 0x9E3779B9,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[inline]
|
|
120
|
+
fn rand(&mut self) -> f32 {
|
|
121
|
+
// xorshift32 — deterministic per system, which keeps a replayed burst
|
|
122
|
+
// identical frame-to-frame for screenshot diffing.
|
|
123
|
+
let mut x = self.rng;
|
|
124
|
+
x ^= x << 13;
|
|
125
|
+
x ^= x >> 17;
|
|
126
|
+
x ^= x << 5;
|
|
127
|
+
self.rng = x;
|
|
128
|
+
(x & 0x00FF_FFFF) as f32 / 16_777_215.0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Symmetric ±1 noise.
|
|
132
|
+
#[inline]
|
|
133
|
+
fn rand_s(&mut self) -> f32 { self.rand() * 2.0 - 1.0 }
|
|
134
|
+
|
|
135
|
+
pub fn configure_from_slice(&mut self, p: &[f32]) {
|
|
136
|
+
let g = |i: usize| -> f32 { p.get(i).copied().unwrap_or(0.0) };
|
|
137
|
+
self.cfg = ParticleConfig {
|
|
138
|
+
life: g(0).max(0.01), life_var: g(1),
|
|
139
|
+
speed: g(2), speed_var: g(3),
|
|
140
|
+
spread: g(4),
|
|
141
|
+
gravity: g(5),
|
|
142
|
+
drag: g(6),
|
|
143
|
+
size0: g(7), size1: g(8), size_var: g(9),
|
|
144
|
+
color0: [g(10), g(11), g(12), g(13)],
|
|
145
|
+
color1: [g(14), g(15), g(16), g(17)],
|
|
146
|
+
spin: g(18), spin_var: g(19),
|
|
147
|
+
pos_jitter: g(20),
|
|
148
|
+
stretch: g(21),
|
|
149
|
+
inherit: g(22),
|
|
150
|
+
frames: g(23).max(1.0),
|
|
151
|
+
floor_y: g(24),
|
|
152
|
+
restitution: g(25),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Spawn `count` particles at `pos`, biased along `dir`. A zero `dir`
|
|
157
|
+
/// means "no preferred direction" and emits into the full sphere, which is
|
|
158
|
+
/// what a blood burst or an explosion core wants; a surface normal gives a
|
|
159
|
+
/// cone, which is what an impact wants.
|
|
160
|
+
pub fn emit(&mut self, pos: [f32; 3], dir: [f32; 3], count: usize) {
|
|
161
|
+
let c = self.cfg;
|
|
162
|
+
let dlen = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
|
|
163
|
+
let base = if dlen > 1e-5 {
|
|
164
|
+
[dir[0] / dlen, dir[1] / dlen, dir[2] / dlen]
|
|
165
|
+
} else {
|
|
166
|
+
[0.0, 1.0, 0.0]
|
|
167
|
+
};
|
|
168
|
+
// Basis around the emit direction, for the cone sample.
|
|
169
|
+
let up = if base[1].abs() > 0.99 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] };
|
|
170
|
+
let right = normalize(cross(up, base));
|
|
171
|
+
let realup = cross(base, right);
|
|
172
|
+
|
|
173
|
+
let spread = if dlen > 1e-5 { c.spread } else { std::f32::consts::PI };
|
|
174
|
+
|
|
175
|
+
for _ in 0..count {
|
|
176
|
+
if self.live >= self.capacity {
|
|
177
|
+
// Full: overwrite the oldest-looking slot rather than dropping
|
|
178
|
+
// the spawn. A burst you asked for should always be visible;
|
|
179
|
+
// it is the *stale* particle that is expendable.
|
|
180
|
+
let victim = (self.rand() * self.capacity as f32) as usize % self.capacity.max(1);
|
|
181
|
+
self.age[victim] = self.life[victim];
|
|
182
|
+
self.kill(victim);
|
|
183
|
+
}
|
|
184
|
+
let i = self.live;
|
|
185
|
+
self.live += 1;
|
|
186
|
+
|
|
187
|
+
// Cone sample: cosine-ish, biased toward the axis.
|
|
188
|
+
let a = self.rand() * std::f32::consts::TAU;
|
|
189
|
+
let t = self.rand().sqrt() * spread;
|
|
190
|
+
let (st, ct) = (t.sin(), t.cos());
|
|
191
|
+
let d = [
|
|
192
|
+
base[0] * ct + (right[0] * a.cos() + realup[0] * a.sin()) * st,
|
|
193
|
+
base[1] * ct + (right[1] * a.cos() + realup[1] * a.sin()) * st,
|
|
194
|
+
base[2] * ct + (right[2] * a.cos() + realup[2] * a.sin()) * st,
|
|
195
|
+
];
|
|
196
|
+
let sp = (c.speed + self.rand_s() * c.speed_var).max(0.0);
|
|
197
|
+
|
|
198
|
+
let jx = self.rand_s() * c.pos_jitter;
|
|
199
|
+
let jy = self.rand_s() * c.pos_jitter;
|
|
200
|
+
let jz = self.rand_s() * c.pos_jitter;
|
|
201
|
+
|
|
202
|
+
self.px[i] = pos[0] + jx;
|
|
203
|
+
self.py[i] = pos[1] + jy;
|
|
204
|
+
self.pz[i] = pos[2] + jz;
|
|
205
|
+
self.vx[i] = d[0] * sp;
|
|
206
|
+
self.vy[i] = d[1] * sp;
|
|
207
|
+
self.vz[i] = d[2] * sp;
|
|
208
|
+
self.age[i] = 0.0;
|
|
209
|
+
self.life[i] = (c.life + self.rand_s() * c.life_var).max(0.02);
|
|
210
|
+
self.rot[i] = self.rand() * std::f32::consts::TAU;
|
|
211
|
+
self.spin[i] = c.spin + self.rand_s() * c.spin_var;
|
|
212
|
+
self.size[i] = 1.0 + self.rand_s() * c.size_var;
|
|
213
|
+
self.seed[i] = self.rand();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[inline]
|
|
218
|
+
fn kill(&mut self, i: usize) {
|
|
219
|
+
let last = self.live - 1;
|
|
220
|
+
if i != last {
|
|
221
|
+
self.px.swap(i, last); self.py.swap(i, last); self.pz.swap(i, last);
|
|
222
|
+
self.vx.swap(i, last); self.vy.swap(i, last); self.vz.swap(i, last);
|
|
223
|
+
self.age.swap(i, last); self.life.swap(i, last);
|
|
224
|
+
self.rot.swap(i, last); self.spin.swap(i, last);
|
|
225
|
+
self.size.swap(i, last); self.seed.swap(i, last);
|
|
226
|
+
}
|
|
227
|
+
self.live = last;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Integrate one step and repack. Returns the live count to draw.
|
|
231
|
+
pub fn update(&mut self, dt: f32) -> u32 {
|
|
232
|
+
let c = self.cfg;
|
|
233
|
+
let dt = dt.clamp(0.0, 0.1); // a hitch must not teleport the sim
|
|
234
|
+
|
|
235
|
+
let mut i = 0usize;
|
|
236
|
+
while i < self.live {
|
|
237
|
+
self.age[i] += dt;
|
|
238
|
+
if self.age[i] >= self.life[i] {
|
|
239
|
+
self.kill(i);
|
|
240
|
+
continue; // a live particle was swapped into i — re-test it
|
|
241
|
+
}
|
|
242
|
+
// Semi-implicit Euler + exponential drag.
|
|
243
|
+
self.vy[i] += c.gravity * dt;
|
|
244
|
+
if c.drag > 0.0 {
|
|
245
|
+
let k = (1.0 - c.drag * dt).max(0.0);
|
|
246
|
+
self.vx[i] *= k; self.vy[i] *= k; self.vz[i] *= k;
|
|
247
|
+
}
|
|
248
|
+
self.px[i] += self.vx[i] * dt;
|
|
249
|
+
self.py[i] += self.vy[i] * dt;
|
|
250
|
+
self.pz[i] += self.vz[i] * dt;
|
|
251
|
+
|
|
252
|
+
if c.restitution > 0.0 && self.py[i] < c.floor_y {
|
|
253
|
+
self.py[i] = c.floor_y;
|
|
254
|
+
self.vy[i] = -self.vy[i] * c.restitution;
|
|
255
|
+
// Kill the horizontal skid too, or shells slide forever.
|
|
256
|
+
self.vx[i] *= 0.6;
|
|
257
|
+
self.vz[i] *= 0.6;
|
|
258
|
+
if self.vy[i].abs() < 0.4 { self.vy[i] = 0.0; }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
self.rot[i] += self.spin[i] * dt;
|
|
262
|
+
i += 1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Pack. Layout must match InstanceData3D: pos.xyz, rot_y, scale,
|
|
266
|
+
// tint.rgba, extra.xyz.
|
|
267
|
+
for i in 0..self.live {
|
|
268
|
+
let t = (self.age[i] / self.life[i]).clamp(0.0, 1.0);
|
|
269
|
+
let size = lerp(c.size0, c.size1, t) * self.size[i];
|
|
270
|
+
let col = [
|
|
271
|
+
lerp(c.color0[0], c.color1[0], t),
|
|
272
|
+
lerp(c.color0[1], c.color1[1], t),
|
|
273
|
+
lerp(c.color0[2], c.color1[2], t),
|
|
274
|
+
lerp(c.color0[3], c.color1[3], t),
|
|
275
|
+
];
|
|
276
|
+
let frame = if c.frames > 1.0 {
|
|
277
|
+
(t * c.frames).floor().min(c.frames - 1.0)
|
|
278
|
+
} else { 0.0 };
|
|
279
|
+
// Velocity-stretch length in metres — the shader elongates the
|
|
280
|
+
// quad along the projected velocity by this much.
|
|
281
|
+
let stretch = if c.stretch > 0.0 {
|
|
282
|
+
let v = (self.vx[i] * self.vx[i] + self.vy[i] * self.vy[i] + self.vz[i] * self.vz[i]).sqrt();
|
|
283
|
+
v * c.stretch
|
|
284
|
+
} else { 0.0 };
|
|
285
|
+
|
|
286
|
+
let o = i * 12;
|
|
287
|
+
self.packed[o] = self.px[i];
|
|
288
|
+
self.packed[o + 1] = self.py[i];
|
|
289
|
+
self.packed[o + 2] = self.pz[i];
|
|
290
|
+
self.packed[o + 3] = self.rot[i];
|
|
291
|
+
self.packed[o + 4] = size;
|
|
292
|
+
self.packed[o + 5] = col[0];
|
|
293
|
+
self.packed[o + 6] = col[1];
|
|
294
|
+
self.packed[o + 7] = col[2];
|
|
295
|
+
self.packed[o + 8] = col[3];
|
|
296
|
+
self.packed[o + 9] = t; // extra.x — normalized age
|
|
297
|
+
self.packed[o + 10] = frame; // extra.y — atlas frame
|
|
298
|
+
self.packed[o + 11] = stretch; // extra.z — stretch metres
|
|
299
|
+
}
|
|
300
|
+
self.live as u32
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
pub fn packed(&self) -> &[f32] { &self.packed }
|
|
304
|
+
|
|
305
|
+
pub fn clear(&mut self) { self.live = 0; }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#[inline] fn lerp(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t }
|
|
309
|
+
#[inline] fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
|
|
310
|
+
[a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0]]
|
|
311
|
+
}
|
|
312
|
+
#[inline] fn normalize(v: [f32; 3]) -> [f32; 3] {
|
|
313
|
+
let l = (v[0]*v[0] + v[1]*v[1] + v[2]*v[2]).sqrt();
|
|
314
|
+
if l > 1e-6 { [v[0]/l, v[1]/l, v[2]/l] } else { [1.0, 0.0, 0.0] }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// Registry of all systems. Handles are 1-based; 0 is "no system".
|
|
318
|
+
pub struct ParticleManager {
|
|
319
|
+
pub systems: Vec<Option<ParticleSystem>>,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
impl ParticleManager {
|
|
323
|
+
pub fn new() -> Self { Self { systems: Vec::new() } }
|
|
324
|
+
|
|
325
|
+
pub fn create(&mut self, capacity: usize, instance_buffer: u32) -> u32 {
|
|
326
|
+
self.systems.push(Some(ParticleSystem::new(capacity, instance_buffer)));
|
|
327
|
+
self.systems.len() as u32
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
pub fn get_mut(&mut self, handle: u32) -> Option<&mut ParticleSystem> {
|
|
331
|
+
if handle == 0 { return None; }
|
|
332
|
+
self.systems.get_mut(handle as usize - 1)?.as_mut()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
impl Default for ParticleManager {
|
|
337
|
+
fn default() -> Self { Self::new() }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#[cfg(test)]
|
|
341
|
+
mod tests {
|
|
342
|
+
use super::*;
|
|
343
|
+
|
|
344
|
+
fn sys() -> ParticleSystem { ParticleSystem::new(64, 1) }
|
|
345
|
+
|
|
346
|
+
#[test]
|
|
347
|
+
fn emit_then_expire() {
|
|
348
|
+
let mut s = sys();
|
|
349
|
+
s.configure_from_slice(&[0.5, 0.0, 1.0, 0.0, 0.3, 0.0, 0.0, 1.0, 1.0, 0.0]);
|
|
350
|
+
s.emit([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 10);
|
|
351
|
+
assert_eq!(s.live, 10);
|
|
352
|
+
assert_eq!(s.update(0.1), 10);
|
|
353
|
+
// Past the 0.5 s lifetime everything must be reclaimed — the swap-remove
|
|
354
|
+
// loop has to re-test the swapped-in particle or it leaks half the pool.
|
|
355
|
+
for _ in 0..10 { s.update(0.1); }
|
|
356
|
+
assert_eq!(s.live, 0);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#[test]
|
|
360
|
+
fn capacity_is_never_exceeded() {
|
|
361
|
+
let mut s = sys();
|
|
362
|
+
s.configure_from_slice(&[10.0]);
|
|
363
|
+
s.emit([0.0; 3], [0.0, 1.0, 0.0], 500);
|
|
364
|
+
assert_eq!(s.live, 64);
|
|
365
|
+
assert_eq!(s.update(0.016), 64);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#[test]
|
|
369
|
+
fn gravity_pulls_down() {
|
|
370
|
+
let mut s = sys();
|
|
371
|
+
// life 10, speed 0, gravity -10
|
|
372
|
+
s.configure_from_slice(&[10.0, 0.0, 0.0, 0.0, 0.0, -10.0]);
|
|
373
|
+
s.emit([0.0, 5.0, 0.0], [0.0, 1.0, 0.0], 1);
|
|
374
|
+
s.update(0.1);
|
|
375
|
+
assert!(s.py[0] < 5.0, "expected fall, got y={}", s.py[0]);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#[test]
|
|
379
|
+
fn floor_bounce_reverses_velocity() {
|
|
380
|
+
let mut s = sys();
|
|
381
|
+
let mut p = vec![0.0f32; 26];
|
|
382
|
+
p[0] = 10.0; // life
|
|
383
|
+
p[5] = -10.0; // gravity
|
|
384
|
+
p[24] = 0.0; // floor_y
|
|
385
|
+
p[25] = 0.5; // restitution
|
|
386
|
+
s.configure_from_slice(&p);
|
|
387
|
+
s.emit([0.0, 0.1, 0.0], [0.0, -1.0, 0.0], 1);
|
|
388
|
+
for _ in 0..20 { s.update(0.016); }
|
|
389
|
+
assert!(s.py[0] >= 0.0, "particle sank through the floor: y={}", s.py[0]);
|
|
390
|
+
}
|
|
391
|
+
}
|