diff --git a/src/particles/particles.jai b/src/particles/particles.jai new file mode 100644 index 0000000..d03c4f1 --- /dev/null +++ b/src/particles/particles.jai @@ -0,0 +1,257 @@ +#scope_export + +g_pending_emitter_world_name : string; + +particles_process_deferred_loads :: () { + if g_pending_emitter_world_name.count == 0 then return; + name := g_pending_emitter_world_name; + g_pending_emitter_world_name = ""; + load_emitters_for_world(name); + free(name.data); +} + +Particle_Emitter_Definition :: struct { + name : string; + sprite_sheet : string; + emission_rate : float = 10.0; + lifetime_min : float = 0.5; + lifetime_max : float = 2.0; + velocity : Vector3; + velocity_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}; + frame_count : int = 1; + gravity : float = 2.0; + + sprite_image : sg_image; // resolved at runtime from sprite_sheet name +} + +Particle_Emitter_Instance :: struct { + definition : *Particle_Emitter_Definition; + definition_name : string; + position : Vector3; + active : bool = true; + spawn_accumulator : float; +} + +Particle :: struct { + position : Vector3; + velocity : Vector3; + age : float; + lifetime : float; + gravity : float; + alive : bool; + definition : *Particle_Emitter_Definition; +} + +MAX_PARTICLES : s32 : 2048; + +g_particles : [2048] Particle; +g_emitter_instances : [..] Particle_Emitter_Instance; +g_emitter_defs : [..] Particle_Emitter_Definition; + +tick_particles :: (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; + } + + for *inst: g_emitter_instances { + if !inst.active then continue; + 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, def); + } + } +} + +add_particle_render_tasks :: () { + for *def: g_emitter_defs { + if def.sprite_image.id == 0 then continue; + + instances : [..] Particle_Instance_Data; + instances.allocator = temp; + + for *p: g_particles { + if !p.alive then continue; + if p.definition != def then continue; + + t := p.age / p.lifetime; + t = clamp(t, 0.0, 1.0); + + frame_count := cast(s32) max(def.frame_count, 1); + frame_idx := cast(s32)(t * cast(float) frame_count); + frame_idx = clamp(frame_idx, 0, frame_count - 1); + + frame_w := 1.0 / cast(float) frame_count; + uv_rect := Vector4.{frame_idx * frame_w, 0.0, frame_w, 1.0}; + + size := def.size_start + (def.size_end - def.size_start) * t; + color := lerp_color(def.color_start, def.color_end, t); + + inst : Particle_Instance_Data; + inst.pos_size = .{p.position.x, p.position.y, p.position.z, size}; + inst.uv_rect = uv_rect; + inst.color = color; + array_add(*instances, inst); + } + + if instances.count == 0 then continue; + + task : Rendering_Task_Particles; + task.instances = instances; + task.sprite_image = def.sprite_image; + add_rendering_task(task); + } +} + +clear_particle_emitter_instances :: () { + array_reset(*g_emitter_instances); + for *p: g_particles { + p.alive = false; + } +} + +place_emitter :: (def_name: string, position: Vector3) { + inst : Particle_Emitter_Instance; + inst.definition_name = sprint("%", def_name); + inst.position = position; + inst.active = true; + resolve_emitter_definition(*inst); + array_add(*g_emitter_instances, inst); +} + +remove_emitter_at_index :: (idx: int) { + if idx < 0 || idx >= g_emitter_instances.count then return; + array_unordered_remove_by_index(*g_emitter_instances, idx); +} + +resolve_all_emitter_instances :: () { + for *inst: g_emitter_instances { + resolve_emitter_definition(inst); + } +} + +get_emitter_def_names :: () -> []string { + names : [..] string; + names.allocator = temp; + for def: g_emitter_defs { + array_add(*names, def.name); + } + return names; +} + +save_particle_definition :: (def: Particle_Emitter_Definition) { + #if OS != .WASM { + file :: #import "File"; + dir := "./game/resources/particles"; + file.make_directory_if_it_does_not_exist(dir, recursive = true); + path := tprint("%/%.emitter.json", dir, def.name); + json := Jaison.json_write_string(def,, temp); + file.write_entire_file(path, json); + log_info("Saved particle definition '%'", def.name); + } +} + +load_emitters_for_world :: (world_name: string) { + #if OS != .WASM { + file :: #import "File"; + path := tprint("./game/resources/worlds/%/emitters.json", world_name); + data, ok := file.read_entire_file(path); + if !ok then return; + defer free(data.data); + + Emitter_Entry :: struct { + definition : string; + position : [3] float; + } + success, entries := Jaison.json_parse_string(cast(string) data, [] Emitter_Entry,, temp); + if !success { + log_error("Failed to parse emitters.json for world '%'", world_name); + return; + } + for entry: entries { + pos := Vector3.{entry.position[0], entry.position[1], entry.position[2]}; + place_emitter(entry.definition, pos); + } + log_info("Loaded % emitters for world '%'", entries.count, world_name); + } +} + +save_emitters_for_world :: (world_name: string) { + #if OS != .WASM { + file :: #import "File"; + dir := tprint("./game/resources/worlds/%", world_name); + file.make_directory_if_it_does_not_exist(dir, recursive = true); + path := tprint("%/emitters.json", dir); + + Emitter_Entry :: struct { + definition : string; + position : [3] float; + } + entries : [..] Emitter_Entry; + entries.allocator = temp; + for inst: g_emitter_instances { + e : Emitter_Entry; + e.definition = inst.definition_name; + e.position[0] = inst.position.x; + e.position[1] = inst.position.y; + e.position[2] = inst.position.z; + array_add(*entries, e); + } + json := Jaison.json_write_string(entries,, temp); + file.write_entire_file(path, json); + log_info("Saved % emitters for world '%'", entries.count, world_name); + } +} + +#scope_file + +Random :: #import "Random"; + +rng :: inline (lo: float, hi: float) -> float { + return lo + Random.random_get_zero_to_one() * (hi - lo); +} + +spawn_one_particle :: (position: Vector3, def: *Particle_Emitter_Definition) { + 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; + 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; + } +} + +lerp_color :: (a: Vector4, b: Vector4, t: float) -> Vector4 { + return a * (1.0 - t) + b * t; +} + +resolve_emitter_definition :: (inst: *Particle_Emitter_Instance) { + for *def: g_emitter_defs { + if def.name == inst.definition_name { + inst.definition = def; + return; + } + } + inst.definition = null; +}