package particles import "core:math" import gpu ".." MAX_PARTICLE_DT :: f32(0.1) System :: struct { config: Config, particles: []Particle, alive_count: u32, emit_accum: f32, rng_state: u32, active: bool, } Config :: struct { max_particles: u32, emission_rate: f32, lifetime: [2]f32, speed: [2]f32, direction: gpu.Vec2, spread: f32, start_color: gpu.Color, end_color: gpu.Color, start_size: f32, end_size: f32, gravity: gpu.Vec2, } Particle :: struct { position: gpu.Vec2, velocity: gpu.Vec2, color: gpu.Color, size: f32, life: f32, max_life: f32, } System_3D :: struct { config: Config_3D, particles: []Particle_3D, alive_count: u32, emit_accum: f32, rng_state: u32, active: bool, } Config_3D :: struct { max_particles: u32, emission_rate: f32, lifetime: [2]f32, speed: [2]f32, direction: gpu.Vec3, spread: f32, start_color: gpu.Color, end_color: gpu.Color, start_size: f32, end_size: f32, gravity: gpu.Vec3, } Particle_3D :: struct { position: gpu.Vec3, velocity: gpu.Vec3, color: gpu.Color, size: f32, life: f32, max_life: f32, } default_config :: proc() -> Config { return { max_particles = 256, emission_rate = 50, lifetime = {0.5, 2.0}, speed = {50, 150}, direction = {0, -1}, spread = 30, start_color = gpu.WHITE, end_color = {1, 1, 1, 0}, start_size = 4, end_size = 1, gravity = {0, 98}, } } default_config_3d :: proc() -> Config_3D { return { max_particles = 256, emission_rate = 50, lifetime = {0.5, 2.0}, speed = {1, 3}, direction = {0, 1, 0}, spread = 30, start_color = gpu.WHITE, end_color = {1, 1, 1, 0}, start_size = 0.15, end_size = 0.03, gravity = {0, -4, 0}, } } init :: proc(config: Config, seed: u32 = 2463534242) -> (system: System, ok: bool) { count := config.max_particles if config.max_particles > 0 else 256 system = { config = config, particles = make([]Particle, count), rng_state = seed if seed != 0 else 1, active = true, } return system, true } init_3d :: proc(config: Config_3D, seed: u32 = 2463534242) -> (system: System_3D, ok: bool) { count := config.max_particles if config.max_particles > 0 else 256 system = { config = config, particles = make([]Particle_3D, count), rng_state = seed if seed != 0 else 1, active = true, } return system, true } destroy :: proc(system: ^System) { if system == nil || !system.active { return } delete(system.particles) system^ = {} } destroy_3d :: proc(system: ^System_3D) { if system == nil || !system.active { return } delete(system.particles) system^ = {} } alive_count :: proc(system: ^System) -> u32 { if system == nil || !system.active { return 0 } return system.alive_count } alive_count_3d :: proc(system: ^System_3D) -> u32 { if system == nil || !system.active { return 0 } return system.alive_count } update :: proc(system: ^System, position: gpu.Vec2, dt: f32) { if system == nil || !system.active || dt <= 0 { return } cfg := &system.config clamped_dt := min(dt, MAX_PARTICLE_DT) i: u32 = 0 for i < system.alive_count { p := &system.particles[i] p.life -= clamped_dt if p.life <= 0 { system.alive_count -= 1 if i < system.alive_count { p^ = system.particles[system.alive_count] } continue } p.velocity += cfg.gravity * clamped_dt p.position += p.velocity * clamped_dt t: f32 = 1.0 if p.max_life > 0 { t = 1.0 - p.life / p.max_life } for c in 0..<4 { p.color[c] = cfg.start_color[c] + (cfg.end_color[c] - cfg.start_color[c]) * t } p.size = cfg.start_size + (cfg.end_size - cfg.start_size) * t i += 1 } if cfg.emission_rate > 0 { system.emit_accum += cfg.emission_rate * clamped_dt emit_count := u32(system.emit_accum) if emit_count > 0 { system.emit_accum -= f32(emit_count) emit(system, position, emit_count) } } } update_3d :: proc(system: ^System_3D, position: gpu.Vec3, dt: f32) { if system == nil || !system.active || dt <= 0 { return } cfg := &system.config clamped_dt := min(dt, MAX_PARTICLE_DT) i: u32 = 0 for i < system.alive_count { p := &system.particles[i] p.life -= clamped_dt if p.life <= 0 { system.alive_count -= 1 if i < system.alive_count { p^ = system.particles[system.alive_count] } continue } p.velocity += cfg.gravity * clamped_dt p.position += p.velocity * clamped_dt t: f32 = 1.0 if p.max_life > 0 { t = 1.0 - p.life / p.max_life } for c in 0..<4 { p.color[c] = cfg.start_color[c] + (cfg.end_color[c] - cfg.start_color[c]) * t } p.size = cfg.start_size + (cfg.end_size - cfg.start_size) * t i += 1 } if cfg.emission_rate > 0 { system.emit_accum += cfg.emission_rate * clamped_dt emit_count := u32(system.emit_accum) if emit_count > 0 { system.emit_accum -= f32(emit_count) emit_3d(system, position, emit_count) } } } emit :: proc(system: ^System, position: gpu.Vec2, count: u32) { if system == nil || !system.active { return } cfg := &system.config max_p := u32(len(system.particles)) dir := cfg.direction if dir.x * dir.x + dir.y * dir.y < 1e-8 { dir = {0, -1} } base_angle := math.atan2(dir.y, dir.x) spread_rad := cfg.spread * math.PI / 180.0 for _ in 0..= max_p { break } p := &system.particles[system.alive_count] life := rand_range(&system.rng_state, cfg.lifetime[0], cfg.lifetime[1]) if life <= 0 { continue } spd := rand_range(&system.rng_state, cfg.speed[0], cfg.speed[1]) angle := base_angle + rand_range(&system.rng_state, -spread_rad, spread_rad) p.position = position p.velocity = {math.cos(angle) * spd, math.sin(angle) * spd} p.color = cfg.start_color p.size = cfg.start_size p.life = life p.max_life = life system.alive_count += 1 } } emit_3d :: proc(system: ^System_3D, position: gpu.Vec3, count: u32) { if system == nil || !system.active { return } cfg := &system.config max_p := u32(len(system.particles)) spread_rad := cfg.spread * math.PI / 180.0 dir := normalize3(cfg.direction) if dir.x * dir.x + dir.y * dir.y + dir.z * dir.z < 0.5 { dir = {0, 1, 0} } up: gpu.Vec3 if math.abs(dir.y) < 0.9 { up = {0, 1, 0} } else { up = {1, 0, 0} } right := normalize3(cross3(dir, up)) up = cross3(right, dir) for _ in 0..= max_p { break } p := &system.particles[system.alive_count] life := rand_range(&system.rng_state, cfg.lifetime[0], cfg.lifetime[1]) if life <= 0 { continue } spd := rand_range(&system.rng_state, cfg.speed[0], cfg.speed[1]) tilt := rand_range(&system.rng_state, 0, spread_rad) spin := rand_range(&system.rng_state, 0, 2.0 * math.PI) perp := right * math.cos(spin) + up * math.sin(spin) vel_dir := normalize3(dir * math.cos(tilt) + cross3(perp, dir) * math.sin(tilt)) p.position = position p.velocity = vel_dir * spd p.color = cfg.start_color p.size = cfg.start_size p.life = life p.max_life = life system.alive_count += 1 } } draw_2d :: proc(frame: ^gpu.Frame, system: ^System, order: gpu.Order_Mode = .Sortable, sort_key: u64 = 0) { if frame == nil || system == nil || !system.active { return } for i in 0.. u32 { s := state^ if s == 0 { s = 1 } s ~= s << 13 s ~= s >> 17 s ~= s << 5 state^ = s return s } @(private) rand_f32 :: proc(state: ^u32) -> f32 { return f32(xorshift32(state)) / f32(max(u32)) } @(private) rand_range :: proc(state: ^u32, lo, hi: f32) -> f32 { return lo + rand_f32(state) * (hi - lo) } @(private) normalize3 :: proc(v: gpu.Vec3) -> gpu.Vec3 { l := math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) if l < 1e-8 { return {0, 0, 0} } return v / l } @(private) cross3 :: proc(a, b: gpu.Vec3) -> gpu.Vec3 { return { a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x, } }