From 3bff0f95015ab75341dd05d88df64d63a10e97d2 Mon Sep 17 00:00:00 2001 From: Katajisto Date: Thu, 7 May 2026 15:11:38 +0300 Subject: [PATCH] upload wip work from desktop to take with me on laptop --- src/editor/level_editor.jai | 18 +-- src/editor/particle_editor.jai | 6 +- src/particles/particles.jai | 213 +++++++++++++++++--------------- src/rendering/backend.jai | 3 - src/rendering/backend_sokol.jai | 6 +- src/rendering/helpers.jai | 4 +- src/rendering/tasks.jai | 6 - src/world.jai | 149 ++++++++++++---------- 8 files changed, 213 insertions(+), 192 deletions(-) diff --git a/src/editor/level_editor.jai b/src/editor/level_editor.jai index eb411fb..84425f3 100644 --- a/src/editor/level_editor.jai +++ b/src/editor/level_editor.jai @@ -320,6 +320,14 @@ draw_tools_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) { r.h = ui_h(4, 0); if keybind_button(r, "Save world", .SAVE, *theme.button_theme) then sworld(); + r.y += r.h; + if GR.button(r, "Delete buried triles", *theme.button_theme, 400) { + curworld := get_current_world(); + if curworld.valid { + removed := delete_buried_triles(*curworld.world); + log_info("Deleted % buried triles\n", removed); + } + } r.y += r.h * 2; // Tool mode buttons @@ -479,7 +487,6 @@ add_trile :: (name: string, x: s32, y: s32, z: s32, orientation: u8 = 0) { array_add(*group.instances, inst); array_add(*chunk.groups, group); } - invalidate_buried_around(*curworld.world, x, y, z); } @Command remove_trile :: (x: s32, y: s32, z: s32) { @@ -491,21 +498,14 @@ remove_trile :: (x: s32, y: s32, z: s32) { lx, ly, lz := world_to_local(x, y, z); - removed := false; for *group: chunk.groups { for inst, idx: group.instances { if inst.x == lx && inst.y == ly && inst.z == lz { array_unordered_remove_by_index(*group.instances, idx); - if group.is_buried.count > idx { - array_unordered_remove_by_index(*group.is_buried, idx); - } - removed = true; - break; + return; } } - if removed break; } - if removed invalidate_buried_around(*curworld.world, x, y, z); } @Command diff --git a/src/editor/particle_editor.jai b/src/editor/particle_editor.jai index 6d916fa..e498a7b 100644 --- a/src/editor/particle_editor.jai +++ b/src/editor/particle_editor.jai @@ -164,11 +164,7 @@ draw_particle_editor_ui :: (theme: *GR.Overall_Theme) { er.y += er.h; - alive_count : s32 = 0; - for p : g_particles { - if p.alive then alive_count += 1; - } - pe_label(*er, tprint("Alive particles: %", alive_count), theme); + pe_label(*er, tprint("Alive particles: %", count_alive_particles()), theme); pe_label(*er, tprint("Emitter defs: %", g_emitter_defs.count), theme); pe_label(*er, tprint("Preview active: %", pe_preview_emitter.active), theme); pe_label(*er, tprint("Preview def: %", pe_preview_emitter.definition != null), theme); diff --git a/src/particles/particles.jai b/src/particles/particles.jai index 82d669b..c33a79c 100644 --- a/src/particles/particles.jai +++ b/src/particles/particles.jai @@ -41,25 +41,61 @@ Particle :: struct { definition : *Particle_Emitter_Config; } -MAX_PARTICLES :: 16384; +BUCKET_MAX_PARTICLES :: 2048; +PARTICLE_BUCKETS :: 16; +MAX_PARTICLES :: PARTICLE_BUCKETS * BUCKET_MAX_PARTICLES; + + +Particle_Bucket :: struct { + in_use : bool; + name : string; + particles : [BUCKET_MAX_PARTICLES] Particle; +} + +g_particle_buckets : [PARTICLE_BUCKETS] Particle_Bucket; + +Particle_Render_Buffer :: struct { + total_count : s32; + pos_size : [MAX_PARTICLES] Vector4; + uv_rects : [MAX_PARTICLES] Vector4; + colors : [MAX_PARTICLES] Vector4; +} + +g_particle_render_buffer : Particle_Render_Buffer; -g_particles : [MAX_PARTICLES] Particle; g_emitter_defs : [..] Particle_Emitter_Config; clear_particles :: () { - for *p: g_particles p.alive = false; + for *bucket: g_particle_buckets { + bucket.in_use = false; + bucket.name = ""; + for *p: bucket.particles p.alive = false; + } +} + +count_alive_particles :: () -> s32 { + count : s32 = 0; + for *bucket: g_particle_buckets { + if !bucket.in_use then continue; + for p: bucket.particles { + if p.alive then count += 1; + } + } + return count; } 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; + for *bucket: g_particle_buckets { + for *p: bucket.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; } - p.velocity.y -= p.gravity * dt; - p.position += p.velocity * dt; } } @@ -98,99 +134,66 @@ tick_emitter_instance :: (inst: *Particle_Emitter_Instance, dt: float) { } add_particle_render_tasks :: () { - Batch :: struct { - anim_name : string; - blend_mode : Particle_Blend_Mode; - anim : *Animation; - sheet : sg_image; - count : s32; - pos_size : *[MAX_PARTICLES]Vector4; - uv_rects : *[MAX_PARTICLES]Vector4; - colors : *[MAX_PARTICLES]Vector4; - } - - batches : [..] Batch; - batches.allocator = temp; - - for *p: g_particles { - if !p.alive then continue; - if p.definition == null 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; - array_add(*batches, .{ - anim_name = def.animation_name, - blend_mode = def.blend_mode, - anim = anim, - sheet = anim.sheet, - pos_size = New([MAX_PARTICLES]Vector4,, temp), - uv_rects = New([MAX_PARTICLES]Vector4,, temp), - colors = New([MAX_PARTICLES]Vector4,, temp), - }); - batch = *batches[batches.count - 1]; - } - - if batch.count >= MAX_PARTICLES then continue; - - t := p.age / p.lifetime; - size := def.size_start + (def.size_end - def.size_start) * t; - col := lerp_color(def.color_start, def.color_end, t); - - idx := batch.count; - batch.pos_size.*[idx] = .{p.position.x, p.position.y, p.position.z, size}; - - anim := batch.anim; - frame_count := anim.frames.count; - frame := cast(s32)(t * cast(float)frame_count); - if frame < 0 then frame = 0; - if frame >= frame_count then frame = cast(s32)(frame_count - 1); - f := anim.frames[frame]; - batch.uv_rects.*[idx] = .{ - 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, - }; - - batch.colors.*[idx] = col; - batch.count += 1; - } - - buf_task := New(Rendering_Task_Particles_Buffer,, temp); + buf := *g_particle_render_buffer; total_count : s32 = 0; - for *b: batches { - if b.count <= 0 then continue; + for *bucket: g_particle_buckets { + if !bucket.in_use then continue; + + def := get_emitter_def(bucket.name); + if def == null then continue; + if def.animation_name.count == 0 then continue; + + anim := get_animation_from_string(def.animation_name); + if anim == null then continue; + if anim.frames.count == 0 then continue; instance_offset := total_count; - memcpy(*buf_task.pos_size[instance_offset], cast(*void)b.pos_size, b.count * size_of(Vector4)); - memcpy(*buf_task.uv_rects[instance_offset], cast(*void)b.uv_rects, b.count * size_of(Vector4)); - memcpy(*buf_task.colors[instance_offset], cast(*void)b.colors, b.count * size_of(Vector4)); - total_count += b.count; + bucket_count : s32 = 0; - draw_task : Rendering_Task_Particles; - draw_task.count = b.count; - draw_task.instance_offset = instance_offset; - draw_task.blend_mode = b.blend_mode; - draw_task.sheet = b.sheet; - add_rendering_task(draw_task); + for *p: bucket.particles { + if !p.alive then continue; + if total_count + bucket_count >= MAX_PARTICLES then break; + + t := p.age / p.lifetime; + size := def.size_start + (def.size_end - def.size_start) * t; + col := lerp_color(def.color_start, def.color_end, t); + + idx := instance_offset + bucket_count; + buf.pos_size[idx] = .{p.position.x, p.position.y, p.position.z, size}; + + frame_count := anim.frames.count; + frame := cast(s32)(t * cast(float)frame_count); + if frame < 0 then frame = 0; + if frame >= frame_count then frame = cast(s32)(frame_count - 1); + f := anim.frames[frame]; + buf.uv_rects[idx] = .{ + 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, + }; + buf.colors[idx] = col; + bucket_count += 1; + } + + if bucket_count > 0 { + draw_task : Rendering_Task_Particles; + draw_task.count = bucket_count; + draw_task.instance_offset = instance_offset; + draw_task.blend_mode = def.blend_mode; + draw_task.sheet = anim.sheet; + add_rendering_task(draw_task); + total_count += bucket_count; + } } + buf.total_count = total_count; + if total_count > 0 { - buf_task.total_count = total_count; - add_rendering_task(buf_task.*); + upload_task : Rendering_Task_Particles_Buffer; + upload_task.total_count = total_count; + add_rendering_task(upload_task); } } @@ -246,8 +249,24 @@ rng :: inline (lo: float, hi: float) -> float { return lo + Random.random_get_zero_to_one() * (hi - lo); } +find_or_claim_bucket :: (name: string) -> *Particle_Bucket { + free_slot : *Particle_Bucket = null; + for *bucket: g_particle_buckets { + if bucket.in_use && bucket.name == name then return bucket; + if !bucket.in_use && free_slot == null then free_slot = bucket; + } + if free_slot { + free_slot.in_use = true; + free_slot.name = name; + } + return free_slot; +} + spawn_one_particle :: (position: Vector3, def: *Particle_Emitter_Config) { - for *p: g_particles { + bucket := find_or_claim_bucket(def.name); + if bucket == null then return; + + for *p: bucket.particles { if p.alive then continue; p.alive = true; p.age = 0; diff --git a/src/rendering/backend.jai b/src/rendering/backend.jai index eb8c8eb..61d19c2 100644 --- a/src/rendering/backend.jai +++ b/src/rendering/backend.jai @@ -110,9 +110,6 @@ Render_Command_Update_Particles :: struct { #as using c : Render_Command; c.type = .UPDATE_PARTICLES; total_count : s32; - pos_size : [MAX_PARTICLES]Vector4; - uv_rects : [MAX_PARTICLES]Vector4; - colors : [MAX_PARTICLES]Vector4; } Render_Command_Draw_Particles :: struct { diff --git a/src/rendering/backend_sokol.jai b/src/rendering/backend_sokol.jai index f011cb9..03943be 100644 --- a/src/rendering/backend_sokol.jai +++ b/src/rendering/backend_sokol.jai @@ -544,9 +544,9 @@ backend_update_particle_buffers :: (cmd: *Render_Command_Update_Particles) { inst_buf1 := gPipelines.particle_additive.bind.vertex_buffers[1]; inst_buf2 := gPipelines.particle_additive.bind.vertex_buffers[2]; inst_buf3 := gPipelines.particle_additive.bind.vertex_buffers[3]; - sg_update_buffer(inst_buf1, *(sg_range.{ ptr = cmd.pos_size.data, size = byte_size })); - sg_update_buffer(inst_buf2, *(sg_range.{ ptr = cmd.uv_rects.data, size = byte_size })); - sg_update_buffer(inst_buf3, *(sg_range.{ ptr = cmd.colors.data, size = byte_size })); + sg_update_buffer(inst_buf1, *(sg_range.{ ptr = g_particle_render_buffer.pos_size.data, size = byte_size })); + sg_update_buffer(inst_buf2, *(sg_range.{ ptr = g_particle_render_buffer.uv_rects.data, size = byte_size })); + sg_update_buffer(inst_buf3, *(sg_range.{ ptr = g_particle_render_buffer.colors.data, size = byte_size })); } backend_draw_particles :: (cmd: *Render_Command_Draw_Particles) { diff --git a/src/rendering/helpers.jai b/src/rendering/helpers.jai index 3e77cec..665331d 100644 --- a/src/rendering/helpers.jai +++ b/src/rendering/helpers.jai @@ -102,9 +102,11 @@ create_world_rendering_tasks :: (world: *World, camera: Camera, plane_height: fl gmin := group.bounding_min; gmax := group.bounding_max; + gavg := group.average_pos; gmin += Vector3.{cast(float)chunk.coord.x * 32, cast(float)chunk.coord.y * 32, cast(float)chunk.coord.z * 32}; gmax += Vector3.{cast(float)chunk.coord.x * 32, cast(float)chunk.coord.y * 32, cast(float)chunk.coord.z * 32}; + gavg += Vector3.{cast(float)chunk.coord.x * 32, cast(float)chunk.coord.y * 32, cast(float)chunk.coord.z * 32}; in_cam := aabb_in_frustum(cam_planes, gmin, gmax); in_reflect := aabb_in_frustum(reflect_planes, gmin, gmax); in_shad := aabb_in_frustum(shadow_planes, gmin, gmax); @@ -112,7 +114,7 @@ create_world_rendering_tasks :: (world: *World, camera: Camera, plane_height: fl if !vis then continue; - diff := group.average_pos - camera.position; + diff := gavg - camera.position; dist := length(diff); lod_index : s32 = 0; for lod_dist : LOD_DISTANCES { diff --git a/src/rendering/tasks.jai b/src/rendering/tasks.jai index fb22003..d00c2b9 100644 --- a/src/rendering/tasks.jai +++ b/src/rendering/tasks.jai @@ -98,9 +98,6 @@ Rendering_Task_Particles_Buffer :: struct { #as using t : Rendering_Task; t.type = .PARTICLES_BUFFER; total_count : s32; - pos_size : [MAX_PARTICLES]Vector4; - uv_rects : [MAX_PARTICLES]Vector4; - colors : [MAX_PARTICLES]Vector4; } Rendering_Task_Set_Camera :: struct { @@ -216,9 +213,6 @@ tasks_to_commands :: () { bufTask := cast(*Rendering_Task_Particles_Buffer)it; uploadCmd := New(Render_Command_Update_Particles,, temp); uploadCmd.total_count = bufTask.total_count; - memcpy(uploadCmd.pos_size.data, bufTask.pos_size.data, bufTask.total_count * size_of(Vector4)); - memcpy(uploadCmd.uv_rects.data, bufTask.uv_rects.data, bufTask.total_count * size_of(Vector4)); - memcpy(uploadCmd.colors.data, bufTask.colors.data, bufTask.total_count * size_of(Vector4)); array_add(*render_command_buckets.setup, uploadCmd); case .PARTICLES; particleTask := cast(*Rendering_Task_Particles)it; diff --git a/src/world.jai b/src/world.jai index 041a4a9..04f27c5 100644 --- a/src/world.jai +++ b/src/world.jai @@ -52,7 +52,6 @@ Chunk_Trile_Group :: struct { average_pos : Vector3; instances : [..]Trile_Instance; - is_buried : [..]bool; // Runtime-only, parallel to instances. Not serialized. } Chunk :: struct { @@ -217,7 +216,6 @@ unload_current_world :: () { for *chunk: current_world.world.chunks { for *group: chunk.groups { array_free(group.instances); - array_free(group.is_buried); } array_free(chunk.groups); } @@ -232,9 +230,6 @@ set_loaded_world :: (world: World) { current_world.world = world; current_world.valid = true; resolve_emitter_definitions(*current_world.world); - for *chunk: current_world.world.chunks { - recompute_buried_for_chunk(*current_world.world, chunk); - } } is_cell_opaque :: (name: string) -> bool { @@ -259,81 +254,100 @@ trile_at_world :: (world: *World, wx: s32, wy: s32, wz: s32) -> string { return ""; } -BURIED_DIRS :: s32.[ 1,0,0, -1,0,0, 0,1,0, 0,-1,0, 0,0,1, 0,0,-1 ]; - -compute_buried_at_world :: (world: *World, wx: s32, wy: s32, wz: s32) -> bool { - for axis: 0..5 { - nwx := wx + BURIED_DIRS[axis*3+0]; - nwy := wy + BURIED_DIRS[axis*3+1]; - nwz := wz + BURIED_DIRS[axis*3+2]; - if !is_cell_opaque(trile_at_world(world, nwx, nwy, nwz)) return false; +recompute_group_bounds :: (group: *Chunk_Trile_Group) { + if group.instances.count == 0 { + group.bounding_min = .{}; + group.bounding_max = .{}; + group.average_pos = .{}; + return; } - return true; + gmin := Vector3.{31, 31, 31}; + gmax := Vector3.{0, 0, 0}; + gavg := Vector3.{0, 0, 0}; + for i : group.instances { + gmin.x = min(gmin.x, xx i.x); + gmin.y = min(gmin.y, xx i.y); + gmin.z = min(gmin.z, xx i.z); + gmax.x = max(gmax.x, xx i.x); + gmax.y = max(gmax.y, xx i.y); + gmax.z = max(gmax.z, xx i.z); + gavg += Vector3.{xx i.x, xx i.y, xx i.z}; + } + gavg /= cast(float) group.instances.count; + group.bounding_min = gmin; + group.bounding_max = gmax; + group.average_pos = gavg; } -recompute_buried_for_chunk :: (world: *World, chunk: *Chunk) { - // Build an O(1) local lookup grid (32^3 trile names) for this chunk so the - // common in-chunk neighbor case is a single index lookup. Cross-chunk - // neighbors fall back to trile_at_world. +// Scans every chunk for triles whose 6 axis-aligned neighbors are all opaque, +// and removes them. One-shot — operates against a snapshot of the original +// state so a fully-enclosed cluster is removed in a single pass. +delete_buried_triles :: (world: *World) -> s32 { + NEIGHBOR_DIRS :: s32.[ 1,0,0, -1,0,0, 0,1,0, 0,-1,0, 0,0,1, 0,0,-1 ]; GRID :: CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE; - grid: [GRID] string; - grid_data := grid; - for *group: chunk.groups { - for inst: group.instances { - idx := cast(int)inst.x + CHUNK_SIZE * (cast(int)inst.y + CHUNK_SIZE * cast(int)inst.z); - grid_data[idx] = group.trile_name; + + cell_idx :: (lx: u8, ly: u8, lz: u8) -> int { + return cast(int)lx + CHUNK_SIZE * (cast(int)ly + CHUNK_SIZE * cast(int)lz); + } + + build_chunk_grid :: (chunk: *Chunk, grid: *[GRID]string) { + for *group : chunk.groups { + for inst : group.instances grid.*[cell_idx(inst.x, inst.y, inst.z)] = group.trile_name; } } - for *group: chunk.groups { - array_resize(*group.is_buried, group.instances.count); - for inst, i: group.instances { - wx, wy, wz := chunk_local_to_world(chunk.coord, inst.x, inst.y, inst.z); - buried := true; - for axis: 0..5 { - nwx := wx + BURIED_DIRS[axis*3+0]; - nwy := wy + BURIED_DIRS[axis*3+1]; - nwz := wz + BURIED_DIRS[axis*3+2]; - neighbor_name: string; - nkey := world_to_chunk_coord(nwx, nwy, nwz); - if nkey == chunk.coord { - nlx, nly, nlz := world_to_local(nwx, nwy, nwz); - nidx := cast(int)nlx + CHUNK_SIZE * (cast(int)nly + CHUNK_SIZE * cast(int)nlz); - neighbor_name = grid_data[nidx]; - } else { - neighbor_name = trile_at_world(world, nwx, nwy, nwz); - } - if !is_cell_opaque(neighbor_name) { - buried = false; - break; - } + neighbor_trile_name :: (world: *World, chunk_coord: Chunk_Key, grid: *[GRID]string, wx: s32, wy: s32, wz: s32) -> string { + if world_to_chunk_coord(wx, wy, wz) == chunk_coord { + lx, ly, lz := world_to_local(wx, wy, wz); + return grid.*[cell_idx(lx, ly, lz)]; + } + return trile_at_world(world, wx, wy, wz); + } + + is_buried :: (world: *World, chunk_coord: Chunk_Key, grid: *[GRID]string, lx: u8, ly: u8, lz: u8) -> bool { + wx0, wy0, wz0 := chunk_local_to_world(chunk_coord, lx, ly, lz); + for axis : 0..5 { + name := neighbor_trile_name(world, chunk_coord, grid, + wx0 + NEIGHBOR_DIRS[axis*3+0], + wy0 + NEIGHBOR_DIRS[axis*3+1], + wz0 + NEIGHBOR_DIRS[axis*3+2]); + if !is_cell_opaque(name) then return false; + } + return true; + } + + Pending :: struct { + group : *Chunk_Trile_Group; + indices : [..]s32; + } + + pending : [..]Pending; + pending.allocator = temp; + + for *chunk : world.chunks { + grid : [GRID]string; + build_chunk_grid(chunk, *grid); + + for *group : chunk.groups { + pr : Pending; + pr.group = group; + pr.indices.allocator = temp; + for inst, i : group.instances { + if is_buried(world, chunk.coord, *grid, inst.x, inst.y, inst.z) then array_add(*pr.indices, cast(s32) i); } - group.is_buried[i] = buried; + if pr.indices.count > 0 then array_add(*pending, pr); } } -} -recompute_buried_at_cell :: (world: *World, wx: s32, wy: s32, wz: s32) { - key := world_to_chunk_coord(wx, wy, wz); - chunk := table_find_pointer(*world.chunks, key); - if !chunk return; - lx, ly, lz := world_to_local(wx, wy, wz); - for *group: chunk.groups { - for inst, i: group.instances { - if inst.x == lx && inst.y == ly && inst.z == lz { - if group.is_buried.count != group.instances.count array_resize(*group.is_buried, group.instances.count); - group.is_buried[i] = compute_buried_at_world(world, wx, wy, wz); - return; - } + total : s32 = 0; + for pr : pending { + for < k : 0..pr.indices.count - 1 { + array_unordered_remove_by_index(*pr.group.instances, pr.indices[k]); + total += 1; } + recompute_group_bounds(pr.group); } -} - -invalidate_buried_around :: (world: *World, wx: s32, wy: s32, wz: s32) { - recompute_buried_at_cell(world, wx, wy, wz); - for axis: 0..5 { - recompute_buried_at_cell(world, wx + BURIED_DIRS[axis*3+0], wy + BURIED_DIRS[axis*3+1], wz + BURIED_DIRS[axis*3+2]); - } + return total; } resolve_emitter_definitions :: (world: *World) { @@ -347,7 +361,6 @@ clear_world :: () { for *chunk: current_world.world.chunks { for *group: chunk.groups { array_free(group.instances); - array_free(group.is_buried); } array_free(chunk.groups); }