Editor tooling improvements

This commit is contained in:
Tuomas Katajisto 2026-03-23 21:05:30 +02:00
parent 1e7078c168
commit 01eaff1c0f
19 changed files with 1251 additions and 216 deletions

4
settings.cfg Normal file
View File

@ -0,0 +1,4 @@
master_volume 1
music_volume 0.2
sfx_volume 1
fullscreen 1

View File

@ -2,6 +2,7 @@
#import "String"; #import "String";
hash :: #import "Hash"; hash :: #import "Hash";
Pool :: #import "Pool";
#load "loaders.jai"; #load "loaders.jai";
@ -77,6 +78,7 @@ fetch_callback :: (res: *sfetch_response_t) #c_call {
mem := NewArray(res.data.size.(s64), u8, false); mem := NewArray(res.data.size.(s64), u8, false);
memcpy(mem.data, res.data.ptr, res.data.size.(s64)); memcpy(mem.data, res.data.ptr, res.data.size.(s64));
pack: Loaded_Pack; pack: Loaded_Pack;
Pool.set_allocators(*pack.pool);
pack.nameHash = hash.get_hash(req.pack_name); pack.nameHash = hash.get_hash(req.pack_name);
pack.name = sprint("%", req.pack_name); pack.name = sprint("%", req.pack_name);
success := init_from_memory(*pack.content, mem, sprint("%", req.pack_name)); success := init_from_memory(*pack.content, mem, sprint("%", req.pack_name));
@ -173,9 +175,17 @@ fetch_callback :: (res: *sfetch_response_t) #c_call {
}; };
chunk := table_find_pointer(*curworld.world.chunks, req.chunk_key); chunk := table_find_pointer(*curworld.world.chunks, req.chunk_key);
if chunk != null { if chunk != null {
chunk.rdm_atlas = req.rdm_pending_atlas; chunk.rdm_atlas = req.rdm_pending_atlas;
chunk.rdm_lookup = sg_make_image(*lookup_desc); chunk.rdm_lookup = sg_make_image(*lookup_desc);
chunk.rdm_valid = true; #if !FLAG_RELEASE_BUILD {
chunk.rdm_lookup_w = header.width;
chunk.rdm_lookup_h = header.height;
num_floats := cast(s64)(header.width * header.height * 4);
cpu_copy := NewArray(num_floats, float);
memcpy(cpu_copy.data, res.data.ptr + header_size, cast(s64)lookup_pixel_bytes);
chunk.rdm_lookup_cpu = cpu_copy;
}
chunk.rdm_valid = true;
log_debug("RDM: loaded chunk %", req.chunk_key); log_debug("RDM: loaded chunk %", req.chunk_key);
} else { } else {
sg_destroy_image(req.rdm_pending_atlas); sg_destroy_image(req.rdm_pending_atlas);
@ -191,11 +201,12 @@ Loaded_Pack :: struct {
textures : Table(string, sg_image); textures : Table(string, sg_image);
animations : Table(string, Animation); animations : Table(string, Animation);
audio : Table(string, Audio_Data); audio : Table(string, Audio_Data);
//fonts : [..]Font??; pool : Pool.Pool;
} }
add_resources_from_pack :: (pack: *Loaded_Pack) { add_resources_from_pack :: (pack: *Loaded_Pack) {
// We need to go trough this at the end. push_allocator(.{Pool.pool_allocator_proc, *pack.pool});
Queued_Sheet_File :: struct { Queued_Sheet_File :: struct {
name : string; name : string;
image : sg_image; image : sg_image;
@ -241,6 +252,21 @@ add_resources_from_pack :: (pack: *Loaded_Pack) {
case "wav"; case "wav";
audio := load_wav_from_memory(v.data); audio := load_wav_from_memory(v.data);
table_set(*pack.audio, name, audio); table_set(*pack.audio, name, audio);
case "json";
if name == "particles" {
s := create_string_from_memory(v.data);
success, defs := Jaison.json_parse_string(s, [..]Particle_Emitter_Config,, temp);
if success {
array_reset(*g_emitter_defs);
for defs {
def := it;
def.name = sprint("%", it.name);
def.animation_name = sprint("%", it.animation_name);
array_add(*g_emitter_defs, def);
}
log_info("Loaded % particle definitions from pack", g_emitter_defs.count);
}
}
case "ttf"; case "ttf";
// Load into a font. Add to free list. // Load into a font. Add to free list.
case; case;
@ -292,10 +318,10 @@ find_pack_by_name :: (name: string) -> (bool, Loaded_Pack) {
free_resources_from_pack :: (pack: *Loaded_Pack) { free_resources_from_pack :: (pack: *Loaded_Pack) {
for pack.textures sg_destroy_image(it); for pack.textures sg_destroy_image(it);
for *pack.audio array_free(it.data);
table_reset(*pack.textures); table_reset(*pack.textures);
table_reset(*pack.audio); table_reset(*pack.audio);
table_reset(*pack.animations); table_reset(*pack.animations);
Pool.reset(*pack.pool);
} }
asset_manager_init :: () { asset_manager_init :: () {

View File

@ -1,9 +1,9 @@
#load "rdm_disk.jai";
#if OS != .WASM { #if OS != .WASM {
#load "iprof.jai"; #load "iprof.jai";
#load "picker.jai"; #load "picker.jai";
#load "trile_editor.jai"; #load "trile_editor.jai";
#load "level_editor.jai"; #load "level_editor.jai";
#load "particle_editor.jai";
} }
#if FLAG_TACOMA_ENABLED { #load "tacoma.jai"; } #if FLAG_TACOMA_ENABLED { #load "tacoma.jai"; }
#load "console.jai"; #load "console.jai";
@ -15,7 +15,7 @@ Editor_View :: enum {
Closed_Editor; Closed_Editor;
Trile_Editor; Trile_Editor;
Level_Editor; Level_Editor;
Material_Editor; Particle_Editor;
}; };
current_editor_view : Editor_View = .Trile_Editor; current_editor_view : Editor_View = .Trile_Editor;
@ -25,8 +25,10 @@ current_editor_view : Editor_View = .Trile_Editor;
in_editor_view : bool = false; in_editor_view : bool = false;
init_editor :: () { init_editor :: () {
#if OS != .WASM {init_profiler();} #if !FLAG_RELEASE_BUILD {
init_console(); #if OS != .WASM {init_profiler();}
init_console();
}
init_keybinds(); init_keybinds();
init_settings_menu(); init_settings_menu();
} }
@ -50,14 +52,15 @@ draw_editor_ui :: (theme: *GR.Overall_Theme) {
r.x -= r.w; r.x -= r.w;
if keybind_button(r, "Trile studio", .STUDIO_TRILE, *t_button_selectable(theme, current_editor_view == .Trile_Editor)) then current_editor_view = .Trile_Editor; if keybind_button(r, "Trile studio", .STUDIO_TRILE, *t_button_selectable(theme, current_editor_view == .Trile_Editor)) then current_editor_view = .Trile_Editor;
r.x -= r.w; r.x -= r.w;
if keybind_button(r, "Material studio", .STUDIO_MATERIAL, *t_button_selectable(theme, current_editor_view == .Material_Editor)) then current_editor_view = .Material_Editor; if keybind_button(r, "Particle studio", .STUDIO_MATERIAL, *t_button_selectable(theme, current_editor_view == .Particle_Editor)) then current_editor_view = .Particle_Editor;
if current_editor_view == { if current_editor_view == {
case .Trile_Editor; case .Trile_Editor;
draw_trile_editor_ui(theme); draw_trile_editor_ui(theme);
case .Level_Editor; case .Level_Editor;
draw_level_editor_ui(theme); draw_level_editor_ui(theme);
case .Particle_Editor;
draw_particle_editor_ui(theme);
} }
} }
draw_profiler(); draw_profiler();
@ -80,6 +83,8 @@ draw_editor :: () {
draw_trile_editor(); draw_trile_editor();
case .Level_Editor; case .Level_Editor;
draw_level_editor(); draw_level_editor();
case .Particle_Editor;
draw_particle_editor();
} }
} }
} }
@ -105,6 +110,8 @@ tick_editor_ui :: () {
tick_trile_editor(); tick_trile_editor();
case .Level_Editor; case .Level_Editor;
tick_level_editor(); tick_level_editor();
case .Particle_Editor;
tick_particle_editor();
} }
} }
} }

View File

