keep moving rendering to new format

This commit is contained in:
Tuomas Katajisto 2026-10-03 22:59:28 +03:00
parent b415b81584
commit 0bd7102b35
48 changed files with 12601 additions and 2379 deletions

View File

@ -80,16 +80,17 @@ add_asset_buffer_sizes_to_compiler_strings :: (trueno_opts: Trueno_Build_Options
pack_dir := ifx use_test_game then "./test_packs" else "./packs"; pack_dir := ifx use_test_game then "./test_packs" else "./packs";
game_resources_dir := ifx use_test_game then "./test_game/resources" else "./game/resources"; game_resources_dir := ifx use_test_game then "./test_game/resources" else "./game/resources";
sizes: [3]s64; sizes: [4]s64;
sizes[0] = max_asset_file_size(pack_dir, ".pack", 1 * 1024 * 1024); sizes[0] = max_asset_file_size(pack_dir, ".pack", 1 * 1024 * 1024);
sizes[1] = max( sizes[1] = max(
max_asset_file_size(game_resources_dir, "world.json", 64 * 1024), max_asset_file_size(game_resources_dir, "world.json", 64 * 1024),
max_asset_file_size(game_resources_dir, "index.world", 64 * 1024) max_asset_file_size(game_resources_dir, "index.world", 64 * 1024)
); );
sizes[2] = max_asset_file_size(game_resources_dir, "chunks.bin", 1 * 1024 * 1024); sizes[2] = max_asset_file_size(game_resources_dir, "chunks.bin", 1 * 1024 * 1024);
sizes[3] = max_asset_file_size(game_resources_dir, "probes.bin", 4 * 1024 * 1024);
names :: string.["MAX_PACK_SIZE", "MAX_WORLD_SIZE", "MAX_CHUNKS_SIZE"]; names :: string.["MAX_PACK_SIZE", "MAX_WORLD_SIZE", "MAX_CHUNKS_SIZE", "MAX_PROBES_SIZE"];
for i: 0..2 { for i: 0..3 {
add_build_string(tprint("% :: %;\n", names[i], sizes[i]), w); add_build_string(tprint("% :: %;\n", names[i], sizes[i]), w);
} }
} }
@ -145,7 +146,8 @@ native_build :: (opts: Build_Options, trueno_opts: Trueno_Build_Options) {
opts.cpu_target = root_opts.cpu_target; opts.cpu_target = root_opts.cpu_target;
opts.os_target = root_opts.os_target; opts.os_target = root_opts.os_target;
opts.backend = ifx (trueno_opts.release_build || OS == .MACOS) then .LLVM else .X64; // opts.backend = ifx (trueno_opts.release_build || OS == .MACOS) then .LLVM else .X64;
opts.backend = .LLVM; // rad debugger doesn't currently work with the x64 backend.
opts.output_executable_name = root_opts.output_executable_name; opts.output_executable_name = root_opts.output_executable_name;
set_build_options(opts, w); set_build_options(opts, w);

BIN
first.rdi Executable file

Binary file not shown.

Binary file not shown.

View File

@ -24,7 +24,7 @@ VK_KHR_deferred_host_operations
VK_KHR_acceleration_structure VK_KHR_acceleration_structure
VK_KHR_ray_query VK_KHR_ray_query
BLAS Compaction: 0.7MB -> 0.2MB (0.5MB saved, 65.9% smaller) BLAS Compaction: 0.4MB -> 0.2MB (0.3MB saved, 65.7% smaller)
_______________ _______________
Vulkan Version: Vulkan Version:
- available: 1.4.309 - available: 1.4.309
@ -51,7 +51,7 @@ VK_KHR_deferred_host_operations
VK_KHR_acceleration_structure VK_KHR_acceleration_structure
VK_KHR_ray_query VK_KHR_ray_query
BLAS Compaction: 0.8MB -> 0.3MB (0.5MB saved, 66.0% smaller) BLAS Compaction: 0.4MB -> 0.2MB (0.3MB saved, 65.7% smaller)
_______________ _______________
Vulkan Version: Vulkan Version:
- available: 1.4.309 - available: 1.4.309
@ -78,85 +78,4 @@ VK_KHR_deferred_host_operations
VK_KHR_acceleration_structure VK_KHR_acceleration_structure
VK_KHR_ray_query VK_KHR_ray_query
BLAS Compaction: 0.8MB -> 0.3MB (0.5MB saved, 66.0% smaller) BLAS Compaction: 0.4MB -> 0.2MB (0.3MB saved, 65.7% smaller)
_______________
Vulkan Version:
- available: 1.4.309
- requesting: 1.3.0
______________________
Used Instance Layers :
VK_LAYER_KHRONOS_validation
Used Instance Extensions :
____________________
Devices : 1
0: AMD Radeon RX 6950 XT
- Compatible
Compatible physical devices found : 1
Using Device:
- Device Name : AMD Radeon RX 6950 XT
- Vendor : AMD
- Driver Version : 2.0.341
- API Version : 1.4.308
- Device Type : Discrete GPU
________________________
Used Device Extensions :
VK_KHR_deferred_host_operations
VK_KHR_acceleration_structure
VK_KHR_ray_query
BLAS Compaction: 0.8MB -> 0.3MB (0.5MB saved, 66.0% smaller)
_______________
Vulkan Version:
- available: 1.4.309
- requesting: 1.3.0
______________________
Used Instance Layers :
VK_LAYER_KHRONOS_validation
Used Instance Extensions :
____________________
Devices : 1
0: AMD Radeon RX 6950 XT
- Compatible
Compatible physical devices found : 1
Using Device:
- Device Name : AMD Radeon RX 6950 XT
- Vendor : AMD
- Driver Version : 2.0.341
- API Version : 1.4.308
- Device Type : Discrete GPU
________________________
Used Device Extensions :
VK_KHR_deferred_host_operations
VK_KHR_acceleration_structure
VK_KHR_ray_query
BLAS Compaction: 0.8MB -> 0.3MB (0.5MB saved, 65.9% smaller)
_______________
Vulkan Version:
- available: 1.4.309
- requesting: 1.3.0
______________________
Used Instance Layers :
VK_LAYER_KHRONOS_validation
Used Instance Extensions :
____________________
Devices : 1
0: AMD Radeon RX 6950 XT
- Compatible
Compatible physical devices found : 1
Using Device:
- Device Name : AMD Radeon RX 6950 XT
- Vendor : AMD
- Driver Version : 2.0.341
- API Version : 1.4.308
- Device Type : Discrete GPU
________________________
Used Device Extensions :
VK_KHR_deferred_host_operations
VK_KHR_acceleration_structure
VK_KHR_ray_query
BLAS Compaction: 0.8MB -> 0.3MB (0.5MB saved, 65.9% smaller)

Binary file not shown.

Binary file not shown.

View File

@ -15,14 +15,19 @@ Sky_Config :: struct {
skyDesaturation : float = 0.0; skyDesaturation : float = 0.0;
} }
Trixel_Data :: struct { // emission is the light it gives off, already scaled. Glass is seen as a solid
empty : bool = false; // surface but lets the sun through, tinted by its colour.
color : Vector3 = .{1.0, 0.0, 0.0}; Swatch :: struct {
material : float = 1.0; color : Vector3;
emission : Vector3;
glass : s32;
} }
SWATCH_COUNT :: 256;
PROBE_FLOATS :: 13; // R, G, B × (c0, y, z, x), then the fraction of rays that hit a back face
Trile_Data :: struct { Trile_Data :: struct {
trixels : [16][16][16]Trixel_Data; trixels : [16][16][16]u8; // swatch ids, 0 = empty
vertices : *float; vertices : *float;
vertexCount : s32; vertexCount : s32;
} }
@ -47,7 +52,7 @@ Tacoma_Context :: struct {}
tacoma_init :: (base_path: *u8) -> *Tacoma_Context #foreign libtacoma; tacoma_init :: (base_path: *u8) -> *Tacoma_Context #foreign libtacoma;
tacoma_destroy :: (ctx: *Tacoma_Context) #foreign libtacoma; tacoma_destroy :: (ctx: *Tacoma_Context) #foreign libtacoma;
tacoma_load_scene :: (ctx: *Tacoma_Context, sky: Sky_Config, ts: Trile_Set, world: World, include_water: s32) #foreign libtacoma; tacoma_load_scene :: (ctx: *Tacoma_Context, sky: Sky_Config, swatches: *Swatch, ts: Trile_Set, world: World, include_water: s32) #foreign libtacoma;
tacoma_render_rdm :: (ctx: *Tacoma_Context, world_trile_index: s32, roughness: s32, quality: s32, size: s32) -> *float #foreign libtacoma; tacoma_render_rdm :: (ctx: *Tacoma_Context, world_trile_index: s32, roughness: s32, quality: s32, size: s32) -> *float #foreign libtacoma;
tacoma_render_reference :: (ctx: *Tacoma_Context, width: s32, height: s32, eye: Vector3, target: Vector3, roughness: float, quality: s32) -> *float #foreign libtacoma; tacoma_render_reference :: (ctx: *Tacoma_Context, width: s32, height: s32, eye: Vector3, target: Vector3, roughness: float, quality: s32) -> *float #foreign libtacoma;
tacoma_render_sh_chunk :: (ctx: *Tacoma_Context, origin_x: float, origin_y: float, origin_z: float, probe_n: s32, probe_spacing: float, quality: s32) -> *float #foreign libtacoma; tacoma_render_sh_chunk :: (ctx: *Tacoma_Context, origin_x: float, origin_y: float, origin_z: float, probe_n: s32, probe_spacing: float, quality: s32) -> *float #foreign libtacoma;

View File

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

View File

