Compare commits

...

9 Commits

Author SHA1 Message Date
b45554afaa work 2026-10-25 00:55:00 +03:00
0b8c135d11 fixes 2026-10-17 15:08:33 +03:00
ff6c1718d0 audio improvements 2026-10-17 15:08:06 +03:00
4bf23b1196 add a per frame tick function to the game 2026-10-17 09:34:39 +03:00
a6f79959c0 improve mouse cursor locking 2026-10-17 09:03:50 +03:00
73cc16f1d4 add mouse cursor locking 2026-10-17 08:58:45 +03:00
9911229282 fix bug with entity autoedit ui 2026-10-17 00:16:37 +03:00
bb2de7d472 fix 2026-10-16 23:45:49 +03:00
92489cc363 add mouse delta to input 2026-10-16 23:28:26 +03:00
19 changed files with 907 additions and 545 deletions

View File

@ -228,7 +228,7 @@ wasm_build :: (opts: Build_Options, trueno_opts: Trueno_Build_Options) {
"emcc",
"src/platform_specific/main.c", "dist/main.o", "modules/sokol-jai/sokol/gfx/sokol_gfx_wasm_gl_release.a", "modules/sokol-jai/sokol/log/sokol_log_wasm_gl_release.a", "modules/sokol-jai/sokol/audio/sokol_audio_wasm_gl_release.a", "modules/sokol-jai/sokol/time/sokol_time_wasm_gl_release.a", "modules/sokol-jai/sokol/app/sokol_app_wasm_gl_release.a", "modules/sokol-jai/sokol/glue/sokol_glue_wasm_gl_release.a", "modules/sokol-jai/sokol/fetch/sokol_fetch_wasm_gl_release.a", "modules/Fontstash/wasm/fontstash.a",
"modules/sokol-jai/sokol/stbi/stb_image.a",
"modules/sokol-jai/sokol/stbv/stb_vorbis.a",
"modules/stb_vorbis/wasm/stb_vorbis.a",
"-o", "dist/index.html",
"-sSTACK_SIZE=10MB",
"-sALLOW_MEMORY_GROWTH",

View File

@ -4,6 +4,33 @@
#load "sokol.jai";
#scope_file
shouldLockMouseCursor : bool = false;
cursorCurrentlyLocked : bool = false;
#scope_export;
set_should_lock_cursor :: (shouldLock: bool) {
shouldLockMouseCursor = shouldLock;
}
is_cursor_locked :: () -> bool {
return shouldLockMouseCursor;
}
unlock_cursor :: () {
cursorCurrentlyLocked = false;
plat_set_cursor_lock(false);
}
// We use this so menu and editor is unlocked and game is locked if the game sets the should lock.
lock_cursor_if_should :: () {
if shouldLockMouseCursor {
plat_set_cursor_lock(true);
cursorCurrentlyLocked = true;
}
}
Event_Type :: enum u32 { // If we set this to u8, our packing will stop matching C's.
UNINITIALIZED :: 0;
KEYBOARD :: 1;
@ -226,6 +253,9 @@ input_per_frame_event_and_flag_update :: () {
// Called once per frame, probably.
for events_this_frame {
if it.key_code == .MOUSE_BUTTON_LEFT && it.key_pressed == 1 && cursorCurrentlyLocked {
plat_set_cursor_lock(true);
}
allocator := it.files.allocator; // Same allocator for filenames and the array itself.
for file: it.files {

View File

@ -7,6 +7,10 @@ input_mouse_y : float;
set_custom_cursor_handling :: (is_custom: bool) {
}
plat_set_cursor_lock :: (locked: bool) {
sapp_lock_mouse(locked);
}
get_key_code :: (key: sapp_keycode) -> Key_Code {
using Key_Code;
// non-ascii ordered as in ./module.jai/Key_Code
@ -162,6 +166,10 @@ handle_sokol_event :: (event: *sapp_event) {
case .MOUSE_MOVE;
input_mouse_x = event.mouse_x;
input_mouse_y = event.mouse_y;
mouse_delta_x = cast(s32)event.mouse_dx;
mouse_delta_y = cast(s32)event.mouse_dy;
return;
case .MOUSE_SCROLL;
mouse_delta_z = cast(int) event.scroll_y;

View File

@ -266,7 +266,7 @@ STBVorbisError :: enum u32 {
lib_stb_vorbis :: #library "switch2/stb_vorbis";
}
} else #if OS == .WASM {
lib_stb_vorbis :: #library "wasm/stb_vorbis";
lib_stb_vorbis :: #library,no_dll "wasm/stb_vorbis";
} else {
#assert false;
}

View File

@ -149,6 +149,8 @@ FOOTER_TEMPLATE :: #string END
} else #if CPU == .ARM64 {
%2 :: #library "switch2/%1";
}
} else #if OS == .WASM {
%2 :: #library,no_dll "wasm/%1";
} else {
#assert false;
}

Binary file not shown.

View File

@ -1,4 +1,4 @@
master_volume 0.205032
master_volume 0.05216
music_volume 1
sfx_volume 1
fullscreen 1

View File

@ -15,6 +15,8 @@ Play_Mode :: enum {
Mixer_Play_Task :: struct {
audio : *Audio_Data;
audio_proc : (n_samples: s32, audio: *Audio_Data, buffer_out: *float,channel_count: s32,volume: float, state: *void) -> (n_out: s32);
audio_state : *void;
bus : Mixer_Bus;
mode : Play_Mode;
unique : bool = true;
@ -65,11 +67,12 @@ mixer_add_task :: (task: Mixer_Play_Task) {
array_add(*g_mixer.tasks, task);
}
mixer_add_task :: (audio: *Audio_Data, bus: Mixer_Bus, mode: Play_Mode) {
mixer_add_task :: (audio: *Audio_Data, bus: Mixer_Bus, mode: Play_Mode, state: *void) {
mixer_add_task(.{
audio = audio,
bus = bus,
mode = mode
mode = mode,
audio_state = state
});
}
@ -89,6 +92,17 @@ mixer_queue_next :: (current_audio: *Audio_Data, next_task: Mixer_Play_Task) {
array_add(*g_mixer.tasks, next_task);
}
mixer_stop :: (audio: *Audio_Data) {
mixer_lock(*g_mixer);
defer mixer_unlock(*g_mixer);
for g_mixer.tasks {
if it.audio != audio continue;
if it.queued_next then free(it.queued_next);
free(it.audio_state);
remove it;
}
}
mixer_get_samples :: (buffer: *float, frame_count: s32, channel_count: s32) {
mixer_lock(*g_mixer);
defer mixer_unlock(*g_mixer);
@ -103,6 +117,7 @@ mixer_get_samples :: (buffer: *float, frame_count: s32, channel_count: s32) {
task := *g_mixer.tasks[i];
if !task.audio {
free(task.audio_state);
array_unordered_remove_by_index(*g_mixer.tasks, i);
continue;
}
@ -124,43 +139,52 @@ mixer_get_samples :: (buffer: *float, frame_count: s32, channel_count: s32) {
task_finished := false;
for f : 0..frame_count-1 {
src_pos := cast(float64)(task.curSample + f) * cast(float64)src_rate / cast(float64)out_rate;
src_frame0 := cast(s64) src_pos;
src_frame1 := src_frame0 + 1;
t := cast(float)(src_pos - cast(float64)src_frame0);
assert(src_rate == out_rate); // TODO for now
if src_frame0 >= num_src_frames {
if task.mode == .ONESHOT {
task_finished = true;
break;
} else {
src_frame0 = src_frame0 % num_src_frames;
src_frame1 = src_frame1 % num_src_frames;
}
} else if src_frame1 >= num_src_frames {
// At the very last source frame: clamp for ONESHOT, wrap for REPEAT.
src_frame1 = ifx task.mode == .ONESHOT then src_frame0 else 0;
}
for ch : 0..channel_count-1 {
out_index := (f * channel_count) + ch;
src_ch := ifx source_channels == 1 then 0 else ch;
s0 := source_data[src_frame0 * source_channels + src_ch];
s1 := source_data[src_frame1 * source_channels + src_ch];
buffer[out_index] += (s0 + (s1 - s0) * t) * vol;
}
n_received_samples := task.audio_proc(frame_count,task.audio,buffer,channel_count,vol,task.audio_state);
if(n_received_samples < frame_count){
task_finished = true;
}
task.curSample += frame_count;
// Keep curSample bounded for REPEAT tracks so it never overflows.
if task.mode == .REPEAT && out_rate > 0 && src_rate > 0 {
loop_period := num_src_frames * out_rate / src_rate;
if loop_period > 0 then task.curSample = task.curSample % loop_period;
}
// for f : 0..frame_count-1 {
// src_pos := cast(float64)(task.curSample + f) * cast(float64)src_rate / cast(float64)out_rate;
// src_frame0 := cast(s64) src_pos;
// src_frame1 := src_frame0 + 1;
// t := cast(float)(src_pos - cast(float64)src_frame0);
// if src_frame0 >= num_src_frames {
// if task.mode == .ONESHOT {
// task_finished = true;
// break;
// } else {
// src_frame0 = src_frame0 % num_src_frames;
// src_frame1 = src_frame1 % num_src_frames;
// }
// } else if src_frame1 >= num_src_frames {
// // At the very last source frame: clamp for ONESHOT, wrap for REPEAT.
// src_frame1 = ifx task.mode == .ONESHOT then src_frame0 else 0;
// }
// for ch : 0..channel_count-1 {
// out_index := (f * channel_count) + ch;
// src_ch := ifx source_channels == 1 then 0 else ch;
// s0 := source_data[src_frame0 * source_channels + src_ch];
// s1 := source_data[src_frame1 * source_channels + src_ch];
// buffer[out_index] += (s0 + (s1 - s0) * t) * vol;
// }
// }
// task.curSample += frame_count;
// // Keep curSample bounded for REPEAT tracks so it never overflows.
// if task.mode == .REPEAT && out_rate > 0 && src_rate > 0 {
// loop_period := num_src_frames * out_rate / src_rate;
// if loop_period > 0 then task.curSample = task.curSample % loop_period;
// }
if task_finished {
if task.queued_next {
@ -168,6 +192,7 @@ mixer_get_samples :: (buffer: *float, frame_count: s32, channel_count: s32) {
free(task.queued_next);
g_mixer.tasks[i] = next;
} else {
free(task.audio_state);
array_unordered_remove_by_index(*g_mixer.tasks, i);
}
}
@ -190,3 +215,43 @@ mixer_unlock :: (mixer: *Mixer) {
}
}
Simple_State :: struct {
cur_sample : s64 = 0;
}
trivial_data_proc_norepeat::(n_samples: s32, audio: *Audio_Data, buffer_out: *float,channel_count: s32,volume: float, state: *void) -> (n_out: s32){
return trivial_data_proc(n_samples,audio,buffer_out,channel_count,volume,state,false);
}
trivial_data_proc_repeat::(n_samples: s32, audio: *Audio_Data, buffer_out: *float,channel_count: s32,volume: float, state: *void) -> (n_out: s32){
return trivial_data_proc(n_samples,audio,buffer_out,channel_count,volume,state,true);
}
trivial_data_proc::(n_samples: s32, audio: *Audio_Data, buffer_out: *float,channel_count: s32,volume: float, state: *void, repeat:bool) -> (n_out: s32){
state_cast : *Simple_State = cast(*Simple_State)state;
source_channels := cast(s64) audio.channels;
source_samples := cast(s64) audio.data.count / source_channels;
samples_read := 0;
for block_sample : 0..n_samples-1{
sample : s64 = block_sample + state_cast.cur_sample;
if(!repeat && sample >= source_samples) {
break;
}
sample %= source_samples;
for ch : 0..channel_count-1{
out_index := (block_sample * channel_count) + ch;
src_ch := ifx source_channels == 1 then 0 else ch;
buffer_out[out_index] += audio.data[sample*source_channels + ch] * volume;
}
samples_read += 1;
}
state_cast.cur_sample += samples_read;
state_cast.cur_sample %= source_samples;
return cast(s32)samples_read;
}

View File

@ -109,6 +109,11 @@ tick_editor_ui :: () {
in_editor_view = !in_editor_view;
g_mixer.paused = in_editor_view;
clear_particles();
if in_editor_view {
unlock_cursor();
} else {
lock_cursor_if_should();
}
}
#if !FLAG_RELEASE_BUILD && OS != .WASM {

View File

@ -1091,7 +1091,7 @@ add_entity_at :: (x: s32, y: s32, z: s32) {
// Runs autoedit on the entity's concrete type, so each type gets an editor
// generated from its own fields.
entity_autoedit :: (rect: GR.Rect, e: *Entity, theme: *GR.Overall_Theme) {
#insert #run,stallable gen_entity_dispatch("autoedit(rect, cast(*%)e, theme, 400)");
#insert #run,stallable gen_entity_dispatch("autoedit(rect, cast(*%)e, theme, cast(s32)e.id)");
}
draw_editor_billboards :: (view: View_Id) {

View File

@ -419,10 +419,7 @@ get_tiling_neighbor_trile :: () -> *Trile {
}
draw_trile_editor :: () {
// A trile is drawn in the light of the world it will sit in.
environment := New(Environment,, temp);
curworld := get_current_world();
if curworld.valid environment = world_environment(*curworld.world);
view := add_view(.{camera = get_trile_editor_camera(), environment = environment, draw_world = false});
mesh, ok := get_trile_mesh(editor_current_trile.name);

View File

@ -108,8 +108,10 @@ init :: () {
#if OS != .WASM { Thread.init(*g_mixer.mutex); }
audio_init_thread_context();
saudio_setup(*(saudio_desc.{
logger = .{ func = slog_func },
stream_cb = sokol_audio_callback,
num_channels = 2,
}));
g_audio_sample_rate = saudio_sample_rate();
stm_setup();
@ -167,7 +169,7 @@ init_after_core :: () {
// We want to do this last.
game_init();
// #if UNCAPPED_FRAMES then sapp_glx_swapinterval(0);
lock_cursor_if_should();
}
@ -229,20 +231,28 @@ frame :: () {
if delta_time_accumulator > (4.0/60.0) then delta_time_accumulator = 4.0/60.0;
}
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);
}
}
for event: Input.events_this_frame {
GR.getrect_handle_event(event);
}
tick_ui();
if should_tick_game && !settings_menu_blocks_game () {
firstTick := true;
while delta_time_accumulator > (1.0/480.0) {
game_tick(1.0/480.0);
if firstTick {
input_per_frame_event_and_flag_update();
firstTick = false;
}
tick_particles(1.0/480.0);
delta_time_accumulator -= (1.0/480.0);
}
} else {
input_per_frame_event_and_flag_update();
}
add_frame_profiling_point("After game ticks");
tick_ui();
add_frame_profiling_point("After UI tick");
if should_tick_game then game_draw();
@ -270,7 +280,6 @@ frame :: () {
#if OS != .WASM { profiler_update(); }
reset_temporary_storage();
frame_end_time := get_time();
input_per_frame_event_and_flag_update();
}
cleanup :: () {

View File

@ -90,6 +90,7 @@ Pass :: struct {
kind : Pass_Kind;
params : View_Params;
shadow : Shadow_Params;
frustum : [6]Vector4;
}
make_pass :: (scene: *Scene, view_index: int, kind: Pass_Kind) -> Pass {
@ -110,6 +111,7 @@ make_pass :: (scene: *Scene, view_index: int, kind: Pass_Kind) -> Pass {
camera = pass.view.camera.target + env.sun_direction * 1000;
view_proj = scene.shadow_view_proj[view_index];
}
pass.frustum = frustum_planes(view_proj);
_, inv_view_proj := inverse(view_proj);
pass.params.view_proj = view_proj.floats;
pass.params.inv_view_proj = inv_view_proj.floats;
@ -131,6 +133,22 @@ make_pass :: (scene: *Scene, view_index: int, kind: Pass_Kind) -> Pass {
return pass;
}
frustum_planes :: (m: Matrix4) -> [6]Vector4 {
column :: (m: Matrix4, j: int) -> Vector4 { return .{m.coef[0][j], m.coef[1][j], m.coef[2][j], m.coef[3][j]}; }
x, y, z, w := column(m, 0), column(m, 1), column(m, 2), column(m, 3);
return .[w + x, w - x, w + y, w - y, w + z, w - z];
}
box_in_frustum :: (planes: [6]Vector4, lo: Vector3, hi: Vector3) -> bool {
for planes {
x := ifx it.x > 0 then hi.x else lo.x;
y := ifx it.y > 0 then hi.y else lo.y;
z := ifx it.z > 0 then hi.z else lo.z;
if it.x * x + it.y * y + it.z * z + it.w < 0 return false;
}
return true;
}
apply_uniforms :: (slot: s32, params: *$T) {
sg_apply_uniforms(slot, *(sg_range.{ params, size_of(T) }));
}

File diff suppressed because it is too large Load Diff

View File

@ -92,8 +92,8 @@ layout(binding=4) uniform texture3D probe_atlas; // per brick 32^3 probes, 3 t
layout(binding=2) uniform sampler probe_smp;
const float GLOSS_GLOW = 4.0; // how much a full gloss highlight blooms
const float SUN_GLOW = 1.5;
const float REFLECTED_GLOW_CURVE = 0.4; // below 1, reflected glow fades slower than reflected colour
const float GLASS_PROJECTION = 2.0; // how strongly coloured sunlight paints its colour on
const int PROBE_BRICKS_PER_COLUMN = 16;
@ -160,20 +160,17 @@ void main() {
if (v_tint > 1.5) albedo = mix(albedo, vec3(1.0, 0.2, 0.2), 0.5);
else if (v_tint > 0.5) albedo = mix(albedo, vec3(0.2, 1.0, 0.2), 0.5);
vec3 sun = v_facing * sun_shadow(v_world, v_normal);
vec3 lit = sun * mix(0.25, 1.0, v_ao);
vec4 probe = probe_min.w > 0.5 ? probe_light(v_world, v_normal) : vec4(0.0);
vec3 shade = v_shade + probe.rgb;
vec3 light = mix(shade * mix(0.6, 1.0, v_ao), v_sun, lit);
vec3 color = albedo * light;
vec3 shade = (v_shade + probe.rgb) * mix(0.6, 1.0, v_ao);
vec3 incoming = v_sun * sun * mix(0.25, 1.0, v_ao);
vec3 color = albedo * (shade + incoming);
// Light through glass paints the glass colour onto what it hits, so the pattern
// shows on dark surfaces too, not only where the albedo can reflect it.
float bright = brightest(sun);
if (bright > 0.001) {
float tinted = (bright - min(sun.r, min(sun.g, sun.b))) / bright;
color = mix(color, sun * v_sun, min(1.0, tinted * GLASS_PROJECTION));
color += incoming * (1.0 - albedo) * tinted;
}
float glow = swatch.a * EMISSIVE_GLOW;
float glow = swatch.a * EMISSIVE_GLOW + brightest(incoming) * SUN_GLOW;
// Glossier is both stronger and sharper.
if (material.b > 0.0) {

View File

@ -73,23 +73,36 @@ triles_upload :: (scene: *Scene) {
triles.palette_image = sg_make_image(*desc);
}
if triles.world_version != scene.world_version world_rebuild(scene);
array_reset_keeping_memory(*triles.instances);
array_reset_keeping_memory(*triles.batches);
chunk_count := triles.world_chunks.count;
array_resize(*triles.cut_chunks, scene.views.count * chunk_count, initialize = false);
memset(triles.cut_chunks.data, 0, triles.cut_chunks.count);
draws : [..]Keyed(Instance);
draws.allocator = temp;
for view, view_index: scene.views {
array_reset_keeping_memory(*draws);
if view.draw_world {
for chunk: scene.chunks for group: chunk.groups for group.instances {
position := chunk.origin + Vector3.{xx it.x, xx it.y, xx it.z};
cut := trile_cut(.{xx view_index}, group.mesh, position);
array_add(*draws, .{group.mesh.id, .{position, xx it.orientation, 0, cut}});
cut_chunks := array_view(triles.cut_chunks, view_index * chunk_count, chunk_count);
for view_cut_chunks(.{xx view_index}) {
index := table_find_pointer(*triles.chunk_index, it);
if index cut_chunks[index.*] = true;
}
for batch: triles.world_batches {
if !batch.cuttable || !cut_chunks[batch.chunk] continue;
for batch.first..batch.first + batch.count - 1 {
instance := triles.world_instances[it];
instance.cut = trile_cut(.{xx view_index}, .{batch.mesh}, instance.position);
array_add(*draws, .{MAIN_ONLY_KEY | cast(s64) batch.mesh, instance});
}
}
}
for scene.triles {
if it.view.index != view_index continue;
preview : s64 = ifx it.tint != .NONE then PREVIEW_KEY else 0;
preview : s64 = ifx it.tint != .NONE then MAIN_ONLY_KEY else 0;
cut := ifx preview then 0.0 else trile_cut(it.view, it.mesh, it.position);
array_add(*draws, .{preview | it.mesh.id, .{it.position, xx it.orientation, xx it.tint, cut}});
}
@ -104,14 +117,13 @@ triles_draw_glass_shadow :: (pass: *Pass) {
apply_uniforms(UB_view_params, *pass.params);
triles.params.show_hidden = 1;
apply_uniforms(UB_trile_params, *triles.params);
for triles.batches {
if it.view_index != pass.view_index || it.key & PREVIEW_KEY continue;
mesh := slot_get(*triles.meshes, xx it.key);
for pass_draws(pass) {
mesh := slot_get(*triles.meshes, it.mesh);
if !mesh || mesh.index_count == 0 continue;
bind : sg_bindings;
bind.vertex_buffers[0] = mesh.vertices;
bind.index_buffer = mesh.indices;
bind.vertex_buffers[1] = triles.stream.buffer;
bind.vertex_buffers[1] = it.buffer;
bind.vertex_buffer_offsets[1] = it.first * size_of(Instance);
bind.images[IMG_palette] = triles.palette_image;
bind.samplers[SMP_palette_smp] = samplers.nearest_clamp;
@ -137,15 +149,14 @@ triles_draw :: (pass: *Pass) {
apply_uniforms(UB_trile_fs_params, *fs_params);
}
for triles.batches {
if it.view_index != pass.view_index || (pass.kind != .MAIN && it.key & PREVIEW_KEY) continue;
mesh := slot_get(*triles.meshes, xx (it.key & ~PREVIEW_KEY));
for pass_draws(pass) {
mesh := slot_get(*triles.meshes, it.mesh);
if !mesh || mesh.index_count == 0 continue;
bind : sg_bindings;
bind.vertex_buffers[0] = mesh.vertices;
bind.index_buffer = mesh.indices;
bind.vertex_buffers[1] = triles.stream.buffer;
bind.vertex_buffers[1] = it.buffer;
bind.vertex_buffer_offsets[1] = it.first * size_of(Instance);
bind.images[IMG_palette] = triles.palette_image;
bind.samplers[SMP_palette_smp] = samplers.nearest_clamp;
@ -176,7 +187,27 @@ Instance :: struct {
cut : float; // 0..1 dissolved away from the camera, still casts shadows
}
PREVIEW_KEY :: 1 << 32;
MAIN_ONLY_KEY :: 1 << 32;
World_Chunk :: struct {
key : Chunk_Key;
origin : Vector3;
}
World_Batch :: struct {
mesh : u32;
chunk : s32;
cuttable : bool;
first : s32;
count : s32;
}
Trile_Draw :: struct {
mesh : u32;
buffer : sg_buffer;
first : s32;
count : s32;
}
triles : struct {
pipeline : sg_pipeline;
@ -192,6 +223,94 @@ triles : struct {
instances : [..]Instance;
batches : [..]Batch;
stream : Stream_Buffer;
world_version : u64;
world_chunks : [..]World_Chunk;
chunk_index : Table(Chunk_Key, s32, chunk_key_hash, chunk_key_compare);
world_batches : [..]World_Batch;
world_instances : [..]Instance;
world_buffer : sg_buffer;
cut_chunks : [..]bool;
}
world_rebuild :: (scene: *Scene) {
triles.world_version = scene.world_version;
array_reset_keeping_memory(*triles.world_chunks);
array_reset_keeping_memory(*triles.world_batches);
array_reset_keeping_memory(*triles.world_instances);
table_reset(*triles.chunk_index);
for scene.chunks array_add(*triles.world_chunks, .{it_index, it.origin});
quick_sort(triles.world_chunks, (a: World_Chunk, b: World_Chunk) -> s64 {
if a.key.x != b.key.x return a.key.x - b.key.x;
if a.key.z != b.key.z return a.key.z - b.key.z;
return a.key.y - b.key.y;
});
Piece :: struct {
key : s64;
chunk : s32;
group : *World_Group;
}
pieces : [..]Piece;
pieces.allocator = temp;
for chunk, index: triles.world_chunks {
table_set(*triles.chunk_index, chunk.key, xx index);
scene_chunk := table_find_pointer(*scene.chunks, chunk.key);
for * scene_chunk.groups array_add(*pieces, .{(cast(s64) it.mesh.id << 32) | index, xx index, it});
}
quick_sort(pieces, (a: Piece, b: Piece) -> s64 { return a.key - b.key; });
occluders := world_occluders();
for pieces {
chunk := triles.world_chunks[it.chunk];
batch := World_Batch.{mesh = it.group.mesh.id, chunk = it.chunk, cuttable = trile_mesh_cuttable(it.group.mesh), first = xx triles.world_instances.count};
for instance: it.group.instances {
if trile_buried(*occluders, chunk.key, instance.x, instance.y, instance.z) continue;
position := chunk.origin + Vector3.{xx instance.x, xx instance.y, xx instance.z};
array_add(*triles.world_instances, .{position, xx instance.orientation, 0, 0});
}
batch.count = xx (triles.world_instances.count - batch.first);
if batch.count array_add(*triles.world_batches, batch);
}
if triles.world_buffer.id sg_destroy_buffer(triles.world_buffer);
triles.world_buffer = .{};
if triles.world_instances.count {
size := triles.world_instances.count * size_of(Instance);
triles.world_buffer = sg_make_buffer(*(sg_buffer_desc.{ data = .{ triles.world_instances.data, xx size } }));
}
}
pass_draws :: (pass: *Pass) -> []Trile_Draw {
draws : [..]Trile_Draw;
draws.allocator = temp;
main := pass.kind == .MAIN;
if pass.view.draw_world && triles.world_batches.count {
chunk_count := triles.world_chunks.count;
visible := NewArray(chunk_count, bool,, temp);
for triles.world_chunks visible[it_index] = box_in_frustum(pass.frustum, it.origin, it.origin + Vector3.{CHUNK_SIZE, CHUNK_SIZE, CHUNK_SIZE});
cut_chunks := array_view(triles.cut_chunks, pass.view_index * chunk_count, chunk_count);
run : Trile_Draw;
for triles.world_batches {
if !visible[it.chunk] || (main && it.cuttable && cut_chunks[it.chunk]) continue;
if run.count && run.mesh == it.mesh && run.first + run.count == it.first {
run.count += it.count;
continue;
}
if run.count array_add(*draws, run);
run = .{it.mesh, triles.world_buffer, it.first, it.count};
}
if run.count array_add(*draws, run);
}
for triles.batches {
if it.view_index != pass.view_index || (!main && it.key & MAIN_ONLY_KEY) continue;
array_add(*draws, .{xx (it.key & ~MAIN_ONLY_KEY), triles.stream.buffer, it.first, it.count});
}
return draws;
}
make_mesh :: (mesh: *Mesh, vertices: []Trile_Vertex, indices: []u16) {

View File

@ -94,19 +94,19 @@ Ui_Vertex :: struct {
trile_mesh_create :: (trile: *Trile) -> Trile_Mesh {
vertices, indices := mesh_trile(trile);
mesh := Trile_Mesh.{renderer_trile_mesh_create(vertices, indices)};
set_mesh_cut(mesh, trile.cut);
set_mesh_info(mesh, trile.cut, trile_fills_cell(trile));
return mesh;
}
trile_mesh_update :: (mesh: Trile_Mesh, trile: *Trile) {
vertices, indices := mesh_trile(trile);
renderer_trile_mesh_update(mesh.id, vertices, indices);
set_mesh_cut(mesh, trile.cut);
set_mesh_info(mesh, trile.cut, trile_fills_cell(trile));
}
trile_mesh_destroy :: (mesh: Trile_Mesh) {
renderer_trile_mesh_destroy(mesh.id);
set_mesh_cut(mesh, .NONE);
set_mesh_info(mesh, .NONE, false);
}
swatch_set :: (id: u8, color: [3]u8, reflectivity: u8, gloss: u8, emittance: u8, glass: bool) {
@ -142,6 +142,7 @@ Probe :: struct {
PROBES_PER_CHUNK :: CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE;
world_set_chunk :: (key: Chunk_Key, origin: Vector3, groups: []World_Group) {
scene.world_version += 1;
chunk := find_or_add(*scene.chunks, key);
chunk.origin = origin;
for chunk.groups array_free(it.instances);
@ -156,12 +157,14 @@ world_set_chunk :: (key: Chunk_Key, origin: Vector3, groups: []World_Group) {
world_remove_chunk :: (key: Chunk_Key) {
chunk := table_find_pointer(*scene.chunks, key);
if !chunk return;
scene.world_version += 1;
for chunk.groups array_free(it.instances);
array_free(chunk.groups);
table_remove(*scene.chunks, key);
}
world_clear :: () {
scene.world_version += 1;
for chunk: scene.chunks {
for chunk.groups array_free(it.instances);
array_free(chunk.groups);
@ -221,6 +224,65 @@ trile_cut :: (view: View_Id, mesh: Trile_Mesh, position: Vector3) -> float {
return result;
}
view_cut_chunks :: (view: View_Id) -> []Chunk_Key {
keys : [..]Chunk_Key;
keys.allocator = temp;
cut := scene.view_cuts[view.index];
if !cut.enabled return keys;
for scene.chunks {
if chunk_column_distance(it.origin, cut.focus) <= CUT_RADIUS array_add(*keys, it_index);
}
unpack :: (bits: s64) -> s32 { return xx (((bits & 0xFFFFF) ^ 0x80000) - 0x80000); }
for scene.cut_fades {
if (it_index >> 60) != view.index continue;
array_add_if_unique(*keys, world_to_chunk_coord(unpack(it_index >> 40), unpack(it_index >> 20), unpack(it_index)), chunk_key_compare);
}
return keys;
}
trile_mesh_cuttable :: (mesh: Trile_Mesh) -> bool {
return mesh_cut(mesh) != .NONE;
}
OCCLUDER_WORDS :: CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE / 64;
World_Occluders :: Table(Chunk_Key, *[OCCLUDER_WORDS]u64, chunk_key_hash, chunk_key_compare);
world_occluders :: () -> World_Occluders {
occluders : World_Occluders;
occluders.allocator = temp;
for chunk: scene.chunks {
bits := New([OCCLUDER_WORDS]u64,, temp);
for group: chunk.groups {
if !mesh_occludes(group.mesh) continue;
for group.instances {
cell := cast(s64) it.x + cast(s64) it.y * CHUNK_SIZE + cast(s64) it.z * CHUNK_SIZE * CHUNK_SIZE;
bits.*[cell >> 6] |= (cast(u64) 1) << cast(u64) (cell & 63);
}
}
table_set(*occluders, it_index, bits);
}
return occluders;
}
trile_buried :: (occluders: *World_Occluders, key: Chunk_Key, x: s32, y: s32, z: s32) -> bool {
occluded :: (occluders: *World_Occluders, key: Chunk_Key, x: s32, y: s32, z: s32) -> bool {
c := s32.[x, y, z];
k := s32.[key.x, key.y, key.z];
for 0..2 {
if c[it] < 0 { c[it] += CHUNK_SIZE; k[it] -= 1; }
if c[it] >= CHUNK_SIZE { c[it] -= CHUNK_SIZE; k[it] += 1; }
}
bits := table_find_pointer(occluders, .{k[0], k[1], k[2]});
if !bits return false;
cell := cast(s64) c[0] + c[1] * CHUNK_SIZE + c[2] * CHUNK_SIZE * CHUNK_SIZE;
return ((bits.*.*[cell >> 6] >> cast(u64) (cell & 63)) & 1) != 0;
}
return occluded(occluders, key, x - 1, y, z) && occluded(occluders, key, x + 1, y, z)
&& occluded(occluders, key, x, y - 1, z) && occluded(occluders, key, x, y + 1, z)
&& occluded(occluders, key, x, y, z - 1) && occluded(occluders, key, x, y, z + 1);
}
draw_trile :: (view: View_Id, mesh: Trile_Mesh, position: Vector3, orientation: u8, tint := Trile_Tint.NONE) {
array_add(*scene.triles, .{view, mesh, position, orientation, tint});
}
@ -309,7 +371,9 @@ Trile_Vertex :: struct {
Scene :: struct {
chunks : Table(Chunk_Key, Scene_Chunk, chunk_key_hash, chunk_key_compare);
mesh_cuts : [..]Trile_Cut; // by mesh id
world_version : u64;
mesh_cuts : [..]Trile_Cut; // by mesh id
mesh_occluders : [..]bool;
views : [..]View;
view_proj : [..]Matrix4;
@ -362,9 +426,34 @@ scene_end_frame :: (width: s32, height: s32, time: float) {
scene : Scene;
set_mesh_cut :: (mesh: Trile_Mesh, cut: Trile_Cut) {
if scene.mesh_cuts.count <= mesh.id array_resize(*scene.mesh_cuts, mesh.id + 1);
set_mesh_info :: (mesh: Trile_Mesh, cut: Trile_Cut, fills_cell: bool) {
if scene.mesh_cuts.count <= mesh.id {
array_resize(*scene.mesh_cuts, mesh.id + 1);
array_resize(*scene.mesh_occluders, mesh.id + 1);
}
scene.mesh_cuts[mesh.id] = cut;
scene.mesh_occluders[mesh.id] = fills_cell && cut == .NONE;
scene.world_version += 1;
}
mesh_occludes :: (mesh: Trile_Mesh) -> bool {
if mesh.id >= scene.mesh_occluders.count return false;
return scene.mesh_occluders[mesh.id];
}
trile_fills_cell :: (trile: *Trile) -> bool {
if !trile.is_opaque return false;
for x: 0..15 for y: 0..15 for z: 0..15 {
id := trile.trixels[x][y][z];
if !id || g_swatches[id].glass return false;
}
return true;
}
chunk_column_distance :: (origin: Vector3, point: Vector3) -> float {
dx := max(max(origin.x - point.x, point.x - origin.x - CHUNK_SIZE), 0);
dz := max(max(origin.z - point.z, point.z - origin.z - CHUNK_SIZE), 0);
return sqrt(dx * dx + dz * dz);
}
mesh_cut :: (mesh: Trile_Mesh) -> Trile_Cut {
@ -382,13 +471,16 @@ make_view_cut :: (view: View) -> View_Cut {
to_camera := Vector2.{view.camera.position.x - view.focus.x, view.camera.position.z - view.focus.z};
if length(to_camera) > 0.0001 cut.toward_camera = normalize(to_camera);
for chunk: scene.chunks for group: chunk.groups {
if mesh_cut(group.mesh) != .ROOF continue;
for group.instances {
p := chunk.origin + Vector3.{xx it.x, xx it.y, xx it.z};
if p.y <= cut.floor continue;
if length(Vector2.{p.x + 0.5 - view.focus.x, p.z + 0.5 - view.focus.z}) > CUT_RADIUS continue;
cut.height = min(cut.height, p.y);
for chunk: scene.chunks {
if chunk.origin.y + CHUNK_SIZE <= cut.floor || chunk_column_distance(chunk.origin, view.focus) > CUT_RADIUS continue;
for group: chunk.groups {
if mesh_cut(group.mesh) != .ROOF continue;
for group.instances {
p := chunk.origin + Vector3.{xx it.x, xx it.y, xx it.z};
if p.y <= cut.floor continue;
if length(Vector2.{p.x + 0.5 - view.focus.x, p.z + 0.5 - view.focus.z}) > CUT_RADIUS continue;
cut.height = min(cut.height, p.y);
}
}
}
return cut;

View File

@ -220,10 +220,15 @@ navigate_to :: (page: Settings_Page) {
g_settings.cursor = 0;
}
close_settings_menu :: () {
save_settings();
lock_cursor_if_should();
g_settings.open = false;
}
go_back :: () {
if g_settings.page == .MAIN {
save_settings();
g_settings.open = false;
close_settings_menu();
} else {
navigate_to(page_parent());
}
@ -234,10 +239,9 @@ handle_enter :: (index: s32 = -1) {
if g_settings.page == {
case .MAIN;
game_items := cast(s32) g_settings_config.main_items.count;
if i == 0 { save_settings(); g_settings.open = false; }
if i == 0 then close_settings_menu();
else if i <= game_items {
save_settings();
g_settings.open = false;
close_settings_menu();
g_settings_config.main_items[i - 1].on_select();
} else {
i -= game_items;
@ -306,6 +310,7 @@ tick_settings_menu :: () {
if is_action_start(Editor_Action.TOGGLE_SETTINGS) {
if !g_settings.open {
g_settings.open = true;
unlock_cursor();
navigate_to(.MAIN);
generate_blobs();
} else {

View File

@ -73,17 +73,8 @@ autoedit_widget_id :: (identifier: s32, field_index: int) -> s32 {
return identifier * 1000 + cast(s32) field_index;
}
// A text input hands back a view into the widget's own buffer, so the value has
// to be copied out on every keystroke. The previous copy is freed only when this
// same helper allocated it and the field still points at it: a field's starting
// value is usually a literal from the struct definition, which must not be freed.
autoedit_owned_text : Table(*string, string);
autoedit_set_text :: (field: *string, text: string) -> string {
found, previous := table_find(*autoedit_owned_text, field);
if found && previous.data == field.data free(previous);
result := copy_string(text);
table_set(*autoedit_owned_text, field, result);
return result;
}
@ -110,7 +101,7 @@ input_code_from_type_and_notes :: (name: string, type: *Type_Info, notes: []stri
print_to_builder(*builder, "r.h = ui_h(7,0);\n");
print_to_builder(*builder, "{\n");
print_to_builder(*builder, "action, _, text_state := GR.text_input(r, value.%, *theme.text_input_theme, autoedit_widget_id(identifier, %));\n", name, index);
print_to_builder(*builder, "if (action & .TEXT_MODIFIED) || (action & .ENTERED) value.% = autoedit_set_text(*value.%, text_state.text);\n", name, name);
print_to_builder(*builder, "if (action & .TEXT_MODIFIED) || (action & .ENTERED) value.% = autoedit_set_text(*value.%, text_state.text); \n", name, name);
print_to_builder(*builder, "}\n");
} else if type == type_info(Vector3) {
if autoconf.kind == .DEFAULT {