@ -6,6 +6,8 @@ MAX_CAMERA_DIST :: 25.0;
MIN_CAMERA_DIST :: 2.0; MIN_CAMERA_DIST :: 2.0;
DIST_SCROLL_SPEED :: 0.8; DIST_SCROLL_SPEED :: 0.8;
mouse1Active : bool;
mouse1ActivationPosition : Vector2;
mouse2Active : bool; mouse2Active : bool;
mouse2ActivationPosition : Vector2; mouse2ActivationPosition : Vector2;
mouse3Active : bool; mouse3Active : bool;
@ -25,6 +27,8 @@ Level_Editor_Tool_Mode :: enum {
BRUSH; BRUSH;
AREA; AREA;
LINE; LINE;
INSPECTOR;
VIEWER;
} }
current_tool_mode : Level_Editor_Tool_Mode = .POINT; current_tool_mode : Level_Editor_Tool_Mode = .POINT;
@ -46,6 +50,16 @@ line_start_z : int;
current_orientation_face : u8 = 0; current_orientation_face : u8 = 0;
current_orientation_twist : u8 = 0; current_orientation_twist : u8 = 0;
inspector_selected : bool;
inspector_x : s32;
inspector_y : s32;
inspector_z : s32;
inspector_emitter_def : s32 = -1;
inspector_rdm_roughness : int = 0;
inspector_note_input : string = "";
editor_billboards_visible : bool = true;
get_current_orientation :: () -> u8 { get_current_orientation :: () -> u8 {
return current_orientation_face * 4 + current_orientation_twist; return current_orientation_face * 4 + current_orientation_twist;
} }
@ -164,6 +178,25 @@ tick_level_editor_camera :: () {
editY = max(editY - 1, 0); editY = max(editY - 1, 0);
} }
if current_tool_mode == .VIEWER && get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT) & .DOWN {
if mouse1Active {
lastInputTime = get_time();
diff := mouse1ActivationPosition - Vector2.{input_mouse_x, input_mouse_y};
diff *= 0.5;
cameraRotation = oldCameraRotation + diff.x / 100;
cameraTilt = oldCameraTilt - diff.y / 100;
cameraTilt = max(0.1, cameraTilt);
cameraTilt = min(PI/2.2, cameraTilt);
} else {
mouse1Active = true;
mouse1ActivationPosition = Vector2.{input_mouse_x, input_mouse_y};
oldCameraRotation = cameraRotation;
oldCameraTilt = cameraTilt;
}
} else {
mouse1Active = false;
}
if get_mouse_state(Key_Code.MOUSE_BUTTON_MIDDLE) & .DOWN { if get_mouse_state(Key_Code.MOUSE_BUTTON_MIDDLE) & .DOWN {
if mouse2Active { if mouse2Active {
lastInputTime = get_time(); lastInputTime = get_time();
@ -292,6 +325,10 @@ draw_tools_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
r.y += r.h; r.y += r.h;
if keybind_button(r, "Line", .LEVEL_TOOL_LINE, *t_button_selectable(theme, current_tool_mode == .LINE)) { current_tool_mode = .LINE; line_active = false; } if keybind_button(r, "Line", .LEVEL_TOOL_LINE, *t_button_selectable(theme, current_tool_mode == .LINE)) { current_tool_mode = .LINE; line_active = false; }
r.y += r.h; r.y += r.h;
if keybind_button(r, "Inspector", .LEVEL_TOOL_INSPECTOR, *t_button_selectable(theme, current_tool_mode == .INSPECTOR)) then current_tool_mode = .INSPECTOR;
r.y += r.h;
if keybind_button(r, "Viewer", .LEVEL_TOOL_VIEWER, *t_button_selectable(theme, current_tool_mode == .VIEWER)) then current_tool_mode = .VIEWER;
r.y += r.h;
// Brush radius/height (only for brush mode) // Brush radius/height (only for brush mode)
if current_tool_mode == .BRUSH { if current_tool_mode == .BRUSH {
@ -327,6 +364,12 @@ draw_tools_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
GR.label(r, "Click start of line", *t_label_left(theme)); GR.label(r, "Click start of line", *t_label_left(theme));
} }
r.y += r.h; r.y += r.h;
} else if current_tool_mode == .INSPECTOR {
r.h = ui_h(3, 2);
if GR.button(r, ifx editor_billboards_visible then "Hide Markers" else "Show Markers", *theme.button_theme, 199) {
editor_billboards_visible = !editor_billboards_visible;
}
r.y += r.h;
} }
// Orientation controls // Orientation controls
@ -457,8 +500,10 @@ tick_level_editor :: () {
if is_action_start(Editor_Action.LEVEL_TOOL_POINT) then current_tool_mode = .POINT; if is_action_start(Editor_Action.LEVEL_TOOL_POINT) then current_tool_mode = .POINT;
if is_action_start(Editor_Action.LEVEL_TOOL_BRUSH) then current_tool_mode = .BRUSH; if is_action_start(Editor_Action.LEVEL_TOOL_BRUSH) then current_tool_mode = .BRUSH;
if is_action_start(Editor_Action.LEVEL_TOOL_AREA) { current_tool_mode = .AREA; area_active = false; } if is_action_start(Editor_Action.LEVEL_TOOL_AREA) { current_tool_mode = .AREA; area_active = false; }
if is_action_start(Editor_Action.LEVEL_TOOL_LINE) { current_tool_mode = .LINE; line_active = false; } if is_action_start(Editor_Action.LEVEL_TOOL_LINE) { current_tool_mode = .LINE; line_active = false; }
if is_action_start(Editor_Action.LEVEL_TOOL_INSPECTOR) then current_tool_mode = .INSPECTOR;
if is_action_start(Editor_Action.LEVEL_TOOL_VIEWER) then current_tool_mode = .VIEWER;
if is_action_start(Editor_Action.LEVEL_TWIST_CCW) { if is_action_start(Editor_Action.LEVEL_TWIST_CCW) {
lastInputTime = get_time(); lastInputTime = get_time();
current_orientation_twist = (current_orientation_twist + 1) % 4; current_orientation_twist = (current_orientation_twist + 1) % 4;
@ -558,6 +603,23 @@ tick_level_editor :: () {
line_active = false; // cancel add selection line_active = false; // cancel add selection
} }
} }
} else if current_tool_mode == .INSPECTOR {
if get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT) & .START {
inspector_selected = true;
inspector_x = cast(s32)px;
inspector_y = cast(s32)py;
inspector_z = cast(s32)pz;
inspector_note_input = "";
curworld2 := get_current_world();
if curworld2.valid {
for note: curworld2.world.notes {
if note.position.x == inspector_x && note.position.y == inspector_y && note.position.z == inspector_z {
inspector_note_input = note.text;
break;
}
}
}
}
} }
} }
} }
@ -635,6 +697,263 @@ create_level_editor_preview_tasks :: () {
add_rendering_task(task); add_rendering_task(task);
} }
get_trile_at :: (world: *World, wx: s32, wy: s32, wz: s32) -> (name: string, orientation: u8, found: bool) {
key := world_to_chunk_coord(wx, wy, wz);
chunk := table_find_pointer(*world.chunks, key);
if !chunk then return "", 0, false;
lx, ly, lz := world_to_local(wx, wy, wz);
for group: chunk.groups {
for inst: group.instances {
if inst.x == lx && inst.y == ly && inst.z == lz {
return group.trile_name, inst.orientation, true;
}
}
}
return "", 0, false;
}
find_emitter_at :: (world: *World, x: s32, y: s32, z: s32) -> (idx: s32, found: bool) {
for inst, i: world.emitter_instances {
ix := cast(s32)inst.position.x;
iy := cast(s32)inst.position.y;
iz := cast(s32)inst.position.z;
if ix == x && iy == y && iz == z then return cast(s32)i, true;
}
return -1, false;
}
find_note_at :: (world: *World, x: s32, y: s32, z: s32) -> (idx: s32, found: bool) {
for note, i: world.notes {
if note.position.x == x && note.position.y == y && note.position.z == z then return cast(s32)i, true;
}
return -1, false;
}
draw_inspector_panel :: (r: *GR.Rect, theme: *GR.Overall_Theme) {
curworld := get_current_world();
if !curworld.valid then return;
world := *curworld.world;
r.h = ui_h(3, 2);
GR.label(r.*, tprint("Slot: (%, %, %)", inspector_x, inspector_y, inspector_z), *t_label_left(theme));
r.y += r.h;
trile_name, orientation, has_trile := get_trile_at(world, inspector_x, inspector_y, inspector_z);
if has_trile {
GR.label(r.*, tprint("Trile: % orient: %", trile_name, orientation), *t_label_left(theme));
} else {
GR.label(r.*, "Trile: (empty)", *t_label_left(theme));
}
r.y += r.h;
r.y += r.h * 0.5;
GR.label(r.*, "-- Emitter --", *t_label_left(theme));
r.y += r.h;
emitter_idx, has_emitter := find_emitter_at(world, inspector_x, inspector_y, inspector_z);
if has_emitter {
inst := *world.emitter_instances[emitter_idx];
GR.label(r.*, tprint("Def: %", inst.definition_name), *t_label_left(theme));
r.y += r.h;
r.h = ui_h(4, 0);
if GR.button(r.*, "Remove Emitter", *theme.button_theme, 300) {
array_ordered_remove_by_index(*world.emitter_instances, emitter_idx);
}
r.y += r.h;
} else {
r.h = ui_h(4, 0);
GR.label(r.*, "Select emitter def:", *t_label_left(theme));
r.y += r.h;
for def, idx: g_emitter_defs {
selected := (cast(s32)idx == inspector_emitter_def);
if GR.button(r.*, def.name, *t_button_selectable(theme, selected), cast(s32)(200 + idx)) {
inspector_emitter_def = cast(s32)idx;
}
r.y += r.h;
}
if inspector_emitter_def >= 0 && inspector_emitter_def < cast(s32)g_emitter_defs.count {
if GR.button(r.*, "Add Emitter", *theme.button_theme, 301) {
def := *g_emitter_defs[inspector_emitter_def];
inst : Particle_Emitter_Instance;
inst.definition = def;
inst.definition_name = def.name;
inst.position = .{cast(float)inspector_x + 0.5, cast(float)inspector_y, cast(float)inspector_z + 0.5};
inst.active = true;
array_add(*world.emitter_instances, inst);
}
r.y += r.h;
}
}
r.h = ui_h(3, 2);
r.y += r.h * 0.5;
GR.label(r.*, "-- Note --", *t_label_left(theme));
r.y += r.h;
note_idx, has_note := find_note_at(world, inspector_x, inspector_y, inspector_z);
if has_note {
note := *world.notes[note_idx];
r.h = ui_h(4, 0);
a, new_text, _ := GR.text_input(r.*, inspector_note_input, *theme.text_input_theme, 500);
if a & .ENTERED {
note.text = copy_string(new_text);
inspector_note_input = note.text;
}
r.y += r.h;
if GR.button(r.*, "Remove Note", *theme.button_theme, 302) {
array_ordered_remove_by_index(*world.notes, note_idx);
inspector_note_input = "";
}
r.y += r.h;
} else {
r.h = ui_h(4, 0);
a, new_text, _ := GR.text_input(r.*, inspector_note_input, *theme.text_input_theme, 501);
if a & .ENTERED {
inspector_note_input = copy_string(new_text);
}
r.y += r.h;
if GR.button(r.*, "Add Note", *theme.button_theme, 303) {
note : Editor_Note;
note.position = .{inspector_x, inspector_y, inspector_z};
note.text = copy_string(ifx inspector_note_input.count > 0 then inspector_note_input else "Note");
array_add(*world.notes, note);
inspector_note_input = note.text;
}
r.y += r.h;
}
r.h = ui_h(3, 2);
r.y += r.h * 0.5;
GR.label(r.*, "-- RDM --", *t_label_left(theme));
r.y += r.h;
chunk_key := world_to_chunk_coord(inspector_x, inspector_y, inspector_z);
chunk := table_find_pointer(*world.chunks, chunk_key);
if chunk != null && chunk.rdm_valid {
GR.label(r.*, "RDM: baked", *t_label_left(theme));
r.y += r.h;
GR.label(r.*, tprint("Chunk: (%, %, %)", chunk_key.x, chunk_key.y, chunk_key.z), *t_label_left(theme));
r.y += r.h;
roughness_mask : u8 = 0;
if has_trile {
roughness_mask = get_trile_roughness_set(trile_name);
}
r.h = ui_h(4, 0);
GR.slider(r.*, *inspector_rdm_roughness, 0, 7, 1, *theme.slider_theme);
r.y += r.h;
r.h = ui_h(3, 2);
has_roughness := (roughness_mask & (1 << cast(u8)inspector_rdm_roughness)) != 0;
if has_roughness {
GR.label(r.*, tprint("Roughness %: present", inspector_rdm_roughness), *t_label_left(theme));
} else {
GR.label(r.*, tprint("Roughness %: not baked", inspector_rdm_roughness), *t_label_left(theme));
}
r.y += r.h;
available: String_Builder;
available.allocator = temp;
append(*available, "Available: ");
for i: 0..7 {
if roughness_mask & (1 << cast(u8)i) then print_to_builder(*available, "% ", i);
}
GR.label(r.*, builder_to_string(*available,, temp), *t_label_left(theme));
r.y += r.h;
lx, ly, lz := world_to_local(inspector_x, inspector_y, inspector_z);
lookup_idx := cast(s32)lx + cast(s32)ly * 32 + cast(s32)lz * 1024 + cast(s32)inspector_rdm_roughness * 32768;
uv0 := Vector2.{0, 0};
uv1 := Vector2.{1, 0};
uv2 := Vector2.{1, 1};
uv3 := Vector2.{0, 1};
has_rect := false;
if chunk.rdm_lookup_cpu.data != null {
tx := lookup_idx % chunk.rdm_lookup_w;
ty := lookup_idx / chunk.rdm_lookup_w;
if tx >= 0 && ty >= 0 && tx < chunk.rdm_lookup_w && ty < chunk.rdm_lookup_h {
pixel_offset := (ty * chunk.rdm_lookup_w + tx) * 4;
rect_x := chunk.rdm_lookup_cpu[pixel_offset + 0];
rect_y := chunk.rdm_lookup_cpu[pixel_offset + 1];
rect_w := chunk.rdm_lookup_cpu[pixel_offset + 2];
rect_h := chunk.rdm_lookup_cpu[pixel_offset + 3];
if rect_w > 0 && rect_h > 0 {
has_rect = true;
fy0 := 1.0 - rect_y;
fy1 := 1.0 - (rect_y + rect_h);
uv0 = .{rect_x, fy0};
uv1 = .{rect_x + rect_w, fy0};
uv2 = .{rect_x + rect_w, fy1};
uv3 = .{rect_x, fy1};
GR.label(r.*, tprint("Rect: %.2,%.2 %.2x%.2", rect_x, rect_y, rect_w, rect_h), *t_label_left(theme));
r.y += r.h;
}
}
}
if !has_rect {
GR.label(r.*, "No RDM entry for this slot/roughness", *t_label_left(theme));
r.y += r.h;
} else {
r.y += r.h * 0.5;
tex_size := r.w * 0.9;
tex_r : GR.Rect;
tex_r.x = r.x + (r.w - tex_size) * 0.5;
tex_r.y = r.y;
tex_r.w = tex_size;
tex_r.h = tex_size * 1.5;
uiTex := New(Ui_Texture,, temp);
uiTex.tex = chunk.rdm_atlas;
if uiTex.tex.id != INVALID_ID {
set_shader_for_images(uiTex);
immediate_quad(
.{tex_r.x, tex_r.y},
.{tex_r.x + tex_r.w, tex_r.y},
.{tex_r.x + tex_r.w, tex_r.y + tex_r.h},
.{tex_r.x, tex_r.y + tex_r.h},
.{1,1,1,1},
uv0, uv1, uv2, uv3
);
set_shader_for_color();
immediate_flush();
}
r.y += tex_r.h + r.h * 0.5;
}
} else {
GR.label(r.*, "RDM: not baked", *t_label_left(theme));
r.y += r.h;
}
}
add_editor_billboards :: () {
if !editor_billboards_visible then return;
curworld := get_current_world();
if !curworld.valid then return;
anim := get_animation_from_string("game_core.ball");
if anim == null then return;
for inst: curworld.world.emitter_instances {
task : Rendering_Task_Billboard;
task.position = inst.position;
task.animation = anim;
task.frame = 0;
add_rendering_task(task);
}
for note: curworld.world.notes {
task : Rendering_Task_Billboard;
task.position = .{cast(float)note.position.x + 0.5, cast(float)note.position.y + 0.5, cast(float)note.position.z + 0.5};
task.animation = anim;
task.frame = 0;
add_rendering_task(task);
}
}
draw_level_editor :: () { draw_level_editor :: () {
curworld := get_current_world(); curworld := get_current_world();
if !curworld.valid then return; if !curworld.valid then return;
@ -644,6 +963,7 @@ draw_level_editor :: () {
if show_trile_preview && !trile_preview_disabled { if show_trile_preview && !trile_preview_disabled {
create_level_editor_preview_tasks(); create_level_editor_preview_tasks();
} }
add_editor_billboards();
} }
draw_level_editor_ui :: (theme: *GR.Overall_Theme) { draw_level_editor_ui :: (theme: *GR.Overall_Theme) {
@ -670,5 +990,18 @@ draw_level_editor_ui :: (theme: *GR.Overall_Theme) {
case .INFO; case .INFO;
if curworld.valid then autoedit(r, *curworld.world.conf, theme); if curworld.valid then autoedit(r, *curworld.world.conf, theme);
} }
draw_picker(theme); if current_tool_mode == .INSPECTOR {
rr := GR.get_rect(ui_w(85,0), ui_h(5,0), ui_w(15, 0), ui_h(95, 0));
draw_bg_rectangle(rr, theme);
ui_add_mouse_occluder(rr);
rr.y += ui_h(1, 0);
if inspector_selected {
draw_inspector_panel(*rr, theme);
} else {
rr.h = ui_h(3, 2);
GR.label(rr, "Click a slot to inspect", *t_label_left(theme));
}
} else if current_tool_mode != .VIEWER {
draw_picker(theme);
}
} }

