Compare commits

...

1 Commits

Author SHA1 Message Date
3bff0f9501 upload wip work from desktop to take with me on laptop 2026-05-07 15:11:38 +03:00
8 changed files with 213 additions and 192 deletions

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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 {

View File

@ -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) {

View File

@ -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 {

View File

@ -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;

View File

@ -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);
}