gizmo-renderer 0.9.1

A custom ECS and physics engine aimed for realistic simulations.
Documentation
// VOLUMETRİK DUMAN SİM (T6-full) — 3B yoğunluk grid'ini semi-Lagrangian advekte eder + kaynaktan
// enjekte + dissipation. Hız alanı PROSEDÜREL: buoyancy (yukarı) + diverjanssız curl-noise
// (zamanla evrilir). Tam basınç-çözücüsü YOK (yükselen/kıvrılan/dağılan duman için yeterli).
// src (okuma) → dst (yazma) ping-pong.

struct SmokeParams {
    bounds_min: vec4<f32>, // xyz = min, w = zaman
    bounds_max: vec4<f32>, // xyz = max, w = absorption (sim'de kullanılmaz)
    p0: vec4<f32>,         // x=density_scale, y=(boş), z=steps, w=dt
    color: vec4<f32>,      // (sim'de kullanılmaz)
    grid: vec4<f32>,       // x=N, y=(boş), z=source_radius, w=inject_amount
    source: vec4<f32>,     // xyz = kaynak, w = dissipation (frame başına çarpan)
    sim: vec4<f32>,        // x=buoyancy, y=curl_strength, z=curl_scale, w=(boş)
};

@group(0) @binding(0) var<uniform> P: SmokeParams;
@group(0) @binding(1) var<storage, read> src: array<f32>;
@group(0) @binding(2) var<storage, read_write> dst: array<f32>;
// Per-cell solidity (0 = open air, 1 = solid geometry). Populated on the CPU from AABBs.
@group(0) @binding(3) var<storage, read> obstacle: array<f32>;

fn curl_potential(p: vec3<f32>, t: f32) -> vec3<f32> {
    return vec3<f32>(
        sin(p.y * 0.9 + t * 0.7) + cos(p.z * 0.7 - t * 0.5),
        sin(p.z * 0.8 - t * 0.6) + cos(p.x * 0.6 + t * 0.4),
        sin(p.x * 0.7 + t * 0.5) + cos(p.y * 0.8 - t * 0.3),
    );
}
fn curl3(p: vec3<f32>, t: f32) -> vec3<f32> {
    let e = 0.35;
    let px = curl_potential(p + vec3<f32>(e, 0.0, 0.0), t);
    let mx = curl_potential(p - vec3<f32>(e, 0.0, 0.0), t);
    let py = curl_potential(p + vec3<f32>(0.0, e, 0.0), t);
    let my = curl_potential(p - vec3<f32>(0.0, e, 0.0), t);
    let pz = curl_potential(p + vec3<f32>(0.0, 0.0, e), t);
    let mz = curl_potential(p - vec3<f32>(0.0, 0.0, e), t);
    let cx = (py.z - my.z) - (pz.y - mz.y);
    let cy = (pz.x - mz.x) - (px.z - mx.z);
    let cz = (px.y - mx.y) - (py.x - my.x);
    return vec3<f32>(cx, cy, cz) / (2.0 * e);
}

fn cell_index(i: i32, j: i32, k: i32, n: i32) -> u32 {
    return u32((k * n + j) * n + i);
}

// True if the grid cell (i,j,k) is inside solid geometry. Out-of-grid = open air (false).
fn is_solid_cell(i: i32, j: i32, k: i32, n: i32) -> bool {
    if (i < 0 || j < 0 || k < 0 || i >= n || j >= n || k >= n) {
        return false;
    }
    return obstacle[cell_index(i, j, k, n)] > 0.5;
}

// True if a WORLD position falls inside a solid cell (out-of-bounds = open air).
fn is_solid_world(wpos: vec3<f32>, n: i32) -> bool {
    let bmin = P.bounds_min.xyz;
    let bmax = P.bounds_max.xyz;
    let g = (wpos - bmin) / (bmax - bmin) * f32(n);
    return is_solid_cell(i32(floor(g.x)), i32(floor(g.y)), i32(floor(g.z)), n);
}

// Dünya konumunda grid yoğunluğunu trilinear örnekler (sınır dışı → 0).
fn sample_grid(wpos: vec3<f32>, n: i32) -> f32 {
    let bmin = P.bounds_min.xyz;
    let bmax = P.bounds_max.xyz;
    let g = (wpos - bmin) / (bmax - bmin) * f32(n) - 0.5;
    let gi = floor(g);
    let f = g - gi;
    var acc = 0.0;
    for (var dz = 0; dz < 2; dz = dz + 1) {
        for (var dy = 0; dy < 2; dy = dy + 1) {
            for (var dx = 0; dx < 2; dx = dx + 1) {
                let ci = i32(gi.x) + dx;
                let cj = i32(gi.y) + dy;
                let ck = i32(gi.z) + dz;
                if (ci < 0 || cj < 0 || ck < 0 || ci >= n || cj >= n || ck >= n) {
                    continue;
                }
                let wx = mix(1.0 - f.x, f.x, f32(dx));
                let wy = mix(1.0 - f.y, f.y, f32(dy));
                let wz = mix(1.0 - f.z, f.z, f32(dz));
                acc += src[cell_index(ci, cj, ck, n)] * wx * wy * wz;
            }
        }
    }
    return acc;
}