@ -24,6 +24,7 @@ Fetch_Type :: enum {
PACK; PACK;
WORLD; WORLD;
WORLD_CHUNKS; WORLD_CHUNKS;
WORLD_PROBES;
} }
Fetch_Request :: struct { Fetch_Request :: struct {
@ -66,6 +67,7 @@ fetch_type_buffer_size :: (type: Fetch_Type) -> s64 {
case .PACK; return MAX_PACK_SIZE; case .PACK; return MAX_PACK_SIZE;
case .WORLD; return MAX_WORLD_SIZE; case .WORLD; return MAX_WORLD_SIZE;
case .WORLD_CHUNKS; return MAX_CHUNKS_SIZE; case .WORLD_CHUNKS; return MAX_CHUNKS_SIZE;
case .WORLD_PROBES; return MAX_PROBES_SIZE;
} }
} }
@ -125,6 +127,9 @@ handle_fetch_failed :: (req: *Fetch_Request) {
free(req.world_json_data.data); free(req.world_json_data.data);
log_error("Failed to load chunks.bin for world '%'", req.world_name); log_error("Failed to load chunks.bin for world '%'", req.world_name);
case .WORLD_PROBES;
log_info("No baked probes for world '%'", req.world_name);
} }
} }
@ -170,10 +175,22 @@ process_completed_fetch :: (req: *Fetch_Request, data: []u8) {
if ok { if ok {
set_loaded_world(world); set_loaded_world(world);
log_info("Loaded world: %", req.world_name); log_info("Loaded world: %", req.world_name);
probes_req: Fetch_Request;
probes_req.type = .WORLD_PROBES;
probes_req.priority = .BACKGROUND;
probes_req.world_name = req.world_name;
probes_req.path = sprint("%/worlds/%/probes.bin", GAME_RESOURCES_DIR, req.world_name);
array_add(*g_asset_manager.queue, probes_req);
} else { } else {
log_error("Failed to parse world '%'", req.world_name); log_error("Failed to parse world '%'", req.world_name);
} }
case .WORLD_PROBES;
// Another world may have been loaded while these were on the way.
curworld := get_current_world();
if !curworld.valid || curworld.world.name != req.world_name return;
if !load_probes(data) log_error("Broken probes.bin for world '%'", req.world_name);
} }
} }
@ -255,7 +272,10 @@ add_resources_from_pack :: (pack: *Loaded_Pack) {
pal : Loaded_Palette; pal : Loaded_Palette;
pal.name = sprint("%", name); pal.name = sprint("%", name);
pal.entries = load_hex_palette_from_memory(v.data); pal.entries = load_hex_palette_from_memory(v.data);
array_add(*g_palettes, pal); // Pack hot reload brings the same palettes in again.
replaced := false;
for *g_palettes if it.name == pal.name { it.* = pal; replaced = true; }
if !replaced array_add(*g_palettes, pal);
log_info("Loaded % colors from hex palette(%)", pal.entries.count, v.name); log_info("Loaded % colors from hex palette(%)", pal.entries.count, v.name);
case "ttf"; case "ttf";
case; case;

View File

@ -253,37 +253,24 @@ draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
r.h = ui_h(3,0); r.h = ui_h(3,0);
if GR.button(r, "Render with Tacoma", *theme.button_theme) { if GR.button(r, "Render with Tacoma", *theme.button_theme) {
cam := get_level_editor_camera(); cam := get_level_editor_camera();
gen_reference(tacomaResolution, tacomaResolution, cam.position, cam.target, tacomaSamples, true, curworld.world); if curworld.valid gen_reference(tacomaResolution, tacomaResolution, cam.position, cam.target, tacomaSamples, *curworld.world);
} }
r.y += r.h; r.y += r.h;
if GR.button(r, "Bake all chunk RDMs", *theme.button_theme) { if GR.button(r, "Bake probes", *t_button_selectable(theme, !probe_bake.active)) {
if curworld.valid then rdm_bake_all_chunks(curworld.world, tacomaSamples, true); if curworld.valid && !probe_bake.active bake_probes(tacomaSamples);
} }
r.y += r.h; r.y += r.h;
if GR.button(r, "Bake all chunk SH grids", *t_button_selectable(theme, !sh_bake.active)) { if probe_bake.active {
if curworld.valid && !sh_bake.active sh_bake_start(tacomaSamples); total := probe_bake.keys.count;
} done := probe_bake.results.count;
r.y += r.h; GR.label(r, tprint("Baking probes: %/% chunks", done, total), *t_label_left(theme));
if rdm_bake.active {
total := cast(s32) rdm_bake.jobs.count;
done := rdm_bake.current_job;
pct := ifx total > 0 then done * 100 / total else 0;
GR.label(r, tprint("Baking RDMs: %/% (\%%)", done, total, pct), *t_label_left(theme));
r.y += r.h; r.y += r.h;
} }
if sh_bake.active { if tacoma_preview.texture.id {
total := cast(s32) sh_bake.chunk_keys.count; aspect := cast(float) tacoma_preview.width / cast(float) tacoma_preview.height;
done := sh_bake.current_chunk;
pct := ifx total > 0 then done * 100 / total else 0;
GR.label(r, tprint("Baking SH grids: %/% (\%%)", done, total, pct), *t_label_left(theme));
r.y += r.h;
}
if current_screenshot.valid {
aspect := cast(float)current_screenshot.width / cast(float)current_screenshot.height;
r.h = r.w / aspect; r.h = r.w / aspect;
uiTex := New(Ui_Texture,, temp); uiTex := New(Ui_Texture,, temp);
uiTex.tex = current_screenshot.image; uiTex.tex = tacoma_preview.texture;
set_shader_for_images(uiTex); set_shader_for_images(uiTex);
immediate_quad(.{r.x, r.y}, .{r.x + r.w, r.y}, .{r.x + r.w, r.y + r.h}, .{r.x, r.y + r.h}); immediate_quad(.{r.x, r.y}, .{r.x + r.w, r.y}, .{r.x + r.w, r.y + r.h}, .{r.x, r.y + r.h});
set_shader_for_color(); set_shader_for_color();
@ -299,13 +286,9 @@ draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
r.y += r.h; r.y += r.h;
GR.slider(r, *tacomaResolution, 10, 5000, 10, *theme.slider_theme); GR.slider(r, *tacomaResolution, 10, 5000, 10, *theme.slider_theme);
r.y += r.h; r.y += r.h;
GR.label(r, tprint("Sky Scale (bake): %", formatFloat(bake_sky_scale, trailing_width=2)), *t_label_left(theme)); GR.label(r, "Emissive light (bake)", *t_label_left(theme));
r.y += r.h; r.y += r.h;
GR.slider(r, *bake_sky_scale, 0.0, 5.0, 0.05, *theme.slider_theme); GR.slider(r, *bake_emissive_light, 0.0, 50.0, 0.5, *theme.slider_theme);
r.y += r.h;
GR.label(r, tprint("Sky Desaturation (bake): %", formatFloat(bake_sky_desaturation, trailing_width=2)), *t_label_left(theme));
r.y += r.h;
GR.slider(r, *bake_sky_desaturation, 0.0, 1.0, 0.05, *theme.slider_theme);
r.y += r.h; r.y += r.h;
GR.end_scrollable_region(region, r.x + r.w, r.y, *tacoma_tab_scroll); GR.end_scrollable_region(region, r.x + r.w, r.y, *tacoma_tab_scroll);
@ -650,7 +633,7 @@ editor_edit_y :: () -> float {
} }
tick_level_editor :: () { tick_level_editor :: () {
#if FLAG_TACOMA_ENABLED { rdm_bake_tick(); sh_bake_tick(); } #if FLAG_TACOMA_ENABLED { probe_bake_tick(); }
tick_level_editor_camera(); tick_level_editor_camera();
tick_particles(cast(float)delta_time); tick_particles(cast(float)delta_time);
@ -1139,7 +1122,8 @@ draw_editor_billboards :: (view: View_Id) {
draw_level_editor :: () { draw_level_editor :: () {
curworld := get_current_world(); curworld := get_current_world();
if !curworld.valid then return; if !curworld.valid then return;
view := add_view(.{camera = get_level_editor_camera(), environment = world_environment(*curworld.world)}); camera := get_level_editor_camera();
view := add_view(.{camera = camera, environment = world_environment(*curworld.world), cut = true, focus = camera.target});
draw_entities(view, *curworld.world); draw_entities(view, *curworld.world);
draw_live_particles(view); draw_live_particles(view);
if show_trile_preview && !trile_preview_disabled { if show_trile_preview && !trile_preview_disabled {

View File

@ -23,6 +23,14 @@ draw_material_studio_ui :: (theme: *GR.Overall_Theme) {
} }
r.y += row_h; r.y += row_h;
// .hex palettes in a pack, one RRGGBB per line.
for g_palettes {
if GR.button(r, tprint("Import % (% colours)", it.name, it.entries.count), *theme.button_theme, it_index) {
log_info("Imported % new materials from %", swatch_import_palette(it), it.name);
}
r.y += row_h;
}
grid := r; grid := r;
grid.h = ui_h(25, 0); grid.h = ui_h(25, 0);
previous := ms_selected; previous := ms_selected;
@ -35,6 +43,8 @@ draw_material_studio_ui :: (theme: *GR.Overall_Theme) {
if ms_selected == 0 || !g_swatches[ms_selected].used return; if ms_selected == 0 || !g_swatches[ms_selected].used return;
swatch := *g_swatches[ms_selected]; swatch := *g_swatches[ms_selected];
GR.label(r, tprint("Material %", ms_selected), *t_label_left(theme));
r.y += row_h;
a, new_text := GR.text_input(r, ms_name_input, *theme.text_input_theme, 128); a, new_text := GR.text_input(r, ms_name_input, *theme.text_input_theme, 128);
if a & .ENTERED { if a & .ENTERED {
ms_name_input = copy_string(new_text); ms_name_input = copy_string(new_text);
@ -42,25 +52,49 @@ draw_material_studio_ui :: (theme: *GR.Overall_Theme) {
} }
r.y += row_h; r.y += row_h;
r.h = ui_h(30, 0);
color := swatch_color(ms_selected);
GR.color_picker(r, *color, *theme.color_picker_theme);
for 0..2 swatch.color[it] = cast(u8) clamp(color.component[it] * 255 + 0.5, 0, 255);
r.y += r.h;
r.h = ui_h(3, 2); r.h = ui_h(3, 2);
GR.label(r, "Roughness", *t_label_left(theme)); GR.label(r, "Reflectivity", *t_label_left(theme));
r.y += r.h; r.y += r.h;
GR.slider(r, *swatch.roughness, 0, 7, 1, *theme.slider_theme); GR.slider(r, *swatch.reflectivity, 0, 255, 1, *theme.slider_theme);
r.y += r.h; r.y += r.h;
GR.label(r, "Metallic", *t_label_left(theme)); GR.label(r, "Gloss", *t_label_left(theme));
r.y += r.h; r.y += r.h;
GR.slider(r, *swatch.metallic, 0, 3, 1, *theme.slider_theme); GR.slider(r, *swatch.gloss, 0, 255, 1, *theme.slider_theme);
r.y += r.h; r.y += r.h;
GR.label(r, "Emittance", *t_label_left(theme)); GR.label(r, "Emittance", *t_label_left(theme));
r.y += r.h; r.y += r.h;
GR.slider(r, *swatch.emittance, 0, 127, 1, *theme.slider_theme); GR.slider(r, *swatch.emittance, 0, 127, 1, *theme.slider_theme);
r.y += r.h;
if GR.base_checkbox(r, "Glass (lets sunlight through)", swatch.glass, *theme.checkbox_theme) swatch.glass = !swatch.glass;
r.y += r.h * 2; r.y += r.h * 2;
// The colour gets its own panel on the right, so the picker has room.
color_panel := GR.get_rect(100*vw - panel.w, panel.y, panel.w, ui_h(45, 0));
ui_add_mouse_occluder(color_panel);
draw_bg_rectangle(color_panel, theme);
picker := color_panel;
picker.h = ui_h(40, 0);
color := swatch_color(ms_selected);
GR.color_picker(picker, *color, *theme.color_picker_theme);
for 0..2 swatch.color[it] = cast(u8) clamp(color.component[it] * 255 + 0.5, 0, 255);
picker.y += picker.h;
picker.h = row_h;
GR.label(picker, tprint("#%", hex_color(swatch.color)), *t_label_left(theme));
used_panel := GR.get_rect(color_panel.x, color_panel.y + color_panel.h, color_panel.w, ui_h(45, 0));
ui_add_mouse_occluder(used_panel);
draw_bg_rectangle(used_panel, theme);
line := used_panel;
line.h = row_h;
GR.label(line, "Used by", *t_label_left(theme));
line.y += row_h;
for trile: get_trile_table_ptr() {
count := trile_swatch_counts(*trile)[ms_selected];
if count == 0 continue;
GR.label(line, tprint("% (%)", trile.name, count), *t_label_left(theme));
line.y += row_h;
}
swatch_changed(ms_selected); swatch_changed(ms_selected);
r.h = row_h; r.h = row_h;
@ -78,3 +112,15 @@ draw_material_studio_ui :: (theme: *GR.Overall_Theme) {
ms_selected = 0; ms_selected = 0;
} }
} }
#scope_file
hex_color :: (c: [3]u8) -> string {
digits :: "0123456789abcdef";
out := talloc_string(6);
for 0..2 {
out[it * 2] = digits[c[it] >> 4];
out[it * 2 + 1] = digits[c[it] & 0xf];
}
return out;
}

View File

@ -151,6 +151,7 @@ draw_particle_editor_ui :: (theme: *GR.Overall_Theme) {
pe_vec4(*er, "Color Start", *def.color_start, 0, 1, theme); pe_vec4(*er, "Color Start", *def.color_start, 0, 1, theme);
pe_vec4(*er, "Color End", *def.color_end, 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_float(*er, tprint("Gravity: %", def.gravity), *def.gravity, -20, 20, 0.1, theme);
pe_float(*er, tprint("Glow: %", def.glow), *def.glow, 0, 1, 0.05, theme);
pe_label(*er, tprint("Blend Mode: %", def.blend_mode), theme); pe_label(*er, tprint("Blend Mode: %", def.blend_mode), theme);
orig_w2 := er.w; orig_w2 := er.w;

View File

@ -5,346 +5,86 @@
Tacoma :: #import "Tacoma"; Tacoma :: #import "Tacoma";
Tacoma_Screenshot :: struct {
image : sg_image;
width : s32;
height : s32;
valid : bool = false;
}
current_screenshot : Tacoma_Screenshot;
// Single global atlas entry: stored on the World once baked.
RDM_Atlas_Entry_Bake :: struct {
world_pos: Vector3;
x, y: s32; // top-left in atlas pixels
w, h: s32; // pixel size of this RDM (= 2*size, 3*size)
}
RDM_ATLAS_MAX_SIZE :: 16384; // GPU 2D texture ceiling.
ctx : *Tacoma.Tacoma_Context; ctx : *Tacoma.Tacoma_Context;
RDM_Bake_Job :: struct { tacoma_preview : struct {
world_trile_index: s32; texture : Texture;
world_pos: Vector3; width : s32;
size: s32; // per-instance RDM side in px (tile = 2*size x 3*size). height : s32;
} }
RDM_Global_Bake :: struct { // Light given off by a fully emissive swatch, relative to full sun on a white face.
data : *float; // CPU atlas buffer (RGBA32F during bake) bake_emissive_light : float = 10.0;
width : s32;
height : s32; // Probes where more of the rays than this hit the back of a face are inside geometry.
cursor_x : s32; PROBE_INSIDE_BACKFACES :: 0.25;
cursor_y : s32;
row_height : s32; Probe_Bake :: struct {
entries : [..]RDM_Atlas_Entry_Bake; active : bool;
quality : s32;
world_name : string;
keys : [..]Chunk_Key;
results : [..]*float; // from Tacoma, one per baked key
} }
RDM_Bake_State :: struct { probe_bake : Probe_Bake;
active : bool = false;
quality : s32 = 100;
jobs : [..]RDM_Bake_Job;
current_job : s32 = 0;
atlas : RDM_Global_Bake;
}
rdm_bake : RDM_Bake_State; bake_probes :: (quality: s32 = 100) {
if probe_bake.active return;
curworld := get_current_world();
if !curworld.valid { log_warn("bake_probes: no world loaded"); return; }
SH_Bake_State :: struct { for _, key: curworld.world.chunks array_add(*probe_bake.keys, key);
active : bool; if !probe_bake.keys.count return;
quality : s32;
include_water : bool;
chunk_keys : [..]Chunk_Key;
current_chunk : s32;
}
sh_bake : SH_Bake_State; tacoma_init_scene(*curworld.world);
probe_bake.active = true;
probe_bake.quality = quality;
probe_bake.world_name = copy_string(curworld.world.name);
} @Command
rdm_job_quality :: (job: RDM_Bake_Job) -> s32 { // Bakes one chunk per call.
return rdm_bake.quality; probe_bake_tick :: () {
} if !probe_bake.active return;
curworld := get_current_world();
rdm_sort_jobs_by_height_desc :: (jobs: *[..]RDM_Bake_Job) { if !curworld.valid || curworld.world.name != probe_bake.world_name {
n := jobs.count; log_warn("Probe bake cancelled: the world changed");
for i: 1..n-1 { probe_bake_end();
key := jobs.data[i]; return;
j := i - 1;
while j >= 0 && jobs.data[j].size < key.size {
jobs.data[j + 1] = jobs.data[j];
j -= 1;
}
jobs.data[j + 1] = key;
} }
} if probe_bake.results.count == probe_bake.keys.count {
probe_bake_finish();
// Simulate shelf pack of the current jobs array into a square atlas of `atlas_size`.
// Returns true if all fit. Assumes jobs are already sorted by tile height descending.
rdm_simulate_shelf_pack :: (jobs: []RDM_Bake_Job, atlas_size: s32) -> bool {
cursor_x : s32 = 0;
cursor_y : s32 = 0;
row_h : s32 = 0;
for job: jobs {
w := 2 * job.size;
h := 3 * job.size;
if w > atlas_size || h > atlas_size return false;
if cursor_x + w > atlas_size {
cursor_y += row_h;
cursor_x = 0;
row_h = 0;
}
if cursor_y + h > atlas_size return false;
cursor_x += w;
if h > row_h then row_h = h;
}
return true;
}
// Choose min pow2 square atlas fitting all jobs (variable tile sizes). Caps at RDM_ATLAS_MAX_SIZE.
rdm_calc_global_atlas_size :: (jobs: []RDM_Bake_Job) -> (s32, s32) {
if jobs.count == 0 return 16, 16;
target : s32 = 16;
while target < RDM_ATLAS_MAX_SIZE {
if rdm_simulate_shelf_pack(jobs, target) return target, target;
target *= 2;
}
if !rdm_simulate_shelf_pack(jobs, RDM_ATLAS_MAX_SIZE) {
log_warn("RDM atlas cap reached (%); some instances may not fit.", RDM_ATLAS_MAX_SIZE);
}
return RDM_ATLAS_MAX_SIZE, RDM_ATLAS_MAX_SIZE;
}
// Build the Tacoma scene from the world and emit one bake job per RDM-flagged instance.
// chunk_keys is accepted for API compatibility but ignored — bakes always cover every flagged
// instance because the global atlas + manifest is rewritten as a whole.
rdm_bake_start :: (world: World, quality: s32, include_water: bool, chunk_keys: []Chunk_Key = .[]) {
if rdm_bake.active then return;
trile_list : [..]Tacoma.Trile_Data;
trile_list.allocator = temp;
world_triles : [..]Tacoma.World_Trile;
world_triles.allocator = temp;
trile_name_to_index: Table(string, s32);
trile_name_to_index.allocator = temp;
for chunk: world.chunks {
for group: chunk.groups {
success, idx := table_find(*trile_name_to_index, group.trile_name);
if !success {
trile, trile_ok := get_trile(group.trile_name);
if !trile_ok continue;
ttrile : Tacoma.Trile_Data;
for x: 0..15 {
for y: 0..15 {
for z: 0..15 {
ttrile.trixels[x][y][z] = .{
trile.trixels[x][y][z].empty,
trile.trixels[x][y][z].material.color,
material_encode_to_float(trile.trixels[x][y][z].material)
};
}
}
}
gfx := get_trile_gfx(group.trile_name);
ttrile.vertices = gfx.vertices.data;
ttrile.vertexCount = cast(s32) (gfx.vertices.count / 3);
idx = cast(s32) trile_list.count;
array_add(*trile_list, ttrile);
table_set(*trile_name_to_index, group.trile_name, idx);
}
for inst: group.instances {
world_trile_idx := cast(s32) world_triles.count;
wx, wy, wz := chunk_local_to_world(chunk.coord, inst.x, inst.y, inst.z);
wpos := Vector3.{cast(float) wx, cast(float) wy, cast(float) wz};
array_add(*world_triles, Tacoma.World_Trile.{idx, wpos, cast(s32) inst.orientation});
inst_size := get_rdm_instance_size(*world, wx, wy, wz);
if inst_size > 0 {
array_add(*rdm_bake.jobs, .{
world_trile_index = world_trile_idx,
world_pos = wpos,
size = inst_size,
});
}
}
}
}
sky : Tacoma.Sky_Config = world_to_sky_config(world);
blases : Tacoma.Trile_Set = .{trile_list.data, cast(s32) trile_list.count};
tlas : Tacoma.World = .{world_triles.data, cast(s32) world_triles.count};
ctx = Tacoma.tacoma_init("./modules/Tacoma/");
Tacoma.tacoma_load_scene(ctx, sky, blases, tlas, cast(s32) include_water);
// Sort jobs tallest-first for a tighter shelf pack.
rdm_sort_jobs_by_height_desc(*rdm_bake.jobs);
// Allocate global CPU atlas large enough for all jobs.
aw, ah := rdm_calc_global_atlas_size(rdm_bake.jobs);
atlas_bytes := cast(s64) aw * cast(s64) ah * 4 * size_of(float);
rdm_bake.atlas.width = aw;
rdm_bake.atlas.height = ah;
rdm_bake.atlas.data = cast(*float) alloc(atlas_bytes);
memset(rdm_bake.atlas.data, 0, atlas_bytes);
log_info("RDM global atlas: %x% (% MB) for % jobs",
aw, ah, cast(float)atlas_bytes / (1024.0 * 1024.0), rdm_bake.jobs.count);
rdm_bake.active = true;
rdm_bake.quality = quality;
if rdm_bake.jobs.count == 0 {
rdm_bake_finish();
}
}
// Queue all RDM-flagged instances. (chunk_keys parameter is ignored — see rdm_bake_start.)
rdm_bake_all_chunks :: (world: World, quality: s32, include_water: bool) {
rdm_bake_start(world, quality, include_water);
}
rdm_bake_chunks :: (chunk_keys: []Chunk_Key, world: World, quality: s32, include_water: bool) {
rdm_bake_start(world, quality, include_water, chunk_keys);
}
// Process at most one RDM per frame.
rdm_bake_tick :: () {
if !rdm_bake.active then return;
if rdm_bake.current_job >= cast(s32) rdm_bake.jobs.count {
rdm_bake_finish();
return; return;
} }
job := rdm_bake.jobs[rdm_bake.current_job]; key := probe_bake.keys[probe_bake.results.count];
x, y, z := chunk_local_to_world(key, 0, 0, 0);
data := Tacoma.tacoma_render_sh_chunk(ctx, xx x, xx y, xx z, CHUNK_SIZE, 1.0, probe_bake.quality);
array_add(*probe_bake.results, data);
log_info("Baked probes for chunk % (%/%)", key, probe_bake.results.count, probe_bake.keys.count);
}
size : s32 = job.size; gen_reference :: (w: s32, h: s32, eye: Vector3, target: Vector3, quality: s32, world: *World) {
w := 2 * size; tacoma_init_scene(world);
h := 3 * size; data := Tacoma.tacoma_render_reference(ctx, w, h, eye, target, 0.01, quality);
ptr := Tacoma.tacoma_render_rdm(ctx, job.world_trile_index, 0, rdm_job_quality(job), size); tacoma_stop();
bake := *rdm_bake.atlas; pixels := NewArray(w * h * 4, u8,, temp);
for i: 0..w * h - 1 {
if bake.cursor_x + w > bake.width { for c: 0..2 pixels[i * 4 + c] = xx (clamp(data[i * 4 + c], 0, 1) * 255 + 0.5);
bake.cursor_y += bake.row_height; pixels[i * 4 + 3] = 255;
bake.cursor_x = 0;
bake.row_height = 0;
} }
Tacoma.tacoma_free_result(data);
ax := bake.cursor_x; if tacoma_preview.texture.id texture_destroy(tacoma_preview.texture);
ay := bake.cursor_y; tacoma_preview.texture = texture_create(w, h, pixels);
tacoma_preview.width = w;
if ay + h <= bake.height { tacoma_preview.height = h;
src := cast(*u8) ptr;
row_bytes := cast(s64) w * 4 * size_of(float);
for row: 0..h-1 {
dst_offset := (cast(s64)(ay + row) * cast(s64) bake.width + cast(s64) ax) * 4 * size_of(float);
src_offset := cast(s64) row * row_bytes;
memcpy(cast(*u8) bake.data + dst_offset, src + src_offset, row_bytes);
}
entry : RDM_Atlas_Entry_Bake;
entry.world_pos = job.world_pos;
entry.x = ax;
entry.y = ay;
entry.w = w;
entry.h = h;
array_add(*bake.entries, entry);
} else {
log_warn("RDM global atlas overflow, skipping (pos=%)", job.world_pos);
}
bake.cursor_x += w;
if h > bake.row_height then bake.row_height = h;
tacoma_handle_result(ptr, w, h);
rdm_bake.current_job += 1;
} }
rdm_bake_finish :: () { #scope_file
if ctx != null then tacoma_stop();
curworld := get_current_world(); tacoma_init_scene :: (world: *World) {
bake := *rdm_bake.atlas;
if bake.entries.count > 0 {
// a) Pack atlas into RGBA16F half-floats and upload to g_rdm_atlas.
upload_global_atlas_image(bake);
// b) Populate world.rdm_lookup with normalized UV rects.
if curworld.valid {
array_reset_keeping_memory(*curworld.world.rdm_lookup);
atlas_w := cast(float) bake.width;
atlas_h := cast(float) bake.height;
for entry: bake.entries {
e : Rdm_Atlas_Entry;
e.x = cast(s32) entry.world_pos.x;
e.y = cast(s32) entry.world_pos.y;
e.z = cast(s32) entry.world_pos.z;
e.atlas_rect = .{
cast(float) entry.x / atlas_w,
cast(float) entry.y / atlas_h,
cast(float) entry.w / atlas_w,
cast(float) entry.h / atlas_h,
};
array_add(*curworld.world.rdm_lookup, e);
}
}
// c) Save atlas + manifest to disk.
rdm_save_global_atlas_to_disk(bake);
if curworld.valid then rdm_save_global_manifest_to_disk(curworld.world.name, curworld.world.rdm_lookup);
}
log_info("RDM bake complete: % entries", bake.entries.count);
if bake.data != null then free(bake.data);
array_free(bake.entries);
array_free(rdm_bake.jobs);
rdm_bake = .{};
}
// Convert RGBA32F CPU atlas to RGBA16F and upload to g_rdm_atlas.
upload_global_atlas_image :: (bake: *RDM_Global_Bake) {
pixel_count := cast(s64) bake.width * cast(s64) bake.height * 4;
halves := cast(*u16) alloc(pixel_count * size_of(u16));
defer free(halves);
for i: 0..pixel_count - 1 halves[i] = f32_to_f16(bake.data[i]);
imgdata : sg_image_data;
imgdata.subimage[0][0] = .{halves, cast(u64)(pixel_count * size_of(u16))};
desc : sg_image_desc = .{
render_target = false,
width = bake.width,
height = bake.height,
pixel_format = sg_pixel_format.RGBA16F,
sample_count = 1,
data = imgdata,
};
if g_rdm_atlas.id != 0 then sg_destroy_image(g_rdm_atlas);
g_rdm_atlas = sg_make_image(*desc);
}
bake_sky_scale : float = 1.0;
bake_sky_desaturation : float = 1.0;
world_to_sky_config :: (world: World) -> Tacoma.Sky_Config {
sky : Tacoma.Sky_Config;
sky.skyBase = world.conf.skyBase;
sky.skyTop = world.conf.skyTop;
sky.sunDisk = world.conf.sunDisk;
sky.horizonHalo = world.conf.horizonHalo;
sky.sunHalo = world.conf.sunHalo;
sky.sunLightColor = world.conf.sunLightColor;
sky.sunPosition = world.conf.sunPosition;
sky.sunIntensity = world.conf.sunIntensity;
sky.skyIntensity = world.conf.skyIntensity * bake_sky_scale;
sky.skyDesaturation = bake_sky_desaturation;
return sky;
}
// Build trile BLASes + world TLAS from a world, then initialize ctx.
tacoma_init_scene :: (world: World, include_water: bool) {
trile_list : [..]Tacoma.Trile_Data; trile_list.allocator = temp; trile_list : [..]Tacoma.Trile_Data; trile_list.allocator = temp;
world_triles : [..]Tacoma.World_Trile; world_triles.allocator = temp; world_triles : [..]Tacoma.World_Trile; world_triles.allocator = temp;
trile_name_to_index : Table(string, s32); trile_name_to_index.allocator = temp; trile_name_to_index : Table(string, s32); trile_name_to_index.allocator = temp;
@ -355,230 +95,136 @@ tacoma_init_scene :: (world: World, include_water: bool) {
if !success { if !success {
trile, trile_ok := get_trile(group.trile_name); trile, trile_ok := get_trile(group.trile_name);
if !trile_ok continue; if !trile_ok continue;
vertices, indices := mesh_trile(trile);
positions := NewArray(indices.count * 3, float,, temp);
for indices for c: 0..2 positions[it_index * 3 + c] = vertices[it].position.component[c];
ttrile : Tacoma.Trile_Data; ttrile : Tacoma.Trile_Data;
for x: 0..15 for y: 0..15 for z: 0..15 { ttrile.trixels = trile.trixels;
ttrile.trixels[x][y][z] = .{ ttrile.vertices = positions.data;
trile.trixels[x][y][z].empty, ttrile.vertexCount = xx indices.count;
trile.trixels[x][y][z].material.color,
material_encode_to_float(trile.trixels[x][y][z].material)
};
}
gfx := get_trile_gfx(group.trile_name);
ttrile.vertices = gfx.vertices.data;
ttrile.vertexCount = cast(s32)(gfx.vertices.count / 3);
idx = cast(s32) trile_list.count; idx = cast(s32) trile_list.count;
array_add(*trile_list, ttrile); array_add(*trile_list, ttrile);
table_set(*trile_name_to_index, group.trile_name, idx); table_set(*trile_name_to_index, group.trile_name, idx);
} }
for inst: group.instances { for inst: group.instances {
wx, wy, wz := chunk_local_to_world(chunk.coord, inst.x, inst.y, inst.z); wx, wy, wz := chunk_local_to_world(chunk.coord, inst.x, inst.y, inst.z);
array_add(*world_triles, Tacoma.World_Trile.{idx, array_add(*world_triles, Tacoma.World_Trile.{idx, .{xx wx, xx wy, xx wz}, xx inst.orientation});
Vector3.{cast(float) wx, cast(float) wy, cast(float) wz},
cast(s32) inst.orientation});
} }
} }
} }
sky : Tacoma.Sky_Config = world_to_sky_config(world); swatches : [Tacoma.SWATCH_COUNT]Tacoma.Swatch;
blases : Tacoma.Trile_Set = .{trile_list.data, cast(s32) trile_list.count}; for g_swatches {
tlas : Tacoma.World = .{world_triles.data, cast(s32) world_triles.count}; if !it.used continue;
color := swatch_color(xx it_index);
emission := color * (min(cast(int) it.emittance, 127) / 127.0) * bake_emissive_light;
swatches[it_index] = .{color, emission, xx it.glass};
}
env := world_environment(world);
sky : Tacoma.Sky_Config;
sky.skyBase = env.sky_base;
sky.skyTop = env.sky_top;
sky.sunDisk = env.sun_disk;
sky.horizonHalo = env.horizon_halo;
sky.sunHalo = env.sun_halo;
sky.sunLightColor = env.sun_color;
sky.sunPosition = env.sun_direction;
sky.sunIntensity = 1;
sky.skyIntensity = ifx env.sky then env.sky_intensity else 0;
blases : Tacoma.Trile_Set = .{trile_list.data, xx trile_list.count};
tlas : Tacoma.World = .{world_triles.data, xx world_triles.count};
ctx = Tacoma.tacoma_init("./modules/Tacoma/"); ctx = Tacoma.tacoma_init("./modules/Tacoma/");
Tacoma.tacoma_load_scene(ctx, sky, blases, tlas, cast(s32) include_water); Tacoma.tacoma_load_scene(ctx, sky, swatches.data, blases, tlas, xx env.water);
} }
tacoma_stop :: () { tacoma_stop :: () {
Tacoma.tacoma_destroy(ctx); Tacoma.tacoma_destroy(ctx);
ctx = null;
} }
tacoma_handle_result :: (ptr: *float, w: s32, h: s32) { probe_bake_finish :: () {
data := cast(*float) talloc(w*h*4*size_of(float));
memcpy(data, ptr, w*h*4*4);
for 0..(w*h) {
color : Vector3;
color.x = data[it * 4 + 0];
color.y = data[it * 4 + 1];
color.z = data[it * 4 + 2];
data[it * 4 + 0] = color.x;
data[it * 4 + 1] = color.y;
data[it * 4 + 2] = color.z;
}
imgdata : sg_image_data;
imgdata.subimage[0][0] = .{data, cast(u64) (w*h*4*4)};
texdesc : sg_image_desc = .{
render_target = false,
width = w,
height = h,
pixel_format = sg_pixel_format.RGBA32F,
sample_count = 1,
data = imgdata
};
if current_screenshot.valid {
sg_destroy_image(current_screenshot.image);
}
current_screenshot = .{
sg_make_image(*texdesc),
w,
h,
true
};
Tacoma.tacoma_free_result(ptr);
}
gen_reference :: (w: s32, h: s32, eye: Vector3, target: Vector3, quality: s32, include_water: bool, world: World) {
tacoma_init_scene(world, include_water);
ptr := Tacoma.tacoma_render_reference(ctx, w, h, eye, target, 0.01, quality);
tacoma_handle_result(ptr, w, h);
tacoma_stop(); tacoma_stop();
chunks := probes_from_bake(probe_bake.keys, probe_bake.results);
for chunks world_set_chunk_probes(it.key, it.probes);
file :: #import "File";
path := tprint("%/worlds/%/probes.bin", GAME_RESOURCES_DIR, probe_bake.world_name);
if file.write_entire_file(path, save_probes(chunks)) log_info("Probe bake complete, saved %", path);
else log_error("Probe bake complete, but writing % failed", path);
for chunks array_free(it.probes);
probe_bake_end();
} }
sh_bake_start :: (quality: s32 = 50, include_water: bool = false) { probe_bake_end :: () {
if sh_bake.active then return; if ctx tacoma_stop();
curworld := get_current_world(); for probe_bake.results Tacoma.tacoma_free_result(it);
if !curworld.valid { log_warn("sh_bake_start: no world loaded"); return; } array_free(probe_bake.results);
array_free(probe_bake.keys);
free(probe_bake.world_name);
probe_bake = .{};
}
world := *curworld.world; // Marks probes inside geometry, and evens out bake noise by averaging each probe
for _, key: world.chunks array_add(*sh_bake.chunk_keys, key); // with its face neighbours, across chunk borders. Probes inside geometry take no part.
probes_from_bake :: (keys: []Chunk_Key, results: []*float) -> []Probe_Chunk {
N :: CHUNK_SIZE;
baked : Table(Chunk_Key, *float, chunk_key_hash, chunk_key_compare);
baked.allocator = temp;
for keys table_set(*baked, it, results[it_index]);
if sh_bake.chunk_keys.count == 0 then return; Baked :: type_of(baked);
baked_probe :: (baked: *Baked, x: s32, y: s32, z: s32) -> *float {
tacoma_init_scene(world.*, include_water); found, data := table_find(baked, world_to_chunk_coord(x, y, z));
sh_bake.active = true; if !found return null;
sh_bake.quality = quality; lx, ly, lz := world_to_local(x, y, z);
sh_bake.include_water = include_water; p := data + (cast(s64) lx + cast(s64) ly * N + cast(s64) lz * N * N) * Tacoma.PROBE_FLOATS;
sh_bake.current_chunk = 0; if p[12] > PROBE_INSIDE_BACKFACES return null;
} @Command return p;
sh_bake_tick :: () {
if !sh_bake.active then return;
if sh_bake.current_chunk >= cast(s32) sh_bake.chunk_keys.count {
sh_bake_finish();
return;
} }
curworld := get_current_world(); NEIGHBOURS :: s32.[1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1];
if !curworld.valid { sh_bake_finish(); return; }
n :: SH_PROBE_N; chunks := NewArray(keys.count, Probe_Chunk,, temp);
tex_w :: SH_TEX_W; for key, chunk_index: keys {
probes := NewArray(PROBES_PER_CHUNK, Probe);
chunk_key := sh_bake.chunk_keys[sh_bake.current_chunk]; for z: 0..N-1 for y: 0..N-1 for x: 0..N-1 {
chunk := table_find_pointer(*curworld.world.chunks, chunk_key); probe := *probes[x + y * N + z * N * N];
sh_bake.current_chunk += 1; wx, wy, wz := chunk_local_to_world(key, xx x, xx y, xx z);
if chunk == null then return; centre := baked_probe(*baked, wx, wy, wz);
if !centre {
ox, oy, oz := chunk_local_to_world(chunk_key, 0, 0, 0); probe.sh[0] = half_from_float(-1);
SH_PAD :: 2.0; continue;
SH_SPACING :: (32.0 + 2.0 * SH_PAD) / n;
ptr := Tacoma.tacoma_render_sh_chunk(ctx,
cast(float) ox - SH_PAD, cast(float) oy - SH_PAD, cast(float) oz - SH_PAD,
n, SH_SPACING, sh_bake.quality);
tex_halfs :: tex_w * n * n * 4;
packed : *u16 = cast(*u16) alloc(tex_halfs * size_of(u16));
defer free(packed);
src := ptr;
for pz: 0..n-1 for py: 0..n-1 for px: 0..n-1 {
probe_idx := px + py * n + pz * n * n;
s := src + probe_idx * 12;
for k: 0..2 {
tex_idx := (pz * n + py) * tex_w + (px * 3 + k);
d := packed + tex_idx * 4;
for ch: 0..3 {
(d + ch).* = f32_to_f16((s + k * 4 + ch).*);
} }
sum : [12]float;
for 0..11 sum[it] = centre[it];
count := 1;
for n: 0..5 {
other := baked_probe(*baked, wx + NEIGHBOURS[n * 3], wy + NEIGHBOURS[n * 3 + 1], wz + NEIGHBOURS[n * 3 + 2]);
if !other continue;
for 0..11 sum[it] += other[it];
count += 1;
}
for 0..11 probe.sh[it] = half_from_float(sum[it] / count);
} }
chunks[chunk_index] = .{key, probes};
} }
return chunks;
tex_byte_size := cast(u64)(tex_halfs * size_of(u16));
imgdata : sg_image_data;
imgdata.subimage[0][0] = .{packed, tex_byte_size};
sh_desc : sg_image_desc = .{
width = tex_w,
height = n * n,
pixel_format = sg_pixel_format.RGBA16F,
sample_count = 1,
data = imgdata,
};
if chunk.sh_valid sg_destroy_image(chunk.sh_probe_grid);
chunk.sh_probe_grid = sg_make_image(*sh_desc);
chunk.sh_valid = true;
chunk.sh_dirty = false;
#if OS != .WASM {
shgrid_save_to_disk(curworld.world.name, chunk_key, ptr, n);
}
Tacoma.tacoma_free_result(ptr);
log_info("SH baked chunk % (%/%)", chunk_key, sh_bake.current_chunk, sh_bake.chunk_keys.count);
} }
sh_bake_finish :: () { half_from_float :: (f: float) -> u16 {
tacoma_stop(); bits := (cast(*u32) *f).*;
array_free(sh_bake.chunk_keys); sign := cast(u16) ((bits >> 16) & 0x8000);
sh_bake = .{}; exponent := cast(s32) ((bits >> 23) & 0xFF) - 127 + 15;
log_info("SH bake complete."); mantissa := bits & 0x7FFFFF;
if exponent <= 0 return sign;
if exponent >= 31 return sign | 0x7BFF;
half := sign | cast(u16) (exponent << 10) | cast(u16) (mantissa >> 13);
if mantissa & 0x1000 half += 1;
return half;
} }
shgrid_save_to_disk :: (world_name: string, chunk_key: Chunk_Key, data: *float, probe_n: s32) {
#if OS != .WASM {
file :: #import "File";
path := shgrid_filename(world_name, chunk_key);
builder : String_Builder;
header := SH_Grid_File_Header.{
magic = SH_FILE_MAGIC,
version = 2,
probe_n = probe_n,
};
write_bytes(*builder, *header, size_of(SH_Grid_File_Header));
total_values := cast(s64) probe_n * probe_n * probe_n * 12;
halves := NewArray(total_values, u16,, temp);
for i: 0..total_values-1 halves[i] = f32_to_f16(data[i]);
write_bytes(*builder, halves.data, total_values * size_of(u16));
file.write_entire_file(path, builder_to_string(*builder));
}
}
rdm_save_global_atlas_to_disk :: (bake: *RDM_Global_Bake) {
#if OS != .WASM {
file :: #import "File";
curworld := get_current_world();
if !curworld.valid then return;
path := rdm_global_atlas_filename(curworld.world.name);
builder : String_Builder;
header := RDM_File_Header.{
magic = RDM_FILE_MAGIC,
width = bake.width,
height = bake.height,
};
write_bytes(*builder, *header, size_of(RDM_File_Header));
// Convert RGBA32F → RGBA16F for the on-disk atlas.
pixel_count := cast(s64) bake.width * cast(s64) bake.height * 4;
halves := NewArray(pixel_count, u16,, temp);
for i: 0..pixel_count - 1 halves[i] = f32_to_f16(bake.data[i]);
write_bytes(*builder, halves.data, pixel_count * size_of(u16));
file.write_entire_file(path, builder_to_string(*builder));
log_info("Saved RDM global atlas: %", path);
}
}
rdm_save_global_manifest_to_disk :: (world_name: string, entries: []Rdm_Atlas_Entry) {
#if OS != .WASM {
file :: #import "File";
path := rdm_global_manifest_filename(world_name);
s := Jaison.json_write_string(entries);
file.write_entire_file(path, s);
log_info("Saved RDM manifest: % (% entries)", path, entries.count);
}
}

View File

@ -23,6 +23,7 @@ Trile_Editor_Tool :: enum {
PAINT; PAINT;
ADD; ADD;
REMOVE; REMOVE;
PICK; // takes the clicked trixel's material
} }
Trile_Editor_Tool_Mode :: enum { Trile_Editor_Tool_Mode :: enum {
@ -57,6 +58,9 @@ ntrile :: (name: string) {
trile_changed(editor_current_trile); trile_changed(editor_current_trile);
newt : Trile; newt : Trile;
newt.name = sprint("%", name); newt.name = sprint("%", name);
swatch := current_swatch;
if !g_swatches[swatch].used swatch = first_used_swatch();
trile_fill(*newt, swatch);
set_trile(newt.name, newt); set_trile(newt.name, newt);
editor_current_trile = get_trile(newt.name); editor_current_trile = get_trile(newt.name);
trile_gen_heightmap(editor_current_trile); trile_gen_heightmap(editor_current_trile);
@ -134,6 +138,10 @@ area_active : bool;
handle_tool_click :: () { handle_tool_click :: () {
if hovered_trixel_x < 0 && hovered_trixel_y < 0 && hovered_trixel_z < 0 then return; if hovered_trixel_x < 0 && hovered_trixel_y < 0 && hovered_trixel_z < 0 then return;
if current_tool == .PICK {
current_swatch = editor_current_trile.trixels[hovered_trixel_x][hovered_trixel_y][hovered_trixel_z];
return;
}
if current_mode == .POINT { if current_mode == .POINT {
push_undo(); push_undo();
if current_tool == .ADD { if current_tool == .ADD {
@ -318,7 +326,9 @@ draw_tool_tab :: (theme: *GR.Overall_Theme, area: GR.Rect) {
if keybind_button(r, "Add", .TRIXEL_TOOL_ADD, *t_button_selectable(theme, current_tool == .ADD)) then current_tool = .ADD; if keybind_button(r, "Add", .TRIXEL_TOOL_ADD, *t_button_selectable(theme, current_tool == .ADD)) then current_tool = .ADD;
r.y += r.h; r.y += r.h;
if keybind_button(r, "Remove", .TRIXEL_TOOL_REMOVE, *t_button_selectable(theme, current_tool == .REMOVE)) then current_tool = .REMOVE; if keybind_button(r, "Remove", .TRIXEL_TOOL_REMOVE, *t_button_selectable(theme, current_tool == .REMOVE)) then current_tool = .REMOVE;
r.y += r.h * 3; r.y += r.h;
if keybind_button(r, "Pick material", .TRIXEL_TOOL_PICK, *t_button_selectable(theme, current_tool == .PICK)) then current_tool = .PICK;
r.y += r.h * 2;
if keybind_button(r, "Point", .TRIXEL_MODE_POINT, *t_button_selectable(theme, current_mode == .POINT)) then current_mode = .POINT; if keybind_button(r, "Point", .TRIXEL_MODE_POINT, *t_button_selectable(theme, current_mode == .POINT)) then current_mode = .POINT;
r.y += r.h; r.y += r.h;
if keybind_button(r, "Area", .TRIXEL_MODE_AREA, *t_button_selectable(theme, current_mode == .AREA)) then current_mode = .AREA; if keybind_button(r, "Area", .TRIXEL_MODE_AREA, *t_button_selectable(theme, current_mode == .AREA)) then current_mode = .AREA;
@ -336,9 +346,35 @@ draw_tool_tab :: (theme: *GR.Overall_Theme, area: GR.Rect) {
} }
draw_material_tab :: (theme: *GR.Overall_Theme, area: GR.Rect) { draw_material_tab :: (theme: *GR.Overall_Theme, area: GR.Rect) {
region, inside := GR.begin_scrollable_region(area, *theme.scrollable_region_theme); row_h := ui_h(3, 2);
r := area;
r.h = row_h;
GR.label(r, tprint("Selected %", swatch_label(current_swatch)), *t_label_left(theme));
r.y += row_h;
hovered := "-";
if editor_current_trile && hovered_trixel_x >= 0 {
hovered = swatch_label(editor_current_trile.trixels[hovered_trixel_x][hovered_trixel_y][hovered_trixel_z]);
}
GR.label(r, tprint("Hovered %", hovered), *t_label_left(theme));
r.y += row_h * 1.5;
grid := r;
grid.h = ui_h(30, 0);
region, inside := GR.begin_scrollable_region(grid, *theme.scrollable_region_theme);
end_y := draw_swatch_grid(inside, *swatch_scroll, *current_swatch, theme); end_y := draw_swatch_grid(inside, *swatch_scroll, *current_swatch, theme);
GR.end_scrollable_region(region, inside.x + inside.w, end_y, *swatch_scroll); GR.end_scrollable_region(region, inside.x + inside.w, end_y, *swatch_scroll);
r.y += grid.h + row_h * 0.5;
if !editor_current_trile return;
GR.label(r, "In this trile (click to select)", *t_label_left(theme));
r.y += row_h;
counts := trile_swatch_counts(editor_current_trile);
for count, id: counts {
if count == 0 continue;
btn := t_button_selectable(theme, current_swatch == id);
if GR.button(r, tprint("% (%)", swatch_label(xx id), count), *btn, 1000 + id) current_swatch = xx id;
r.y += row_h;
}
} }
// Shared with the material studio. Returns the bottom of the grid. // Shared with the material studio. Returns the bottom of the grid.
@ -436,6 +472,16 @@ draw_meta_tab :: (theme: *GR.Overall_Theme, area: GR.Rect) {
r.y += row_h; r.y += row_h;
changed = GR.base_checkbox(r, "Is stairs", editor_current_trile.is_stairs, *theme.checkbox_theme); changed = GR.base_checkbox(r, "Is stairs", editor_current_trile.is_stairs, *theme.checkbox_theme);
if changed editor_current_trile.is_stairs = !editor_current_trile.is_stairs; if changed editor_current_trile.is_stairs = !editor_current_trile.is_stairs;
r.y += row_h;
for cut: Trile_Cut.[.ROOF, .WALL] {
label := ifx cut == .ROOF then "Roof (hidden from below)" else "Wall (cut away toward camera)";
if GR.base_checkbox(r, label, editor_current_trile.cut == cut, *theme.checkbox_theme, xx cut) {
editor_current_trile.cut = ifx editor_current_trile.cut == cut then .NONE else cut;
trile_changed(editor_current_trile);
}
r.y += row_h;
}
r.y -= row_h;
r.y += row_h * 2; r.y += row_h * 2;
r.h = row_h; r.h = row_h;

View File

@ -48,6 +48,7 @@ Editor_Action :: enum {
// Trile editor — undo/redo // Trile editor — undo/redo
TRIXEL_UNDO; TRIXEL_UNDO;
TRIXEL_REDO; TRIXEL_REDO;
TRIXEL_TOOL_PICK;
} }
#scope_file; #scope_file;
@ -217,4 +218,5 @@ set_default_bindings :: () {
set(.TRIXEL_TILING_ROTATE, cast(Key_Code) #char "R"); set(.TRIXEL_TILING_ROTATE, cast(Key_Code) #char "R");
set(.TRIXEL_UNDO, cast(Key_Code) #char "Z", ctrl = true); set(.TRIXEL_UNDO, cast(Key_Code) #char "Z", ctrl = true);
set(.TRIXEL_REDO, cast(Key_Code) #char "Y", ctrl = true); set(.TRIXEL_REDO, cast(Key_Code) #char "Y", ctrl = true);
set(.TRIXEL_TOOL_PICK, cast(Key_Code) #char "K");
} }

View File

@ -140,7 +140,9 @@ init_after_core :: () {
ltriles(); ltriles();
tt := get_trile_table_ptr(); tt := get_trile_table_ptr();
if tt.count == 0 { if tt.count == 0 {
set_trile("test", Trile.{}); test : Trile;
trile_fill(*test, first_used_swatch());
set_trile("test", test);
} else { } else {
name : string; name : string;
for v : tt { for v : tt {

View File

@ -19,6 +19,7 @@ Particle_Emitter_Config :: struct {
color_start : Vector4 = .{1, 1, 1, 1}; color_start : Vector4 = .{1, 1, 1, 1};
color_end : Vector4 = .{1, 1, 1, 0}; color_end : Vector4 = .{1, 1, 1, 0};
gravity : float = 2.0; gravity : float = 2.0;
glow : float; // 0..1, like swatch emittance
blend_mode : Particle_Blend_Mode = .ADDITIVE; blend_mode : Particle_Blend_Mode = .ADDITIVE;
} }
@ -152,6 +153,7 @@ draw_live_particles :: (view: View_Id) {
cast(float)f.h / cast(float)anim.sheet_h, cast(float)f.h / cast(float)anim.sheet_h,
}, },
color = lerp_color(def.color_start, def.color_end, t), color = lerp_color(def.color_start, def.color_end, t),
glow = def.glow,
}); });
} }

View File

@ -1,59 +0,0 @@
billboards_init :: () {
desc := pipeline_desc(billboard_shader_desc(sg_query_backend()), .VIEW, depth_write = true);
desc.layout.buffers[0].step_func = .PER_INSTANCE;
desc.layout.attrs[ATTR_billboard_center] = .{ format = .FLOAT3 };
desc.layout.attrs[ATTR_billboard_size] = .{ format = .FLOAT2 };
desc.layout.attrs[ATTR_billboard_uv_rect] = .{ format = .FLOAT4 };
desc.layout.attrs[ATTR_billboard_face] = .{ format = .FLOAT3 };
billboards.pipeline = sg_make_pipeline(*desc);
}
billboards_upload :: (scene: *Scene) {
array_reset_keeping_memory(*billboards.instances);
array_reset_keeping_memory(*billboards.batches);
draws : [..]Keyed(Instance);
draws.allocator = temp;
for view_index: 0..scene.views.count-1 {
array_reset_keeping_memory(*draws);
for scene.billboards {
if it.view.index != view_index continue;
array_add(*draws, .{it.sheet.id, .{it.position, it.size, it.uv_rect, it.face}});
}
batch_instances(view_index, draws, *billboards.batches, *billboards.instances);
}
stream_upload(*billboards.stream, billboards.instances);
}
billboards_draw :: (pass: *Pass) {
sg_apply_pipeline(billboards.pipeline);
apply_uniforms(UB_view_params, *pass.params);
for billboards.batches {
if it.view_index != pass.view_index continue;
bind : sg_bindings;
bind.vertex_buffers[0] = billboards.stream.buffer;
bind.vertex_buffer_offsets[0] = it.first * size_of(Instance);
bind.images[IMG_sprite] = texture_image(xx it.key);
bind.samplers[SMP_sprite_smp] = samplers.nearest_clamp;
sg_apply_bindings(*bind);
sg_draw(0, 6, it.count);
}
}
#scope_file
Instance :: struct {
center : Vector3;
size : Vector2;
uv_rect : Vector4;
face : Vector3;
}
billboards : struct {
pipeline : sg_pipeline;
instances : [..]Instance;
batches : [..]Batch;
stream : Stream_Buffer;
}

View File

@ -1,5 +1,9 @@
VIEW_COLOR_FORMAT :: sg_pixel_format.RGBA8; // Float, so brightness above 1 survives until post-processing.
VIEW_COLOR_FORMAT :: sg_pixel_format.RGBA16F;
VIEW_DEPTH_FORMAT :: sg_pixel_format.DEPTH; VIEW_DEPTH_FORMAT :: sg_pixel_format.DEPTH;
VIEW_SCENE_FORMAT :: sg_pixel_format.R32F;
VIEW_GLOW_FORMAT :: sg_pixel_format.R16F;
SHADOW_TINT_FORMAT :: sg_pixel_format.RGBA8; // how strongly each pixel blooms, can exceed 1
ALPHA_BLEND :: sg_blend_state.{ ALPHA_BLEND :: sg_blend_state.{
enabled = true, enabled = true,
@ -7,61 +11,123 @@ ALPHA_BLEND :: sg_blend_state.{
dst_factor_rgb = .ONE_MINUS_SRC_ALPHA, dst_factor_rgb = .ONE_MINUS_SRC_ALPHA,
}; };
MULTIPLY_BLEND :: sg_blend_state.{
enabled = true,
src_factor_rgb = .DST_COLOR,
dst_factor_rgb = .ZERO,
};
ADDITIVE_BLEND :: sg_blend_state.{
enabled = true,
src_factor_rgb = .SRC_ALPHA,
dst_factor_rgb = .ONE,
};
Pipeline_Target :: enum { Pipeline_Target :: enum {
SCREEN; SCREEN;
VIEW; VIEW_OPAQUE;
VIEW_TRANSPARENT;
SHADOW;
POST; // colour only, view format
} }
samplers : struct { samplers : struct {
nearest_clamp : sg_sampler; nearest_clamp : sg_sampler;
linear_clamp : sg_sampler; linear_clamp : sg_sampler;
linear_repeat : sg_sampler; linear_repeat : sg_sampler;
shadow : sg_sampler;
} }
samplers_init :: () { samplers_init :: () {
samplers.nearest_clamp = sg_make_sampler(*(sg_sampler_desc.{ min_filter = .NEAREST, mag_filter = .NEAREST, wrap_u = .CLAMP_TO_EDGE, wrap_v = .CLAMP_TO_EDGE })); samplers.nearest_clamp = sg_make_sampler(*(sg_sampler_desc.{ min_filter = .NEAREST, mag_filter = .NEAREST, wrap_u = .CLAMP_TO_EDGE, wrap_v = .CLAMP_TO_EDGE }));
samplers.linear_clamp = sg_make_sampler(*(sg_sampler_desc.{ min_filter = .LINEAR, mag_filter = .LINEAR, wrap_u = .CLAMP_TO_EDGE, wrap_v = .CLAMP_TO_EDGE })); samplers.linear_clamp = sg_make_sampler(*(sg_sampler_desc.{ min_filter = .LINEAR, mag_filter = .LINEAR, wrap_u = .CLAMP_TO_EDGE, wrap_v = .CLAMP_TO_EDGE }));
samplers.linear_repeat = sg_make_sampler(*(sg_sampler_desc.{ min_filter = .LINEAR, mag_filter = .LINEAR, wrap_u = .REPEAT, wrap_v = .REPEAT })); samplers.linear_repeat = sg_make_sampler(*(sg_sampler_desc.{ min_filter = .LINEAR, mag_filter = .LINEAR, wrap_u = .REPEAT, wrap_v = .REPEAT }));
samplers.shadow = sg_make_sampler(*(sg_sampler_desc.{ min_filter = .NEAREST, mag_filter = .NEAREST, wrap_u = .CLAMP_TO_EDGE, wrap_v = .CLAMP_TO_EDGE, compare = .LESS_EQUAL }));
} }
// Vertex layout and anything else specific is filled in by the caller. // Vertex layout and anything else specific is filled in by the caller.
pipeline_desc :: (shader: sg_shader_desc, target: Pipeline_Target, blend := false, depth_write := false) -> sg_pipeline_desc { pipeline_desc :: (shader: sg_shader_desc, target: Pipeline_Target, blend := sg_blend_state.{}, depth_write := false) -> sg_pipeline_desc {
shader_desc := shader; shader_desc := shader;
desc : sg_pipeline_desc; desc : sg_pipeline_desc;
desc.shader = sg_make_shader(*shader_desc); desc.shader = sg_make_shader(*shader_desc);
if blend desc.colors[0].blend = ALPHA_BLEND; desc.colors[0].blend = blend;
if target == .VIEW { if target != .SCREEN {
desc.colors[0].pixel_format = VIEW_COLOR_FORMAT; desc.colors[0].pixel_format = VIEW_COLOR_FORMAT;
desc.depth.pixel_format = VIEW_DEPTH_FORMAT; desc.depth.pixel_format = VIEW_DEPTH_FORMAT;
desc.depth.compare = .LESS_EQUAL; desc.depth.compare = .LESS_EQUAL;
desc.depth.write_enabled = depth_write; desc.depth.write_enabled = depth_write;
desc.sample_count = 1; desc.sample_count = 1;
} }
// Glow is attachment 1 in both view passes, blended like colour.
if target == .VIEW_OPAQUE || target == .VIEW_TRANSPARENT {
desc.color_count = 2;
desc.colors[1].pixel_format = VIEW_GLOW_FORMAT;
desc.colors[1].blend = blend;
}
if target == .VIEW_OPAQUE {
desc.color_count = 3;
desc.colors[2].pixel_format = VIEW_SCENE_FORMAT;
}
// Shadow casters write depth only; glass turns the tint back on.
if target == .SHADOW {
desc.colors[0].pixel_format = SHADOW_TINT_FORMAT;
desc.colors[0].write_mask = .NONE;
}
if target == .POST desc.depth.pixel_format = .NONE;
return desc; return desc;
} }
Pass_Kind :: enum {
MAIN;
REFLECTION;
SHADOW;
}
Pass :: struct { Pass :: struct {
scene : *Scene; scene : *Scene;
view_index : int; view_index : int;
view : View; view : View;
reflection : bool; kind : Pass_Kind;
params : View_Params; params : View_Params;
shadow : Shadow_Params;
} }
make_pass :: (scene: *Scene, view_index: int, reflection: bool) -> Pass { make_pass :: (scene: *Scene, view_index: int, kind: Pass_Kind) -> Pass {
pass := Pass.{ scene = scene, view_index = view_index, view = scene.views[view_index], reflection = reflection }; pass := Pass.{ scene = scene, view_index = view_index, view = scene.views[view_index], kind = kind };
height := ifx pass.view.environment then pass.view.environment.reflection_height else 0; env := view_environment(pass.view);
height := env.reflection_height;
camera := pass.view.camera.position; camera := pass.view.camera.position;
view_proj := scene.view_proj[view_index]; view_proj := scene.view_proj[view_index];
pass.params.clip = .[0, 0, 0, 1]; pass.params.clip = .[0, 0, 0, 1];
if reflection { if kind == .REFLECTION {
camera.y = 2 * height - camera.y; camera.y = 2 * height - camera.y;
view_proj = scene.reflection_view_proj[view_index]; view_proj = scene.reflection_view_proj[view_index];
pass.params.clip = .[0, 1, 0, -height]; pass.params.clip = .[0, 1, 0, -height];
} }
pass.params.view_proj = view_proj.floats; if kind == .SHADOW {
// Camera-facing billboards turn to the sun, so they cast their full silhouette.
camera = pass.view.camera.target + env.sun_direction * 1000;
view_proj = scene.shadow_view_proj[view_index];
}
_, inv_view_proj := inverse(view_proj);
pass.params.view_proj = view_proj.floats;
pass.params.inv_view_proj = inv_view_proj.floats;
pass.params.camera = .[camera.x, camera.y, camera.z, scene.time]; pass.params.camera = .[camera.x, camera.y, camera.z, scene.time];
pass.params.sun_direction = .[env.sun_direction.x, env.sun_direction.y, env.sun_direction.z, 0];
pass.params.sun_color = .[env.sun_color.x, env.sun_color.y, env.sun_color.z, 0];
pass.params.shade_color = .[env.shade_color.x, env.shade_color.y, env.shade_color.z, 0];
// From shadow clip space to shadow map uv and stored depth, which differ per backend.
gl := sg_query_backend() == .GLCORE || sg_query_backend() == .GLES3;
flip := sg_query_features().origin_top_left;
to_uv := Matrix4.{
0.5, 0, 0, 0.5,
0, ifx flip then -0.5 else 0.5, 0, 0.5,
0, 0, ifx gl then 0.5 else 1, ifx gl then 0.5 else 0,
0, 0, 0, 1,
};
pass.shadow.shadow_uv = (scene.shadow_view_proj[view_index] * transpose(to_uv)).floats;
return pass; return pass;
} }
@ -133,3 +199,14 @@ batch_instances :: (view_index: int, draws: []Keyed($I), batches: *[..]Batch, in
batches.*[batches.count - 1].count += 1; batches.*[batches.count - 1].count += 1;
} }
} }
bind_shadow :: (pass: *Pass, bind: *sg_bindings) {
bind.images[IMG_shadow_map] = view_shadow_image(pass.view_index);
bind.samplers[SMP_shadow_smp] = samplers.shadow;
bind.images[IMG_shadow_tint] = view_shadow_tint_image(pass.view_index);
bind.samplers[SMP_shadow_tint_smp] = samplers.nearest_clamp;
}
view_environment :: (view: View) -> Environment {
return ifx view.environment then view.environment.* else .{};
}

View File

@ -0,0 +1,49 @@
lines_init :: () {
make :: (depth_test: bool) -> sg_pipeline {
desc := pipeline_desc(line_shader_desc(sg_query_backend()), .VIEW_TRANSPARENT, ALPHA_BLEND);
desc.layout.buffers[0].step_func = .PER_INSTANCE;
desc.layout.attrs[ATTR_line_a] = .{ format = .FLOAT3 };
desc.layout.attrs[ATTR_line_b] = .{ format = .FLOAT3 };
desc.layout.attrs[ATTR_line_color] = .{ format = .FLOAT4 };
desc.layout.attrs[ATTR_line_width] = .{ format = .FLOAT };
if !depth_test desc.depth.compare = .ALWAYS;
return sg_make_pipeline(*desc);
}
lines.depth_tested = make(true);
lines.overlay = make(false);
}
lines_upload :: (scene: *Scene) {
array_reset_keeping_memory(*lines.all);
array_add(*lines.all, ..scene.debug_lines);
array_add(*lines.all, ..scene.overlay_lines);
stream_upload(*lines.stream, lines.all);
}
lines_draw :: (pass: *Pass, width: s32, height: s32) {
if pass.kind != .MAIN return;
params := Line_Params.{ viewport = .[width * 0.5, height * 0.5] };
draw :: (pipeline: sg_pipeline, first: int, count: int) #expand {
if count == 0 return;
sg_apply_pipeline(pipeline);
apply_uniforms(UB_view_params, *pass.params);
apply_uniforms(UB_line_params, *params);
bind : sg_bindings;
bind.vertex_buffers[0] = lines.stream.buffer;
bind.vertex_buffer_offsets[0] = xx (first * size_of(Debug_Line));
sg_apply_bindings(*bind);
sg_draw(0, 6, xx count);
}
draw(lines.depth_tested, 0, pass.scene.debug_lines.count);
draw(lines.overlay, pass.scene.debug_lines.count, pass.scene.overlay_lines.count);
}
#scope_file
lines : struct {
depth_tested : sg_pipeline;
overlay : sg_pipeline;
all : [..]Debug_Line;
stream : Stream_Buffer;
}

View File

@ -0,0 +1,107 @@
// Each chunk's probes are a brick in one 3D atlas, found through an index
// texture with one texel per chunk. The trile shader filters them itself.
PROBE_BRICKS_PER_COLUMN :: 16; // bricks stack along y, then the next column along z
probes_init :: () {
zero : [8]u8;
probes.empty_index = make_volume(1, 1, 1, .RGBA8, .{4, zero.data});
probes.empty_atlas = make_volume(1, 1, 1, .RGBA16F, .{8, zero.data});
probes.index = probes.empty_index;
probes.atlas = probes.empty_atlas;
}
renderer_probes_set :: (key: Chunk_Key, data: []Probe) {
brick := table_find_pointer(*probes.bricks, key);
if brick array_free(brick.*);
else brick = table_set(*probes.bricks, key, .[]);
brick.* = array_copy(data);
probes.dirty = true;
}
renderer_probes_clear :: () {
for probes.bricks array_free(it);
table_reset(*probes.bricks);
probes.dirty = true;
}
probes_upload :: () {
if !probes.dirty return;
probes.dirty = false;
if probes.index.id != probes.empty_index.id sg_destroy_image(probes.index);
if probes.atlas.id != probes.empty_atlas.id sg_destroy_image(probes.atlas);
probes.index = probes.empty_index;
probes.atlas = probes.empty_atlas;
probes.min = .[0, 0, 0, 0];
probes.size = .[0, 0, 0, 0];
count := probes.bricks.count;
if !count return;
lo := Chunk_Key.{S32_MAX, S32_MAX, S32_MAX};
hi := Chunk_Key.{S32_MIN, S32_MIN, S32_MIN};
for probes.bricks {
lo = .{min(lo.x, it_index.x), min(lo.y, it_index.y), min(lo.z, it_index.z)};
hi = .{max(hi.x, it_index.x), max(hi.y, it_index.y), max(hi.z, it_index.z)};
}
size := s32.[hi.x - lo.x + 1, hi.y - lo.y + 1, hi.z - lo.z + 1];
index := NewArray(size[0] * size[1] * size[2] * 4, u8,, temp);
N :: CHUNK_SIZE;
columns := min(count, PROBE_BRICKS_PER_COLUMN);
layers := (count + PROBE_BRICKS_PER_COLUMN - 1) / PROBE_BRICKS_PER_COLUMN;
atlas_w, atlas_h, atlas_d := N * 3, N * columns, N * layers;
atlas := NewArray(atlas_w * atlas_h * atlas_d * 4, u16,, temp);
brick_index := 0;
for brick, key: probes.bricks {
number := brick_index + 1;
i := ((key.z - lo.z) * size[1] + (key.y - lo.y)) * size[0] + (key.x - lo.x);
index[i * 4 + 0] = xx (number & 0xFF);
index[i * 4 + 1] = xx (number >> 8);
y0 := (brick_index % PROBE_BRICKS_PER_COLUMN) * N;
z0 := (brick_index / PROBE_BRICKS_PER_COLUMN) * N;
for z: 0..N-1 for y: 0..N-1 for x: 0..N-1 {
probe := *brick[x + y * N + z * N * N];
row := ((z0 + z) * atlas_h + (y0 + y)) * atlas_w + x * 3;
memcpy(*atlas[row * 4], probe.sh.data, size_of(Probe));
}
brick_index += 1;
}
probes.index = make_volume(size[0], size[1], size[2], .RGBA8, index);
probes.atlas = make_volume(xx atlas_w, xx atlas_h, xx atlas_d, .RGBA16F, .{atlas.count * size_of(u16), xx atlas.data});
probes.min = .[xx lo.x, xx lo.y, xx lo.z, 1];
probes.size = .[xx size[0], xx size[1], xx size[2], 0];
}
probes_set_params :: (params: *Trile_Fs_Params) {
params.probe_min = probes.min;
params.probe_size = probes.size;
}
probes_bind :: (bind: *sg_bindings) {
bind.images[IMG_probe_index] = probes.index;
bind.images[IMG_probe_atlas] = probes.atlas;
bind.samplers[SMP_probe_smp] = samplers.nearest_clamp;
}
#scope_file
probes : struct {
bricks : Table(Chunk_Key, []Probe, chunk_key_hash, chunk_key_compare);
dirty : bool;
index : sg_image;
atlas : sg_image;
empty_index : sg_image;
empty_atlas : sg_image;
min : [4]float; // first chunk in the index; w = 1 when there are probes
size : [4]float; // index size in chunks
}
make_volume :: (width: s32, height: s32, depth: s32, format: sg_pixel_format, data: []u8) -> sg_image {
desc := sg_image_desc.{ type = ._3D, width = width, height = height, num_slices = depth, pixel_format = format };
desc.data.subimage[0][0] = .{ data.data, xx data.count };
return sg_make_image(*desc);
}

View File

@ -6,14 +6,21 @@
#load "gpu.jai"; #load "gpu.jai";
#load "ui.jai"; #load "ui.jai";
#load "triles.jai"; #load "triles.jai";
#load "probes.jai";
#load "views.jai"; #load "views.jai";
#load "water.jai"; #load "water.jai";
#load "billboards.jai"; #load "sprites.jai";
#load "sky.jai";
#load "lines.jai";
renderer_init :: (orientations: [ORIENTATION_COUNT]Matrix3) { renderer_init :: (orientations: [ORIENTATION_COUNT]Matrix3) {
sg_setup(*(sg_desc.{ sg_setup(*(sg_desc.{
environment = cast,force(sg_environment) sglue_environment(), environment = cast,force(sg_environment) sglue_environment(),
logger = .{ func = slog_func }, logger = .{ func = slog_func },
// Each view holds 10 attachments and 15 images; each trile mesh 2 buffers.
attachments_pool_size = 64,
image_pool_size = 512,
buffer_pool_size = 1024,
})); }));
white := u8.[255, 255, 255, 255]; white := u8.[255, 255, 255, 255];
@ -22,9 +29,12 @@ renderer_init :: (orientations: [ORIENTATION_COUNT]Matrix3) {
samplers_init(); samplers_init();
ui_init(); ui_init();
triles_init(orientations); triles_init(orientations);
probes_init();
views_init(); views_init();
water_init(); water_init();
billboards_init(); sprites_init();
sky_init();
lines_init();
} }
renderer_shutdown :: () { renderer_shutdown :: () {
@ -54,24 +64,42 @@ renderer_texture_destroy :: (id: u32) {
renderer_render :: (scene: *Scene, width: s32, height: s32) { renderer_render :: (scene: *Scene, width: s32, height: s32) {
triles_upload(scene); triles_upload(scene);
billboards_upload(scene); probes_upload();
sprites_upload(scene);
lines_upload(scene);
ui_upload(scene); ui_upload(scene);
for view, view_index: scene.views { for view, view_index: scene.views {
for reflection: bool.[true, false] { shadow := make_pass(scene, view_index, .SHADOW);
pass := make_pass(scene, view_index, reflection); view_begin_shadow_pass(view_index);
view_begin_pass(view_index, width, height, reflection); triles_draw(*shadow);
sprites_draw(*shadow, transparent = false);
triles_draw_glass_shadow(*shadow);
sg_end_pass();
for kind: Pass_Kind.[.REFLECTION, .MAIN] {
pass := make_pass(scene, view_index, kind);
view_begin_pass(view_index, width, height, kind, opaque = true);
sky_draw(*pass);
water_draw_floor(*pass);
triles_draw(*pass); triles_draw(*pass);
billboards_draw(*pass); sprites_draw(*pass, transparent = false);
sg_end_pass();
view_begin_pass(view_index, width, height, kind, opaque = false);
water_draw(*pass); water_draw(*pass);
sprites_draw(*pass, transparent = true);
lines_draw(*pass, width, height);
sg_end_pass(); sg_end_pass();
} }
view_bloom(view_index);
} }
action : sg_pass_action; action : sg_pass_action;
action.colors[0] = .{ load_action = .CLEAR, clear_value = .{0, 0, 0, 1} }; action.colors[0] = .{ load_action = .CLEAR, clear_value = .{0, 0, 0, 1} };
sg_begin_pass(*(sg_pass.{ action = action, swapchain = cast,force(sg_swapchain) sglue_swapchain() })); sg_begin_pass(*(sg_pass.{ action = action, swapchain = cast,force(sg_swapchain) sglue_swapchain() }));
for scene.views view_composite(it_index); for scene.views view_composite(it_index, view_environment(it));
ui_draw(scene, width, height); ui_draw(scene, width, height);
sg_end_pass(); sg_end_pass();
sg_commit(); sg_commit();

View File

@ -1,43 +0,0 @@
@vs vs_billboard
@include_block view
@include_block quad
in vec3 center; // bottom middle
in vec2 size;
in vec4 uv_rect;
in vec3 face;
out float v_clip;
out vec2 v_uv;
void main() {
vec2 c = quad_corner(gl_VertexIndex);
vec3 look = face;
if (face.x < -10.0) look = vec3(center.x - camera.x, 0.0, center.z - camera.z);
if (dot(look.xz, look.xz) < 0.0001) look = vec3(0.0, 0.0, 1.0);
vec3 right = normalize(cross(vec3(0.0, 1.0, 0.0), look));
vec3 world = center + right * ((c.x - 0.5) * size.x) + vec3(0.0, c.y * size.y, 0.0);
gl_Position = view_proj * vec4(world, 1.0);
v_clip = dot(vec4(world, 1.0), clip);
v_uv = vec2(uv_rect.x + c.x * uv_rect.z, uv_rect.y + (1.0 - c.y) * uv_rect.w);
}
@end
@fs fs_billboard
in float v_clip;
in vec2 v_uv;
out vec4 frag_color;
layout(binding=0) uniform texture2D sprite;
layout(binding=0) uniform sampler sprite_smp;
void main() {
if (v_clip < 0.0) discard;
vec4 sampled = texture(sampler2D(sprite, sprite_smp), v_uv);
if (sampled.a < 0.01) discard;
frag_color = vec4(sampled.rgb, 1.0);
}
@end
@program billboard vs_billboard fs_billboard

View File

@ -1,8 +1,12 @@
@block view @block view
layout(binding=0) uniform view_params { layout(binding=0) uniform view_params {
mat4 view_proj; mat4 view_proj;
mat4 inv_view_proj;
vec4 camera; // xyz = position, w = time vec4 camera; // xyz = position, w = time
vec4 clip; // plane; geometry on its negative side is dropped vec4 clip; // plane; geometry on its negative side is dropped
vec4 sun_direction;
vec4 sun_color;
vec4 shade_color;
}; };
@end @end
@ -12,3 +16,61 @@ vec2 quad_corner(int vertex_index) {
return corners[vertex_index]; return corners[vertex_index];
} }
@end @end
// Opaque passes also write how much each pixel glows, and its distance from the
// camera for water depth.
@block scene_out
layout(location=0) out vec4 frag_color;
layout(location=1) out vec4 frag_glow;
layout(location=2) out vec4 frag_scene;
void write_scene(vec3 to_camera, float glow) {
frag_glow = vec4(glow);
frag_scene = vec4(length(to_camera));
}
@end
// Transparent passes blend glow with the same alpha as colour.
@block transparent_out
layout(location=0) out vec4 frag_color;
layout(location=1) out vec4 frag_glow;
void write_transparent(vec4 color, float glow) {
frag_color = color;
frag_glow = vec4(glow, 0.0, 0.0, color.a);
}
@end
@block emissive
const float EMISSIVE_GLOW = 8.0; // bloom strength of something fully emissive
@end
@block shadow
layout(binding=3) uniform shadow_params {
mat4 shadow_uv; // world to shadow map uv and depth
};
layout(binding=7) uniform texture2D shadow_map;
layout(binding=7) uniform sampler shadow_smp;
layout(binding=8) uniform texture2D shadow_tint; // what colour glass gave the sunlight
layout(binding=8) uniform sampler shadow_tint_smp;
@image_sample_type shadow_map depth
@sampler_type shadow_smp comparison
float brightest(vec3 c) { return max(c.r, max(c.g, c.b)); }
// White in sunlight, black in shadow, coloured behind glass. Looked up at the
// centre of the trixel face, so shadow edges follow the trixel grid.
vec3 sun_shadow(vec3 world, vec3 normal) {
vec3 half_trixel = normal * (0.5 / 16.0);
vec3 face = (floor((world - half_trixel) * 16.0) + 0.5) / 16.0 + half_trixel;
vec4 p = shadow_uv * vec4(face + normal * 0.02, 1.0);
if (p.x < 0.0 || p.x > 1.0 || p.y < 0.0 || p.y > 1.0 || p.z > 1.0) return vec3(1.0);
float visible = texture(sampler2DShadow(shadow_map, shadow_smp), vec3(p.xy, p.z - 0.0005));
return visible * texture(sampler2D(shadow_tint, shadow_tint_smp), p.xy).rgb;
}
@end
@block fullscreen
// Clip-space corner of a triangle that covers the screen, for vertices 0..2.
vec2 fullscreen_position(int vertex_index) {
return vec2((vertex_index << 1) & 2, vertex_index & 2) * 2.0 - 1.0;
}
@end

View File

@ -1,27 +0,0 @@
@vs vs_composite
layout(binding=0) uniform composite_vs_params {
float flip_y;
};
out vec2 v_uv;
void main() {
vec2 p = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
v_uv = vec2(p.x, mix(p.y, 1.0 - p.y, flip_y));
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
}
@end
@fs fs_composite
in vec2 v_uv;
out vec4 frag_color;
layout(binding=0) uniform texture2D view_color;
layout(binding=0) uniform sampler composite_smp;
void main() {
frag_color = texture(sampler2D(view_color, composite_smp), v_uv);
}
@end
@program composite vs_composite fs_composite

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
@vs vs_line
@include_block view
@include_block quad
layout(binding=1) uniform line_params {
vec2 viewport; // in pixels
};
in vec3 a;
in vec3 b;
in vec4 color;
in float width; // in pixels
out vec4 v_color;
// A quad from a to b, widened across the line in screen space.
void main() {
vec2 c = quad_corner(gl_VertexIndex); // x: a to b, y: side
vec4 clip_a = view_proj * vec4(a, 1.0);
vec4 clip_b = view_proj * vec4(b, 1.0);
vec2 screen_a = clip_a.xy / max(clip_a.w, 0.0001) * viewport;
vec2 screen_b = clip_b.xy / max(clip_b.w, 0.0001) * viewport;
vec2 dir = screen_b - screen_a;
dir = dot(dir, dir) > 0.0001 ? normalize(dir) : vec2(1.0, 0.0);
vec4 p = mix(clip_a, clip_b, c.x);
p.xy += vec2(-dir.y, dir.x) * (c.y - 0.5) * width / viewport * p.w;
gl_Position = p;
v_color = color;
}
@end
@fs fs_line
@include_block transparent_out
in vec4 v_color;
void main() {
write_transparent(v_color, 0.0);
}
@end
@program line vs_line fs_line

View File

@ -0,0 +1,90 @@
// Texture coordinates come from the pixel position, which lines up with
// render-target textures the same way on every backend.
@vs vs_fullscreen
@include_block fullscreen
void main() {
gl_Position = vec4(fullscreen_position(gl_VertexIndex), 0.0, 1.0);
}
@end
// The first level: colour times glow, averaged over each 2x2 block of view pixels.
@fs fs_bloom_glow
layout(binding=0) uniform texture2D bloom_source;
layout(binding=1) uniform texture2D bloom_glow;
layout(binding=0) uniform sampler bloom_smp;
out vec4 frag_color;
vec3 glowing(ivec2 p) {
return texelFetch(sampler2D(bloom_source, bloom_smp), p, 0).rgb * texelFetch(sampler2D(bloom_glow, bloom_smp), p, 0).r;
}
void main() {
ivec2 p = ivec2(gl_FragCoord.xy) * 2;
vec3 c = (glowing(p) + glowing(p + ivec2(1, 0)) + glowing(p + ivec2(0, 1)) + glowing(p + ivec2(1, 1))) * 0.25;
frag_color = vec4(c, 1.0);
}
@end
@fs fs_bloom_down
layout(binding=0) uniform texture2D bloom_source;
layout(binding=0) uniform sampler bloom_smp;
out vec4 frag_color;
vec3 tap(vec2 uv) { return texture(sampler2D(bloom_source, bloom_smp), uv).rgb; }
void main() {
vec2 texel = 1.0 / vec2(textureSize(sampler2D(bloom_source, bloom_smp), 0));
vec2 uv = gl_FragCoord.xy * 2.0 * texel;
vec3 c = (tap(uv + texel * vec2(-1, -1)) + tap(uv + texel * vec2(1, -1)) +
tap(uv + texel * vec2(-1, 1)) + tap(uv + texel * vec2(1, 1))) * 0.25;
frag_color = vec4(c, 1.0);
}
@end
@fs fs_bloom_up
layout(binding=0) uniform texture2D bloom_source;
layout(binding=0) uniform sampler bloom_smp;
out vec4 frag_color;
vec3 tap(vec2 uv) { return texture(sampler2D(bloom_source, bloom_smp), uv).rgb; }
// A 3x3 tent over the smaller level, added onto the larger one.
void main() {
vec2 texel = 1.0 / vec2(textureSize(sampler2D(bloom_source, bloom_smp), 0));
vec2 uv = gl_FragCoord.xy * 0.5 * texel;
vec3 c = tap(uv) * 4.0
+ (tap(uv + texel * vec2(-1, 0)) + tap(uv + texel * vec2(1, 0)) + tap(uv + texel * vec2(0, -1)) + tap(uv + texel * vec2(0, 1))) * 2.0
+ (tap(uv + texel * vec2(-1, -1)) + tap(uv + texel * vec2(1, -1)) + tap(uv + texel * vec2(-1, 1)) + tap(uv + texel * vec2(1, 1)));
frag_color = vec4(c / 16.0, 1.0);
}
@end
@fs fs_post
layout(binding=0) uniform post_params {
float exposure; // in stops
float contrast;
float saturation;
float bloom_amount;
};
layout(binding=0) uniform texture2D view_color;
layout(binding=1) uniform texture2D bloom;
layout(binding=0) uniform sampler view_smp;
layout(binding=1) uniform sampler bloom_view_smp;
out vec4 frag_color;
void main() {
vec2 uv = gl_FragCoord.xy / vec2(textureSize(sampler2D(view_color, view_smp), 0));
vec3 c = texture(sampler2D(view_color, view_smp), uv).rgb;
c += texture(sampler2D(bloom, bloom_view_smp), uv).rgb * bloom_amount;
c *= exp2(exposure);
c = (c - 0.5) * contrast + 0.5;
c = mix(vec3(dot(c, vec3(0.2126, 0.7152, 0.0722))), c, saturation);
frag_color = vec4(clamp(c, 0.0, 1.0), 1.0);
}
@end
@program bloom_glow vs_fullscreen fs_bloom_glow
@program bloom_down vs_fullscreen fs_bloom_down
@program bloom_up vs_fullscreen fs_bloom_up
@program post vs_fullscreen fs_post

View File

@ -1,6 +1,8 @@
@include common.glsl @include common.glsl
@include ui.glsl @include ui.glsl
@include composite.glsl @include post.glsl
@include trile.glsl @include trile.glsl
@include water.glsl @include water.glsl
@include billboard.glsl @include sprite.glsl
@include sky.glsl
@include line.glsl

View File

@ -0,0 +1,53 @@
@vs vs_sky
@include_block view
@include_block fullscreen
out vec4 v_point;
out vec3 v_camera;
void main() {
vec2 p = fullscreen_position(gl_VertexIndex);
gl_Position = vec4(p, 0.0, 1.0);
v_point = inv_view_proj * vec4(p, 0.5, 1.0);
v_camera = camera.xyz;
}
@end
@fs fs_sky
@include_block scene_out
in vec4 v_point;
in vec3 v_camera;
layout(binding=1) uniform sky_params {
vec4 sky_base;
vec4 sky_top;
vec4 sun_disk;
vec4 sun_halo;
vec4 horizon_halo;
vec4 sun; // xyz = direction, w = sky intensity
};
void main() {
vec3 dir = normalize(v_point.xyz / v_point.w - v_camera);
vec3 color = mix(sky_base.rgb, sky_top.rgb, clamp(dir.y * 2.0, 0.0, 0.7));
// The sun and its halo are squares in the sun's own frame.
vec3 sun_dir = sun.xyz;
vec3 sun_right = normalize(cross(sun_dir, vec3(0.0, 1.0, 0.0)));
vec3 sun_up = cross(sun_right, sun_dir);
vec3 off = dir - sun_dir * dot(dir, sun_dir);
float square = max(abs(dot(off, sun_right)), abs(dot(off, sun_up)));
float halo = clamp((0.25 - square) * 6.0, 0.0, 1.0);
color += sun_halo.rgb * halo * halo;
bool disk = square < 0.032 && dot(dir, sun_dir) > 0.0;
if (disk) color = sun_disk.rgb * 3.0;
color += horizon_halo.rgb * (1.0 - clamp(abs(dir.y) * 20.0, 0.0, 1.0)) * 0.8;
frag_color = vec4(color * sun.w, 1.0);
frag_glow = vec4(disk ? 1.0 : 0.0);
frag_scene = vec4(1.0e4);
}
@end
@program sky vs_sky fs_sky

View File

@ -0,0 +1,97 @@
@vs vs_sprite
@include_block view
@include_block quad
layout(binding=1) uniform sprite_params {
float lit;
};
in vec3 center; // bottom middle
in vec2 size;
in vec4 uv_rect;
in vec3 face;
in vec4 color;
in float glow;
out float v_clip;
out vec3 v_to_camera;
out vec2 v_uv;
out vec4 v_color;
out vec3 v_sun;
out vec3 v_shade;
out vec3 v_anchor;
out float v_glow;
void main() {
vec2 c = quad_corner(gl_VertexIndex);
vec3 look = face;
if (face.x < -10.0) look = vec3(center.x - camera.x, 0.0, center.z - camera.z);
if (dot(look.xz, look.xz) < 0.0001) look = vec3(0.0, 0.0, 1.0);
vec3 right = normalize(cross(vec3(0.0, 1.0, 0.0), look));
vec3 world = center + right * ((c.x - 0.5) * size.x) + vec3(0.0, c.y * size.y, 0.0);
gl_Position = view_proj * vec4(world, 1.0);
v_clip = dot(vec4(world, 1.0), clip);
v_to_camera = camera.xyz - world;
v_uv = vec2(uv_rect.x + c.x * uv_rect.z, uv_rect.y + (1.0 - c.y) * uv_rect.w);
v_color = color;
v_glow = glow;
v_sun = lit > 0.5 ? sun_color.rgb : vec3(1.0);
v_shade = lit > 0.5 ? shade_color.rgb : vec3(1.0);
// The whole sprite takes the shadow found inside its body, nudged toward the
// sun so it doesn't find its own shadow.
v_anchor = center + vec3(0.0, size.y * 0.5, 0.0) + sun_direction.xyz * 0.3;
}
@end
@block sprite_fs
in float v_clip;
in vec3 v_to_camera;
in vec2 v_uv;
in vec4 v_color;
in vec3 v_sun;
in vec3 v_shade;
in vec3 v_anchor;
in float v_glow;
layout(binding=0) uniform texture2D sprite;
layout(binding=0) uniform sampler sprite_smp;
vec4 sprite_color() {
if (v_clip < 0.0) discard;
vec4 sampled = texture(sampler2D(sprite, sprite_smp), v_uv);
if (sampled.a < 0.01) discard;
return sampled * v_color;
}
@end
@fs fs_sprite
@include_block scene_out
@include_block shadow
@include_block sprite_fs
void main() {
vec3 light = mix(v_shade, v_sun, sun_shadow(v_anchor, vec3(0.0, 1.0, 0.0)));
frag_color = sprite_color() * vec4(light, 1.0);
write_scene(v_to_camera, 0.0);
}
@end
@fs fs_particle
@include_block sprite_fs
@include_block transparent_out
@include_block emissive
void main() {
write_transparent(sprite_color(), v_glow * EMISSIVE_GLOW);
}
@end
@fs fs_sprite_shadow
@include_block sprite_fs
void main() {
sprite_color();
}
@end
@program sprite vs_sprite fs_sprite
@program particle vs_sprite fs_particle
@program sprite_shadow vs_sprite fs_sprite_shadow

View File

@ -3,51 +3,223 @@
layout(binding=1) uniform trile_params { layout(binding=1) uniform trile_params {
vec4 orientations[72]; vec4 orientations[72];
float show_hidden; // shadow and reflection passes still draw cut triles
}; };
in vec3 position; in vec3 position;
in float swatch; in vec4 data_n; // bytes / 255: x = swatch, y = ao (0..3), z = normal index
in float ao;
in vec4 instance; // xyz = position, w = orientation in vec4 instance; // xyz = position, w = orientation
in float tint; in float tint;
in float cut;
out float v_clip; out float v_clip;
out vec3 v_to_camera;
out vec3 v_world;
out vec3 v_normal;
out float v_swatch; out float v_swatch;
out float v_facing;
out float v_ao; out float v_ao;
out vec3 v_shade;
out vec3 v_sun;
out float v_tint; out float v_tint;
out float v_cut;
void main() { void main() {
vec4 data = data_n * 255.0;
int ori = int(instance.w + 0.5) * 3; int ori = int(instance.w + 0.5) * 3;
vec3 p = position - 0.5; mat3 rotation = transpose(mat3(orientations[ori].xyz, orientations[ori + 1].xyz, orientations[ori + 2].xyz));
vec3 world = vec3(dot(orientations[ori].xyz, p), vec3 world = rotation * (position - 0.5) + 0.5 + instance.xyz;
dot(orientations[ori + 1].xyz, p),
dot(orientations[ori + 2].xyz, p)) + 0.5 + instance.xyz; int n = int(data.z + 0.5);
vec3 normal = vec3(0.0);
normal[n / 2] = (n % 2 == 0) ? 1.0 : -1.0;
v_normal = rotation * normal;
v_facing = smoothstep(0.0, 0.3, dot(v_normal, sun_direction.xyz));
v_ao = data.y / 3.0;
v_shade = shade_color.rgb;
v_sun = sun_color.rgb;
gl_Position = view_proj * vec4(world, 1.0); gl_Position = view_proj * vec4(world, 1.0);
v_cut = show_hidden > 0.5 ? 0.0 : cut;
// Fully cut triles are collapsed outside the clip volume, so nothing is drawn.
if (v_cut > 0.999) gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
v_to_camera = camera.xyz - world;
v_world = world;
v_clip = dot(vec4(world, 1.0), clip); v_clip = dot(vec4(world, 1.0), clip);
v_swatch = swatch; v_swatch = data.x;
v_ao = ao;
v_tint = tint; v_tint = tint;
} }
@end @end
@fs fs_trile @block trile_in
in float v_clip; in float v_clip;
in vec3 v_to_camera;
in vec3 v_world;
in vec3 v_normal;
in float v_swatch; in float v_swatch;
in float v_facing;
in float v_ao; in float v_ao;
in vec3 v_shade;
in vec3 v_sun;
in float v_tint; in float v_tint;
out vec4 frag_color; in float v_cut;
@end
@block palette
layout(binding=0) uniform texture2D palette; layout(binding=0) uniform texture2D palette;
layout(binding=0) uniform sampler palette_smp; layout(binding=0) uniform sampler palette_smp;
@end
@fs fs_trile
@include_block scene_out
@include_block emissive
@include_block shadow
@include_block trile_in
@include_block palette
layout(binding=2) uniform trile_fs_params {
vec4 sun_dir; // towards the sun
vec4 probe_min; // xyz = first chunk in probe_index, w = 1 when there are probes
vec4 probe_size; // xyz = probe_index size in chunks
float use_reflection; // the reflection pass can't read its own image
float reflection_height;
};
layout(binding=1) uniform texture2D trile_reflection;
layout(binding=2) uniform texture2D trile_reflection_glow;
layout(binding=1) uniform sampler trile_reflection_smp;
layout(binding=3) uniform texture3D probe_index; // per chunk: brick number + 1 in r + g * 256, 0 = none
layout(binding=4) uniform texture3D probe_atlas; // per brick 32^3 probes, 3 texels each: red, green, blue
layout(binding=2) uniform sampler probe_smp;
const float GLOSS_GLOW = 4.0; // how much a full gloss highlight blooms
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;
// The probe at the centre of the trile at cell, unless there is none or it is inside geometry.
bool probe_at(ivec3 cell, out vec4 r, out vec4 g, out vec4 b) {
ivec3 chunk = ivec3(floor(vec3(cell) / 32.0));
ivec3 slot = chunk - ivec3(probe_min.xyz);
if (any(lessThan(slot, ivec3(0))) || any(greaterThanEqual(slot, ivec3(probe_size.xyz)))) return false;
vec2 number = texelFetch(sampler3D(probe_index, probe_smp), slot, 0).rg * 255.0 + 0.5;
int brick = int(number.x) + int(number.y) * 256 - 1;
if (brick < 0) return false;
ivec3 local = cell - chunk * 32;
ivec3 texel = ivec3(local.x * 3, (brick % PROBE_BRICKS_PER_COLUMN) * 32 + local.y, (brick / PROBE_BRICKS_PER_COLUMN) * 32 + local.z);
r = texelFetch(sampler3D(probe_atlas, probe_smp), texel, 0);
g = texelFetch(sampler3D(probe_atlas, probe_smp), texel + ivec3(1, 0, 0), 0);
b = texelFetch(sampler3D(probe_atlas, probe_smp), texel + ivec3(2, 0, 0), 0);
return r.x >= 0.0;
}
// Light from the eight probes around a point half a trile out from the surface,
// leaving out probes behind the surface or inside geometry. Like sun_shadow it is
// taken at the trixel face centre, so it steps per trixel. a = 0 when no probe counts.
vec4 probe_light(vec3 world, vec3 normal) {
vec3 half_trixel = normal * (0.5 / 16.0);
vec3 face = (floor((world - half_trixel) * 16.0) + 0.5) / 16.0 + half_trixel;
vec3 grid = face + normal * 0.5 - 0.5;
ivec3 base = ivec3(floor(grid));
vec3 f = grid - vec3(base);
vec4 sum_r = vec4(0.0), sum_g = vec4(0.0), sum_b = vec4(0.0);
float total = 0.0;
for (int i = 0; i < 8; i++) {
ivec3 offset = ivec3(i & 1, (i >> 1) & 1, i >> 2);
ivec3 cell = base + offset;
if (dot(vec3(cell) + 0.5 - face, normal) < 0.01) continue;
vec4 r, g, b;
if (!probe_at(cell, r, g, b)) continue;
vec3 t = mix(1.0 - f, f, vec3(offset));
float w = max(t.x * t.y * t.z, 0.0001);
sum_r += r * w;
sum_g += g * w;
sum_b += b * w;
total += w;
}
if (total == 0.0) return vec4(0.0);
// Irradiance over pi from the SH: the cosine lobe convolved, both bands.
vec4 lobe = vec4(0.282095, 0.325735 * normal.y, 0.325735 * normal.z, 0.325735 * normal.x);
return vec4(max(vec3(dot(sum_r, lobe), dot(sum_g, lobe), dot(sum_b, lobe)) / total, 0.0), 1.0);
}
// Ordered dither, so dissolving triles fade without blending.
float dither(vec2 pixel) {
const float bayer[16] = float[16](0.0, 8.0, 2.0, 10.0, 12.0, 4.0, 14.0, 6.0, 3.0, 11.0, 1.0, 9.0, 15.0, 7.0, 13.0, 5.0);
ivec2 p = ivec2(pixel) % 4;
return (bayer[p.y * 4 + p.x] + 0.5) / 16.0;
}
void main() { void main() {
if (v_clip < 0.0) discard; if (v_clip < 0.0 || v_cut > dither(gl_FragCoord.xy)) discard;
vec3 albedo = texelFetch(sampler2D(palette, palette_smp), ivec2(int(v_swatch + 0.5), 0), 0).rgb; int id = int(v_swatch + 0.5);
albedo *= mix(0.4, 1.0, v_ao); vec4 swatch = texelFetch(sampler2D(palette, palette_smp), ivec2(id, 0), 0);
vec4 material = texelFetch(sampler2D(palette, palette_smp), ivec2(id, 1), 0); // g = reflectivity, b = gloss
vec3 albedo = swatch.rgb;
if (v_tint > 1.5) albedo = mix(albedo, vec3(1.0, 0.2, 0.2), 0.5); 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); else if (v_tint > 0.5) albedo = mix(albedo, vec3(0.2, 1.0, 0.2), 0.5);
frag_color = vec4(albedo, 1.0); 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;
// 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));
}
float glow = swatch.a * EMISSIVE_GLOW;
// Glossier is both stronger and sharper.
if (material.b > 0.0) {
vec3 halfway = normalize(normalize(v_to_camera) + sun_dir.xyz);
float highlight = pow(max(dot(v_normal, halfway), 0.0), mix(4.0, 96.0, material.b)) * material.b;
color += v_sun * sun * highlight;
glow += highlight * brightest(sun) * GLOSS_GLOW;
}
// Only floors lying on the reflection plane can show the planar reflection.
if (use_reflection > 0.5 && material.g > 0.0 && v_normal.y > 0.5 && abs(v_world.y - reflection_height) < 0.02) {
vec2 screen = gl_FragCoord.xy / vec2(textureSize(sampler2D(trile_reflection, trile_reflection_smp), 0));
vec2 uv = vec2(screen.x, 1.0 - screen.y);
color = mix(color, texture(sampler2D(trile_reflection, trile_reflection_smp), uv).rgb, material.g);
glow = max(glow, texture(sampler2D(trile_reflection_glow, trile_reflection_smp), uv).r * pow(material.g, REFLECTED_GLOW_CURVE));
}
// Emissive swatches show their own colour whatever the lighting.
frag_color = vec4(mix(color, albedo, swatch.a), 1.0);
write_scene(v_to_camera, glow);
}
@end
@fs fs_trile_shadow
@include_block trile_in
@include_block palette
void main() {
// Glass lets the sun through.
if (texelFetch(sampler2D(palette, palette_smp), ivec2(int(v_swatch + 0.5), 1), 0).r > 0.5) discard;
}
@end
// Drawn after the opaque casters and multiplied into the shadow tint, so light
// through several panes takes all their colours.
@fs fs_trile_glass_shadow
@include_block trile_in
@include_block palette
out vec4 frag_color;
void main() {
int id = int(v_swatch + 0.5);
if (texelFetch(sampler2D(palette, palette_smp), ivec2(id, 1), 0).r < 0.5) discard;
vec3 c = texelFetch(sampler2D(palette, palette_smp), ivec2(id, 0), 0).rgb;
// Tinted rather than darkened: the glass's brightest channel passes fully.
frag_color = vec4(c / max(max(c.r, max(c.g, c.b)), 0.001), 1.0);
} }
@end @end
@program trile vs_trile fs_trile @program trile vs_trile fs_trile
@program trile_shadow vs_trile fs_trile_shadow
@program trile_glass_shadow vs_trile fs_trile_glass_shadow

View File

@ -8,57 +8,80 @@ layout(binding=1) uniform water_vs_params {
}; };
out vec3 v_to_camera; out vec3 v_to_camera;
out vec4 v_normal_uv; out vec3 v_world;
out vec2 v_xz;
void main() { void main() {
vec2 xz = camera.xz + (quad_corner(gl_VertexIndex) * 2.0 - 1.0) * half_size; v_xz = camera.xz + (quad_corner(gl_VertexIndex) * 2.0 - 1.0) * half_size;
vec3 world = vec3(xz.x, height, xz.y); vec3 world = vec3(v_xz.x, height, v_xz.y);
gl_Position = view_proj * vec4(world, 1.0); gl_Position = view_proj * vec4(world, 1.0);
vec2 flow = camera.w * vec2(-0.005, -0.012);
v_normal_uv = vec4(xz * 0.4 + flow * 1.5, xz * 0.1 + flow * 1.7);
v_to_camera = camera.xyz - world; v_to_camera = camera.xyz - world;
v_world = world;
}
@end
@fs fs_water_floor
@include_block scene_out
in vec3 v_to_camera;
in vec3 v_world;
in vec2 v_xz;
layout(binding=2) uniform water_floor_params {
vec3 floor_color;
};
void main() {
frag_color = vec4(floor_color, 1.0);
write_scene(v_to_camera, 0.0);
} }
@end @end
@fs fs_water @fs fs_water
@include_block shadow
@include_block transparent_out
in vec3 v_to_camera; in vec3 v_to_camera;
in vec4 v_normal_uv; in vec3 v_world;
out vec4 frag_color; in vec2 v_xz;
layout(binding=2) uniform water_fs_params { layout(binding=2) uniform water_fs_params {
vec3 color; vec3 water_color;
float surface; vec3 sun_direction; float shininess;
vec3 sun_color; float time;
float reflectivity; float reflectivity;
float distortion; float distortion;
}; };
layout(binding=0) uniform texture2D reflection; layout(binding=0) uniform texture2D reflection;
layout(binding=1) uniform texture2D normals; layout(binding=1) uniform texture2D normals;
layout(binding=2) uniform texture2D reflection_glow;
layout(binding=0) uniform sampler reflection_smp; layout(binding=0) uniform sampler reflection_smp;
layout(binding=1) uniform sampler normals_smp; layout(binding=1) uniform sampler normals_smp;
void main() { void main() {
if (surface < 0.5) { vec2 flow = time * vec2(-0.005, -0.012);
frag_color = vec4(color, 1.0); vec3 n1 = texture(sampler2D(normals, normals_smp), v_xz * 0.4 + flow * 1.5).xzy * 2.0 - 1.0;
return; vec3 n2 = texture(sampler2D(normals, normals_smp), v_xz * 0.1 + flow * 1.7).xzy * 2.0 - 1.0;
}
vec3 n1 = texture(sampler2D(normals, normals_smp), v_normal_uv.xy).xzy * 2.0 - 1.0;
vec3 n2 = texture(sampler2D(normals, normals_smp), v_normal_uv.zw).xzy * 2.0 - 1.0;
vec3 normal = normalize(n1 + n2); vec3 normal = normalize(n1 + n2);
vec3 view_dir = normalize(v_to_camera);
float facing = max(dot(normalize(v_to_camera), normal), 0.0); float facing = max(dot(view_dir, normal), 0.0);
float fresnel = min(1.0, 0.02 + 0.98 * pow(1.0 - facing, 5.0) + 0.45); float fresnel = min(1.0, 0.02 + 0.98 * pow(1.0 - facing, 5.0) + 0.45);
vec2 uv = gl_FragCoord.xy / vec2(textureSize(sampler2D(reflection, reflection_smp), 0)); vec2 screen = gl_FragCoord.xy / vec2(textureSize(sampler2D(reflection, reflection_smp), 0));
uv.y = 1.0 - uv.y; vec2 reflect_uv = vec2(screen.x, 1.0 - screen.y) + normal.xz * distortion;
vec3 reflected = texture(sampler2D(reflection, reflection_smp), uv + normal.xz * distortion).rgb; vec3 reflected = texture(sampler2D(reflection, reflection_smp), reflect_uv).rgb;
vec3 surface_color = mix(color, reflected, mix(fresnel, 1.0, reflectivity)); float reflect_amount = mix(fresnel, 1.0, reflectivity);
vec3 color = mix(water_color, reflected, reflect_amount);
float alpha = mix(mix(0.3, 0.5, fresnel), 1.0, reflectivity); float alpha = mix(mix(0.3, 0.5, fresnel), 1.0, reflectivity);
frag_color = vec4(surface_color, alpha);
vec3 halfway = normalize(view_dir + sun_direction);
vec3 sun = sun_shadow(v_world, vec3(0.0, 1.0, 0.0));
float glint = smoothstep(0.5, 0.55, pow(max(dot(normal, halfway), 0.0), shininess)) * brightest(sun);
float reflected_glow = texture(sampler2D(reflection_glow, reflection_smp), reflect_uv).r * reflect_amount;
write_transparent(vec4(mix(color, sun_color, glint), max(alpha, glint)), max(glint, reflected_glow));
} }
@end @end
@program water_floor vs_water fs_water_floor
@program water vs_water fs_water @program water vs_water fs_water

View File

@ -0,0 +1,29 @@
sky_init :: () {
desc := pipeline_desc(sky_shader_desc(sg_query_backend()), .VIEW_OPAQUE);
desc.depth.compare = .ALWAYS;
sky.pipeline = sg_make_pipeline(*desc);
}
sky_draw :: (pass: *Pass) {
env := view_environment(pass.view);
if !env.sky return;
params := Sky_Params.{
sky_base = .[env.sky_base.x, env.sky_base.y, env.sky_base.z, 0],
sky_top = .[env.sky_top.x, env.sky_top.y, env.sky_top.z, 0],
sun_disk = .[env.sun_disk.x, env.sun_disk.y, env.sun_disk.z, 0],
sun_halo = .[env.sun_halo.x, env.sun_halo.y, env.sun_halo.z, 0],
horizon_halo = .[env.horizon_halo.x, env.horizon_halo.y, env.horizon_halo.z, 0],
sun = .[env.sun_direction.x, env.sun_direction.y, env.sun_direction.z, env.sky_intensity],
};
sg_apply_pipeline(sky.pipeline);
apply_uniforms(UB_view_params, *pass.params);
apply_uniforms(UB_sky_params, *params);
sg_draw(0, 3, 1);
}
#scope_file
sky : struct {
pipeline : sg_pipeline;
}

View File

@ -0,0 +1,101 @@
sprites_init :: () {
// Billboards and particles share the vertex shader, so they share attribute slots.
make :: (shader: sg_shader_desc, target: Pipeline_Target, blend := sg_blend_state.{}) -> sg_pipeline {
desc := pipeline_desc(shader, target, blend, depth_write = target != .VIEW_TRANSPARENT);
desc.layout.buffers[0].step_func = .PER_INSTANCE;
desc.layout.attrs[ATTR_sprite_center] = .{ format = .FLOAT3 };
desc.layout.attrs[ATTR_sprite_size] = .{ format = .FLOAT2 };
desc.layout.attrs[ATTR_sprite_uv_rect] = .{ format = .FLOAT4 };
desc.layout.attrs[ATTR_sprite_face] = .{ format = .FLOAT3 };
desc.layout.attrs[ATTR_sprite_color] = .{ format = .FLOAT4 };
desc.layout.attrs[ATTR_sprite_glow] = .{ format = .FLOAT };
return sg_make_pipeline(*desc);
}
backend := sg_query_backend();
sprites.pipelines[Kind.BILLBOARD] = make(sprite_shader_desc(backend), .VIEW_OPAQUE);
sprites.pipelines[Kind.PARTICLE_ALPHA] = make(particle_shader_desc(backend), .VIEW_TRANSPARENT, ALPHA_BLEND);
sprites.pipelines[Kind.PARTICLE_ADDITIVE] = make(particle_shader_desc(backend), .VIEW_TRANSPARENT, ADDITIVE_BLEND);
sprites.shadow_pipeline = make(sprite_shadow_shader_desc(backend), .SHADOW);
}
sprites_upload :: (scene: *Scene) {
array_reset_keeping_memory(*sprites.instances);
array_reset_keeping_memory(*sprites.batches);
draws : [..]Keyed(Instance);
draws.allocator = temp;
for view_index: 0..scene.views.count-1 {
array_reset_keeping_memory(*draws);
for scene.billboards {
if it.view.index != view_index continue;
array_add(*draws, .{key(.BILLBOARD, it.sheet), .{it.position, it.size, it.uv_rect, it.face, .{1, 1, 1, 1}, 0}});
}
for batch: scene.particles {
if batch.view.index != view_index continue;
kind := ifx batch.blend == .ADDITIVE then Kind.PARTICLE_ADDITIVE else .PARTICLE_ALPHA;
for batch.particles {
array_add(*draws, .{key(kind, batch.sheet), .{it.position, .{it.size, it.size}, it.uv_rect, BILLBOARD_FACE_CAMERA, it.color, it.glow}});
}
}
batch_instances(view_index, draws, *sprites.batches, *sprites.instances);
}
stream_upload(*sprites.stream, sprites.instances);
}
// Opaque sprites go before water, transparent ones after it. Only billboards cast shadows.
sprites_draw :: (pass: *Pass, transparent: bool) {
shadow := pass.kind == .SHADOW;
current : Kind = xx -1;
for sprites.batches {
if it.view_index != pass.view_index continue;
kind := cast(Kind) (it.key >> 32);
if (kind != .BILLBOARD) != transparent continue;
if kind != current {
current = kind;
sg_apply_pipeline(ifx shadow then sprites.shadow_pipeline else sprites.pipelines[kind]);
apply_uniforms(UB_view_params, *pass.params);
params := Sprite_Params.{ lit = xx (kind == .BILLBOARD) };
apply_uniforms(UB_sprite_params, *params);
if kind == .BILLBOARD && !shadow apply_uniforms(UB_shadow_params, *pass.shadow);
}
bind : sg_bindings;
bind.vertex_buffers[0] = sprites.stream.buffer;
bind.vertex_buffer_offsets[0] = it.first * size_of(Instance);
bind.images[IMG_sprite] = texture_image(xx (it.key & 0xFFFF_FFFF));
bind.samplers[SMP_sprite_smp] = samplers.nearest_clamp;
if kind == .BILLBOARD && !shadow bind_shadow(pass, *bind);
sg_apply_bindings(*bind);
sg_draw(0, 6, it.count);
}
}
#scope_file
Kind :: enum {
BILLBOARD;
PARTICLE_ALPHA;
PARTICLE_ADDITIVE;
}
Instance :: struct {
center : Vector3;
size : Vector2;
uv_rect : Vector4;
face : Vector3;
color : Vector4;
glow : float;
}
sprites : struct {
pipelines : [#run enum_highest_value(Kind) + 1]sg_pipeline;
shadow_pipeline : sg_pipeline;
instances : [..]Instance;
batches : [..]Batch;
stream : Stream_Buffer;
}
key :: (kind: Kind, sheet: Texture) -> s64 {
return (cast(s64) kind << 32) | sheet.id;
}

View File

@ -1,16 +1,32 @@
triles_init :: (orientations: [ORIENTATION_COUNT]Matrix3) { triles_init :: (orientations: [ORIENTATION_COUNT]Matrix3) {
desc := pipeline_desc(trile_shader_desc(sg_query_backend()), .VIEW, depth_write = true); desc := pipeline_desc(trile_shader_desc(sg_query_backend()), .VIEW_OPAQUE, depth_write = true);
desc.layout.buffers[1].step_func = .PER_INSTANCE; desc.layout.buffers[1].step_func = .PER_INSTANCE;
desc.layout.attrs[ATTR_trile_position] = .{ format = .FLOAT3 }; desc.layout.attrs[ATTR_trile_position] = .{ format = .FLOAT3 };
desc.layout.attrs[ATTR_trile_swatch] = .{ format = .FLOAT }; desc.layout.attrs[ATTR_trile_data_n] = .{ format = .UBYTE4N };
desc.layout.attrs[ATTR_trile_ao] = .{ format = .FLOAT };
desc.layout.attrs[ATTR_trile_instance] = .{ format = .FLOAT4, buffer_index = 1 }; desc.layout.attrs[ATTR_trile_instance] = .{ format = .FLOAT4, buffer_index = 1 };
desc.layout.attrs[ATTR_trile_tint] = .{ format = .FLOAT, buffer_index = 1 }; desc.layout.attrs[ATTR_trile_tint] = .{ format = .FLOAT, buffer_index = 1 };
desc.layout.attrs[ATTR_trile_cut] = .{ format = .FLOAT, buffer_index = 1 };
desc.index_type = .UINT16; desc.index_type = .UINT16;
desc.cull_mode = .BACK; desc.cull_mode = .BACK;
desc.face_winding = .CCW; desc.face_winding = .CCW;
triles.pipeline = sg_make_pipeline(*desc); triles.pipeline = sg_make_pipeline(*desc);
// Back faces only, so lit faces don't shadow themselves.
shadow_desc := pipeline_desc(trile_shadow_shader_desc(sg_query_backend()), .SHADOW, depth_write = true);
shadow_desc.layout = desc.layout;
shadow_desc.index_type = .UINT16;
shadow_desc.cull_mode = .FRONT;
shadow_desc.face_winding = .CCW;
triles.shadow_pipeline = sg_make_pipeline(*shadow_desc);
glass_desc := pipeline_desc(trile_glass_shadow_shader_desc(sg_query_backend()), .SHADOW, MULTIPLY_BLEND);
glass_desc.colors[0].write_mask = .RGB;
glass_desc.layout = desc.layout;
glass_desc.index_type = .UINT16;
glass_desc.cull_mode = .FRONT;
glass_desc.face_winding = .CCW;
triles.glass_shadow_pipeline = sg_make_pipeline(*glass_desc);
for orientations { for orientations {
for r: 0..2 for c: 0..2 triles.params.orientations[it_index * 3 + r][c] = it.coef[r][c]; for r: 0..2 for c: 0..2 triles.params.orientations[it_index * 3 + r][c] = it.coef[r][c];
} }
@ -34,9 +50,12 @@ renderer_trile_mesh_destroy :: (id: u32) {
slot_remove(*triles.meshes, id); slot_remove(*triles.meshes, id);
} }
renderer_swatch_set :: (id: u8, color: [3]u8, roughness: u8, metallic: u8, emittance: u8) { renderer_swatch_set :: (id: u8, color: [3]u8, reflectivity: u8, gloss: u8, emittance: u8, glass: bool) {
for 0..2 triles.palette[id][it] = color[it]; for 0..2 triles.palette[0][id][it] = color[it];
triles.palette[id][3] = 255; triles.palette[0][id][3] = xx (min(cast(int) emittance, 127) * 255 / 127); // emittance is 0..127
triles.palette[1][id][0] = xx ifx glass then 255 else 0;
triles.palette[1][id][1] = reflectivity;
triles.palette[1][id][2] = gloss;
triles.palette_dirty = true; triles.palette_dirty = true;
} }
@ -44,7 +63,7 @@ triles_upload :: (scene: *Scene) {
if triles.palette_dirty { if triles.palette_dirty {
triles.palette_dirty = false; triles.palette_dirty = false;
if triles.palette_image.id sg_destroy_image(triles.palette_image); if triles.palette_image.id sg_destroy_image(triles.palette_image);
desc := sg_image_desc.{ width = 256, height = 1, pixel_format = .RGBA8 }; desc := sg_image_desc.{ width = 256, height = 2, pixel_format = .RGBA8 };
desc.data.subimage[0][0] = .{ triles.palette.data, size_of(type_of(triles.palette)) }; desc.data.subimage[0][0] = .{ triles.palette.data, size_of(type_of(triles.palette)) };
triles.palette_image = sg_make_image(*desc); triles.palette_image = sg_make_image(*desc);
} }
@ -59,26 +78,62 @@ triles_upload :: (scene: *Scene) {
if view.draw_world { if view.draw_world {
for chunk: scene.chunks for group: chunk.groups for group.instances { for chunk: scene.chunks for group: chunk.groups for group.instances {
position := chunk.origin + Vector3.{xx it.x, xx it.y, xx it.z}; position := chunk.origin + Vector3.{xx it.x, xx it.y, xx it.z};
array_add(*draws, .{group.mesh.id, .{position, xx it.orientation, 0}}); cut := trile_cut(.{xx view_index}, group.mesh, position);
array_add(*draws, .{group.mesh.id, .{position, xx it.orientation, 0, cut}});
} }
} }
for scene.triles { for scene.triles {
if it.view.index != view_index continue; 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 PREVIEW_KEY else 0;
array_add(*draws, .{preview | it.mesh.id, .{it.position, xx it.orientation, xx it.tint}}); 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}});
} }
batch_instances(view_index, draws, *triles.batches, *triles.instances); batch_instances(view_index, draws, *triles.batches, *triles.instances);
} }
stream_upload(*triles.stream, triles.instances); stream_upload(*triles.stream, triles.instances);
} }
// After every opaque caster, so glass behind something solid doesn't tint.
triles_draw_glass_shadow :: (pass: *Pass) {
sg_apply_pipeline(triles.glass_shadow_pipeline);
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);
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_buffer_offsets[1] = it.first * size_of(Instance);
bind.images[IMG_palette] = triles.palette_image;
bind.samplers[SMP_palette_smp] = samplers.nearest_clamp;
sg_apply_bindings(*bind);
sg_draw(0, mesh.index_count, it.count);
}
}
triles_draw :: (pass: *Pass) { triles_draw :: (pass: *Pass) {
sg_apply_pipeline(triles.pipeline); // Cut triles are only hidden from the camera itself, not from shadows or reflections.
triles.params.show_hidden = xx (pass.kind != .MAIN);
sg_apply_pipeline(ifx pass.kind == .SHADOW then triles.shadow_pipeline else triles.pipeline);
apply_uniforms(UB_view_params, *pass.params); apply_uniforms(UB_view_params, *pass.params);
apply_uniforms(UB_trile_params, *triles.params); apply_uniforms(UB_trile_params, *triles.params);
if pass.kind != .SHADOW {
apply_uniforms(UB_shadow_params, *pass.shadow);
fs_params := Trile_Fs_Params.{
sun_dir = pass.params.sun_direction,
use_reflection = xx (pass.kind == .MAIN),
reflection_height = view_environment(pass.view).reflection_height,
};
probes_set_params(*fs_params);
apply_uniforms(UB_trile_fs_params, *fs_params);
}
for triles.batches { for triles.batches {
if it.view_index != pass.view_index || (pass.reflection && it.key & PREVIEW_KEY) continue; 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)); mesh := slot_get(*triles.meshes, xx (it.key & ~PREVIEW_KEY));
if !mesh || mesh.index_count == 0 continue; if !mesh || mesh.index_count == 0 continue;
@ -89,6 +144,13 @@ triles_draw :: (pass: *Pass) {
bind.vertex_buffer_offsets[1] = it.first * size_of(Instance); bind.vertex_buffer_offsets[1] = it.first * size_of(Instance);
bind.images[IMG_palette] = triles.palette_image; bind.images[IMG_palette] = triles.palette_image;
bind.samplers[SMP_palette_smp] = samplers.nearest_clamp; bind.samplers[SMP_palette_smp] = samplers.nearest_clamp;
if pass.kind != .SHADOW {
bind_shadow(pass, *bind);
bind.images[IMG_trile_reflection] = ifx pass.kind == .MAIN then view_reflection_image(pass.view_index) else texture_image(0);
bind.images[IMG_trile_reflection_glow] = ifx pass.kind == .MAIN then view_reflection_glow_image(pass.view_index) else texture_image(0);
bind.samplers[SMP_trile_reflection_smp] = samplers.linear_clamp;
probes_bind(*bind);
}
sg_apply_bindings(*bind); sg_apply_bindings(*bind);
sg_draw(0, mesh.index_count, it.count); sg_draw(0, mesh.index_count, it.count);
} }
@ -106,16 +168,19 @@ Instance :: struct {
position : Vector3; position : Vector3;
orientation : float; orientation : float;
tint : float; tint : float;
cut : float; // 0..1 dissolved away from the camera, still casts shadows
} }
PREVIEW_KEY :: 1 << 32; PREVIEW_KEY :: 1 << 32;
triles : struct { triles : struct {
pipeline : sg_pipeline; pipeline : sg_pipeline;
params : Trile_Params; shadow_pipeline : sg_pipeline;
glass_shadow_pipeline : sg_pipeline;
params : Trile_Params;
meshes : Slots(Mesh); meshes : Slots(Mesh);
palette : [256][4]u8; palette : [2][256][4]u8; // row 0: colour, emittance; row 1: glass, reflectivity, gloss
palette_image : sg_image; palette_image : sg_image;
palette_dirty : bool; palette_dirty : bool;

View File

@ -1,5 +1,5 @@
ui_init :: () { ui_init :: () {
desc := pipeline_desc(ui_shader_desc(sg_query_backend()), .SCREEN, blend = true); desc := pipeline_desc(ui_shader_desc(sg_query_backend()), .SCREEN, blend = ALPHA_BLEND);
desc.layout.attrs[ATTR_ui_position] = .{ format = .FLOAT2 }; desc.layout.attrs[ATTR_ui_position] = .{ format = .FLOAT2 };
desc.layout.attrs[ATTR_ui_uv] = .{ format = .FLOAT2 }; desc.layout.attrs[ATTR_ui_uv] = .{ format = .FLOAT2 };
desc.layout.attrs[ATTR_ui_color0] = .{ format = .FLOAT4 }; desc.layout.attrs[ATTR_ui_color0] = .{ format = .FLOAT4 };

View File

@ -1,40 +1,140 @@
BLOOM_LEVELS :: 5;
BLOOM_SCATTER :: 0.7;
views_init :: () { views_init :: () {
desc := pipeline_desc(composite_shader_desc(sg_query_backend()), .SCREEN); backend := sg_query_backend();
views.composite = sg_make_pipeline(*desc); post_desc := pipeline_desc(post_shader_desc(backend), .SCREEN);
views.post = sg_make_pipeline(*post_desc);
glow_desc := pipeline_desc(bloom_glow_shader_desc(backend), .POST);
views.bloom_glow = sg_make_pipeline(*glow_desc);
down_desc := pipeline_desc(bloom_down_shader_desc(backend), .POST);
views.bloom_down = sg_make_pipeline(*down_desc);
// Each level becomes a blend toward the blurrier level below it, so the glow is
// mostly wide blur rather than a copy of the bright pixels.
up_desc := pipeline_desc(bloom_up_shader_desc(backend), .POST, sg_blend_state.{ enabled = true, src_factor_rgb = .BLEND_COLOR, dst_factor_rgb = .ONE_MINUS_BLEND_COLOR });
up_desc.blend_color = .{ BLOOM_SCATTER, BLOOM_SCATTER, BLOOM_SCATTER, 1 };
views.bloom_up = sg_make_pipeline(*up_desc);
} }
view_begin_pass :: (index: int, width: s32, height: s32, reflection: bool) { view_begin_pass :: (index: int, width: s32, height: s32, kind: Pass_Kind, opaque: bool) {
if width != views.width || height != views.height { if width != views.width || height != views.height {
for views.targets { for views.targets {
destroy_target(it.main); destroy_target(it.main);
destroy_target(it.reflection); destroy_target(it.reflection);
for level: it.bloom {
sg_destroy_attachments(level.attachments);
sg_destroy_image(level.image);
}
} }
array_reset_keeping_memory(*views.targets); array_reset_keeping_memory(*views.targets);
views.width = width; views.width = width;
views.height = height; views.height = height;
} }
while views.targets.count <= index array_add(*views.targets, .{make_target(width, height), make_target(width, height)}); while views.targets.count <= index {
targets := array_add(*views.targets);
targets.main = make_target(width, height);
targets.reflection = make_target(width, height);
w, h := width, height;
for *level: targets.bloom {
w, h = max(w / 2, 1), max(h / 2, 1);
level.image = sg_make_image(*(sg_image_desc.{ render_target = true, width = w, height = h, pixel_format = VIEW_COLOR_FORMAT, sample_count = 1 }));
desc : sg_attachments_desc;
desc.colors[0].image = level.image;
level.attachments = sg_make_attachments(*desc);
}
}
targets := views.targets[index]; targets := views.targets[index];
target := ifx kind == .REFLECTION then targets.reflection else targets.main;
action : sg_pass_action; action : sg_pass_action;
action.colors[0] = .{ load_action = .CLEAR, clear_value = .{0, 0, 0, 1} }; if opaque {
action.depth = .{ load_action = .CLEAR, clear_value = 1 }; action.colors[0] = .{ load_action = .CLEAR, clear_value = .{0, 0, 0, 1} };
target := ifx reflection then targets.reflection else targets.main; action.colors[1] = .{ load_action = .CLEAR, clear_value = .{0, 0, 0, 0} };
sg_begin_pass(*(sg_pass.{ action = action, attachments = target.attachments })); action.colors[2] = .{ load_action = .CLEAR, clear_value = .{1.0e4, 0, 0, 0} };
action.depth = .{ load_action = .CLEAR, clear_value = 1 };
sg_begin_pass(*(sg_pass.{ action = action, attachments = target.opaque }));
} else {
action.colors[0] = .{ load_action = .LOAD };
action.colors[1] = .{ load_action = .LOAD };
action.depth = .{ load_action = .LOAD };
sg_begin_pass(*(sg_pass.{ action = action, attachments = target.transparent }));
}
}
view_begin_shadow_pass :: (index: int) {
while views.shadows.count <= index {
shadow := array_add(*views.shadows);
shadow.tint = sg_make_image(*(sg_image_desc.{ render_target = true, width = SHADOW_MAP_SIZE, height = SHADOW_MAP_SIZE, pixel_format = SHADOW_TINT_FORMAT, sample_count = 1 }));
shadow.depth = sg_make_image(*(sg_image_desc.{ render_target = true, width = SHADOW_MAP_SIZE, height = SHADOW_MAP_SIZE, pixel_format = VIEW_DEPTH_FORMAT, sample_count = 1 }));
desc : sg_attachments_desc;
desc.colors[0].image = shadow.tint;
desc.depth_stencil.image = shadow.depth;
shadow.attachments = sg_make_attachments(*desc);
}
action : sg_pass_action;
action.depth = .{ load_action = .CLEAR, clear_value = 1 };
action.colors[0] = .{ load_action = .CLEAR, clear_value = .{1, 1, 1, 1} };
sg_begin_pass(*(sg_pass.{ action = action, attachments = views.shadows[index].attachments }));
}
view_shadow_image :: (index: int) -> sg_image {
return views.shadows[index].depth;
}
view_shadow_tint_image :: (index: int) -> sg_image {
return views.shadows[index].tint;
}
view_scene_image :: (index: int) -> sg_image {
return views.targets[index].main.scene;
} }
view_reflection_image :: (index: int) -> sg_image { view_reflection_image :: (index: int) -> sg_image {
return views.targets[index].reflection.color; return views.targets[index].reflection.color;
} }
view_composite :: (index: int) { view_reflection_glow_image :: (index: int) -> sg_image {
sg_apply_pipeline(views.composite); return views.targets[index].reflection.glow;
params := Composite_Vs_Params.{ flip_y = ifx sg_query_features().origin_top_left then 1.0 else 0.0 }; }
apply_uniforms(UB_composite_vs_params, *params);
// Halves the glowing parts of the view down a chain of levels, then adds the
// levels back up, leaving the glow in the first level.
view_bloom :: (index: int) {
targets := *views.targets[index];
run :: (pipeline: sg_pipeline, source: sg_image, target: sg_attachments, load: bool) #expand {
action : sg_pass_action;
action.colors[0] = ifx load then sg_color_attachment_action.{ load_action = .LOAD } else .{ load_action = .DONTCARE };
sg_begin_pass(*(sg_pass.{ action = action, attachments = target }));
sg_apply_pipeline(pipeline);
bind : sg_bindings;
bind.images[IMG_bloom_source] = source;
bind.samplers[SMP_bloom_smp] = samplers.linear_clamp;
if pipeline.id == views.bloom_glow.id bind.images[IMG_bloom_glow] = targets.main.glow;
sg_apply_bindings(*bind);
sg_draw(0, 3, 1);
sg_end_pass();
}
run(views.bloom_glow, targets.main.color, targets.bloom[0].attachments, false);
for 1..BLOOM_LEVELS-1 run(views.bloom_down, targets.bloom[it - 1].image, targets.bloom[it].attachments, false);
for < 1..BLOOM_LEVELS-1 run(views.bloom_up, targets.bloom[it].image, targets.bloom[it - 1].attachments, true);
}
view_composite :: (index: int, env: Environment) {
sg_apply_pipeline(views.post);
params := Post_Params.{
exposure = env.exposure,
contrast = env.contrast,
saturation = env.saturation,
bloom_amount = env.bloom_amount,
};
apply_uniforms(UB_post_params, *params);
bind : sg_bindings; bind : sg_bindings;
bind.images[IMG_view_color] = views.targets[index].main.color; bind.images[IMG_view_color] = views.targets[index].main.color;
bind.samplers[SMP_composite_smp] = samplers.nearest_clamp; bind.images[IMG_bloom] = views.targets[index].bloom[0].image;
bind.samplers[SMP_view_smp] = samplers.nearest_clamp;
bind.samplers[SMP_bloom_view_smp] = samplers.linear_clamp;
sg_apply_bindings(*bind); sg_apply_bindings(*bind);
sg_draw(0, 3, 1); sg_draw(0, 3, 1);
} }
@ -43,13 +143,20 @@ view_composite :: (index: int) {
View_Target :: struct { View_Target :: struct {
color : sg_image; color : sg_image;
glow : sg_image;
scene : sg_image;
depth : sg_image; depth : sg_image;
attachments : sg_attachments; opaque : sg_attachments;
transparent : sg_attachments;
} }
views : struct { views : struct {
composite : sg_pipeline; post : sg_pipeline;
targets : [..]struct { main, reflection: View_Target; }; bloom_glow : sg_pipeline;
bloom_down : sg_pipeline;
bloom_up : sg_pipeline;
targets : [..]struct { main, reflection: View_Target; bloom: [BLOOM_LEVELS]struct { image: sg_image; attachments: sg_attachments; }; };
shadows : [..]struct { depth, tint: sg_image; attachments: sg_attachments; };
width : s32; width : s32;
height : s32; height : s32;
} }
@ -57,16 +164,24 @@ views : struct {
make_target :: (width: s32, height: s32) -> View_Target { make_target :: (width: s32, height: s32) -> View_Target {
t : View_Target; t : View_Target;
t.color = sg_make_image(*(sg_image_desc.{ render_target = true, width = width, height = height, pixel_format = VIEW_COLOR_FORMAT, sample_count = 1 })); t.color = sg_make_image(*(sg_image_desc.{ render_target = true, width = width, height = height, pixel_format = VIEW_COLOR_FORMAT, sample_count = 1 }));
t.glow = sg_make_image(*(sg_image_desc.{ render_target = true, width = width, height = height, pixel_format = VIEW_GLOW_FORMAT, sample_count = 1 }));
t.scene = sg_make_image(*(sg_image_desc.{ render_target = true, width = width, height = height, pixel_format = VIEW_SCENE_FORMAT, sample_count = 1 }));
t.depth = sg_make_image(*(sg_image_desc.{ render_target = true, width = width, height = height, pixel_format = VIEW_DEPTH_FORMAT, sample_count = 1 })); t.depth = sg_make_image(*(sg_image_desc.{ render_target = true, width = width, height = height, pixel_format = VIEW_DEPTH_FORMAT, sample_count = 1 }));
desc : sg_attachments_desc; desc : sg_attachments_desc;
desc.colors[0].image = t.color; desc.colors[0].image = t.color;
desc.colors[1].image = t.glow;
desc.depth_stencil.image = t.depth; desc.depth_stencil.image = t.depth;
t.attachments = sg_make_attachments(*desc); t.transparent = sg_make_attachments(*desc);
desc.colors[2].image = t.scene;
t.opaque = sg_make_attachments(*desc);
return t; return t;
} }
destroy_target :: (t: View_Target) { destroy_target :: (t: View_Target) {
sg_destroy_attachments(t.attachments); sg_destroy_attachments(t.opaque);
sg_destroy_attachments(t.transparent);
sg_destroy_image(t.color); sg_destroy_image(t.color);
sg_destroy_image(t.glow);
sg_destroy_image(t.scene);
sg_destroy_image(t.depth); sg_destroy_image(t.depth);
} }

View File

@ -1,42 +1,62 @@
WATER_HALF_SIZE :: 1000.0; WATER_HALF_SIZE :: 1000.0;
water_init :: () { water_init :: () {
desc := pipeline_desc(water_shader_desc(sg_query_backend()), .VIEW, blend = true); backend := sg_query_backend();
water.pipeline = sg_make_pipeline(*desc); floor_desc := pipeline_desc(water_floor_shader_desc(backend), .VIEW_OPAQUE, depth_write = true);
water.floor = sg_make_pipeline(*floor_desc);
surface_desc := pipeline_desc(water_shader_desc(backend), .VIEW_TRANSPARENT, ALPHA_BLEND);
water.surface = sg_make_pipeline(*surface_desc);
}
// The deep plane is opaque, so the surface can measure how much water lies above it.
water_draw_floor :: (pass: *Pass) {
env := pass.view.environment;
if pass.kind != .MAIN || !pass.view.draw_world || !env || !env.water return;
sg_apply_pipeline(water.floor);
apply_uniforms(UB_view_params, *pass.params);
vs := Water_Vs_Params.{ height = 0, half_size = WATER_HALF_SIZE };
fs := Water_Floor_Params.{ floor_color = env.deep_color.component };
apply_uniforms(UB_water_vs_params, *vs);
apply_uniforms(UB_water_floor_params, *fs);
sg_draw(0, 6, 1);
} }
water_draw :: (pass: *Pass) { water_draw :: (pass: *Pass) {
env := pass.view.environment; env := pass.view.environment;
if pass.reflection || !pass.view.draw_world || !env || !env.water return; if pass.kind != .MAIN || !pass.view.draw_world || !env || !env.water return;
sg_apply_pipeline(water.pipeline); sg_apply_pipeline(water.surface);
apply_uniforms(UB_view_params, *pass.params); apply_uniforms(UB_view_params, *pass.params);
apply_uniforms(UB_shadow_params, *pass.shadow);
bind : sg_bindings; bind : sg_bindings;
bind.images[IMG_reflection] = view_reflection_image(pass.view_index); bind.images[IMG_reflection] = view_reflection_image(pass.view_index);
bind.images[IMG_normals] = texture_image(env.water_normals.id); bind.images[IMG_normals] = texture_image(env.water_normals.id);
bind.images[IMG_reflection_glow] = view_reflection_glow_image(pass.view_index);
bind.samplers[SMP_reflection_smp] = samplers.linear_clamp; bind.samplers[SMP_reflection_smp] = samplers.linear_clamp;
bind.samplers[SMP_normals_smp] = samplers.linear_repeat; bind.samplers[SMP_normals_smp] = samplers.linear_repeat;
bind_shadow(pass, *bind);
sg_apply_bindings(*bind); sg_apply_bindings(*bind);
draw_plane :: (height: float, color: Vector3, surface: bool) #expand { vs := Water_Vs_Params.{ height = env.reflection_height, half_size = WATER_HALF_SIZE };
vs := Water_Vs_Params.{ height = height, half_size = WATER_HALF_SIZE }; fs := Water_Fs_Params.{
fs := Water_Fs_Params.{ water_color = env.water_color.component,
color = color.component, sun_direction = env.sun_direction.component,
surface = xx surface, shininess = env.water_shininess,
reflectivity = env.water_reflectivity, sun_color = env.sun_color.component,
distortion = env.water_distortion, time = pass.scene.time,
}; reflectivity = env.water_reflectivity,
apply_uniforms(UB_water_vs_params, *vs); distortion = env.water_distortion,
apply_uniforms(UB_water_fs_params, *fs); };
sg_draw(0, 6, 1); apply_uniforms(UB_water_vs_params, *vs);
} apply_uniforms(UB_water_fs_params, *fs);
draw_plane(0, env.deep_color, false); sg_draw(0, 6, 1);
draw_plane(env.reflection_height, env.water_color, true);
} }
#scope_file #scope_file
water : struct { water : struct {
pipeline : sg_pipeline; floor : sg_pipeline;
surface : sg_pipeline;
} }

View File

@ -41,3 +41,26 @@ create_lookat :: (camera: *Camera) -> Matrix4 {
create_viewproj :: (camera: *Camera) -> Matrix4 { create_viewproj :: (camera: *Camera) -> Matrix4 {
return create_lookat(camera) * create_perspective(camera); return create_lookat(camera) * create_perspective(camera);
} }
// An orthographic box around focus, seen from the sun. Depth goes 0..1 away from
// the sun. The box moves in whole shadow texels so shadows don't shimmer.
create_sun_viewproj :: (focus: Vector3, sun_direction: Vector3, radius: float, depth: float, map_size: int) -> Matrix4 {
A := normalize(sun_direction);
up := ifx abs(A.y) > 0.99 then Vector3.{0, 0, 1} else Vector3.{0, 1, 0};
B := normalize(cross(up, A));
C := cross(A, B);
texel := 2 * radius / map_size;
cx := floor(dot(B, focus) / texel) * texel;
cy := floor(dot(C, focus) / texel) * texel;
cz := dot(A, focus);
// Rows of the transform; stored transposed like create_lookat.
m := Matrix4.{
B.x / radius, B.y / radius, B.z / radius, -cx / radius,
C.x / radius, C.y / radius, C.z / radius, -cy / radius,
-A.x / (2 * depth), -A.y / (2 * depth), -A.z / (2 * depth), (cz + depth) / (2 * depth),
0, 0, 0, 1,
};
return transpose(m);
}

View File

@ -16,6 +16,8 @@
render :: () { render :: () {
text_end_frame(); text_end_frame();
draw_debug_lines(.{g_debug_line_count, g_debug_lines.data}, overlay = false);
draw_debug_lines(.{g_overlay_line_count, g_overlay_lines.data}, overlay = true);
w, h := get_window_size(); w, h := get_window_size();
scene_end_frame(w, h, xx get_time()); scene_end_frame(w, h, xx get_time());
g_debug_line_count = 0; g_debug_line_count = 0;

View File

@ -11,19 +11,55 @@ View_Id :: struct { index: s32; }
Environment :: struct { Environment :: struct {
reflection_height : float; reflection_height : float;
water : bool; water : bool;
sky : bool = true; // off: black outside the level
water_color : Vector3 = .{1, 1, 1}; water_color : Vector3 = .{1, 1, 1};
deep_color : Vector3 = .{1, 1, 1}; deep_color : Vector3 = .{1, 1, 1};
water_reflectivity : float; water_reflectivity : float;
water_distortion : float = 0.007; water_distortion : float = 0.007;
water_shininess : float = 64;
water_normals : Texture; water_normals : Texture;
sun_direction : Vector3 = #run normalize(Vector3.{0.2, 0.3, 0.4}); // towards the sun
sun_color : Vector3 = .{1, 1, 1};
shade_color : Vector3 = .{0.45, 0.45, 0.75};
sky_base : Vector3 = .{0.38, 0.81, 0.95};
sky_top : Vector3 = .{0.17, 0.4, 0.95};
sun_disk : Vector3 = .{1, 1, 1};
sun_halo : Vector3 = .{1, 1, 1};
horizon_halo : Vector3 = .{1, 1, 1};
sky_intensity : float = 0.3;
exposure : float; // in stops
contrast : float = 1;
saturation : float = 1;
bloom_amount : float = 0.4;
} }
// With cut on, triles marked ROOF or WALL are hidden from the camera around the
// focus: everything marked from the lowest roof above the focus upward, so the
// storeys above go whole, and walls on the camera's side of the focus down to a
// one-trile stub. They dissolve in and out rather than popping, and still cast
// shadows and show in reflections.
View :: struct { View :: struct {
camera : Camera; camera : Camera;
environment : *Environment; environment : *Environment;
draw_world : bool = true; draw_world : bool = true;
cut : bool;
focus : Vector3;
} }
Trile_Cut :: enum u8 {
NONE;
ROOF;
WALL;
}
CUT_RADIUS :: 12.0; // horizontal reach from the focus, for roofs and walls
CUT_WALL_MARGIN :: 0.5; // walls this close to the focus, toward the camera, stay
CUT_FADE_TIME :: 0.25; // seconds to dissolve in or out
SHADOW_MAP_SIZE :: 2048;
SHADOW_RADIUS :: 40.0;
SHADOW_DEPTH :: 100.0;
Trile_Tint :: enum u8 { Trile_Tint :: enum u8 {
NONE; NONE;
ADD_PREVIEW; ADD_PREVIEW;
@ -43,6 +79,7 @@ Particle_Instance :: struct {
size : float; size : float;
uv_rect : Vector4; // x, y, w, h, 0..1 uv_rect : Vector4; // x, y, w, h, 0..1
color : Vector4; color : Vector4;
glow : float; // 0..1, like swatch emittance
} }
// Pixels from top left. // Pixels from top left.
@ -54,20 +91,24 @@ Ui_Vertex :: struct {
trile_mesh_create :: (trile: *Trile) -> Trile_Mesh { trile_mesh_create :: (trile: *Trile) -> Trile_Mesh {
vertices, indices := mesh_trile(trile); vertices, indices := mesh_trile(trile);
return .{renderer_trile_mesh_create(vertices, indices)}; mesh := Trile_Mesh.{renderer_trile_mesh_create(vertices, indices)};
set_mesh_cut(mesh, trile.cut);
return mesh;
} }
trile_mesh_update :: (mesh: Trile_Mesh, trile: *Trile) { trile_mesh_update :: (mesh: Trile_Mesh, trile: *Trile) {
vertices, indices := mesh_trile(trile); vertices, indices := mesh_trile(trile);
renderer_trile_mesh_update(mesh.id, vertices, indices); renderer_trile_mesh_update(mesh.id, vertices, indices);
set_mesh_cut(mesh, trile.cut);
} }
trile_mesh_destroy :: (mesh: Trile_Mesh) { trile_mesh_destroy :: (mesh: Trile_Mesh) {
renderer_trile_mesh_destroy(mesh.id); renderer_trile_mesh_destroy(mesh.id);
set_mesh_cut(mesh, .NONE);
} }
swatch_set :: (id: u8, color: [3]u8, roughness: u8, metallic: u8, emittance: u8) { swatch_set :: (id: u8, color: [3]u8, reflectivity: u8, gloss: u8, emittance: u8, glass: bool) {
renderer_swatch_set(id, color, roughness, metallic, emittance); renderer_swatch_set(id, color, reflectivity, gloss, emittance, glass);
} }
// Pixels are RGBA8. // Pixels are RGBA8.
@ -88,6 +129,16 @@ World_Group :: struct {
instances : []Trile_Instance; instances : []Trile_Instance;
} }
// Baked light at a trile centre, as L1 spherical harmonics in half floats: red,
// green and blue, each (constant, y, z, x). A negative red constant marks a probe
// inside geometry.
Probe :: struct {
sh : [12]u16;
}
// One probe per trile, indexed x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE.
PROBES_PER_CHUNK :: CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE;
world_set_chunk :: (key: Chunk_Key, origin: Vector3, groups: []World_Group) { world_set_chunk :: (key: Chunk_Key, origin: Vector3, groups: []World_Group) {
chunk := find_or_add(*scene.chunks, key); chunk := find_or_add(*scene.chunks, key);
chunk.origin = origin; chunk.origin = origin;
@ -114,6 +165,14 @@ world_clear :: () {
array_free(chunk.groups); array_free(chunk.groups);
} }
table_reset(*scene.chunks); table_reset(*scene.chunks);
table_reset(*scene.cut_fades);
renderer_probes_clear();
}
// Probe light is added to the shade colour, so baking only ever brightens.
world_set_chunk_probes :: (key: Chunk_Key, probes: []Probe) {
assert(probes.count == PROBES_PER_CHUNK);
renderer_probes_set(key, probes);
} }
add_view :: (view: View) -> View_Id { add_view :: (view: View) -> View_Id {
@ -121,13 +180,45 @@ add_view :: (view: View) -> View_Id {
camera := view.camera; camera := view.camera;
array_add(*scene.view_proj, create_viewproj(*camera)); array_add(*scene.view_proj, create_viewproj(*camera));
height := ifx view.environment then view.environment.reflection_height else 0; env := ifx view.environment then view.environment.* else .{};
array_add(*scene.shadow_view_proj, create_sun_viewproj(camera.target, env.sun_direction, SHADOW_RADIUS, SHADOW_DEPTH, SHADOW_MAP_SIZE));
height := env.reflection_height;
camera.position.y = 2 * height - camera.position.y; camera.position.y = 2 * height - camera.position.y;
camera.target.y = 2 * height - camera.target.y; camera.target.y = 2 * height - camera.target.y;
array_add(*scene.reflection_view_proj, create_viewproj(*camera)); array_add(*scene.reflection_view_proj, create_viewproj(*camera));
array_add(*scene.view_cuts, make_view_cut(view));
return .{xx (scene.views.count - 1)}; return .{xx (scene.views.count - 1)};
} }
// How far the view's camera has dissolved this trile, 0 shown to 1 gone.
// position is the trile's corner. Call once per trile per frame; it advances the fade.
trile_cut :: (view: View_Id, mesh: Trile_Mesh, position: Vector3) -> float {
cut := scene.view_cuts[view.index];
kind := mesh_cut(mesh);
if !cut.enabled || kind == .NONE return 0;
target := 0.0;
to_trile := Vector2.{position.x + 0.5 - cut.focus.x, position.z + 0.5 - cut.focus.z};
if length(to_trile) <= CUT_RADIUS {
if position.y >= cut.height target = 1;
if kind == .WALL && position.y > cut.floor && dot(to_trile, cut.toward_camera) > CUT_WALL_MARGIN target = 1;
}
x, y, z := cast(s64) floor(position.x), cast(s64) floor(position.y), cast(s64) floor(position.z);
key := (cast(s64) view.index << 60) | ((x & 0xFFFFF) << 40) | ((y & 0xFFFFF) << 20) | (z & 0xFFFFF);
fade := table_find_pointer(*scene.cut_fades, key);
if !fade {
if target == 0 return 0;
fade = table_set(*scene.cut_fades, key, 0);
}
step := (scene.time - scene.cut_time) / CUT_FADE_TIME;
fade.* = ifx target > fade.* then min(target, fade.* + step) else max(target, fade.* - step);
result := fade.*;
if result == 0 table_remove(*scene.cut_fades, key);
return result;
}
draw_trile :: (view: View_Id, mesh: Trile_Mesh, position: Vector3, orientation: u8, tint := Trile_Tint.NONE) { 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}); array_add(*scene.triles, .{view, mesh, position, orientation, tint});
} }
@ -141,6 +232,13 @@ draw_particles :: (view: View_Id, sheet: Texture, blend: Blend_Mode, particles:
array_add(*scene.particles, .{view, sheet, blend, array_copy(particles,, temp)}); array_add(*scene.particles, .{view, sheet, blend, array_copy(particles,, temp)});
} }
// Lines are drawn in every view; overlay lines ignore depth. The slice must stay
// valid until render().
draw_debug_lines :: (lines: []Debug_Line, overlay: bool) {
if overlay scene.overlay_lines = lines;
else scene.debug_lines = lines;
}
// Untextured triangles pass a zero texture. // Untextured triangles pass a zero texture.
ui_triangles :: (vertices: []Ui_Vertex, texture: Texture) { ui_triangles :: (vertices: []Ui_Vertex, texture: Texture) {
array_add(*scene.ui, .{texture = texture, vertices = array_copy(vertices,, temp)}); array_add(*scene.ui, .{texture = texture, vertices = array_copy(vertices,, temp)});
@ -154,6 +252,14 @@ ui_scissor_clear :: () {
array_add(*scene.ui, .{clear_scissor = true}); array_add(*scene.ui, .{clear_scissor = true});
} }
View_Cut :: struct {
enabled : bool;
focus : Vector3;
floor : float; // the trile layer the focus stands in
height : float; // marked triles from here up are hidden
toward_camera : Vector2; // horizontal, normalized
}
Scene_Chunk :: struct { Scene_Chunk :: struct {
origin : Vector3; origin : Vector3;
groups : [..]World_Group; groups : [..]World_Group;
@ -193,21 +299,31 @@ Scene_Ui :: struct {
Trile_Vertex :: struct { Trile_Vertex :: struct {
position : Vector3; position : Vector3;
swatch : float; swatch : u8;
ao : float; // 0 = fully occluded, 1 = open ao : u8; // 0 = fully occluded, 3 = open
normal : u8; // axis * 2, +1 when facing the negative direction
_ : u8;
} }
Scene :: struct { Scene :: struct {
chunks : Table(Chunk_Key, Scene_Chunk, chunk_key_hash, chunk_key_compare); chunks : Table(Chunk_Key, Scene_Chunk, chunk_key_hash, chunk_key_compare);
mesh_cuts : [..]Trile_Cut; // by mesh id
views : [..]View; views : [..]View;
view_proj : [..]Matrix4; view_proj : [..]Matrix4;
reflection_view_proj : [..]Matrix4; reflection_view_proj : [..]Matrix4;
shadow_view_proj : [..]Matrix4;
view_cuts : [..]View_Cut;
cut_fades : Table(s64, float); // dissolving or cut triles, by view and position
cut_time : float; // when the fades last advanced
time : float; time : float;
triles : [..]Scene_Trile; triles : [..]Scene_Trile;
billboards : [..]Scene_Billboard; billboards : [..]Scene_Billboard;
particles : [..]Scene_Particles; particles : [..]Scene_Particles;
ui : [..]Scene_Ui; ui : [..]Scene_Ui;
debug_lines : []Debug_Line;
overlay_lines : []Debug_Line;
} }
scene_init :: () { scene_init :: () {
@ -226,15 +342,52 @@ scene_shutdown :: () {
scene_end_frame :: (width: s32, height: s32, time: float) { scene_end_frame :: (width: s32, height: s32, time: float) {
scene.time = time; scene.time = time;
renderer_render(*scene, width, height); renderer_render(*scene, width, height);
scene.cut_time = time;
array_reset_keeping_memory(*scene.views); array_reset_keeping_memory(*scene.views);
array_reset_keeping_memory(*scene.view_proj); array_reset_keeping_memory(*scene.view_proj);
array_reset_keeping_memory(*scene.reflection_view_proj); array_reset_keeping_memory(*scene.reflection_view_proj);
array_reset_keeping_memory(*scene.shadow_view_proj);
array_reset_keeping_memory(*scene.view_cuts);
array_reset_keeping_memory(*scene.triles); array_reset_keeping_memory(*scene.triles);
array_reset_keeping_memory(*scene.billboards); array_reset_keeping_memory(*scene.billboards);
array_reset_keeping_memory(*scene.particles); array_reset_keeping_memory(*scene.particles);
array_reset_keeping_memory(*scene.ui); array_reset_keeping_memory(*scene.ui);
scene.debug_lines = .[];
scene.overlay_lines = .[];
} }
#scope_file #scope_file
scene : Scene; 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);
scene.mesh_cuts[mesh.id] = cut;
}
mesh_cut :: (mesh: Trile_Mesh) -> Trile_Cut {
if mesh.id >= scene.mesh_cuts.count return .NONE;
return scene.mesh_cuts[mesh.id];
}
make_view_cut :: (view: View) -> View_Cut {
cut : View_Cut;
if !view.cut return cut;
cut.enabled = true;
cut.focus = view.focus;
cut.floor = floor(view.focus.y + 0.5);
cut.height = FLOAT32_MAX;
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);
}
}
return cut;
}

View File

@ -41,12 +41,13 @@ mesh_trile :: (trile: *Trile) -> vertices: []Trile_Vertex, indices: []u16 {
for u: u0..u1-1 for v: v0..v1-1 mask[u][v] = .{}; for u: u0..u1-1 for v: v0..v1-1 mask[u][v] = .{};
plane := cast(float)(depth + ifx sign > 0 then 1 else 0); plane := cast(float)(depth + ifx sign > 0 then 1 else 0);
normal := cast(u8)(axis * 2 + ifx sign < 0 then 1 else 0);
corner :: (u: int, v: int, ao: u8) -> Trile_Vertex #expand { corner :: (u: int, v: int, ao: u8) -> Trile_Vertex #expand {
p : Vector3; p : Vector3;
p.component[`axis] = `plane * TRIXEL_SIZE; p.component[`axis] = `plane * TRIXEL_SIZE;
p.component[`u_axis] = u * TRIXEL_SIZE; p.component[`u_axis] = u * TRIXEL_SIZE;
p.component[`v_axis] = v * TRIXEL_SIZE; p.component[`v_axis] = v * TRIXEL_SIZE;
return .{p, xx `face.swatch, ao / 3.0}; return .{p, `face.swatch, ao, `normal, 0};
} }
q := Trile_Vertex.[ q := Trile_Vertex.[
corner(u0, v0, face.ao[0]), corner(u0, v0, face.ao[0]),

View File

@ -146,18 +146,19 @@ MAX_SWATCHES :: 256;
Swatch :: struct { Swatch :: struct {
used : bool; used : bool;
name : string; name : string;
color : [3]u8; color : [3]u8;
roughness : u8 = 4; reflectivity : u8; // how much of the floor reflection shows, 0..255
metallic : u8; gloss : u8; // strength and sharpness of the sun highlight, 0..255
emittance : u8; emittance : u8;
glass : bool; // lets sunlight through, but can't be seen through
} }
g_swatches : [MAX_SWATCHES]Swatch; g_swatches : [MAX_SWATCHES]Swatch;
swatch_add :: (name: string, color: [3]u8, roughness: u8 = 4, metallic: u8 = 0, emittance: u8 = 0) -> u8, success: bool { swatch_add :: (name: string, color: [3]u8) -> u8, success: bool {
for 1..MAX_SWATCHES-1 { for 1..MAX_SWATCHES-1 {
if g_swatches[it].used continue; if g_swatches[it].used continue;
g_swatches[it] = .{used = true, name = copy_string(name), color = color, roughness = roughness, metallic = metallic, emittance = emittance}; g_swatches[it] = .{used = true, name = copy_string(name), color = color};
swatch_changed(xx it); swatch_changed(xx it);
return cast(u8) it, true; return cast(u8) it, true;
} }
@ -165,9 +166,20 @@ swatch_add :: (name: string, color: [3]u8, roughness: u8 = 4, metallic: u8 = 0,
return 0, false; return 0, false;
} }
// 1 when there are no swatches yet, so the trixels still exist.
first_used_swatch :: () -> u8 {
for 1..MAX_SWATCHES-1 if g_swatches[it].used return xx it;
return 1;
}
// New triles start as a solid cube, so the editor has something to click on.
trile_fill :: (trile: *Trile, swatch: u8) {
for x: 0..15 for y: 0..15 for z: 0..15 trile.trixels[x][y][z] = swatch;
}
swatch_changed :: (id: u8) { swatch_changed :: (id: u8) {
s := g_swatches[id]; s := g_swatches[id];
swatch_set(id, s.color, s.roughness, s.metallic, s.emittance); swatch_set(id, s.color, s.reflectivity, s.gloss, s.emittance, s.glass);
} }
swatch_color :: (id: u8) -> Vector3 { swatch_color :: (id: u8) -> Vector3 {
@ -175,6 +187,19 @@ swatch_color :: (id: u8) -> Vector3 {
return .{c[0] / 255.0, c[1] / 255.0, c[2] / 255.0}; return .{c[0] / 255.0, c[1] / 255.0, c[2] / 255.0};
} }
// How many trixels of each swatch the trile has, by swatch id.
trile_swatch_counts :: (trile: *Trile) -> [MAX_SWATCHES]s32 {
counts : [MAX_SWATCHES]s32;
for x: 0..15 for y: 0..15 for z: 0..15 counts[trile.trixels[x][y][z]] += 1;
counts[0] = 0;
return counts;
}
swatch_label :: (id: u8) -> string {
if !g_swatches[id].used return tprint("%: (missing)", id);
return tprint("%: %", id, g_swatches[id].name);
}
swatch_in_use :: (id: u8) -> bool { swatch_in_use :: (id: u8) -> bool {
for trile: trile_table { for trile: trile_table {
for x: 0..15 for y: 0..15 for z: 0..15 { for x: 0..15 for y: 0..15 for z: 0..15 {
@ -187,10 +212,11 @@ swatch_in_use :: (id: u8) -> bool {
Swatch_Serialize :: struct { Swatch_Serialize :: struct {
id : int; id : int;
name : string; name : string;
color : [3]u8; color : [3]u8;
roughness : u8; reflectivity : u8;
metallic : u8; gloss : u8;
emittance : u8; emittance : u8;
glass : bool;
} }
sswatches :: () { sswatches :: () {
@ -199,7 +225,7 @@ sswatches :: () {
list.allocator = temp; list.allocator = temp;
for g_swatches { for g_swatches {
if !it.used continue; if !it.used continue;
array_add(*list, .{it_index, it.name, it.color, it.roughness, it.metallic, it.emittance}); array_add(*list, .{it_index, it.name, it.color, it.reflectivity, it.gloss, it.emittance, it.glass});
} }
#if OS != .WASM { #if OS != .WASM {
file :: #import "File"; file :: #import "File";
@ -216,7 +242,7 @@ lswatches :: () {
_, list := Jaison.json_parse_string(xx data, [..]Swatch_Serialize,, temp); _, list := Jaison.json_parse_string(xx data, [..]Swatch_Serialize,, temp);
for list { for list {
if it.id < 1 || it.id >= MAX_SWATCHES continue; if it.id < 1 || it.id >= MAX_SWATCHES continue;
g_swatches[it.id] = .{used = true, name = sprint("%", it.name), color = it.color, roughness = it.roughness, metallic = it.metallic, emittance = it.emittance}; g_swatches[it.id] = .{used = true, name = sprint("%", it.name), color = it.color, reflectivity = it.reflectivity, gloss = it.gloss, emittance = it.emittance, glass = it.glass};
} }
for 0..MAX_SWATCHES-1 swatch_changed(xx it); for 0..MAX_SWATCHES-1 swatch_changed(xx it);
} @Command } @Command
@ -226,9 +252,6 @@ Palette_Entry :: struct {
r : float = 1.0; r : float = 1.0;
g : float = 0.0; g : float = 0.0;
b : float = 1.0; b : float = 1.0;
roughness : int = 4;
metallic : int = 0;
emittance : int = 0;
} }
Loaded_Palette :: struct { Loaded_Palette :: struct {
@ -238,6 +261,30 @@ Loaded_Palette :: struct {
g_palettes : [..]Loaded_Palette; g_palettes : [..]Loaded_Palette;
// Adds a swatch for every colour in the palette that no swatch has yet.
swatch_import_palette :: (palette: Loaded_Palette) -> added: int {
added := 0;
for entry: palette.entries {
color := u8.[
cast(u8) clamp(entry.r * 255 + 0.5, 0, 255),
cast(u8) clamp(entry.g * 255 + 0.5, 0, 255),
cast(u8) clamp(entry.b * 255 + 0.5, 0, 255),
];
exists := false;
for g_swatches {
if it.used && it.color[0] == color[0] && it.color[1] == color[1] && it.color[2] == color[2] {
exists = true;
break;
}
}
if exists continue;
_, ok := swatch_add(entry.name, color);
if !ok break;
added += 1;
}
return added;
}
Trile :: struct { Trile :: struct {
name : string = "test"; name : string = "test";
is_opaque : bool = true; is_opaque : bool = true;
@ -252,6 +299,8 @@ Trile :: struct {
// in the Y axis is generated for it. // in the Y axis is generated for it.
is_stairs : bool; is_stairs : bool;
heightmap : [16][16]u8; heightmap : [16][16]u8;
cut : Trile_Cut; // what the camera may cut away indoors
}; };
TrileSerialize :: struct { TrileSerialize :: struct {
@ -259,6 +308,7 @@ TrileSerialize :: struct {
is_opaque : u8 = 1; is_opaque : u8 = 1;
is_stairs : u8 = 0; is_stairs : u8 = 0;
skip_collision : u8 = 0; skip_collision : u8 = 0;
cut : u8 = 0;
trixels : string; // base64 of the 16^3 swatch ids trixels : string; // base64 of the 16^3 swatch ids
}; };
@ -268,6 +318,7 @@ trile_to_serialize_form :: (t: Trile) -> TrileSerialize {
is_opaque = ifx t.is_opaque then cast(u8)1 else cast(u8)0, is_opaque = ifx t.is_opaque then cast(u8)1 else cast(u8)0,
is_stairs = ifx t.is_stairs then cast(u8)1 else cast(u8)0, is_stairs = ifx t.is_stairs then cast(u8)1 else cast(u8)0,
skip_collision = ifx t.skip_collision then cast(u8)1 else cast(u8)0, skip_collision = ifx t.skip_collision then cast(u8)1 else cast(u8)0,
cut = xx t.cut,
trixels = base64_encode(.{size_of(type_of(t.trixels)), cast(*u8) *t.trixels},, temp), trixels = base64_encode(.{size_of(type_of(t.trixels)), cast(*u8) *t.trixels},, temp),
}; };
} }
@ -277,6 +328,7 @@ trile_from_serialize_form :: (ts: TrileSerialize) -> Trile {
t.is_opaque = ts.is_opaque != 0; t.is_opaque = ts.is_opaque != 0;
t.is_stairs = ts.is_stairs != 0; t.is_stairs = ts.is_stairs != 0;
t.skip_collision = ts.skip_collision != 0; t.skip_collision = ts.skip_collision != 0;
t.cut = xx ts.cut;
ids, ok := base64_decode(ts.trixels,, temp); ids, ok := base64_decode(ts.trixels,, temp);
if ok && ids.count == size_of(type_of(t.trixels)) { if ok && ids.count == size_of(type_of(t.trixels)) {
memcpy(*t.trixels, ids.data, ids.count); memcpy(*t.trixels, ids.data, ids.count);

View File

@ -2,8 +2,6 @@
Pool :: #import "Pool"; Pool :: #import "Pool";
CHUNK_SIZE :: 32;
Current_World :: struct { Current_World :: struct {
world : World; world : World;
pool : Pool.Pool; // A memory pool to allocate stuff for the lifetime of this level being active. pool : Pool.Pool; // A memory pool to allocate stuff for the lifetime of this level being active.
@ -14,6 +12,8 @@ current_world : Current_World;
#scope_export #scope_export
CHUNK_SIZE :: 32;
Chunk_Key :: struct { Chunk_Key :: struct {
x: s32; x: s32;
y: s32; y: s32;
@ -346,6 +346,11 @@ World_Json_Config :: struct {
waterReflectivity : float = 0.0; waterReflectivity : float = 0.0;
waterDistortion : float = 0.007; waterDistortion : float = 0.007;
indoor : s32; indoor : s32;
shadeColor : [3]float = .[0.45, 0.45, 0.75];
exposure : float = 0;
contrast : float = 1;
saturation : float = 1;
bloomAmount : float = 0.4;
} }
World_Json_Chunk :: struct { World_Json_Chunk :: struct {
@ -502,6 +507,11 @@ world_config_to_json :: (conf: *World_Config) -> World_Json_Config {
jc.waterReflectivity = conf.waterReflectivity; jc.waterReflectivity = conf.waterReflectivity;
jc.waterDistortion = conf.waterDistortion; jc.waterDistortion = conf.waterDistortion;
jc.indoor = conf.indoor; jc.indoor = conf.indoor;
jc.shadeColor = conf.shadeColor.component;
jc.exposure = conf.exposure;
jc.contrast = conf.contrast;
jc.saturation = conf.saturation;
jc.bloomAmount = conf.bloomAmount;
return jc; return jc;
} }
@ -525,6 +535,11 @@ world_config_from_json :: (jc: *World_Json_Config) -> World_Config {
conf.waterReflectivity = jc.waterReflectivity; conf.waterReflectivity = jc.waterReflectivity;
conf.waterDistortion = jc.waterDistortion; conf.waterDistortion = jc.waterDistortion;
conf.indoor = jc.indoor; conf.indoor = jc.indoor;
conf.shadeColor.component = jc.shadeColor;
conf.exposure = jc.exposure;
conf.contrast = jc.contrast;
conf.saturation = jc.saturation;
conf.bloomAmount = jc.bloomAmount;
return conf; return conf;
} }
@ -687,6 +702,49 @@ load_world_from_json :: (json_str: string, chunk_bin: []u8) -> (World, bool) {
return world, true; return world, true;
} }
// --- Baked light probes (probes.bin) ---
PROBES_MAGIC :: u32.[0x42505254][0]; // "TRPB" as little-endian u32
PROBES_VERSION :: cast(u32) 1;
Probes_File_Header :: struct {
magic : u32;
version : u32;
chunk_count : u32;
}
Probe_Chunk :: struct {
key : Chunk_Key;
probes : []Probe;
}
save_probes :: (chunks: []Probe_Chunk) -> string {
builder: String_Builder;
write_value(*builder, Probes_File_Header.{PROBES_MAGIC, PROBES_VERSION, xx chunks.count});
for chunks {
assert(it.probes.count == PROBES_PER_CHUNK);
write_value(*builder, it.key);
write_bytes(*builder, it.probes.data, PROBES_PER_CHUNK * size_of(Probe));
}
return builder_to_string(*builder);
}
// Hands the probes in a probes.bin to the renderer.
load_probes :: (data: []u8) -> bool {
cursor : s64;
if data.count < size_of(Probes_File_Header) return false;
header := read_value(data, *cursor, Probes_File_Header);
if header.magic != PROBES_MAGIC || header.version != PROBES_VERSION return false;
chunk_bytes := size_of(Chunk_Key) + PROBES_PER_CHUNK * size_of(Probe);
if cursor + header.chunk_count * chunk_bytes > data.count return false;
for 1..header.chunk_count {
key := read_value(data, *cursor, Chunk_Key);
world_set_chunk_probes(key, .{PROBES_PER_CHUNK, xx (data.data + cursor)});
cursor += PROBES_PER_CHUNK * size_of(Probe);
}
return true;
}
World_Config :: struct { World_Config :: struct {
// All of the @Notes are for the autoedit functionality. // All of the @Notes are for the autoedit functionality.
skyBase : Vector3 = .{0.38, 0.81, 0.95}; @Color skyBase : Vector3 = .{0.38, 0.81, 0.95}; @Color
@ -695,6 +753,12 @@ World_Config :: struct {
horizonHalo : Vector3 = .{1.0, 1.0, 1.0}; @Color horizonHalo : Vector3 = .{1.0, 1.0, 1.0}; @Color
sunHalo : Vector3 = .{1.0, 1.0, 1.0}; @Color sunHalo : Vector3 = .{1.0, 1.0, 1.0}; @Color
sunLightColor : Vector3 = .{1.0, 1.0, 1.0}; @Color sunLightColor : Vector3 = .{1.0, 1.0, 1.0}; @Color
shadeColor : Vector3 = .{0.45, 0.45, 0.75}; @Color
exposure : float = 0; @Slider,-3,3,0.1
contrast : float = 1; @Slider,0,3,0.05
saturation : float = 1; @Slider,0,3,0.05
bloomAmount : float = 0.4; @Slider,0,3,0.05
sunPosition : Vector3 = #run normalize(Vector3.{0.2, 0.3, 0.4}); sunPosition : Vector3 = #run normalize(Vector3.{0.2, 0.3, 0.4});
sunIntensity : float = 1.0; @Slider,0,4,0.1 sunIntensity : float = 1.0; @Slider,0,4,0.1
skyIntensity : float = 0.3; @Slider,0,5,0.1 skyIntensity : float = 0.3; @Slider,0,5,0.1
@ -709,9 +773,9 @@ World_Config :: struct {
waterReflectivity : float = 0.0; @Slider,0,1,0.05 waterReflectivity : float = 0.0; @Slider,0,1,0.05
waterDistortion : float = 0.007; @Slider,0,0.05,0.001 waterDistortion : float = 0.007; @Slider,0,0.05,0.001
hsv_lighting : s32 = 1; @Slider,0,1,1 hsv_lighting : s32 = 1; @Slider,0,1,1
// Indoor levels: no sun shadows (faces toward the sun still light up), a // Indoor levels float in black: no sky and no water. Sunlight gets in through
// flat skyBase instead of the sky, and no water. The reflection plane moves // glass. The reflection plane moves to the top of the floor layer (y = 1) for
// to the top of the floor layer (y = 1) for floor reflections. // floor reflections.
indoor : s32 = 0; @Slider,0,1,1 indoor : s32 = 0; @Slider,0,1,1
// ambientColor : Vector3 = .{1.0, 1.0, 1.0}; @Color // ambientColor : Vector3 = .{1.0, 1.0, 1.0}; @Color
@ -726,11 +790,26 @@ world_environment :: (world: *World) -> *Environment {
conf := *world.conf; conf := *world.conf;
env.reflection_height = effective_plane_height(conf); env.reflection_height = effective_plane_height(conf);
env.water = !conf.indoor; env.water = !conf.indoor;
env.sky = !conf.indoor;
env.water_color = conf.waterColor; env.water_color = conf.waterColor;
env.deep_color = conf.deepColor; env.deep_color = conf.deepColor;
env.water_reflectivity = conf.waterReflectivity; env.water_reflectivity = conf.waterReflectivity;
env.water_distortion = conf.waterDistortion; env.water_distortion = conf.waterDistortion;
env.water_shininess = conf.waterShininess;
env.water_normals = get_texture_from_pack("core", "utiltex/water_small.png"); env.water_normals = get_texture_from_pack("core", "utiltex/water_small.png");
env.sun_direction = normalize(conf.sunPosition);
env.sun_color = conf.sunLightColor;
env.shade_color = conf.shadeColor;
env.sky_base = conf.skyBase;
env.sky_top = conf.skyTop;
env.sun_disk = conf.sunDisk;
env.sun_halo = conf.sunHalo;
env.horizon_halo = conf.horizonHalo;
env.sky_intensity = conf.skyIntensity;
env.exposure = conf.exposure;
env.contrast = conf.contrast;
env.saturation = conf.saturation;
env.bloom_amount = conf.bloomAmount;
return env; return env;
} }

Binary file not shown.