View File

@ -0,0 +1,200 @@
#scope_file
pe_selected_index : s32 = -1;
pe_preview_emitter : Particle_Emitter_Instance;
pe_cam : Camera = .{
far = 2000.0,
near = 1.0,
target = .{0, 1, 0},
position = .{5, 3, 0},
};
pe_scroll : float = 0;
pe_name_input_start : string = "";
pe_anim_input_start : string = "";
pe_label :: (r: *GR.Rect, text: string, theme: *GR.Overall_Theme) {
GR.label(r.*, text, *t_label_left(theme));
r.y += r.h;
}
pe_float :: (r: *GR.Rect, label: string, value: *float, lo: float, hi: float, step: float, theme: *GR.Overall_Theme, loc := #caller_location) {
GR.label(r.*, label, *t_label_left(theme));
r.y += r.h;
GR.slider(r.*, value, lo, hi, step, *theme.slider_theme, loc = loc);
r.y += r.h;
}
pe_vec3 :: (r: *GR.Rect, label: string, value: *Vector3, lo: float, hi: float, theme: *GR.Overall_Theme, loc := #caller_location) {
number_theme : GR.Number_Input_Theme;
GR.label(r.*, label, *t_label_left(theme));
r.y += r.h;
orig_w := r.w;
orig_x := r.x;
r.w = orig_w / 3;
GR.number_input(r.*, tprint("%", value.x), *value.x, lo, hi, *number_theme, 0, loc);
r.x += r.w;
GR.number_input(r.*, tprint("%", value.y), *value.y, lo, hi, *number_theme, 1, loc);
r.x += r.w;
GR.number_input(r.*, tprint("%", value.z), *value.z, lo, hi, *number_theme, 2, loc);
r.x = orig_x;
r.w = orig_w;
r.y += r.h;
}
pe_vec4 :: (r: *GR.Rect, label: string, value: *Vector4, lo: float, hi: float, theme: *GR.Overall_Theme, loc := #caller_location) {
number_theme : GR.Number_Input_Theme;
GR.label(r.*, label, *t_label_left(theme));
r.y += r.h;
orig_w := r.w;
orig_x := r.x;
r.w = orig_w / 4;
GR.number_input(r.*, tprint("%", value.x), *value.x, lo, hi, *number_theme, 0, loc);
r.x += r.w;
GR.number_input(r.*, tprint("%", value.y), *value.y, lo, hi, *number_theme, 1, loc);
r.x += r.w;
GR.number_input(r.*, tprint("%", value.z), *value.z, lo, hi, *number_theme, 2, loc);
r.x += r.w;
GR.number_input(r.*, tprint("%", value.w), *value.w, lo, hi, *number_theme, 3, loc);
r.x = orig_x;
r.w = orig_w;
r.y += r.h;
}
#scope_export
draw_particle_editor_ui :: (theme: *GR.Overall_Theme) {
panel_w := ui_w(20, 20);
row_h := ui_h(3, 0);
list_r := GR.get_rect(ui_w(80, 20), ui_h(5,0), panel_w, ui_h(95, 0));
ui_add_mouse_occluder(list_r);
draw_bg_rectangle(list_r, theme);
r := list_r;
r.h = row_h;
if GR.button(r, "New Emitter", *theme.button_theme) {
def : Particle_Emitter_Config;
def.name = sprint("emitter_%", g_emitter_defs.count);
array_add(*g_emitter_defs, def);
pe_selected_index = cast(s32)(g_emitter_defs.count - 1);
}
r.y += r.h;
for def, idx : g_emitter_defs {
if r.y > list_r.y + list_r.h - row_h * 3 then break;
selected := (cast(s32)idx == pe_selected_index);
if GR.button(r, def.name, *t_button_selectable(theme, selected), cast(s32)idx) {
pe_selected_index = cast(s32)idx;
pe_name_input_start = def.name;
pe_anim_input_start = def.animation_name;
}
r.y += r.h;
}
r.y = list_r.y + list_r.h - row_h * 2;
half_w := r.w / 2;
orig_w := r.w;
r.w = half_w;
if keybind_button(r, "Save", .SAVE, *theme.button_theme) {
save_particle_defs();
}
r.x += half_w;
if GR.button(r, "Delete", *theme.button_theme) {
if pe_selected_index >= 0 && pe_selected_index < cast(s32)g_emitter_defs.count {
array_ordered_remove_by_index(*g_emitter_defs, pe_selected_index);
if pe_selected_index >= cast(s32)g_emitter_defs.count then pe_selected_index -= 1;
}
}
r.w = orig_w;
if pe_selected_index < 0 || pe_selected_index >= cast(s32)g_emitter_defs.count then return;
def := *g_emitter_defs[pe_selected_index];
edit_r := GR.get_rect(0, ui_h(5,0), panel_w, ui_h(95, 0));
ui_add_mouse_occluder(edit_r);
draw_bg_rectangle(edit_r, theme);
region, inside := GR.begin_scrollable_region(edit_r, *theme.scrollable_region_theme);
er := inside;
er.y -= pe_scroll;
er.h = row_h;
pe_label(*er, "Name", theme);
a, new_name, _ := GR.text_input(er, pe_name_input_start, *theme.text_input_theme, 0);
if a & .ENTERED {
def.name = copy_string(new_name);
pe_name_input_start = def.name;
}
er.y += er.h;
pe_label(*er, "Animation", theme);
a2, new_anim, _ := GR.text_input(er, pe_anim_input_start, *theme.text_input_theme, 1);
if a2 & .ENTERED {
def.animation_name = copy_string(new_anim);
pe_anim_input_start = def.animation_name;
}
er.y += er.h;
pe_float(*er, tprint("Emission Rate: %", def.emission_rate), *def.emission_rate, 0, 200, 1, theme);
pe_float(*er, tprint("Lifetime Min: %", def.lifetime_min), *def.lifetime_min, 0, 10, 0.1, theme);
pe_float(*er, tprint("Lifetime Max: %", def.lifetime_max), *def.lifetime_max, 0, 10, 0.1, theme);
pe_vec3(*er, "Velocity", *def.velocity, -50, 50, theme);
pe_vec3(*er, "Velocity Spread", *def.velocity_spread, 0, 50, theme);
pe_vec3(*er, "Position Spread", *def.position_spread, 0, 10, theme);
pe_float(*er, tprint("Size Start: %", def.size_start), *def.size_start, 0, 10, 0.05, theme);
pe_float(*er, tprint("Size End: %", def.size_end), *def.size_end, 0, 10, 0.05, theme);
pe_vec4(*er, "Color Start", *def.color_start, 0, 1, theme);
pe_vec4(*er, "Color End", *def.color_end, 0, 1, theme);
pe_float(*er, tprint("Gravity: %", def.gravity), *def.gravity, -20, 20, 0.1, theme);
pe_label(*er, tprint("Blend Mode: %", def.blend_mode), theme);
orig_w2 := er.w;
er.w = orig_w2 / 2;
if GR.button(er, "Additive", *t_button_selectable(theme, def.blend_mode == .ADDITIVE), 100) then def.blend_mode = .ADDITIVE;
er.x += er.w;
if GR.button(er, "Alpha", *t_button_selectable(theme, def.blend_mode == .ALPHA), 101) then def.blend_mode = .ALPHA;
er.x -= er.w;
er.w = orig_w2;
er.y += er.h;
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("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);
anim := get_animation_from_string(def.animation_name);
if anim != null {
pe_label(*er, tprint("Animation: % frames", anim.frames.count), theme);
} else {
pe_label(*er, tprint("Animation: not found ('%')", def.animation_name), theme);
}
pe_label(*er, tprint("Spawn accum: %", pe_preview_emitter.spawn_accumulator), theme);
GR.end_scrollable_region(region, er.x + er.w, er.y, *pe_scroll);
}
draw_particle_editor :: () {
create_set_cam_rendering_task(pe_cam, 0.0);
if pe_selected_index >= 0 && pe_selected_index < cast(s32)g_emitter_defs.count {
def := *g_emitter_defs[pe_selected_index];
pe_preview_emitter.definition = def;
pe_preview_emitter.position = .{0, 1, 0};
pe_preview_emitter.active = true;
tick_emitter_instance(*pe_preview_emitter, cast(float)delta_time);
}
}
tick_particle_editor :: () {
tick_particles(cast(float)delta_time);
}

View File

@ -57,23 +57,6 @@ RDM_Bake_State :: struct {
rdm_bake : RDM_Bake_State; rdm_bake : RDM_Bake_State;
// Get the set of roughness values present in a trile's trixels.
// Always includes 7 (used for diffuse light). Returns a bitmask.
get_trile_roughness_set :: (trile_name: string) -> u8 {
trile := get_trile(trile_name);
mask : u8 = 1 << 7; // Always include roughness 7.
for x: 0..15 {
for y: 0..15 {
for z: 0..15 {
if !trile.trixels[x][y][z].empty {
mask |= cast(u8)(1 << trile.trixels[x][y][z].material.roughness);
}
}
}
}
return mask;
}
// Predicted pixel dimensions of a single RDM entry at a given roughness level. // Predicted pixel dimensions of a single RDM entry at a given roughness level.
// roughness 0 → 512×768, roughness 1 → 256×384, ..., roughness 7 → 4×6. // roughness 0 → 512×768, roughness 1 → 256×384, ..., roughness 7 → 4×6.
rdm_entry_predicted_size :: (roughness: s32) -> (w: s32, h: s32) { rdm_entry_predicted_size :: (roughness: s32) -> (w: s32, h: s32) {

View File

@ -26,6 +26,8 @@ Editor_Action :: enum {
LEVEL_TOOL_BRUSH; LEVEL_TOOL_BRUSH;
LEVEL_TOOL_AREA; LEVEL_TOOL_AREA;
LEVEL_TOOL_LINE; LEVEL_TOOL_LINE;
LEVEL_TOOL_INSPECTOR;
LEVEL_TOOL_VIEWER;
// Level editor — tabs // Level editor — tabs
LEVEL_TAB_TOOLS; LEVEL_TAB_TOOLS;
LEVEL_TAB_INFO; LEVEL_TAB_INFO;
@ -168,6 +170,8 @@ set_default_bindings :: () {
set(.LEVEL_TOOL_BRUSH, cast(Key_Code) #char "2"); set(.LEVEL_TOOL_BRUSH, cast(Key_Code) #char "2");
set(.LEVEL_TOOL_AREA, cast(Key_Code) #char "3"); set(.LEVEL_TOOL_AREA, cast(Key_Code) #char "3");
set(.LEVEL_TOOL_LINE, cast(Key_Code) #char "4"); set(.LEVEL_TOOL_LINE, cast(Key_Code) #char "4");
set(.LEVEL_TOOL_INSPECTOR, cast(Key_Code) #char "5");
set(.LEVEL_TOOL_VIEWER, cast(Key_Code) #char "6");
set(.LEVEL_TAB_TOOLS, cast(Key_Code) #char "T"); set(.LEVEL_TAB_TOOLS, cast(Key_Code) #char "T");
set(.LEVEL_TAB_INFO, cast(Key_Code) #char "I"); set(.LEVEL_TAB_INFO, cast(Key_Code) #char "I");
set(.LEVEL_TAB_TACOMA, cast(Key_Code) #char "X"); set(.LEVEL_TAB_TACOMA, cast(Key_Code) #char "X");

View File

@ -17,14 +17,12 @@ _emit :: (level: Log_Level, message: string) {
else ifx level == .ERROR then "[ERROR] " else ifx level == .ERROR then "[ERROR] "
else "[INFO] "; else "[INFO] ";
// Always allocate on the heap regardless of context.allocator (e.g. mesh pool).
old_alloc := context.allocator; old_alloc := context.allocator;
context.allocator = default_context.allocator; context.allocator = default_context.allocator;
line := copy_string(tprint("%1%2", prefix, message)); line := copy_string(tprint("%1%2", prefix, message));
context.allocator = old_alloc;
print("%\n", line); print("%\n", line);
console_add_output_line(line); console_add_output_line(line);
context.allocator = old_alloc;
} }
#scope_export #scope_export
@ -49,6 +47,17 @@ log_info :: (fmt: string, args: ..Any) { logger(.INFO, fmt, ..args); }
log_warn :: (fmt: string, args: ..Any) { logger(.WARN, fmt, ..args); } log_warn :: (fmt: string, args: ..Any) { logger(.WARN, fmt, ..args); }
log_error :: (fmt: string, args: ..Any) { logger(.ERROR, fmt, ..args); } log_error :: (fmt: string, args: ..Any) { logger(.ERROR, fmt, ..args); }
#if FLAG_RELEASE_BUILD {
console_add_output_line :: (s: string) {}
console_open_ignore_input : bool = false;
in_editor_view : bool = false;
console_command_procs : [..]([]string) -> string;
console_command_names : [..]string;
verify_argument_count :: (range_start: s64, range_end: s64, count: s64) -> bool { return count >= range_start && count <= range_end; }
tick_profiler :: () {}
profiler_update :: () {}
}
set_log_level :: (level_str: string) -> string { set_log_level :: (level_str: string) -> string {
if level_str == { if level_str == {
case "DEBUG"; log_min_level = .DEBUG; case "DEBUG"; log_min_level = .DEBUG;

View File

@ -24,13 +24,14 @@ stbi :: #import "stb_image";
#load "rendering/rendering.jai"; #load "rendering/rendering.jai";
#load "input/hotkeys.jai"; #load "input/hotkeys.jai";
#load "ui/ui.jai"; #load "ui/ui.jai";
#load "editor/editor.jai"; #load "editor/rdm_disk.jai";
#if !FLAG_RELEASE_BUILD { #load "editor/editor.jai"; }
#load "time.jai"; #load "time.jai";
#load "events.jai"; #load "events.jai";
#load "load.jai"; #load "load.jai";
#load "ray.jai"; #load "ray.jai";
#load "profiling.jai"; #load "profiling.jai";
// #load "particles/particles.jai"; #load "particles/particles.jai";
#load "world.jai"; #load "world.jai";
#load "utils.jai"; #load "utils.jai";
#load "audio/audio.jai"; #load "audio/audio.jai";
@ -141,7 +142,12 @@ init_after_core :: () {
break; break;
} }
} }
init_editor(); #if !FLAG_RELEASE_BUILD {
init_editor();
} else {
init_keybinds();
init_settings_menu();
}
init_rendering(); init_rendering();
load_post_process_from_pack(); load_post_process_from_pack();
@ -191,11 +197,14 @@ frame :: () {
#if OS != .WASM { tick_profiler(); } #if OS != .WASM { tick_profiler(); }
if !in_editor_view then delta_time_accumulator += delta_time; should_tick_game := #ifx FLAG_RELEASE_BUILD then true else !in_editor_view;
if !in_editor_view && !settings_menu_blocks_game() { if should_tick_game then delta_time_accumulator += delta_time;
if should_tick_game && !settings_menu_blocks_game() {
while delta_time_accumulator > (1.0/480.0) { while delta_time_accumulator > (1.0/480.0) {
game_tick(1.0/480.0); game_tick(1.0/480.0);
tick_particles(1.0/480.0);
delta_time_accumulator -= (1.0/480.0); delta_time_accumulator -= (1.0/480.0);
} }
} }
@ -210,7 +219,7 @@ frame :: () {
add_frame_profiling_point("After UI tick"); add_frame_profiling_point("After UI tick");
// This populates our render task queue. // This populates our render task queue.
if !in_editor_view then game_draw(); if should_tick_game then game_draw();
add_frame_profiling_point("After game draw"); add_frame_profiling_point("After game draw");
ui_clear_mouse_occluders(); ui_clear_mouse_occluders();
@ -218,7 +227,8 @@ frame :: () {
add_frame_profiling_point("After UI draw"); add_frame_profiling_point("After UI draw");
prepare_text(debug_font, tprint("frametime: % ms", latest_frametime * 1000)); prepare_text(debug_font, tprint("frametime: % ms", latest_frametime * 1000));
draw_prepared_text(debug_font, 10, 10, .{0.0, 1.0, 0.0, 1.0}); draw_prepared_text(debug_font, 10, 10, .{0.0, 1.0, 0.0, 1.0});
draw_editor(); #if !FLAG_RELEASE_BUILD { draw_editor(); }
add_particle_render_tasks();
add_frame_profiling_point("After editor draw"); add_frame_profiling_point("After editor draw");
render(); render();
add_frame_profiling_point("After rendering"); add_frame_profiling_point("After rendering");

View File

@ -1,5 +1,10 @@
#scope_export #scope_export
Particle_Blend_Mode :: enum {
ADDITIVE;
ALPHA;
}
Particle_Emitter_Config :: struct { Particle_Emitter_Config :: struct {
name : string; name : string;
animation_name : string; animation_name : string;
@ -8,13 +13,13 @@ Particle_Emitter_Config :: struct {
lifetime_max : float = 2.0; lifetime_max : float = 2.0;
velocity : Vector3; velocity : Vector3;
velocity_spread : Vector3; velocity_spread : Vector3;
position_spread : Vector3;
size_start : float = 0.5; size_start : float = 0.5;
size_end : float = 0.0; size_end : float = 0.0;
color_start : Vector4 = .{1, 1, 1, 1}; color_start : Vector4 = .{1, 1, 1, 1};
color_end : Vector4 = .{1, 1, 1, 0}; color_end : Vector4 = .{1, 1, 1, 0};
gravity : float = 2.0; gravity : float = 2.0;
blend_mode : Particle_Blend_Mode = .ADDITIVE;
animation : *Animation; // resolved at runtime from animation_name
} }
Particle_Emitter_Instance :: struct { Particle_Emitter_Instance :: struct {
@ -41,7 +46,6 @@ g_particles : [2048] Particle;
g_emitter_defs : [..] Particle_Emitter_Config; g_emitter_defs : [..] Particle_Emitter_Config;
tick_particles :: (dt: float) { tick_particles :: (dt: float) {
return;
for *p: g_particles { for *p: g_particles {
if !p.alive then continue; if !p.alive then continue;
p.age += dt; p.age += dt;
@ -53,10 +57,14 @@ tick_particles :: (dt: float) {
p.position += p.velocity * dt; p.position += p.velocity * dt;
} }
for *inst: get_current_world().world.emitter_instances { curworld := get_current_world();
if inst == null then continue; if !curworld.valid then return;
for *inst: curworld.world.emitter_instances {
if !inst.active then continue; if !inst.active then continue;
if inst.definition == null then continue; if inst.definition == null {
inst.definition = get_emitter_def(inst.definition_name);
if inst.definition == null then continue;
}
def := inst.definition; def := inst.definition;
inst.spawn_accumulator += def.emission_rate * dt; inst.spawn_accumulator += def.emission_rate * dt;
while inst.spawn_accumulator >= 1.0 { while inst.spawn_accumulator >= 1.0 {
@ -78,7 +86,100 @@ tick_emitter_instance :: (inst: *Particle_Emitter_Instance, dt: float) {
} }
add_particle_render_tasks :: () { add_particle_render_tasks :: () {
// TODO Batch :: struct {
anim_name : string;
blend_mode : Particle_Blend_Mode;
anim : *Animation;
task : *Rendering_Task_Particles;
count : s32;
}
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;
array_add(*batches, .{ anim_name = def.animation_name, blend_mode = def.blend_mode, anim = anim, task = New(Rendering_Task_Particles,, temp) });
batch = *batches[batches.count - 1];
batch.task.sheet = anim.sheet;
batch.task.blend_mode = def.blend_mode;
}
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.task.pos_size[idx] = .{p.position.x, p.position.y, p.position.z, size};
anim := batch.anim;
frame_count := anim.frames.count;
if frame_count == 0 then continue;
frame := cast(s32)(t * cast(float)(frame_count - 1));
if frame < 0 then frame = 0;
if frame >= frame_count then frame = cast(s32)(frame_count - 1);
f := anim.frames[frame];
batch.task.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.task.colors[idx] = col;
batch.count += 1;
}
for *b: batches {
if b.count > 0 {
b.task.count = b.count;
add_rendering_task(b.task.*);
}
}
}
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 { get_emitter_def_names :: () -> []string {
@ -90,15 +191,12 @@ get_emitter_def_names :: () -> []string {
return names; return names;
} }
save_particle_definition :: (def: Particle_Emitter_Config) { save_particle_defs :: () {
#if OS != .WASM { #if OS != .WASM {
file :: #import "File"; file :: #import "File";
dir := "./game/resources/particles"; json := Jaison.json_write_string(g_emitter_defs, " ");
file.make_directory_if_it_does_not_exist(dir, recursive = true); file.write_entire_file("./game/resources/game_core/particles.json", json);
path := tprint("%/%.emitter.json", dir, def.name); log_info("Saved % particle definitions", g_emitter_defs.count);
json := Jaison.json_write_string(def,, temp);
file.write_entire_file(path, json);
log_info("Saved particle definition '%'", def.name);
} }
} }
@ -116,7 +214,11 @@ spawn_one_particle :: (position: Vector3, def: *Particle_Emitter_Config) {
p.alive = true; p.alive = true;
p.age = 0; p.age = 0;
p.lifetime = rng(def.lifetime_min, def.lifetime_max); p.lifetime = rng(def.lifetime_min, def.lifetime_max);
p.position = position; 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.{ p.velocity = def.velocity + Vector3.{
rng(-def.velocity_spread.x, def.velocity_spread.x), rng(-def.velocity_spread.x, def.velocity_spread.x),
rng(-def.velocity_spread.y, def.velocity_spread.y), rng(-def.velocity_spread.y, def.velocity_spread.y),

View File

@ -1,6 +1,5 @@
g_animations: Table(string, Animation); g_animations: Table(string, Animation);
#scope_file
get_animation_from_string :: (animation: string) -> *Animation { get_animation_from_string :: (animation: string) -> *Animation {
ok, pack, anim := split_from_left(animation, "."); ok, pack, anim := split_from_left(animation, ".");
if !ok { if !ok {
@ -9,7 +8,6 @@ get_animation_from_string :: (animation: string) -> *Animation {
} }
return get_animation_from_pack(pack, anim); return get_animation_from_pack(pack, anim);
} }
#scope_export
Animation_Player :: struct { Animation_Player :: struct {
current_animation : *Animation; current_animation : *Animation;

View File

@ -22,6 +22,7 @@ Render_Command_Type :: enum {
DRAW_TRIXELS; DRAW_TRIXELS;
SET_LIGHT; SET_LIGHT;
DRAW_BILLBOARD; DRAW_BILLBOARD;
DRAW_PARTICLES;
} }
Render_Command :: struct { Render_Command :: struct {
@ -79,6 +80,17 @@ Render_Command_Draw_Trixels :: struct {
c.type = .DRAW_TRIXELS; c.type = .DRAW_TRIXELS;
} }
Render_Command_Draw_Particles :: struct {
#as using c : Render_Command;
c.type = .DRAW_PARTICLES;
count : s32;
blend_mode : Particle_Blend_Mode;
sheet : sg_image;
pos_size : [2048]Vector4;
uv_rects : [2048]Vector4;
colors : [2048]Vector4;
}
Render_Command_Draw_Ground :: struct { Render_Command_Draw_Ground :: struct {
#as using c : Render_Command; #as using c : Render_Command;
c.type = .DRAW_GROUND; c.type = .DRAW_GROUND;

View File

@ -50,6 +50,9 @@ backend_handle_command :: (cmd: *Render_Command) {
} else { } else {
backend_gbuffer_draw_billboard(command.position, command.animation, command.frame, command.flipX); backend_gbuffer_draw_billboard(command.position, command.animation, command.frame, command.flipX);
} }
case .DRAW_PARTICLES;
particles_cmd := cast(*Render_Command_Draw_Particles)cmd;
backend_draw_particles(particles_cmd);
} }
} }
@ -346,6 +349,36 @@ backend_gbuffer_draw_billboard :: (position: Vector3, anim: *Animation, frame_id
sg_draw(0, 6, 1); sg_draw(0, 6, 1);
} }
backend_draw_particles :: (cmd: *Render_Command_Draw_Particles) {
if cmd.count <= 0 then return;
pip := ifx cmd.blend_mode == .ALPHA then *gPipelines.particle_alpha else *gPipelines.particle_additive;
sg_update_buffer(pip.bind.vertex_buffers[1], *(sg_range.{
ptr = cmd.pos_size.data,
size = cast(u64)(cmd.count * size_of(Vector4)),
}));
sg_update_buffer(pip.bind.vertex_buffers[2], *(sg_range.{
ptr = cmd.uv_rects.data,
size = cast(u64)(cmd.count * size_of(Vector4)),
}));
sg_update_buffer(pip.bind.vertex_buffers[3], *(sg_range.{
ptr = cmd.colors.data,
size = cast(u64)(cmd.count * size_of(Vector4)),
}));
mvp := create_viewproj(*camera);
vs_params : Particle_Vs_Params;
vs_params.mvp = mvp.floats;
vs_params.cam = camera.position.component;
sg_apply_pipeline(pip.pipeline);
pip.bind.images[IMG_particle_sprite] = cmd.sheet;
sg_apply_bindings(*pip.bind);
sg_apply_uniforms(UB_particle_vs_params, *(sg_range.{ ptr = *vs_params, size = size_of(type_of(vs_params)) }));
sg_draw(0, 6, cmd.count);
}
backend_draw_ground_gbuf :: (wc: *World_Config) { backend_draw_ground_gbuf :: (wc: *World_Config) {
mvp := create_viewproj(*camera); mvp := create_viewproj(*camera);
view := create_lookat(*camera); view := create_lookat(*camera);

View File

@ -73,7 +73,9 @@ gPipelines : struct {
gbuffer_billboard : Pipeline_Binding; gbuffer_billboard : Pipeline_Binding;
// Renders the SSAO texture using things from the gbuffer pass. particle_additive : Pipeline_Binding;
particle_alpha : Pipeline_Binding;
ssao: Pipeline_Binding; ssao: Pipeline_Binding;
} }
@ -152,6 +154,7 @@ create_pipelines :: () {
create_mix_pipeline(); create_mix_pipeline();
create_billboard_pipeline(); create_billboard_pipeline();
create_gbuffer_billboard_pipeline(); create_gbuffer_billboard_pipeline();
create_particle_pipeline();
create_shadowmap_image(); create_shadowmap_image();
create_final_image(); create_final_image();
@ -1185,3 +1188,87 @@ create_gbuffer_impostors :: () {
g_plane_gbuffer_instance_buffer = sg_make_buffer(*instance_buffer); g_plane_gbuffer_instance_buffer = sg_make_buffer(*instance_buffer);
} }
create_particle_pipeline :: () {
pipeline: sg_pipeline_desc;
shader_desc := particle_shader_desc(sg_query_backend());
shd := sg_make_shader(*shader_desc);
pipeline.shader = shd;
pipeline.layout.buffers[0].stride = 4 * 3;
pipeline.layout.buffers[1].stride = 4 * 4;
pipeline.layout.buffers[1].step_func = .PER_INSTANCE;
pipeline.layout.buffers[2].stride = 4 * 4;
pipeline.layout.buffers[2].step_func = .PER_INSTANCE;
pipeline.layout.buffers[3].stride = 4 * 4;
pipeline.layout.buffers[3].step_func = .PER_INSTANCE;
pipeline.layout.attrs[ATTR_particle_position] = .{ format = .FLOAT3, buffer_index = 0 };
pipeline.layout.attrs[ATTR_particle_inst_pos_size] = .{ format = .FLOAT4, buffer_index = 1 };
pipeline.layout.attrs[ATTR_particle_inst_uv_rect] = .{ format = .FLOAT4, buffer_index = 2 };
pipeline.layout.attrs[ATTR_particle_inst_color] = .{ format = .FLOAT4, buffer_index = 3 };
pipeline.index_type = .UINT16;
pipeline.depth = .{
write_enabled = false,
compare = .LESS_EQUAL,
pixel_format = .DEPTH,
};
pipeline.color_count = 1;
pipeline.colors[0] = .{
pixel_format = .RGBA32F,
blend = .{
enabled = true,
src_factor_rgb = .SRC_ALPHA,
dst_factor_rgb = .ONE,
src_factor_alpha = .ONE,
dst_factor_alpha = .ONE,
},
};
gPipelines.particle_additive.pipeline = sg_make_pipeline(*pipeline);
pipeline.colors[0].blend = .{
enabled = true,
src_factor_rgb = .SRC_ALPHA,
dst_factor_rgb = .ONE_MINUS_SRC_ALPHA,
src_factor_alpha = .ONE,
dst_factor_alpha = .ONE_MINUS_SRC_ALPHA,
};
gPipelines.particle_alpha.pipeline = sg_make_pipeline(*pipeline);
vertices: [4]Vector3 = .[
.{ 0.0, 0.0, 0.0 },
.{ 1.0, 0.0, 0.0 },
.{ 1.0, 1.0, 0.0 },
.{ 0.0, 1.0, 0.0 },
];
indices: [6]u16 = .[
0, 1, 2,
0, 2, 3,
];
idx_buf := sg_make_buffer(*(sg_buffer_desc.{ type = .INDEXBUFFER, data = .{ ptr = indices.data, size = 6 * 2 } }));
vtx_buf := sg_make_buffer(*(sg_buffer_desc.{ data = .{ ptr = vertices.data, size = 4 * 3 * 4 } }));
inst1 := sg_make_buffer(*(sg_buffer_desc.{ size = cast(u64)(MAX_PARTICLES * size_of(Vector4)), usage = .STREAM }));
inst2 := sg_make_buffer(*(sg_buffer_desc.{ size = cast(u64)(MAX_PARTICLES * size_of(Vector4)), usage = .STREAM }));
inst3 := sg_make_buffer(*(sg_buffer_desc.{ size = cast(u64)(MAX_PARTICLES * size_of(Vector4)), usage = .STREAM }));
smp := sg_make_sampler(*(sg_sampler_desc.{
wrap_u = .CLAMP_TO_EDGE,
wrap_v = .CLAMP_TO_EDGE,
min_filter = .LINEAR,
mag_filter = .LINEAR,
}));
setup_bind :: (bind: *sg_bindings, idx_buf: sg_buffer, vtx_buf: sg_buffer, inst1: sg_buffer, inst2: sg_buffer, inst3: sg_buffer, smp: sg_sampler) {
bind.index_buffer = idx_buf;
bind.vertex_buffers[0] = vtx_buf;
bind.vertex_buffers[1] = inst1;
bind.vertex_buffers[2] = inst2;
bind.vertex_buffers[3] = inst3;
bind.samplers[SMP_particle_spritesmp] = smp;
}
setup_bind(*gPipelines.particle_additive.bind, idx_buf, vtx_buf, inst1, inst2, inst3, smp);
setup_bind(*gPipelines.particle_alpha.bind, idx_buf, vtx_buf, inst1, inst2, inst3, smp);
}

View File

@ -67,6 +67,17 @@ Rendering_Task_Trixels :: struct {
trile : *Trile; trile : *Trile;
} }
Rendering_Task_Particles :: struct {
#as using t : Rendering_Task;
t.type = .PARTICLES;
count : s32;
blend_mode : Particle_Blend_Mode;
sheet : sg_image;
pos_size : [2048]Vector4;
uv_rects : [2048]Vector4;
colors : [2048]Vector4;
}
Rendering_Task_Set_Camera :: struct { Rendering_Task_Set_Camera :: struct {
#as using t : Rendering_Task; #as using t : Rendering_Task;
camera : Camera; camera : Camera;
@ -151,6 +162,16 @@ tasks_to_commands :: () {
array_add(*render_command_buckets.main, commandDrawBillboard); array_add(*render_command_buckets.main, commandDrawBillboard);
array_add(*render_command_buckets.shadow, commandDrawBillboard); array_add(*render_command_buckets.shadow, commandDrawBillboard);
array_add(*render_command_buckets.reflection, commandDrawBillboard); array_add(*render_command_buckets.reflection, commandDrawBillboard);
case .PARTICLES;
particleTask := (cast(*Rendering_Task_Particles)it);
drawCmd := New(Render_Command_Draw_Particles,, temp);
drawCmd.count = particleTask.count;
drawCmd.blend_mode = particleTask.blend_mode;
drawCmd.sheet = particleTask.sheet;
memcpy(drawCmd.pos_size.data, particleTask.pos_size.data, particleTask.count * size_of(Vector4));
memcpy(drawCmd.uv_rects.data, particleTask.uv_rects.data, particleTask.count * size_of(Vector4));
memcpy(drawCmd.colors.data, particleTask.colors.data, particleTask.count * size_of(Vector4));
array_add(*render_command_buckets.main, drawCmd);
case .SET_CAMERA; case .SET_CAMERA;
task := (cast(*Rendering_Task_Set_Camera)it); task := (cast(*Rendering_Task_Set_Camera)it);
command := New(Render_Command_Set_Camera,, temp); command := New(Render_Command_Set_Camera,, temp);

View File

@ -2,44 +2,80 @@
Random :: #import "Random"; Random :: #import "Random";
TRANSITION_SPEED :: 7.0; TRANSITION_SPEED :: 7.0;
MAX_BLOBS :: 32; MAX_BLOBS :: 32;
VOLUME_STEP_INITIAL :: 0.1;
VOLUME_STEP_HELD :: 0.3;
VOLUME_HOLD_DELAY :: 0.35;
CONFIG_PATH :: "settings.cfg";
Settings_Page :: enum { Settings_Page :: enum {
MAIN; MAIN;
SETTINGS; SETTINGS;
AUDIO; AUDIO;
GRAPHICS;
} }
Settings_State :: struct { Settings_State :: struct {
open : bool = false; open : bool = false;
transition : float = 0.0; transition : float = 0.0;
page : Settings_Page = .MAIN; page : Settings_Page = .MAIN;
cursor : s32 = 0; cursor : s32 = 0;
title_font : *Font; title_font : *Font;
item_font : *Font; item_font : *Font;
last_screen_h : s32 = 0;
blobs : [MAX_BLOBS]Blob; vol_hold_time : float = 0.0;
blob_count : s32 = 0; vol_hold_dir : s32 = 0;
mouse_hover : s32 = -1;
mouse_moved : bool = false;
last_mouse_x : float = -1;
last_mouse_y : float = -1;
blobs : [MAX_BLOBS]Blob;
blob_count : s32 = 0;
} }
Blob :: struct { Blob :: struct {
cx : float; cx, cy, radius : float;
cy : float; r, g, b : float;
radius : float; freq : float;
r, g, b: float; drift_x : float;
freq : float; drift_y : float;
drift_x: float; phase : float;
drift_y: float;
phase : float;
} }
g_settings : Settings_State; g_settings : Settings_State;
g_settings_config : Settings_Menu_Config; g_settings_config : Settings_Menu_Config;
MAIN_ITEMS :: string.["Resume", "Settings", "Exit"]; MAIN_ITEMS :: string.["Resume", "Settings", "Exit"];
SETTINGS_ITEMS :: string.["Audio"]; SETTINGS_ITEMS :: string.["Audio", "Graphics"];
AUDIO_LABELS :: string.["Master Volume", "Music Volume", "Sound Effects"]; AUDIO_LABELS :: string.["Master Volume", "Music Volume", "Sound Effects"];
GRAPHICS_ITEMS :: string.["Fullscreen"];
page_items :: () -> []string {
if g_settings.page == .MAIN return MAIN_ITEMS;
if g_settings.page == .SETTINGS return SETTINGS_ITEMS;
if g_settings.page == .AUDIO return AUDIO_LABELS;
if g_settings.page == .GRAPHICS return GRAPHICS_ITEMS;
return .[];
}
page_parent :: () -> Settings_Page {
if g_settings.page == .AUDIO return .SETTINGS;
if g_settings.page == .GRAPHICS return .SETTINGS;
if g_settings.page == .SETTINGS return .MAIN;
return .MAIN;
}
page_hint :: () -> string {
if g_settings.page == .MAIN return "Up/Down navigate Enter select";
if g_settings.page == .SETTINGS return "Up/Down navigate Enter select Esc back";
if g_settings.page == .AUDIO return "Up/Down navigate Left/Right adjust Esc back";
if g_settings.page == .GRAPHICS return "Enter toggle Esc back";
return "";
}
audio_get :: (i: s32) -> float { audio_get :: (i: s32) -> float {
if i == 0 return g_mixer.config.masterVolume; if i == 0 return g_mixer.config.masterVolume;
@ -54,11 +90,18 @@ audio_set :: (i: s32, v: float) {
if i == 2 then g_mixer.config.soundEffectVolume = clamp(v, 0.0, 1.0); if i == 2 then g_mixer.config.soundEffectVolume = clamp(v, 0.0, 1.0);
} }
page_count :: () -> s32 { get_item_label :: (page: Settings_Page, index: s32) -> string {
if g_settings.page == .MAIN return MAIN_ITEMS.count; items := page_items();
if g_settings.page == .SETTINGS return SETTINGS_ITEMS.count; if index < 0 || index >= items.count then return "";
if g_settings.page == .AUDIO return AUDIO_LABELS.count;
return 0; if page == .AUDIO {
pct := cast(s32)(audio_get(index) * 100.0 + 0.5);
return tprint("% %", items[index], make_volume_bar(pct));
}
if page == .GRAPHICS && index == 0 {
return tprint("%: %", items[index], ifx sapp_is_fullscreen() then "On" else "Off");
}
return items[index];
} }
rand_range :: (lo: float, hi: float) -> float { rand_range :: (lo: float, hi: float) -> float {
@ -72,14 +115,13 @@ generate_blobs :: () {
for i: 0..count-1 { for i: 0..count-1 {
blob := *g_settings.blobs[i]; blob := *g_settings.blobs[i];
blob.cx = rand_range(0.05, 0.95);
blob.cx = rand_range(0.05, 0.95); blob.cy = rand_range(0.15, 0.85);
blob.cy = rand_range(0.15, 0.85); blob.radius = rand_range(cfg.radius_min, cfg.radius_max);
blob.radius = rand_range(cfg.radius_min, cfg.radius_max); blob.freq = rand_range(cfg.freq_min, cfg.freq_max);
blob.freq = rand_range(cfg.freq_min, cfg.freq_max);
blob.drift_x = rand_range(cfg.drift_min, cfg.drift_max); blob.drift_x = rand_range(cfg.drift_min, cfg.drift_max);
blob.drift_y = rand_range(cfg.drift_min, cfg.drift_max); blob.drift_y = rand_range(cfg.drift_min, cfg.drift_max);
blob.phase = rand_range(0.0, 6.28); blob.phase = rand_range(0.0, 6.28);
if cfg.palette.count > 0 { if cfg.palette.count > 0 {
idx := cast(s32)(Random.random_get_zero_to_one() * cast(float)(cfg.palette.count - 1) + 0.5); idx := cast(s32)(Random.random_get_zero_to_one() * cast(float)(cfg.palette.count - 1) + 0.5);
@ -96,20 +138,120 @@ generate_blobs :: () {
} }
} }
update_fonts_for_screen :: () {
_, h := get_window_size();
if h == g_settings.last_screen_h then return;
g_settings.last_screen_h = h;
g_settings.title_font = get_font_at_size(max(cast(s32)(cast(float)h * 0.055), 20));
g_settings.item_font = get_font_at_size(max(cast(s32)(cast(float)h * 0.028), 14));
}
save_settings :: () {
file :: #import "File";
builder : String_Builder;
append(*builder, tprint("master_volume %\n", g_mixer.config.masterVolume));
append(*builder, tprint("music_volume %\n", g_mixer.config.musicVolume));
append(*builder, tprint("sfx_volume %\n", g_mixer.config.soundEffectVolume));
append(*builder, tprint("fullscreen %\n", cast(s32) sapp_is_fullscreen()));
file.write_entire_file(CONFIG_PATH, builder_to_string(*builder,, allocator = temp));
}
load_settings :: () {
file :: #import "File";
data, ok := file.read_entire_file(CONFIG_PATH,, allocator = temp);
if !ok then return;
text := cast(string) data;
while text.count > 0 {
line := consume_line(*text);
if line.count == 0 then continue;
key, val := split_first(line, #char " ");
if key == "master_volume" then g_mixer.config.masterVolume = parse_float(val);
if key == "music_volume" then g_mixer.config.musicVolume = parse_float(val);
if key == "sfx_volume" then g_mixer.config.soundEffectVolume = parse_float(val);
if key == "fullscreen" {
if (parse_int(val) != 0) != sapp_is_fullscreen() then sapp_toggle_fullscreen();
}
}
}
consume_line :: (text: *string) -> string {
s := text.*;
for i: 0..s.count-1 {
if s[i] == #char "\n" {
line := string.{count = i, data = s.data};
text.data += i + 1;
text.count -= i + 1;
return line;
}
}
result := s;
text.count = 0;
return result;
}
split_first :: (s: string, sep: u8) -> (string, string) {
for i: 0..s.count-1 {
if s[i] == sep {
return string.{count = i, data = s.data},
string.{count = s.count - i - 1, data = s.data + i + 1};
}
}
return s, "";
}
parse_float :: (s: string) -> float {
val, ok := string_to_float(s);
if ok return val;
return 0.0;
}
parse_int :: (s: string) -> s64 {
val, ok := string_to_int(s);
if ok return val;
return 0;
}
navigate_to :: (page: Settings_Page) {
g_settings.page = page;
g_settings.cursor = 0;
}
go_back :: () {
if g_settings.page == .AUDIO || g_settings.page == .GRAPHICS then save_settings();
if g_settings.page == .MAIN {
g_settings.open = false;
} else {
navigate_to(page_parent());
}
}
handle_enter :: () {
if g_settings.page == {
case .MAIN;
if g_settings.cursor == 0 then g_settings.open = false;
if g_settings.cursor == 1 then navigate_to(.SETTINGS);
if g_settings.cursor == 2 { save_settings(); sapp_request_quit(); }
case .SETTINGS;
if g_settings.cursor == 0 then navigate_to(.AUDIO);
if g_settings.cursor == 1 then navigate_to(.GRAPHICS);
case .GRAPHICS;
if g_settings.cursor == 0 then sapp_toggle_fullscreen();
}
}
#scope_export #scope_export
Phosphene_Config :: struct { Phosphene_Config :: struct {
blob_count : s32 = 12; blob_count : s32 = 12;
alpha : float = 0.06; alpha : float = 0.06;
layers : s32 = 4; layers : s32 = 4;
radius_min : float = 0.08; radius_min : float = 0.08;
radius_max : float = 0.28; radius_max : float = 0.28;
freq_min : float = 0.1; freq_min : float = 0.1;
freq_max : float = 0.4; freq_max : float = 0.4;
drift_min : float = 0.02; drift_min : float = 0.02;
drift_max : float = 0.07; drift_max : float = 0.07;
color_jitter : float = 0.1; color_jitter : float = 0.1;
palette : []Vector3; palette : []Vector3;
} }
@ -135,28 +277,22 @@ settings_menu_blocks_game :: () -> bool {
} }
init_settings_menu :: () { init_settings_menu :: () {
// TODO: scale font sizes with screen resolution
g_settings.title_font = get_font_at_size(60); g_settings.title_font = get_font_at_size(60);
g_settings.item_font = get_font_at_size(30); g_settings.item_font = get_font_at_size(30);
load_settings();
} }
tick_settings_menu :: () { tick_settings_menu :: () {
if in_editor_view then return; if in_editor_view then return;
update_fonts_for_screen();
if is_action_start(Editor_Action.TOGGLE_SETTINGS) { if is_action_start(Editor_Action.TOGGLE_SETTINGS) {
if !g_settings.open { if !g_settings.open {
g_settings.open = true; g_settings.open = true;
g_settings.page = .MAIN; navigate_to(.MAIN);
g_settings.cursor = 0;
generate_blobs(); generate_blobs();
} else if g_settings.page == .AUDIO {
g_settings.page = .SETTINGS;
g_settings.cursor = 0;
} else if g_settings.page == .SETTINGS {
g_settings.page = .MAIN;
g_settings.cursor = 0;
} else { } else {
g_settings.open = false; go_back();
} }
} }
@ -170,41 +306,53 @@ tick_settings_menu :: () {
if !g_settings.open then return; if !g_settings.open then return;
if console_open_ignore_input then return; if console_open_ignore_input then return;
count := page_count(); count := cast(s32) page_items().count;
up := cast(bool)(input_button_states[Key_Code.ARROW_UP] & .START); mx := input_mouse_x;
down := cast(bool)(input_button_states[Key_Code.ARROW_DOWN] & .START); my := input_mouse_y;
left := cast(bool)(input_button_states[Key_Code.ARROW_LEFT] & .START); g_settings.mouse_moved = (mx != g_settings.last_mouse_x || my != g_settings.last_mouse_y);
right := cast(bool)(input_button_states[Key_Code.ARROW_RIGHT]& .START); g_settings.last_mouse_x = mx;
enter := cast(bool)(input_button_states[Key_Code.ENTER] & .START); g_settings.last_mouse_y = my;
if g_settings.mouse_moved && g_settings.mouse_hover >= 0 && g_settings.mouse_hover < count {
g_settings.cursor = g_settings.mouse_hover;
}
up := cast(bool)(input_button_states[Key_Code.ARROW_UP] & .START);
down := cast(bool)(input_button_states[Key_Code.ARROW_DOWN] & .START);
if up then g_settings.cursor = (g_settings.cursor - 1 + count) % count; if up then g_settings.cursor = (g_settings.cursor - 1 + count) % count;
if down then g_settings.cursor = (g_settings.cursor + 1) % count; if down then g_settings.cursor = (g_settings.cursor + 1) % count;
if g_settings.page == .MAIN && enter { enter := cast(bool)(input_button_states[Key_Code.ENTER] & .START);
if g_settings.cursor == 0 { click := cast(bool)(input_button_states[Key_Code.MOUSE_BUTTON_LEFT] & .START);
// Resume if click && g_settings.mouse_hover >= 0 && g_settings.mouse_hover < count {
g_settings.open = false; g_settings.cursor = g_settings.mouse_hover;
} else if g_settings.cursor == 1 { enter = true;
// Settings
g_settings.page = .SETTINGS;
g_settings.cursor = 0;
} else if g_settings.cursor == 2 {
// Exit
sapp_request_quit();
}
} else if g_settings.page == .SETTINGS && enter {
if g_settings.cursor == 0 {
g_settings.page = .AUDIO;
g_settings.cursor = 0;
}
} }
if enter then handle_enter();
if g_settings.page == .AUDIO { if g_settings.page == .AUDIO {
delta : float = 0.0; left := cast(bool)(input_button_states[Key_Code.ARROW_LEFT] & .START);
if left then delta = -0.1; right := cast(bool)(input_button_states[Key_Code.ARROW_RIGHT] & .START);
if right then delta = 0.1; left_held := cast(bool)(input_button_states[Key_Code.ARROW_LEFT] & .DOWN);
if delta != 0.0 then audio_set(g_settings.cursor, audio_get(g_settings.cursor) + delta); right_held := cast(bool)(input_button_states[Key_Code.ARROW_RIGHT] & .DOWN);
if left { audio_set(g_settings.cursor, audio_get(g_settings.cursor) - VOLUME_STEP_INITIAL); g_settings.vol_hold_time = 0; g_settings.vol_hold_dir = -1; }
if right { audio_set(g_settings.cursor, audio_get(g_settings.cursor) + VOLUME_STEP_INITIAL); g_settings.vol_hold_time = 0; g_settings.vol_hold_dir = 1; }
dir : s32 = 0;
if left_held then dir = -1;
if right_held then dir = 1;
if dir != 0 && dir == g_settings.vol_hold_dir {
g_settings.vol_hold_time += dt;
if g_settings.vol_hold_time > VOLUME_HOLD_DELAY {
audio_set(g_settings.cursor, audio_get(g_settings.cursor) + cast(float)dir * VOLUME_STEP_HELD * dt);
}
} else if dir == 0 {
g_settings.vol_hold_time = 0;
g_settings.vol_hold_dir = 0;
}
} }
} }
@ -212,26 +360,21 @@ draw_settings_menu :: (theme: *GR.Overall_Theme) {
t := g_settings.transition; t := g_settings.transition;
if t < 0.001 then return; if t < 0.001 then return;
fw := vw * 100.0; fw := vw * 100.0;
fh := vh * 100.0; fh := vh * 100.0;
half_h := fh * 0.5; bar_h := t * fh * 0.5;
bar_h := t * half_h;
cfg := *g_settings_config; cfg := *g_settings_config;
bg := cfg.bg_color;
set_shader_for_color(); set_shader_for_color();
immediate_quad(.{0, 0}, .{fw, 0}, .{fw, bar_h}, .{0, bar_h}, bg); immediate_quad(.{0, 0}, .{fw, 0}, .{fw, bar_h}, .{0, bar_h}, cfg.bg_color);
immediate_flush(); immediate_flush();
bottom_y := fh - bar_h; bottom_y := fh - bar_h;
immediate_quad(.{0, bottom_y}, .{fw, bottom_y}, .{fw, fh}, .{0, fh}, bg); immediate_quad(.{0, bottom_y}, .{fw, bottom_y}, .{fw, fh}, .{0, fh}, cfg.bg_color);
immediate_flush(); immediate_flush();
if t > 0.5 { if t > 0.5 {
phosphene_fade := clamp((t - 0.5) / 0.5, 0.0, 1.0); draw_phosphenes(fw, fh, cast(float) get_time(), clamp((t - 0.5) / 0.5, 0.0, 1.0));
time := cast(float) get_time();
draw_phosphenes(fw, fh, bar_h, bottom_y, time, phosphene_fade);
} }
content_alpha := clamp((t - 0.75) / 0.25, 0.0, 1.0); content_alpha := clamp((t - 0.75) / 0.25, 0.0, 1.0);
@ -241,70 +384,50 @@ draw_settings_menu :: (theme: *GR.Overall_Theme) {
return .{base.x, base.y, base.z, base.w * a}; return .{base.x, base.y, base.z, base.w * a};
} }
title_col := apply_alpha(cfg.title_color, content_alpha); title_col := apply_alpha(cfg.title_color, content_alpha);
item_col := apply_alpha(cfg.item_color, content_alpha); item_col := apply_alpha(cfg.item_color, content_alpha);
sel_col := apply_alpha(cfg.selected_color, content_alpha); sel_col := apply_alpha(cfg.selected_color, content_alpha);
hint_col := apply_alpha(cfg.hint_color, content_alpha); hint_col := apply_alpha(cfg.hint_color, content_alpha);
credits_col := apply_alpha(cfg.credits_color, content_alpha); credits_col := apply_alpha(cfg.credits_color, content_alpha);
prepare_text(g_settings.title_font, cfg.game_title); prepare_text(g_settings.title_font, cfg.game_title);
title_x := (fw - cast(float) gPreppedTextWidth) * 0.5; draw_prepared_text(g_settings.title_font, xx ((fw - cast(float)gPreppedTextWidth) * 0.5), xx (fh * 0.22), title_col);
draw_prepared_text(g_settings.title_font, xx title_x, xx (fh * 0.22), title_col);
item_h := cast(float)(g_settings.item_font.character_height) * 1.7; items := page_items();
item_h := cast(float)(g_settings.item_font.character_height) * 1.7;
total_h := cast(float)items.count * item_h;
start_y := fh * 0.5 - total_h * 0.5;
if g_settings.page == .MAIN { g_settings.mouse_hover = -1;
draw_item_list(MAIN_ITEMS, item_h, fw, fh, item_col, sel_col); for i: 0..items.count-1 {
draw_hint("Up/Down navigate Enter select", fw, fh, hint_col); col := ifx cast(s32)i == g_settings.cursor then sel_col else item_col;
label := get_item_label(g_settings.page, cast(s32)i);
prepare_text(g_settings.item_font, label);
x := (fw - cast(float)gPreppedTextWidth) * 0.5;
row_y := start_y + cast(float)i * item_h;
draw_prepared_text(g_settings.item_font, xx x, xx row_y, col);
if cfg.credits.count > 0 { if input_mouse_y >= row_y && input_mouse_y < row_y + item_h {
prepare_text(g_settings.item_font, cfg.credits); g_settings.mouse_hover = cast(s32)i;
cx := (fw - cast(float) gPreppedTextWidth) * 0.5;
draw_prepared_text(g_settings.item_font, xx cx, xx (fh * 0.92), credits_col);
} }
}
} else if g_settings.page == .SETTINGS { draw_centered_text(g_settings.item_font, page_hint(), fw, fh * 0.82, hint_col);
draw_item_list(SETTINGS_ITEMS, item_h, fw, fh, item_col, sel_col);
draw_hint("Up/Down navigate Enter select Esc back", fw, fh, hint_col);
} else if g_settings.page == .AUDIO { if g_settings.page == .MAIN && cfg.credits.count > 0 {
total_h := AUDIO_LABELS.count * item_h; draw_centered_text(g_settings.item_font, cfg.credits, fw, fh * 0.92, credits_col);
start_y := fh * 0.5 - total_h * 0.5;
for i: 0..AUDIO_LABELS.count-1 {
col := ifx cast(s32)i == g_settings.cursor then sel_col else item_col;
pct := cast(s32)(audio_get(cast(s32)i) * 100.0 + 0.5);
bar := make_volume_bar(pct, cast(s32)i == g_settings.cursor);
label := tprint("% %", AUDIO_LABELS[i], bar);
prepare_text(g_settings.item_font, label);
x := (fw - cast(float) gPreppedTextWidth) * 0.5;
draw_prepared_text(g_settings.item_font, xx x, xx (start_y + cast(float)i * item_h), col);
}
draw_hint("Up/Down navigate Left/Right adjust Esc back", fw, fh, hint_col);
} }
} }
#scope_file #scope_file
draw_item_list :: (items: []string, item_h: float, fw: float, fh: float, item_col: Vector4, sel_col: Vector4) { draw_centered_text :: (font: *Font, text: string, fw: float, y: float, color: Vector4) {
total_h := items.count * item_h; prepare_text(font, text);
start_y := fh * 0.5 - total_h * 0.5; draw_prepared_text(font, xx ((fw - cast(float)gPreppedTextWidth) * 0.5), xx y, color);
for i: 0..items.count-1 {
col := ifx cast(s32)i == g_settings.cursor then sel_col else item_col;
prepare_text(g_settings.item_font, items[i]);
x := (fw - cast(float) gPreppedTextWidth) * 0.5;
draw_prepared_text(g_settings.item_font, xx x, xx (start_y + cast(float)i * item_h), col);
}
} }
draw_hint :: (text: string, fw: float, fh: float, color: Vector4) { draw_phosphenes :: (fw: float, fh: float, time: float, fade: float) {
prepare_text(g_settings.item_font, text);
hint_x := (fw - cast(float) gPreppedTextWidth) * 0.5;
draw_prepared_text(g_settings.item_font, xx hint_x, xx (fh * 0.82), color);
}
draw_phosphenes :: (fw: float, fh: float, bar_h: float, bottom_y: float, time: float, fade: float) {
if g_settings.blob_count < 1 then return; if g_settings.blob_count < 1 then return;
set_shader_for_color(); set_shader_for_color();
max_alpha := g_settings_config.phosphenes.alpha; max_alpha := g_settings_config.phosphenes.alpha;
@ -313,35 +436,24 @@ draw_phosphenes :: (fw: float, fh: float, bar_h: float, bottom_y: float, time: f
for i: 0..g_settings.blob_count-1 { for i: 0..g_settings.blob_count-1 {
blob := g_settings.blobs[i]; blob := g_settings.blobs[i];
// Smooth, gentle pulse — stays in 0.6..1.0 range to avoid harsh pop
pulse := 0.5 + 0.5 * sin(time * blob.freq * 6.28 + blob.phase); pulse := 0.5 + 0.5 * sin(time * blob.freq * 6.28 + blob.phase);
alpha := fade * max_alpha * (0.6 + 0.4 * pulse); alpha := fade * max_alpha * (0.6 + 0.4 * pulse);
cx := (blob.cx + sin(time * 0.3 + blob.phase) * blob.drift_x) * fw; cx := (blob.cx + sin(time * 0.3 + blob.phase) * blob.drift_x) * fw;
cy := (blob.cy + cos(time * 0.2 + blob.phase * 1.3) * blob.drift_y) * fh; cy := (blob.cy + cos(time * 0.2 + blob.phase * 1.3) * blob.drift_y) * fh;
blob_r := blob.radius * fh; blob_r := blob.radius * fh;
// Draw concentric layers for soft falloff
for li: 0..num_layers-1 { for li: 0..num_layers-1 {
frac := cast(float)(num_layers - li) / cast(float)num_layers; frac := cast(float)(num_layers - li) / cast(float)num_layers;
r := blob_r * frac; r := blob_r * frac;
layer_alpha := alpha * (1.0 - frac * 0.7); layer_alpha := alpha * (1.0 - frac * 0.7);
col := Vector4.{blob.r, blob.g, blob.b, layer_alpha}; col := Vector4.{blob.r, blob.g, blob.b, layer_alpha};
immediate_quad( immediate_quad(.{cx - r, cy - r}, .{cx + r, cy - r}, .{cx + r, cy + r}, .{cx - r, cy + r}, col);
.{cx - r, cy - r},
.{cx + r, cy - r},
.{cx + r, cy + r},
.{cx - r, cy + r},
col
);
} }
} }
immediate_flush(); immediate_flush();
} }
make_volume_bar :: (pct: s32, active: bool) -> string { make_volume_bar :: (pct: s32) -> string {
STEPS :: 10; STEPS :: 10;
filled := (pct + 5) / STEPS; filled := (pct + 5) / STEPS;
builder : String_Builder; builder : String_Builder;

View File

@ -85,6 +85,22 @@ get_trile :: (name: string) -> (*Trile, success: bool) {
return trileptr, true; return trileptr, true;
} }
get_trile_roughness_set :: (trile_name: string) -> u8 {
trile, ok := get_trile(trile_name);
if !ok then return 1 << 7;
mask : u8 = 1 << 7;
for x: 0..15 {
for y: 0..15 {
for z: 0..15 {
if !trile.trixels[x][y][z].empty {
mask |= cast(u8)(1 << trile.trixels[x][y][z].material.roughness);
}
}
}
}
return mask;
}
lstrile :: () -> string { lstrile :: () -> string {
count := 0; count := 0;
for v : trile_table { for v : trile_table {

View File

@ -369,7 +369,11 @@ tick_ui :: () {
array_reset_keeping_memory(*ui_events); array_reset_keeping_memory(*ui_events);
GR.ui_per_frame_update(1, xx w, xx h, get_time()); GR.ui_per_frame_update(1, xx w, xx h, get_time());
tick_editor_ui(); #if !FLAG_RELEASE_BUILD {
tick_editor_ui();
} else {
tick_settings_menu();
}
} }
checkboxTest : bool = false; checkboxTest : bool = false;
@ -423,11 +427,15 @@ render_ui :: () {
proc := GR.default_theme_procs[0]; proc := GR.default_theme_procs[0];
my_theme := proc(); my_theme := proc();
GR.set_default_theme(my_theme); GR.set_default_theme(my_theme);
draw_editor_ui(*my_theme); #if !FLAG_RELEASE_BUILD {
if !in_editor_view then game_ui(*my_theme); draw_editor_ui(*my_theme);
if !in_editor_view then game_ui(*my_theme);
draw_console(*my_theme);
} else {
game_ui(*my_theme);
}
#if FLAG_DEMO_BUILD { if !in_editor_view then draw_demo_ui(*my_theme); } #if FLAG_DEMO_BUILD { if !in_editor_view then draw_demo_ui(*my_theme); }
draw_settings_menu(*my_theme); draw_settings_menu(*my_theme);
draw_console(*my_theme);
} }
ui_pass :: () { ui_pass :: () {

View File

@ -52,18 +52,27 @@ Chunk :: struct {
coord: Chunk_Key; coord: Chunk_Key;
groups: [..]Chunk_Trile_Group; groups: [..]Chunk_Trile_Group;
// RDM bake results (populated by tacoma baking) rdm_atlas: sg_image;
rdm_atlas: sg_image; rdm_lookup: sg_image;
rdm_lookup: sg_image; rdm_valid: bool;
rdm_valid: bool; #if !FLAG_RELEASE_BUILD {
rdm_lookup_cpu: []float;
rdm_lookup_w: s32;
rdm_lookup_h: s32;
}
}
Editor_Note :: struct {
position : Chunk_Key;
text : string;
} }
World :: struct { World :: struct {
name : string; name : string;
conf : World_Config; conf : World_Config;
chunks : Table(Chunk_Key, Chunk, chunk_key_hash, chunk_key_compare); chunks : Table(Chunk_Key, Chunk, chunk_key_hash, chunk_key_compare);
// emitter_instances : [..]Particle_Emitter_Instance; emitter_instances : [..]Particle_Emitter_Instance;
emitter_instances : [..]string; notes : [..]Editor_Note;
} }
// Convert a world-space integer position to chunk coordinate. // Convert a world-space integer position to chunk coordinate.
@ -123,6 +132,10 @@ unload_current_world :: () {
if chunk.rdm_valid { if chunk.rdm_valid {
sg_destroy_image(chunk.rdm_atlas); sg_destroy_image(chunk.rdm_atlas);
sg_destroy_image(chunk.rdm_lookup); sg_destroy_image(chunk.rdm_lookup);
#if !FLAG_RELEASE_BUILD {
if chunk.rdm_lookup_cpu.data then free(chunk.rdm_lookup_cpu.data);
chunk.rdm_lookup_cpu = .{};
}
chunk.rdm_atlas = .{}; chunk.rdm_atlas = .{};
chunk.rdm_lookup = .{}; chunk.rdm_lookup = .{};
chunk.rdm_valid = false; chunk.rdm_valid = false;
@ -145,7 +158,13 @@ set_loaded_world :: (world: World) {
unload_current_world(); unload_current_world();
current_world.world = world; current_world.world = world;
current_world.valid = true; current_world.valid = true;
// RDM loading is kicked off by the asset manager after calling this. resolve_emitter_definitions(*current_world.world);
}
resolve_emitter_definitions :: (world: *World) {
for *inst: world.emitter_instances {
inst.definition = get_emitter_def(inst.definition_name);
}
} }
clear_world :: () { clear_world :: () {
@ -167,7 +186,7 @@ get_current_world :: () -> *Current_World {
// --- Binary serialization (.world format) --- // --- Binary serialization (.world format) ---
WORLD_MAGIC :: u32.[0x4C575254][0]; // "TRWL" as little-endian u32 WORLD_MAGIC :: u32.[0x4C575254][0]; // "TRWL" as little-endian u32
WORLD_VERSION :: cast(u16) 1; WORLD_VERSION :: cast(u16) 3;
// World_Config serialized as a fixed-size binary blob. // World_Config serialized as a fixed-size binary blob.
// We serialize it field-by-field to avoid padding issues. // We serialize it field-by-field to avoid padding issues.
@ -345,6 +364,30 @@ save_world :: (world: *World) -> string {
append(*builder, entry.data); append(*builder, entry.data);
} }
// Write emitter instances
num_emitters := cast(u16) world.emitter_instances.count;
write_value(*builder, num_emitters);
for inst: world.emitter_instances {
name_len := cast(u16) inst.definition_name.count;
write_value(*builder, name_len);
append(*builder, inst.definition_name);
write_value(*builder, inst.position.x);
write_value(*builder, inst.position.y);
write_value(*builder, inst.position.z);
}
// Write notes
num_notes := cast(u16) world.notes.count;
write_value(*builder, num_notes);
for note: world.notes {
text_len := cast(u16) note.text.count;
write_value(*builder, text_len);
append(*builder, note.text);
write_value(*builder, note.position.x);
write_value(*builder, note.position.y);
write_value(*builder, note.position.z);
}
return builder_to_string(*builder); return builder_to_string(*builder);
} }
@ -365,7 +408,7 @@ load_world_from_data :: (data: []u8) -> (World, bool) {
} }
version := read_value(data, *cursor, u16); version := read_value(data, *cursor, u16);
if version != WORLD_VERSION { if version != 1 && version != 2 && version != 3 {
log_error("Unsupported world version: %", version); log_error("Unsupported world version: %", version);
return world, false; return world, false;
} }
@ -419,6 +462,33 @@ load_world_from_data :: (data: []u8) -> (World, bool) {
table_set(*world.chunks, chunk.coord, chunk); table_set(*world.chunks, chunk.coord, chunk);
} }
if version >= 2 && cursor < data.count {
num_emitters := cast(s64) read_value(data, *cursor, u16);
for i: 0..num_emitters-1 {
inst: Particle_Emitter_Instance;
name_len := cast(s64) read_value(data, *cursor, u16);
inst.definition_name = read_string(data, *cursor, name_len);
inst.position.x = read_value(data, *cursor, float);
inst.position.y = read_value(data, *cursor, float);
inst.position.z = read_value(data, *cursor, float);
inst.active = true;
array_add(*world.emitter_instances, inst);
}
}
if version >= 3 && cursor < data.count {
num_notes := cast(s64) read_value(data, *cursor, u16);
for i: 0..num_notes-1 {
note: Editor_Note;
text_len := cast(s64) read_value(data, *cursor, u16);
note.text = read_string(data, *cursor, text_len);
note.position.x = read_value(data, *cursor, s32);
note.position.y = read_value(data, *cursor, s32);
note.position.z = read_value(data, *cursor, s32);
array_add(*world.notes, note);
}
}
return world, true; return world, true;
} }