250 lines
7.3 KiB
Plaintext
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;
|
|
}
|