trueno/src/particles/particles.jai
2026-10-02 20:07:49 +03:00

250 lines
7.3 KiB
Plaintext

#scope_export
Particle_Blend_Mode :: enum {
ADDITIVE;
ALPHA;
}
Particle_Emitter_Config :: struct {
name : string;
animation_name : string;
emission_rate : float = 10.0;
lifetime_min : float = 0.5;
lifetime_max : float = 2.0;
velocity : Vector3;
velocity_spread : Vector3;
position_spread : Vector3;
size_start : float = 0.5;
size_end : float = 0.0;
color_start : Vector4 = .{1, 1, 1, 1};
color_end : Vector4 = .{1, 1, 1, 0};
gravity : float = 2.0;
blend_mode : Particle_Blend_Mode = .ADDITIVE;
}
Particle_Emitter_Instance :: struct {
definition : *Particle_Emitter_Config;
definition_name : string;
position : Vector3;
offset : Vector3;
active : bool = true;
spawn_accumulator : float;
}
Particle :: struct {
position : Vector3;
velocity : Vector3;
age : float;
lifetime : float;
gravity : float;
alive : bool;
definition : *Particle_Emitter_Config;
}
MAX_PARTICLES :: 16384;
g_particles : [MAX_PARTICLES] Particle;
g_emitter_defs : [..] Particle_Emitter_Config;
clear_particles :: () {
for *p: g_particles p.alive = false;
}
tick_particles_physics :: (dt: float) {
for *p: g_particles {
if !p.alive then continue;
p.age += dt;
if p.age >= p.lifetime {
p.alive = false;
continue;
}
p.velocity.y -= p.gravity * dt;
p.position += p.velocity * dt;
}
}
tick_world_emitters :: (dt: float) {
curworld := get_current_world();
if !curworld.valid then return;
for *inst: curworld.world.emitter_instances {
if !inst.active then continue;
if inst.definition == null {
inst.definition = get_emitter_def(inst.definition_name);
if inst.definition == null then continue;
}
def := inst.definition;
inst.spawn_accumulator += def.emission_rate * dt;
while inst.spawn_accumulator >= 1.0 {
inst.spawn_accumulator -= 1.0;
spawn_one_particle(inst.position + inst.offset, def);
}
}
}
tick_particles :: (dt: float) {
tick_particles_physics(dt);
tick_world_emitters(dt);
}
tick_emitter_instance :: (inst: *Particle_Emitter_Instance, dt: float) {
if !inst.active then return;
if inst.definition == null then return;
def := inst.definition;
inst.spawn_accumulator += def.emission_rate * dt;
while inst.spawn_accumulator >= 1.0 {
inst.spawn_accumulator -= 1.0;
spawn_one_particle(inst.position, def);
}
}
// Live particles are global, so the game and each editor view draw them into
// their own view.
draw_live_particles :: (view: View_Id) {
Batch :: struct {
anim_name : string;
blend_mode : Particle_Blend_Mode;
anim : *Animation;
particles : [..]Particle_Instance;
}
batches : [..] Batch;
batches.allocator = temp;
has_water, water_y := particle_water_height();
for *p: g_particles {
if !p.alive then continue;
if p.definition == null then continue;
if has_water && p.position.y < water_y then continue;
def := p.definition;
if def.animation_name.count == 0 then continue;
batch : *Batch = null;
for *b: batches {
if b.anim_name == def.animation_name && b.blend_mode == def.blend_mode {
batch = b;
break;
}
}
if batch == null {
anim := get_animation_from_string(def.animation_name);
if anim == null then continue;
if anim.frames.count == 0 then continue;
batch = array_add(*batches);
batch.anim_name = def.animation_name;
batch.blend_mode = def.blend_mode;
batch.anim = anim;
batch.particles.allocator = temp;
}
t := p.age / p.lifetime;
anim := batch.anim;
frame := clamp(cast(s32)(t * cast(float)anim.frames.count), 0, cast(s32)(anim.frames.count - 1));
f := anim.frames[frame];
array_add(*batch.particles, .{
position = p.position,
size = def.size_start + (def.size_end - def.size_start) * t,
uv_rect = .{
cast(float)f.x / cast(float)anim.sheet_w,
cast(float)f.y / cast(float)anim.sheet_h,
cast(float)f.w / cast(float)anim.sheet_w,
cast(float)f.h / cast(float)anim.sheet_h,
},
color = lerp_color(def.color_start, def.color_end, t),
});
}
for batches {
blend := ifx it.blend_mode == .ADDITIVE then Blend_Mode.ADDITIVE else .ALPHA;
draw_particles(view, it.anim.sheet, blend, it.particles);
}
}
particle_water_height :: () -> (has_water: bool, height: float) {
#if !FLAG_RELEASE_BUILD {
if in_editor_view && !editor_shows_water() return false, 0;
}
curworld := get_current_world();
if !curworld.valid || curworld.world.conf.indoor return false, 0;
return true, effective_plane_height(*curworld.world.conf);
}
get_emitter_def :: (name: string) -> *Particle_Emitter_Config {
for *def: g_emitter_defs {
if def.name == name then return def;
}
return null;
}
spawn_particles_at :: (position: Vector3, def_name: string) {
for *def: g_emitter_defs {
if def.name == def_name {
spawn_one_particle(position, def);
return;
}
}
}
register_emitter_def :: (def: Particle_Emitter_Config) {
for *existing: g_emitter_defs {
if existing.name == def.name {
existing.* = def;
return;
}
}
array_add(*g_emitter_defs, def);
}
get_emitter_def_names :: () -> []string {
names : [..] string;
names.allocator = temp;
for def: g_emitter_defs {
array_add(*names, def.name);
}
return names;
}
save_particle_defs :: () {
#if OS != .WASM {
file :: #import "File";
json := Jaison.json_write_string(g_emitter_defs, " ");
file.write_entire_file(tprint("%/game_core/particles.json", GAME_RESOURCES_DIR), json);
log_info("Saved % particle definitions", g_emitter_defs.count);
}
}
spawn_one_particle :: (position: Vector3, def: *Particle_Emitter_Config) {
for *p: g_particles {
if p.alive then continue;
p.alive = true;
p.age = 0;
p.lifetime = rng(def.lifetime_min, def.lifetime_max);
p.position = position + Vector3.{
rng(-def.position_spread.x, def.position_spread.x),
rng(-def.position_spread.y, def.position_spread.y),
rng(-def.position_spread.z, def.position_spread.z),
};
p.velocity = def.velocity + Vector3.{
rng(-def.velocity_spread.x, def.velocity_spread.x),
rng(-def.velocity_spread.y, def.velocity_spread.y),
rng(-def.velocity_spread.z, def.velocity_spread.z),
};
p.gravity = def.gravity;
p.definition = def;
return;
}
}
#scope_file
Random :: #import "Random";
rng :: inline (lo: float, hi: float) -> float {
return lo + Random.random_get_zero_to_one() * (hi - lo);
}
lerp_color :: (a: Vector4, b: Vector4, t: float) -> Vector4 {
return a * (1.0 - t) + b * t;
}