@compute
@workgroup_size(4, 4, 4)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
    let n = i32(P.grid.x);
    let i = i32(gid.x);
    let j = i32(gid.y);
    let k = i32(gid.z);
    if (i >= n || j >= n || k >= n) {
        return;
    }
    let bmin = P.bounds_min.xyz;
    let bmax = P.bounds_max.xyz;
    let cs = (bmax - bmin) / f32(n);
    let world = bmin + (vec3<f32>(f32(i), f32(j), f32(k)) + 0.5) * cs;
    let dt = P.p0.w;
    let t = P.bounds_min.w;

    // Solid cells hold NO smoke — smoke can't occupy a wall/pillar.
    if (is_solid_cell(i, j, k, n)) {
        dst[cell_index(i, j, k, n)] = 0.0;
        return;
    }

    // Prosedürel hız: buoyancy (yukarı) + diverjanssız curl (kıvrılma).
    var vel = vec3<f32>(0.0, P.sim.x, 0.0);
    vel += curl3(world * P.sim.z, t) * P.sim.y;

    // CS2-tarzı radyal doldurma: kaynaktan DIŞA it → duman hacmi doldurur (yalnız yükselmez);
    // fill_radius'ta itiş sıfıra iner (sınırlı genişleme). fill_strength=0 → saf tüten sütun.
    let fill_strength = P.sim.w;
    if (fill_strength > 0.0) {
        let to = world - P.source.xyz;
        let r = length(to);
        let fill_radius = max(P.p0.y, 1e-3);
        if (r > 1e-3) {
            vel += (to / r) * fill_strength * clamp(1.0 - r / fill_radius, 0.0, 1.0);
        }
    }

    // No-penetration: hız duvara doğru bileşeninde sıfırlanır → duman kutuya/tavana itilmez,
    // altında/önünde birikir (conform). Komşu solid ise o eksen bileşeni kesilir.
    if (vel.x > 0.0 && is_solid_world(world + vec3<f32>(cs.x, 0.0, 0.0), n)) { vel.x = 0.0; }
    if (vel.x < 0.0 && is_solid_world(world - vec3<f32>(cs.x, 0.0, 0.0), n)) { vel.x = 0.0; }
    if (vel.y > 0.0 && is_solid_world(world + vec3<f32>(0.0, cs.y, 0.0), n)) { vel.y = 0.0; }
    if (vel.y < 0.0 && is_solid_world(world - vec3<f32>(0.0, cs.y, 0.0), n)) { vel.y = 0.0; }
    if (vel.z > 0.0 && is_solid_world(world + vec3<f32>(0.0, 0.0, cs.z), n)) { vel.z = 0.0; }
    if (vel.z < 0.0 && is_solid_world(world - vec3<f32>(0.0, 0.0, cs.z), n)) { vel.z = 0.0; }

    // Semi-Lagrangian backtrace, MARCHED as a segment (not only the endpoint) so density cannot
    // tunnel through a thin / fast-crossed wall, nor slip a diagonal corner: step from `world`
    // (guaranteed open — solids returned above) toward `back`, stop at the last OPEN sample
    // before the first solid, and pull from there. Endpoint-only checks miss walls thinner than
    // |vel*dt| and corner seams; marching the whole segment seals both.
    let back = world - vel * dt;
    let seg = back - world;
    let cell_min = min(cs.x, min(cs.y, cs.z));
    // ~2 samples per traversed cell (≥1, capped so a pathological velocity can't blow up the loop).
    let msteps = clamp(i32(ceil(length(seg) / (0.5 * cell_min))), 1, 16);
    var sample_pos = world;
    for (var s = 1; s <= msteps; s = s + 1) {
        let p = world + seg * (f32(s) / f32(msteps));
        if (is_solid_world(p, n)) {
            break;
        }
        sample_pos = p;
    }
    // Dissipation is FRAME-RATE INDEPENDENT: P.source.w is the per-60fps-frame multiplier, so
    // raise it to dt*60 (the number of 60fps-frames this step represents). Applying it once per
    // frame (as `* P.source.w`) made the smoke decay far faster at high fps — at a release
    // build's frame rate it dissipated before it could rise/spread, leaving only a thin sliver
    // near the source while a headless 60fps run filled the volume.
    var d = sample_grid(sample_pos, n) * pow(P.source.w, dt * 60.0);

    // Kaynaktan enjeksiyon (yumuşak küre).
    let sdist = length(world - P.source.xyz);
    if (sdist < P.grid.z) {
        d += P.grid.w * dt * (1.0 - sdist / P.grid.z);
    }

    dst[cell_index(i, j, k, n)] = clamp(d, 0.0, 6.0);
}