Harbor

branch main
showing the latest snapshot on main
particles.odin 8.5 KB · Plain text
particles/particles.odin 0644 Raw
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..<count {
		if system.alive_count >= 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..<count {
		if system.alive_count >= 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..<system.alive_count {
		p := &system.particles[i]
		half := p.size * 0.5
		_ = gpu.draw_rect(frame, {
			x = p.position.x - half,
			y = p.position.y - half,
			width = p.size,
			height = p.size,
			color = p.color,
			order = order,
			sort_key = sort_key + u64(i),
		})
	}
}

draw_3d :: proc(frame: ^gpu.Frame, system: ^System_3D, order: gpu.Order_Mode = .Sortable, sort_key: u64 = 0) {
	if frame == nil || system == nil || !system.active {
		return
	}
	for i in 0..<system.alive_count {
		p := &system.particles[i]
		_ = gpu.draw_billboard_frame(frame, {
			position = p.position,
			size = p.size,
			color = p.color,
			order = order,
			sort_key = sort_key + u64(i),
		})
	}
}

@(private)
xorshift32 :: proc(state: ^u32) -> 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,
	}
}