// 示踪粒子内核(assembly/tracers.ts 的 Moonbit 同构移植):粒子状态全量驻留 wasm 内存,
// 宿主每 tick 只写热源表并单次调用推进——风场采样直调流体(同包零跨界)、地形走宿主烘焙 SDF 场。
// 粒子纯视觉不涉判定:PRNG 用确定性 mulberry32(宿主播种),与 AS 位级一致。
///|
let t_count : Int = 400
///|
let t_trail_len : Int = 24
///|
let t_trail_sample : Double = 0.45
///|
let t_respawn_tries : Int = 8
///|
let t_plume_radius : Double = 1.6
///|
let t_plume_life_min : Double = 0.9
///|
let t_plume_life_span : Double = 1.2
// 自然死亡转生羽流的概率:稳态羽流密度 ≈ 死亡率×概率×羽流寿命,与原强夺式注入同量级
///|
let t_plume_chance : Double = 0.7
///|
let t_plume_tries : Int = 4
// 触地淡出时长:与宿主包络 FADE_OUT 同源(app/sim/particles.ts envelope)——
// 尾迹可见度被 env 调制,同一常数保证粒子与尾迹同步溶散
// 地形场容量 = 流体网格上限(grid.mbt 单点),超限 init 拒绝
///|
let t_fade_out : Double = 0.7
///|
let sdf_capacity : Int = grid_max_cells
///|
let src_capacity : Int = 32
///|
let tx : FixedArray[Float] = FixedArray::make(t_count, 0.0)
///|
let ty : FixedArray[Float] = FixedArray::make(t_count, 0.0)
///|
let t_life : FixedArray[Float] = FixedArray::make(t_count, 0.0)
///|
let t_max_life : FixedArray[Float] = FixedArray::make(t_count, 0.0)
///|
let t_odo : FixedArray[Float] = FixedArray::make(t_count, 0.0)
///|
let t_last_odo : FixedArray[Float] = FixedArray::make(t_count, 0.0)
///|
let trail_x : FixedArray[Float] = FixedArray::make(t_count * t_trail_len, 0.0)
///|
let trail_y : FixedArray[Float] = FixedArray::make(t_count * t_trail_len, 0.0)
///|
let trail_t : FixedArray[Float] = FixedArray::make(t_count * t_trail_len, 0.0)
///|
let trail_n : FixedArray[Byte] = FixedArray::make(t_count, b'\x00')
// 地形 SDF 场:宿主烘焙后原样上传(与流体掩码/飞机碰撞同源),格心值、双线性采样
///|
let t_sdf : FixedArray[Float] = FixedArray::make(sdf_capacity, 0.0)
///|
let src_buf : FixedArray[Float] = FixedArray::make(src_capacity * 2, 0.0)
///|
priv struct TState {
mut time : Double
mut world_w : Double
mut world_h : Double
mut margin : Double
mut snx : Int
mut sny : Int
mut scell : Double
mut sox : Double
mut soy : Double
mut rng_state : UInt
mut src_now : Int
}
///|
let ts : TState = TState::{
time: 0.0,
world_w: 0.0,
world_h: 0.0,
margin: 0.0,
snx: 0,
sny: 0,
scell: 1.0,
sox: 0.0,
soy: 0.0,
rng_state: 0x9e3779b9U,
src_now: 0,
}
// mulberry32:UInt 算术自然回绕、>> 为逻辑右移,与 JS/AS 的 imul + >>> 位级等价
///|
fn rnd() -> Double {
ts.rng_state = ts.rng_state + 0x6d2b79f5U
let mut z = ts.rng_state
z = (z ^ (z >> 15)) * (z | 1U)
z = z ^ (z + (z ^ (z >> 7)) * (z | 61U))
(z ^ (z >> 14)).to_double() / 4294967296.0
}
// 双线性采样烘焙场:clamp 约定与流体 sample 同构(域外取边缘值 = 地形延展)
///|
fn sdf_at(x : Double, y : Double) -> Double {
let mut gx = x / ts.scell - 0.5 + ts.sox
let mut gy = y / ts.scell - 0.5 + ts.soy
if gx < 0.0 {
gx = 0.0
} else if gx > ts.snx.to_double() - 1.001 {
gx = ts.snx.to_double() - 1.001
}
if gy < 0.0 {
gy = 0.0
} else if gy > ts.sny.to_double() - 1.001 {
gy = ts.sny.to_double() - 1.001
}
bilinear4(t_sdf, ts.snx, gx, gy)
}
///|
fn reset_trail(i : Int) -> Unit {
t_odo[i] = 0.0
t_last_odo[i] = 0.0
trail_n[i] = b'\x00'
}
///|
fn record_trail(i : Int) -> Unit {
let base = i * t_trail_len
let n = trail_n[i].to_int()
if n < t_trail_len {
trail_x[base + n] = tx[i]
trail_y[base + n] = ty[i]
trail_t[base + n] = Float::from_double(ts.time)
trail_n[i] = (n + 1).to_byte()
} else {
for k in 0..<(t_trail_len - 1) {
trail_x[base + k] = trail_x[base + k + 1]
trail_y[base + k] = trail_y[base + k + 1]
trail_t[base + k] = trail_t[base + k + 1]
}
trail_x[base + t_trail_len - 1] = tx[i]
trail_y[base + t_trail_len - 1] = ty[i]
trail_t[base + t_trail_len - 1] = Float::from_double(ts.time)
}
t_last_odo[i] = t_odo[i]
}
// 有热源时自然死亡按概率转生为羽流:粒子本已淡出完毕,无 alpha 突变;
// 旧式强夺活粒子会让被夺者可见轨迹瞬消
///|
fn respawn(i : Int, scatter : Bool) -> Unit {
if !scatter && ts.src_now > 0 && rnd() < t_plume_chance {
for _ in 0.. Int {
if count != t_count || trail_len != t_trail_len {
return 1
}
// scell 非正/NaN(采样除零/NaN 场)拒绝;!(>0) 同时捕获 NaN(NaN 比较恒 false)
if snx < 2 || sny < 2 || snx * sny > sdf_capacity || !(scell > 0.0) {
return 2
}
ts.world_w = world_w
ts.world_h = world_h
ts.margin = margin
ts.snx = snx
ts.sny = sny
ts.scell = scell
ts.sox = sox
ts.soy = soy
ts.rng_state = seed
ts.time = 0.0
for i in 0.. Unit {
ts.time = ts.time + dt
ts.src_now = src_count
let m = ts.margin
for i in 0.. ts.world_w + m - 1.0 {
if t_life[i].to_double() > t_fade_out {
t_life[i] = Float::from_double(t_fade_out)
}
continue
}
let dx = nx - tx[i].to_double()
let dy = ny - ty[i].to_double()
t_odo[i] = Float::from_double(
t_odo[i].to_double() + (dx * dx + dy * dy).sqrt(),
)
tx[i] = Float::from_double(nx)
ty[i] = Float::from_double(ny)
if t_odo[i].to_double() - t_last_odo[i].to_double() >= t_trail_sample {
record_trail(i)
}
}
}
///|
/// Current simulation time (seconds).
#export_name("tTime")
pub fn t_time() -> Double {
ts.time
}
///|
/// Linear-memory address of the tracer X positions (Float32Array, count).
#export_name("tXBuf")
pub fn t_x_buf() -> Int {
addr_of_f32(tx)
}
///|
/// Linear-memory address of the tracer Y positions (Float32Array, count).
#export_name("tYBuf")
pub fn t_y_buf() -> Int {
addr_of_f32(ty)
}
///|
/// Linear-memory address of the remaining lifetime per tracer (Float32Array, count).
#export_name("tLifeBuf")
pub fn t_life_buf() -> Int {
addr_of_f32(t_life)
}
///|
/// Linear-memory address of the max lifetime per tracer (Float32Array, count).
#export_name("tMaxLifeBuf")
pub fn t_max_life_buf() -> Int {
addr_of_f32(t_max_life)
}
///|
/// Linear-memory address of the trail X control points (Float32Array, count*trailLen).
#export_name("tTrailXBuf")
pub fn t_trail_x_buf() -> Int {
addr_of_f32(trail_x)
}
///|
/// Linear-memory address of the trail Y control points (Float32Array, count*trailLen).
#export_name("tTrailYBuf")
pub fn t_trail_y_buf() -> Int {
addr_of_f32(trail_y)
}
///|
/// Linear-memory address of the trail write times (Float32Array, count*trailLen).
#export_name("tTrailTBuf")
pub fn t_trail_t_buf() -> Int {
addr_of_f32(trail_t)
}
///|
/// Linear-memory address of the trail length per tracer (Uint8Array, count).
#export_name("tTrailNBuf")
pub fn t_trail_n_buf() -> Int {
addr_of_u8(trail_n)
}
///|
/// Linear-memory address of the tracer SDF field (Float32Array, snx*sny).
#export_name("tSdfBuf")
pub fn t_sdf_buf() -> Int {
addr_of_f32(t_sdf)
}
///|
/// Tracer SDF field capacity (in cells, equals the fluid grid upper bound).
#export_name("tSdfCap")
pub fn t_sdf_cap() -> Int {
sdf_capacity
}
///|
/// Linear-memory address of the hot/cold source table (Float32Array, src_cap*6).
#export_name("tSrcBuf")
pub fn t_src_buf() -> Int {
addr_of_f32(src_buf)
}
///|
/// Source table capacity (in source slots).
#export_name("tSrcCap")
pub fn t_src_cap() -> Int {
src_capacity
}