From 01eaff1c0fcdf9abcdcc10a499c73b98bdd61950 Mon Sep 17 00:00:00 2001 From: katajisto Date: Mon, 23 Mar 2026 21:05:30 +0200 Subject: [PATCH] Editor tooling improvements --- settings.cfg | 4 + src/assets/asset_manager.jai | 38 ++- src/editor/editor.jai | 21 +- src/editor/level_editor.jai | 339 ++++++++++++++++++++++++++- src/editor/particle_editor.jai | 200 ++++++++++++++++ src/editor/tacoma.jai | 17 -- src/input/keybinds.jai | 4 + src/logging.jai | 15 +- src/main.jai | 24 +- src/particles/particles.jai | 132 +++++++++-- src/rendering/animation.jai | 2 - src/rendering/backend.jai | 12 + src/rendering/backend_sokol.jai | 33 +++ src/rendering/pipelines.jai | 89 ++++++- src/rendering/tasks.jai | 21 ++ src/settings_menu.jai | 396 ++++++++++++++++++++------------ src/trile.jai | 16 ++ src/ui/ui.jai | 16 +- src/world.jai | 88 ++++++- 19 files changed, 1251 insertions(+), 216 deletions(-) create mode 100644 settings.cfg create mode 100644 src/editor/particle_editor.jai diff --git a/settings.cfg b/settings.cfg new file mode 100644 index 0000000..16fb0b8 --- /dev/null +++ b/settings.cfg @@ -0,0 +1,4 @@ +master_volume 1 +music_volume 0.2 +sfx_volume 1 +fullscreen 1 diff --git a/src/assets/asset_manager.jai b/src/assets/asset_manager.jai index ff515ae..8055ab4 100644 --- a/src/assets/asset_manager.jai +++ b/src/assets/asset_manager.jai @@ -2,6 +2,7 @@ #import "String"; hash :: #import "Hash"; +Pool :: #import "Pool"; #load "loaders.jai"; @@ -77,6 +78,7 @@ fetch_callback :: (res: *sfetch_response_t) #c_call { mem := NewArray(res.data.size.(s64), u8, false); memcpy(mem.data, res.data.ptr, res.data.size.(s64)); pack: Loaded_Pack; + Pool.set_allocators(*pack.pool); pack.nameHash = hash.get_hash(req.pack_name); pack.name = 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); if chunk != null { - chunk.rdm_atlas = req.rdm_pending_atlas; - chunk.rdm_lookup = sg_make_image(*lookup_desc); - chunk.rdm_valid = true; + chunk.rdm_atlas = req.rdm_pending_atlas; + chunk.rdm_lookup = sg_make_image(*lookup_desc); + #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); } else { sg_destroy_image(req.rdm_pending_atlas); @@ -191,11 +201,12 @@ Loaded_Pack :: struct { textures : Table(string, sg_image); animations : Table(string, Animation); audio : Table(string, Audio_Data); - //fonts : [..]Font??; + pool : Pool.Pool; } 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 { name : string; image : sg_image; @@ -241,6 +252,21 @@ add_resources_from_pack :: (pack: *Loaded_Pack) { case "wav"; audio := load_wav_from_memory(v.data); 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"; // Load into a font. Add to free list. case; @@ -292,10 +318,10 @@ find_pack_by_name :: (name: string) -> (bool, Loaded_Pack) { free_resources_from_pack :: (pack: *Loaded_Pack) { for pack.textures sg_destroy_image(it); - for *pack.audio array_free(it.data); table_reset(*pack.textures); table_reset(*pack.audio); table_reset(*pack.animations); + Pool.reset(*pack.pool); } asset_manager_init :: () { diff --git a/src/editor/editor.jai b/src/editor/editor.jai index 91b00fc..94ac554 100644 --- a/src/editor/editor.jai +++ b/src/editor/editor.jai @@ -1,9 +1,9 @@ -#load "rdm_disk.jai"; #if OS != .WASM { #load "iprof.jai"; #load "picker.jai"; #load "trile_editor.jai"; #load "level_editor.jai"; + #load "particle_editor.jai"; } #if FLAG_TACOMA_ENABLED { #load "tacoma.jai"; } #load "console.jai"; @@ -14,8 +14,8 @@ Editor_View :: enum { Closed_Editor; Trile_Editor; - Level_Editor; - Material_Editor; + Level_Editor; + Particle_Editor; }; current_editor_view : Editor_View = .Trile_Editor; @@ -25,8 +25,10 @@ current_editor_view : Editor_View = .Trile_Editor; in_editor_view : bool = false; init_editor :: () { - #if OS != .WASM {init_profiler();} - init_console(); + #if !FLAG_RELEASE_BUILD { + #if OS != .WASM {init_profiler();} + init_console(); + } init_keybinds(); init_settings_menu(); } @@ -50,14 +52,15 @@ draw_editor_ui :: (theme: *GR.Overall_Theme) { 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; 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 == { case .Trile_Editor; draw_trile_editor_ui(theme); case .Level_Editor; draw_level_editor_ui(theme); + case .Particle_Editor; + draw_particle_editor_ui(theme); } } draw_profiler(); @@ -80,6 +83,8 @@ draw_editor :: () { draw_trile_editor(); case .Level_Editor; draw_level_editor(); + case .Particle_Editor; + draw_particle_editor(); } } } @@ -105,6 +110,8 @@ tick_editor_ui :: () { tick_trile_editor(); case .Level_Editor; tick_level_editor(); + case .Particle_Editor; + tick_particle_editor(); } } } diff --git a/src/editor/level_editor.jai b/src/editor/level_editor.jai index bfbc8fb..55d27bf 100644 --- a/src/editor/level_editor.jai +++ b/src/editor/level_editor.jai @@ -6,6 +6,8 @@ MAX_CAMERA_DIST :: 25.0; MIN_CAMERA_DIST :: 2.0; DIST_SCROLL_SPEED :: 0.8; +mouse1Active : bool; +mouse1ActivationPosition : Vector2; mouse2Active : bool; mouse2ActivationPosition : Vector2; mouse3Active : bool; @@ -25,6 +27,8 @@ Level_Editor_Tool_Mode :: enum { BRUSH; AREA; LINE; + INSPECTOR; + VIEWER; } current_tool_mode : Level_Editor_Tool_Mode = .POINT; @@ -46,6 +50,16 @@ line_start_z : int; current_orientation_face : 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 { return current_orientation_face * 4 + current_orientation_twist; } @@ -164,6 +178,25 @@ tick_level_editor_camera :: () { 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 mouse2Active { lastInputTime = get_time(); @@ -292,6 +325,10 @@ draw_tools_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) { 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; } 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) 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)); } 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 @@ -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_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_LINE) { current_tool_mode = .LINE; line_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_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) { lastInputTime = get_time(); current_orientation_twist = (current_orientation_twist + 1) % 4; @@ -558,6 +603,23 @@ tick_level_editor :: () { 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); } +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 :: () { curworld := get_current_world(); if !curworld.valid then return; @@ -644,6 +963,7 @@ draw_level_editor :: () { if show_trile_preview && !trile_preview_disabled { create_level_editor_preview_tasks(); } + add_editor_billboards(); } draw_level_editor_ui :: (theme: *GR.Overall_Theme) { @@ -670,5 +990,18 @@ draw_level_editor_ui :: (theme: *GR.Overall_Theme) { case .INFO; 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); + } } diff --git a/src/editor/particle_editor.jai b/src/editor/particle_editor.jai new file mode 100644 index 0000000..9bfb3e9 --- /dev/null +++ b/src/editor/particle_editor.jai @@ -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); +} diff --git a/src/editor/tacoma.jai b/src/editor/tacoma.jai index 82c0402..d0d621c 100644 --- a/src/editor/tacoma.jai +++ b/src/editor/tacoma.jai @@ -57,23 +57,6 @@ RDM_Bake_State :: struct { 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. // roughness 0 → 512×768, roughness 1 → 256×384, ..., roughness 7 → 4×6. rdm_entry_predicted_size :: (roughness: s32) -> (w: s32, h: s32) { diff --git a/src/input/keybinds.jai b/src/input/keybinds.jai index 3479cd8..d73d6d7 100644 --- a/src/input/keybinds.jai +++ b/src/input/keybinds.jai @@ -26,6 +26,8 @@ Editor_Action :: enum { LEVEL_TOOL_BRUSH; LEVEL_TOOL_AREA; LEVEL_TOOL_LINE; + LEVEL_TOOL_INSPECTOR; + LEVEL_TOOL_VIEWER; // Level editor — tabs LEVEL_TAB_TOOLS; LEVEL_TAB_INFO; @@ -168,6 +170,8 @@ set_default_bindings :: () { set(.LEVEL_TOOL_BRUSH, cast(Key_Code) #char "2"); set(.LEVEL_TOOL_AREA, cast(Key_Code) #char "3"); 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_INFO, cast(Key_Code) #char "I"); set(.LEVEL_TAB_TACOMA, cast(Key_Code) #char "X"); diff --git a/src/logging.jai b/src/logging.jai index ad679ed..423e567 100644 --- a/src/logging.jai +++ b/src/logging.jai @@ -17,14 +17,12 @@ _emit :: (level: Log_Level, message: string) { else ifx level == .ERROR then "[ERROR] " else "[INFO] "; - // Always allocate on the heap regardless of context.allocator (e.g. mesh pool). old_alloc := context.allocator; context.allocator = default_context.allocator; line := copy_string(tprint("%1%2", prefix, message)); - context.allocator = old_alloc; - print("%\n", line); console_add_output_line(line); + context.allocator = old_alloc; } #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_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 { if level_str == { case "DEBUG"; log_min_level = .DEBUG; diff --git a/src/main.jai b/src/main.jai index be790b3..d9768a5 100644 --- a/src/main.jai +++ b/src/main.jai @@ -24,13 +24,14 @@ stbi :: #import "stb_image"; #load "rendering/rendering.jai"; #load "input/hotkeys.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 "events.jai"; #load "load.jai"; #load "ray.jai"; #load "profiling.jai"; -// #load "particles/particles.jai"; +#load "particles/particles.jai"; #load "world.jai"; #load "utils.jai"; #load "audio/audio.jai"; @@ -141,7 +142,12 @@ init_after_core :: () { break; } } - init_editor(); + #if !FLAG_RELEASE_BUILD { + init_editor(); + } else { + init_keybinds(); + init_settings_menu(); + } init_rendering(); load_post_process_from_pack(); @@ -191,11 +197,14 @@ frame :: () { #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) { game_tick(1.0/480.0); + tick_particles(1.0/480.0); delta_time_accumulator -= (1.0/480.0); } } @@ -210,7 +219,7 @@ frame :: () { add_frame_profiling_point("After UI tick"); // 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"); ui_clear_mouse_occluders(); @@ -218,7 +227,8 @@ frame :: () { add_frame_profiling_point("After UI draw"); 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_editor(); + #if !FLAG_RELEASE_BUILD { draw_editor(); } + add_particle_render_tasks(); add_frame_profiling_point("After editor draw"); render(); add_frame_profiling_point("After rendering"); diff --git a/src/particles/particles.jai b/src/particles/particles.jai index c682623..343b0c0 100644 --- a/src/particles/particles.jai +++ b/src/particles/particles.jai @@ -1,5 +1,10 @@ #scope_export +Particle_Blend_Mode :: enum { + ADDITIVE; + ALPHA; +} + Particle_Emitter_Config :: struct { name : string; animation_name : string; @@ -8,13 +13,13 @@ Particle_Emitter_Config :: struct { lifetime_max : float = 2.0; velocity : Vector3; velocity_spread : Vector3; + position_spread : Vector3; size_start : float = 0.5; size_end : float = 0.0; color_start : Vector4 = .{1, 1, 1, 1}; color_end : Vector4 = .{1, 1, 1, 0}; gravity : float = 2.0; - - animation : *Animation; // resolved at runtime from animation_name + blend_mode : Particle_Blend_Mode = .ADDITIVE; } Particle_Emitter_Instance :: struct { @@ -41,7 +46,6 @@ g_particles : [2048] Particle; g_emitter_defs : [..] Particle_Emitter_Config; tick_particles :: (dt: float) { - return; for *p: g_particles { if !p.alive then continue; p.age += dt; @@ -53,10 +57,14 @@ tick_particles :: (dt: float) { p.position += p.velocity * dt; } - for *inst: get_current_world().world.emitter_instances { - if inst == null then continue; + curworld := get_current_world(); + if !curworld.valid then return; + for *inst: curworld.world.emitter_instances { 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; inst.spawn_accumulator += def.emission_rate * dt; while inst.spawn_accumulator >= 1.0 { @@ -78,7 +86,100 @@ tick_emitter_instance :: (inst: *Particle_Emitter_Instance, dt: float) { } 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 { @@ -90,15 +191,12 @@ get_emitter_def_names :: () -> []string { return names; } -save_particle_definition :: (def: Particle_Emitter_Config) { +save_particle_defs :: () { #if OS != .WASM { file :: #import "File"; - dir := "./game/resources/particles"; - file.make_directory_if_it_does_not_exist(dir, recursive = true); - path := tprint("%/%.emitter.json", dir, def.name); - json := Jaison.json_write_string(def,, temp); - file.write_entire_file(path, json); - log_info("Saved particle definition '%'", def.name); + json := Jaison.json_write_string(g_emitter_defs, " "); + file.write_entire_file("./game/resources/game_core/particles.json", json); + log_info("Saved % particle definitions", g_emitter_defs.count); } } @@ -116,7 +214,11 @@ spawn_one_particle :: (position: Vector3, def: *Particle_Emitter_Config) { p.alive = true; p.age = 0; 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.{ rng(-def.velocity_spread.x, def.velocity_spread.x), rng(-def.velocity_spread.y, def.velocity_spread.y), diff --git a/src/rendering/animation.jai b/src/rendering/animation.jai index 537a799..9511a1b 100644 --- a/src/rendering/animation.jai +++ b/src/rendering/animation.jai @@ -1,6 +1,5 @@ g_animations: Table(string, Animation); -#scope_file get_animation_from_string :: (animation: string) -> *Animation { ok, pack, anim := split_from_left(animation, "."); if !ok { @@ -9,7 +8,6 @@ get_animation_from_string :: (animation: string) -> *Animation { } return get_animation_from_pack(pack, anim); } -#scope_export Animation_Player :: struct { current_animation : *Animation; diff --git a/src/rendering/backend.jai b/src/rendering/backend.jai index 6731a6d..7060549 100644 --- a/src/rendering/backend.jai +++ b/src/rendering/backend.jai @@ -22,6 +22,7 @@ Render_Command_Type :: enum { DRAW_TRIXELS; SET_LIGHT; DRAW_BILLBOARD; + DRAW_PARTICLES; } Render_Command :: struct { @@ -79,6 +80,17 @@ Render_Command_Draw_Trixels :: struct { 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 { #as using c : Render_Command; c.type = .DRAW_GROUND; diff --git a/src/rendering/backend_sokol.jai b/src/rendering/backend_sokol.jai index 1885308..f8cbf26 100644 --- a/src/rendering/backend_sokol.jai +++ b/src/rendering/backend_sokol.jai @@ -50,6 +50,9 @@ backend_handle_command :: (cmd: *Render_Command) { } else { 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); } +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) { mvp := create_viewproj(*camera); view := create_lookat(*camera); diff --git a/src/rendering/pipelines.jai b/src/rendering/pipelines.jai index f1cb05e..e1dd3b9 100644 --- a/src/rendering/pipelines.jai +++ b/src/rendering/pipelines.jai @@ -73,7 +73,9 @@ gPipelines : struct { 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; } @@ -152,6 +154,7 @@ create_pipelines :: () { create_mix_pipeline(); create_billboard_pipeline(); create_gbuffer_billboard_pipeline(); + create_particle_pipeline(); create_shadowmap_image(); create_final_image(); @@ -1185,3 +1188,87 @@ create_gbuffer_impostors :: () { 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); +} diff --git a/src/rendering/tasks.jai b/src/rendering/tasks.jai index 21f5d1f..d77f5e3 100644 --- a/src/rendering/tasks.jai +++ b/src/rendering/tasks.jai @@ -67,6 +67,17 @@ Rendering_Task_Trixels :: struct { 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 { #as using t : Rendering_Task; camera : Camera; @@ -151,6 +162,16 @@ tasks_to_commands :: () { array_add(*render_command_buckets.main, commandDrawBillboard); array_add(*render_command_buckets.shadow, 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; task := (cast(*Rendering_Task_Set_Camera)it); command := New(Render_Command_Set_Camera,, temp); diff --git a/src/settings_menu.jai b/src/settings_menu.jai index 3283f0d..cd6cac7 100644 --- a/src/settings_menu.jai +++ b/src/settings_menu.jai @@ -2,44 +2,80 @@ Random :: #import "Random"; -TRANSITION_SPEED :: 7.0; -MAX_BLOBS :: 32; +TRANSITION_SPEED :: 7.0; +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 { MAIN; SETTINGS; AUDIO; + GRAPHICS; } Settings_State :: struct { - open : bool = false; - transition : float = 0.0; - page : Settings_Page = .MAIN; - cursor : s32 = 0; - title_font : *Font; - item_font : *Font; + open : bool = false; + transition : float = 0.0; + page : Settings_Page = .MAIN; + cursor : s32 = 0; + title_font : *Font; + item_font : *Font; + last_screen_h : s32 = 0; - blobs : [MAX_BLOBS]Blob; - blob_count : s32 = 0; + vol_hold_time : float = 0.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 { - cx : float; - cy : float; - radius : float; - r, g, b: float; - freq : float; - drift_x: float; - drift_y: float; - phase : float; + cx, cy, radius : float; + r, g, b : float; + freq : float; + drift_x : float; + drift_y : float; + phase : float; } g_settings : Settings_State; g_settings_config : Settings_Menu_Config; MAIN_ITEMS :: string.["Resume", "Settings", "Exit"]; -SETTINGS_ITEMS :: string.["Audio"]; +SETTINGS_ITEMS :: string.["Audio", "Graphics"]; 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 { 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); } -page_count :: () -> s32 { - if g_settings.page == .MAIN return MAIN_ITEMS.count; - if g_settings.page == .SETTINGS return SETTINGS_ITEMS.count; - if g_settings.page == .AUDIO return AUDIO_LABELS.count; - return 0; +get_item_label :: (page: Settings_Page, index: s32) -> string { + items := page_items(); + if index < 0 || index >= items.count then return ""; + + 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 { @@ -72,14 +115,13 @@ generate_blobs :: () { for i: 0..count-1 { blob := *g_settings.blobs[i]; - - blob.cx = rand_range(0.05, 0.95); - blob.cy = rand_range(0.15, 0.85); - blob.radius = rand_range(cfg.radius_min, cfg.radius_max); - blob.freq = rand_range(cfg.freq_min, cfg.freq_max); + blob.cx = rand_range(0.05, 0.95); + blob.cy = rand_range(0.15, 0.85); + blob.radius = rand_range(cfg.radius_min, cfg.radius_max); + blob.freq = rand_range(cfg.freq_min, cfg.freq_max); blob.drift_x = 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 { 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 Phosphene_Config :: struct { blob_count : s32 = 12; alpha : float = 0.06; layers : s32 = 4; - radius_min : float = 0.08; radius_max : float = 0.28; freq_min : float = 0.1; freq_max : float = 0.4; drift_min : float = 0.02; drift_max : float = 0.07; - color_jitter : float = 0.1; palette : []Vector3; } @@ -135,28 +277,22 @@ settings_menu_blocks_game :: () -> bool { } init_settings_menu :: () { - // TODO: scale font sizes with screen resolution g_settings.title_font = get_font_at_size(60); g_settings.item_font = get_font_at_size(30); + load_settings(); } tick_settings_menu :: () { if in_editor_view then return; + update_fonts_for_screen(); if is_action_start(Editor_Action.TOGGLE_SETTINGS) { if !g_settings.open { - g_settings.open = true; - g_settings.page = .MAIN; - g_settings.cursor = 0; + g_settings.open = true; + navigate_to(.MAIN); 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 { - g_settings.open = false; + go_back(); } } @@ -170,41 +306,53 @@ tick_settings_menu :: () { if !g_settings.open 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); - down := cast(bool)(input_button_states[Key_Code.ARROW_DOWN] & .START); - left := cast(bool)(input_button_states[Key_Code.ARROW_LEFT] & .START); - right := cast(bool)(input_button_states[Key_Code.ARROW_RIGHT]& .START); - enter := cast(bool)(input_button_states[Key_Code.ENTER] & .START); + mx := input_mouse_x; + my := input_mouse_y; + g_settings.mouse_moved = (mx != g_settings.last_mouse_x || my != g_settings.last_mouse_y); + g_settings.last_mouse_x = mx; + 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 down then g_settings.cursor = (g_settings.cursor + 1) % count; - if g_settings.page == .MAIN && enter { - if g_settings.cursor == 0 { - // Resume - g_settings.open = false; - } else if g_settings.cursor == 1 { - // 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; - } + enter := cast(bool)(input_button_states[Key_Code.ENTER] & .START); + click := cast(bool)(input_button_states[Key_Code.MOUSE_BUTTON_LEFT] & .START); + if click && g_settings.mouse_hover >= 0 && g_settings.mouse_hover < count { + g_settings.cursor = g_settings.mouse_hover; + enter = true; } + if enter then handle_enter(); if g_settings.page == .AUDIO { - delta : float = 0.0; - if left then delta = -0.1; - if right then delta = 0.1; - if delta != 0.0 then audio_set(g_settings.cursor, audio_get(g_settings.cursor) + delta); + left := cast(bool)(input_button_states[Key_Code.ARROW_LEFT] & .START); + right := cast(bool)(input_button_states[Key_Code.ARROW_RIGHT] & .START); + left_held := cast(bool)(input_button_states[Key_Code.ARROW_LEFT] & .DOWN); + 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; if t < 0.001 then return; - fw := vw * 100.0; - fh := vh * 100.0; - half_h := fh * 0.5; - bar_h := t * half_h; + fw := vw * 100.0; + fh := vh * 100.0; + bar_h := t * fh * 0.5; cfg := *g_settings_config; - bg := cfg.bg_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(); - 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(); if t > 0.5 { - phosphene_fade := 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); + draw_phosphenes(fw, fh, cast(float) get_time(), clamp((t - 0.5) / 0.5, 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}; } - title_col := apply_alpha(cfg.title_color, content_alpha); - item_col := apply_alpha(cfg.item_color, content_alpha); - sel_col := apply_alpha(cfg.selected_color, content_alpha); - hint_col := apply_alpha(cfg.hint_color, content_alpha); - credits_col := apply_alpha(cfg.credits_color, content_alpha); + title_col := apply_alpha(cfg.title_color, content_alpha); + item_col := apply_alpha(cfg.item_color, content_alpha); + sel_col := apply_alpha(cfg.selected_color, content_alpha); + hint_col := apply_alpha(cfg.hint_color, content_alpha); + credits_col := apply_alpha(cfg.credits_color, content_alpha); 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 title_x, xx (fh * 0.22), title_col); + draw_prepared_text(g_settings.title_font, xx ((fw - cast(float)gPreppedTextWidth) * 0.5), 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 { - draw_item_list(MAIN_ITEMS, item_h, fw, fh, item_col, sel_col); - draw_hint("Up/Down navigate Enter select", fw, fh, hint_col); + g_settings.mouse_hover = -1; + for i: 0..items.count-1 { + 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 { - prepare_text(g_settings.item_font, cfg.credits); - cx := (fw - cast(float) gPreppedTextWidth) * 0.5; - draw_prepared_text(g_settings.item_font, xx cx, xx (fh * 0.92), credits_col); + if input_mouse_y >= row_y && input_mouse_y < row_y + item_h { + g_settings.mouse_hover = cast(s32)i; } + } - } else if g_settings.page == .SETTINGS { - 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); + draw_centered_text(g_settings.item_font, page_hint(), fw, fh * 0.82, hint_col); - } else if g_settings.page == .AUDIO { - total_h := AUDIO_LABELS.count * item_h; - 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); + if g_settings.page == .MAIN && cfg.credits.count > 0 { + draw_centered_text(g_settings.item_font, cfg.credits, fw, fh * 0.92, credits_col); } } #scope_file -draw_item_list :: (items: []string, item_h: float, fw: float, fh: float, item_col: Vector4, sel_col: Vector4) { - total_h := items.count * item_h; - start_y := fh * 0.5 - total_h * 0.5; - 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_centered_text :: (font: *Font, text: string, fw: float, y: float, color: Vector4) { + prepare_text(font, text); + draw_prepared_text(font, xx ((fw - cast(float)gPreppedTextWidth) * 0.5), xx y, color); } -draw_hint :: (text: string, fw: float, fh: float, color: Vector4) { - 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) { +draw_phosphenes :: (fw: float, fh: float, time: float, fade: float) { if g_settings.blob_count < 1 then return; - set_shader_for_color(); 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 { 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); alpha := fade * max_alpha * (0.6 + 0.4 * pulse); - 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; - blob_r := blob.radius * fh; - // Draw concentric layers for soft falloff for li: 0..num_layers-1 { frac := cast(float)(num_layers - li) / cast(float)num_layers; r := blob_r * frac; layer_alpha := alpha * (1.0 - frac * 0.7); col := Vector4.{blob.r, blob.g, blob.b, layer_alpha}; - immediate_quad( - .{cx - r, cy - r}, - .{cx + r, cy - r}, - .{cx + r, cy + r}, - .{cx - r, cy + r}, - col - ); + immediate_quad(.{cx - r, cy - r}, .{cx + r, cy - r}, .{cx + r, cy + r}, .{cx - r, cy + r}, col); } } immediate_flush(); } -make_volume_bar :: (pct: s32, active: bool) -> string { +make_volume_bar :: (pct: s32) -> string { STEPS :: 10; filled := (pct + 5) / STEPS; builder : String_Builder; diff --git a/src/trile.jai b/src/trile.jai index e9413ab..359a43b 100644 --- a/src/trile.jai +++ b/src/trile.jai @@ -85,6 +85,22 @@ get_trile :: (name: string) -> (*Trile, success: bool) { 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 { count := 0; for v : trile_table { diff --git a/src/ui/ui.jai b/src/ui/ui.jai index 6adcbee..b090a40 100644 --- a/src/ui/ui.jai +++ b/src/ui/ui.jai @@ -369,7 +369,11 @@ tick_ui :: () { array_reset_keeping_memory(*ui_events); 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; @@ -423,11 +427,15 @@ render_ui :: () { proc := GR.default_theme_procs[0]; my_theme := proc(); GR.set_default_theme(my_theme); - draw_editor_ui(*my_theme); - if !in_editor_view then game_ui(*my_theme); + #if !FLAG_RELEASE_BUILD { + 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); } draw_settings_menu(*my_theme); - draw_console(*my_theme); } ui_pass :: () { diff --git a/src/world.jai b/src/world.jai index abf4e30..4052e40 100644 --- a/src/world.jai +++ b/src/world.jai @@ -52,18 +52,27 @@ Chunk :: struct { coord: Chunk_Key; groups: [..]Chunk_Trile_Group; - // RDM bake results (populated by tacoma baking) - rdm_atlas: sg_image; - rdm_lookup: sg_image; - rdm_valid: bool; + rdm_atlas: sg_image; + rdm_lookup: sg_image; + 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 { name : string; conf : World_Config; chunks : Table(Chunk_Key, Chunk, chunk_key_hash, chunk_key_compare); - // emitter_instances : [..]Particle_Emitter_Instance; - emitter_instances : [..]string; + emitter_instances : [..]Particle_Emitter_Instance; + notes : [..]Editor_Note; } // Convert a world-space integer position to chunk coordinate. @@ -123,6 +132,10 @@ unload_current_world :: () { if chunk.rdm_valid { sg_destroy_image(chunk.rdm_atlas); 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_lookup = .{}; chunk.rdm_valid = false; @@ -145,7 +158,13 @@ set_loaded_world :: (world: World) { unload_current_world(); current_world.world = world; 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 :: () { @@ -167,7 +186,7 @@ get_current_world :: () -> *Current_World { // --- Binary serialization (.world format) --- 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. // We serialize it field-by-field to avoid padding issues. @@ -345,6 +364,30 @@ save_world :: (world: *World) -> string { 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); } @@ -365,7 +408,7 @@ load_world_from_data :: (data: []u8) -> (World, bool) { } version := read_value(data, *cursor, u16); - if version != WORLD_VERSION { + if version != 1 && version != 2 && version != 3 { log_error("Unsupported world version: %", version); return world, false; } @@ -419,6 +462,33 @@ load_world_from_data :: (data: []u8) -> (World, bool) { 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; }