keep moving rendering to new format
This commit is contained in:
parent
b415b81584
commit
0bd7102b35
10
first.jai
10
first.jai
@ -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";
|
||||
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[1] = max(
|
||||
max_asset_file_size(game_resources_dir, "world.json", 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[3] = max_asset_file_size(game_resources_dir, "probes.bin", 4 * 1024 * 1024);
|
||||
|
||||
names :: string.["MAX_PACK_SIZE", "MAX_WORLD_SIZE", "MAX_CHUNKS_SIZE"];
|
||||
for i: 0..2 {
|
||||
names :: string.["MAX_PACK_SIZE", "MAX_WORLD_SIZE", "MAX_CHUNKS_SIZE", "MAX_PROBES_SIZE"];
|
||||
for i: 0..3 {
|
||||
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.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;
|
||||
|
||||
set_build_options(opts, w);
|
||||
|
||||
BIN
libtacoma.so
BIN
libtacoma.so
Binary file not shown.
@ -24,7 +24,7 @@ VK_KHR_deferred_host_operations
|
||||
VK_KHR_acceleration_structure
|
||||
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:
|
||||
- available: 1.4.309
|
||||
@ -51,7 +51,7 @@ 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)
|
||||
BLAS Compaction: 0.4MB -> 0.2MB (0.3MB saved, 65.7% smaller)
|
||||
_______________
|
||||
Vulkan Version:
|
||||
- available: 1.4.309
|
||||
@ -78,85 +78,4 @@ 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, 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)
|
||||
BLAS Compaction: 0.4MB -> 0.2MB (0.3MB saved, 65.7% smaller)
|
||||
|
||||
BIN
modules/Fontstash/wasm/fontstash.a
Normal file
BIN
modules/Fontstash/wasm/fontstash.a
Normal file
Binary file not shown.
Binary file not shown.
@ -15,14 +15,19 @@ Sky_Config :: struct {
|
||||
skyDesaturation : float = 0.0;
|
||||
}
|
||||
|
||||
Trixel_Data :: struct {
|
||||
empty : bool = false;
|
||||
color : Vector3 = .{1.0, 0.0, 0.0};
|
||||
material : float = 1.0;
|
||||
// emission is the light it gives off, already scaled. Glass is seen as a solid
|
||||
// surface but lets the sun through, tinted by its colour.
|
||||
Swatch :: struct {
|
||||
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 {
|
||||
trixels : [16][16][16]Trixel_Data;
|
||||
trixels : [16][16][16]u8; // swatch ids, 0 = empty
|
||||
vertices : *float;
|
||||
vertexCount : s32;
|
||||
}
|
||||
@ -47,7 +52,7 @@ Tacoma_Context :: struct {}
|
||||
|
||||
tacoma_init :: (base_path: *u8) -> *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_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;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
master_volume 0.205032
|
||||
music_volume 1
|
||||
sfx_volume 1
|
||||
fullscreen 0
|
||||
fullscreen 1
|
||||
|
||||
@ -24,6 +24,7 @@ Fetch_Type :: enum {
|
||||
PACK;
|
||||
WORLD;
|
||||
WORLD_CHUNKS;
|
||||
WORLD_PROBES;
|
||||
}
|
||||
|
||||
Fetch_Request :: struct {
|
||||
@ -66,6 +67,7 @@ fetch_type_buffer_size :: (type: Fetch_Type) -> s64 {
|
||||
case .PACK; return MAX_PACK_SIZE;
|
||||
case .WORLD; return MAX_WORLD_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);
|
||||
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 {
|
||||
set_loaded_world(world);
|
||||
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 {
|
||||
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.name = sprint("%", name);
|
||||
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);
|
||||
case "ttf";
|
||||
case;
|
||||
|
||||
@ -253,37 +253,24 @@ draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
|
||||
r.h = ui_h(3,0);
|
||||
if GR.button(r, "Render with Tacoma", *theme.button_theme) {
|
||||
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;
|
||||
if GR.button(r, "Bake all chunk RDMs", *theme.button_theme) {
|
||||
if curworld.valid then rdm_bake_all_chunks(curworld.world, tacomaSamples, true);
|
||||
if GR.button(r, "Bake probes", *t_button_selectable(theme, !probe_bake.active)) {
|
||||
if curworld.valid && !probe_bake.active bake_probes(tacomaSamples);
|
||||
}
|
||||
r.y += r.h;
|
||||
if GR.button(r, "Bake all chunk SH grids", *t_button_selectable(theme, !sh_bake.active)) {
|
||||
if curworld.valid && !sh_bake.active sh_bake_start(tacomaSamples);
|
||||
}
|
||||
r.y += r.h;
|
||||
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));
|
||||
if probe_bake.active {
|
||||
total := probe_bake.keys.count;
|
||||
done := probe_bake.results.count;
|
||||
GR.label(r, tprint("Baking probes: %/% chunks", done, total), *t_label_left(theme));
|
||||
r.y += r.h;
|
||||
}
|
||||
if sh_bake.active {
|
||||
total := cast(s32) sh_bake.chunk_keys.count;
|
||||
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;
|
||||
|
||||
if tacoma_preview.texture.id {
|
||||
aspect := cast(float) tacoma_preview.width / cast(float) tacoma_preview.height;
|
||||
r.h = r.w / aspect;
|
||||
uiTex := New(Ui_Texture,, temp);
|
||||
uiTex.tex = current_screenshot.image;
|
||||
uiTex.tex = tacoma_preview.texture;
|
||||
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});
|
||||
set_shader_for_color();
|
||||
@ -299,13 +286,9 @@ draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
|
||||
r.y += r.h;
|
||||
GR.slider(r, *tacomaResolution, 10, 5000, 10, *theme.slider_theme);
|
||||
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;
|
||||
GR.slider(r, *bake_sky_scale, 0.0, 5.0, 0.05, *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);
|
||||
GR.slider(r, *bake_emissive_light, 0.0, 50.0, 0.5, *theme.slider_theme);
|
||||
r.y += r.h;
|
||||
|
||||
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 :: () {
|
||||
#if FLAG_TACOMA_ENABLED { rdm_bake_tick(); sh_bake_tick(); }
|
||||
#if FLAG_TACOMA_ENABLED { probe_bake_tick(); }
|
||||
tick_level_editor_camera();
|
||||
tick_particles(cast(float)delta_time);
|
||||
|
||||
@ -1139,7 +1122,8 @@ draw_editor_billboards :: (view: View_Id) {
|
||||
draw_level_editor :: () {
|
||||
curworld := get_current_world();
|
||||
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_live_particles(view);
|
||||
if show_trile_preview && !trile_preview_disabled {
|
||||
|
||||
@ -23,6 +23,14 @@ draw_material_studio_ui :: (theme: *GR.Overall_Theme) {
|
||||
}
|
||||
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.h = ui_h(25, 0);
|
||||
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;
|
||||
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);
|
||||
if a & .ENTERED {
|
||||
ms_name_input = copy_string(new_text);
|
||||
@ -42,25 +52,49 @@ draw_material_studio_ui :: (theme: *GR.Overall_Theme) {
|
||||
}
|
||||
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);
|
||||
GR.label(r, "Roughness", *t_label_left(theme));
|
||||
GR.label(r, "Reflectivity", *t_label_left(theme));
|
||||
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;
|
||||
GR.label(r, "Metallic", *t_label_left(theme));
|
||||
GR.label(r, "Gloss", *t_label_left(theme));
|
||||
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;
|
||||
GR.label(r, "Emittance", *t_label_left(theme));
|
||||
r.y += r.h;
|
||||
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;
|
||||
|
||||
// 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);
|
||||
|
||||
r.h = row_h;
|
||||
@ -78,3 +112,15 @@ draw_material_studio_ui :: (theme: *GR.Overall_Theme) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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 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("Glow: %", def.glow), *def.glow, 0, 1, 0.05, theme);
|
||||
|
||||
pe_label(*er, tprint("Blend Mode: %", def.blend_mode), theme);
|
||||
orig_w2 := er.w;
|
||||
|
||||
@ -5,346 +5,86 @@
|
||||
|
||||
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;
|
||||
|
||||
RDM_Bake_Job :: struct {
|
||||
world_trile_index: s32;
|
||||
world_pos: Vector3;
|
||||
size: s32; // per-instance RDM side in px (tile = 2*size x 3*size).
|
||||
}
|
||||
|
||||
RDM_Global_Bake :: struct {
|
||||
data : *float; // CPU atlas buffer (RGBA32F during bake)
|
||||
tacoma_preview : struct {
|
||||
texture : Texture;
|
||||
width : s32;
|
||||
height : s32;
|
||||
cursor_x : s32;
|
||||
cursor_y : s32;
|
||||
row_height : s32;
|
||||
entries : [..]RDM_Atlas_Entry_Bake;
|
||||
}
|
||||
|
||||
RDM_Bake_State :: struct {
|
||||
active : bool = false;
|
||||
quality : s32 = 100;
|
||||
jobs : [..]RDM_Bake_Job;
|
||||
current_job : s32 = 0;
|
||||
atlas : RDM_Global_Bake;
|
||||
}
|
||||
// Light given off by a fully emissive swatch, relative to full sun on a white face.
|
||||
bake_emissive_light : float = 10.0;
|
||||
|
||||
rdm_bake : RDM_Bake_State;
|
||||
// Probes where more of the rays than this hit the back of a face are inside geometry.
|
||||
PROBE_INSIDE_BACKFACES :: 0.25;
|
||||
|
||||
SH_Bake_State :: struct {
|
||||
Probe_Bake :: struct {
|
||||
active : bool;
|
||||
quality : s32;
|
||||
include_water : bool;
|
||||
chunk_keys : [..]Chunk_Key;
|
||||
current_chunk : s32;
|
||||
world_name : string;
|
||||
keys : [..]Chunk_Key;
|
||||
results : [..]*float; // from Tacoma, one per baked key
|
||||
}
|
||||
|
||||
sh_bake : SH_Bake_State;
|
||||
probe_bake : Probe_Bake;
|
||||
|
||||
rdm_job_quality :: (job: RDM_Bake_Job) -> s32 {
|
||||
return rdm_bake.quality;
|
||||
}
|
||||
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; }
|
||||
|
||||
rdm_sort_jobs_by_height_desc :: (jobs: *[..]RDM_Bake_Job) {
|
||||
n := jobs.count;
|
||||
for i: 1..n-1 {
|
||||
key := jobs.data[i];
|
||||
j := i - 1;
|
||||
while j >= 0 && jobs.data[j].size < key.size {
|
||||
jobs.data[j + 1] = jobs.data[j];
|
||||
j -= 1;
|
||||
for _, key: curworld.world.chunks array_add(*probe_bake.keys, key);
|
||||
if !probe_bake.keys.count return;
|
||||
|
||||
tacoma_init_scene(*curworld.world);
|
||||
probe_bake.active = true;
|
||||
probe_bake.quality = quality;
|
||||
probe_bake.world_name = copy_string(curworld.world.name);
|
||||
} @Command
|
||||
|
||||
// Bakes one chunk per call.
|
||||
probe_bake_tick :: () {
|
||||
if !probe_bake.active return;
|
||||
curworld := get_current_world();
|
||||
if !curworld.valid || curworld.world.name != probe_bake.world_name {
|
||||
log_warn("Probe bake cancelled: the world changed");
|
||||
probe_bake_end();
|
||||
return;
|
||||
}
|
||||
jobs.data[j + 1] = key;
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
if probe_bake.results.count == probe_bake.keys.count {
|
||||
probe_bake_finish();
|
||||
return;
|
||||
}
|
||||
|
||||
job := rdm_bake.jobs[rdm_bake.current_job];
|
||||
|
||||
size : s32 = job.size;
|
||||
w := 2 * size;
|
||||
h := 3 * size;
|
||||
ptr := Tacoma.tacoma_render_rdm(ctx, job.world_trile_index, 0, rdm_job_quality(job), size);
|
||||
|
||||
bake := *rdm_bake.atlas;
|
||||
|
||||
if bake.cursor_x + w > bake.width {
|
||||
bake.cursor_y += bake.row_height;
|
||||
bake.cursor_x = 0;
|
||||
bake.row_height = 0;
|
||||
}
|
||||
|
||||
ax := bake.cursor_x;
|
||||
ay := bake.cursor_y;
|
||||
|
||||
if ay + h <= bake.height {
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
rdm_bake_finish :: () {
|
||||
if ctx != null then tacoma_stop();
|
||||
gen_reference :: (w: s32, h: s32, eye: Vector3, target: Vector3, quality: s32, world: *World) {
|
||||
tacoma_init_scene(world);
|
||||
data := Tacoma.tacoma_render_reference(ctx, w, h, eye, target, 0.01, quality);
|
||||
tacoma_stop();
|
||||
|
||||
curworld := get_current_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);
|
||||
}
|
||||
pixels := NewArray(w * h * 4, u8,, temp);
|
||||
for i: 0..w * h - 1 {
|
||||
for c: 0..2 pixels[i * 4 + c] = xx (clamp(data[i * 4 + c], 0, 1) * 255 + 0.5);
|
||||
pixels[i * 4 + 3] = 255;
|
||||
}
|
||||
Tacoma.tacoma_free_result(data);
|
||||
|
||||
// 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 = .{};
|
||||
if tacoma_preview.texture.id texture_destroy(tacoma_preview.texture);
|
||||
tacoma_preview.texture = texture_create(w, h, pixels);
|
||||
tacoma_preview.width = w;
|
||||
tacoma_preview.height = h;
|
||||
}
|
||||
|
||||
// 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]);
|
||||
#scope_file
|
||||
|
||||
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) {
|
||||
tacoma_init_scene :: (world: *World) {
|
||||
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;
|
||||
@ -355,230 +95,136 @@ tacoma_init_scene :: (world: World, include_water: bool) {
|
||||
if !success {
|
||||
trile, trile_ok := get_trile(group.trile_name);
|
||||
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;
|
||||
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);
|
||||
ttrile.trixels = trile.trixels;
|
||||
ttrile.vertices = positions.data;
|
||||
ttrile.vertexCount = xx indices.count;
|
||||
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 {
|
||||
wx, wy, wz := chunk_local_to_world(chunk.coord, inst.x, inst.y, inst.z);
|
||||
array_add(*world_triles, Tacoma.World_Trile.{idx,
|
||||
Vector3.{cast(float) wx, cast(float) wy, cast(float) wz},
|
||||
cast(s32) inst.orientation});
|
||||
array_add(*world_triles, Tacoma.World_Trile.{idx, .{xx wx, xx wy, xx wz}, xx inst.orientation});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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};
|
||||
swatches : [Tacoma.SWATCH_COUNT]Tacoma.Swatch;
|
||||
for g_swatches {
|
||||
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/");
|
||||
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.tacoma_destroy(ctx);
|
||||
ctx = null;
|
||||
}
|
||||
|
||||
tacoma_handle_result :: (ptr: *float, w: s32, h: s32) {
|
||||
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);
|
||||
probe_bake_finish :: () {
|
||||
tacoma_stop();
|
||||
}
|
||||
|
||||
sh_bake_start :: (quality: s32 = 50, include_water: bool = false) {
|
||||
if sh_bake.active then return;
|
||||
curworld := get_current_world();
|
||||
if !curworld.valid { log_warn("sh_bake_start: no world loaded"); return; }
|
||||
chunks := probes_from_bake(probe_bake.keys, probe_bake.results);
|
||||
for chunks world_set_chunk_probes(it.key, it.probes);
|
||||
|
||||
world := *curworld.world;
|
||||
for _, key: world.chunks array_add(*sh_bake.chunk_keys, key);
|
||||
|
||||
if sh_bake.chunk_keys.count == 0 then return;
|
||||
|
||||
tacoma_init_scene(world.*, include_water);
|
||||
sh_bake.active = true;
|
||||
sh_bake.quality = quality;
|
||||
sh_bake.include_water = include_water;
|
||||
sh_bake.current_chunk = 0;
|
||||
} @Command
|
||||
|
||||
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();
|
||||
if !curworld.valid { sh_bake_finish(); return; }
|
||||
|
||||
n :: SH_PROBE_N;
|
||||
tex_w :: SH_TEX_W;
|
||||
|
||||
chunk_key := sh_bake.chunk_keys[sh_bake.current_chunk];
|
||||
chunk := table_find_pointer(*curworld.world.chunks, chunk_key);
|
||||
sh_bake.current_chunk += 1;
|
||||
if chunk == null then return;
|
||||
|
||||
ox, oy, oz := chunk_local_to_world(chunk_key, 0, 0, 0);
|
||||
SH_PAD :: 2.0;
|
||||
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).*);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 :: () {
|
||||
tacoma_stop();
|
||||
array_free(sh_bake.chunk_keys);
|
||||
sh_bake = .{};
|
||||
log_info("SH bake complete.");
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
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));
|
||||
}
|
||||
for chunks array_free(it.probes);
|
||||
probe_bake_end();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
probe_bake_end :: () {
|
||||
if ctx tacoma_stop();
|
||||
for probe_bake.results Tacoma.tacoma_free_result(it);
|
||||
array_free(probe_bake.results);
|
||||
array_free(probe_bake.keys);
|
||||
free(probe_bake.world_name);
|
||||
probe_bake = .{};
|
||||
}
|
||||
|
||||
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);
|
||||
// Marks probes inside geometry, and evens out bake noise by averaging each probe
|
||||
// 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]);
|
||||
|
||||
Baked :: type_of(baked);
|
||||
baked_probe :: (baked: *Baked, x: s32, y: s32, z: s32) -> *float {
|
||||
found, data := table_find(baked, world_to_chunk_coord(x, y, z));
|
||||
if !found return null;
|
||||
lx, ly, lz := world_to_local(x, y, z);
|
||||
p := data + (cast(s64) lx + cast(s64) ly * N + cast(s64) lz * N * N) * Tacoma.PROBE_FLOATS;
|
||||
if p[12] > PROBE_INSIDE_BACKFACES return null;
|
||||
return p;
|
||||
}
|
||||
|
||||
NEIGHBOURS :: s32.[1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1];
|
||||
|
||||
chunks := NewArray(keys.count, Probe_Chunk,, temp);
|
||||
for key, chunk_index: keys {
|
||||
probes := NewArray(PROBES_PER_CHUNK, Probe);
|
||||
for z: 0..N-1 for y: 0..N-1 for x: 0..N-1 {
|
||||
probe := *probes[x + y * N + z * N * N];
|
||||
wx, wy, wz := chunk_local_to_world(key, xx x, xx y, xx z);
|
||||
centre := baked_probe(*baked, wx, wy, wz);
|
||||
if !centre {
|
||||
probe.sh[0] = half_from_float(-1);
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
half_from_float :: (f: float) -> u16 {
|
||||
bits := (cast(*u32) *f).*;
|
||||
sign := cast(u16) ((bits >> 16) & 0x8000);
|
||||
exponent := cast(s32) ((bits >> 23) & 0xFF) - 127 + 15;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ Trile_Editor_Tool :: enum {
|
||||
PAINT;
|
||||
ADD;
|
||||
REMOVE;
|
||||
PICK; // takes the clicked trixel's material
|
||||
}
|
||||
|
||||
Trile_Editor_Tool_Mode :: enum {
|
||||
@ -57,6 +58,9 @@ ntrile :: (name: string) {
|
||||
trile_changed(editor_current_trile);
|
||||
newt : Trile;
|
||||
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);
|
||||
editor_current_trile = get_trile(newt.name);
|
||||
trile_gen_heightmap(editor_current_trile);
|
||||
@ -134,6 +138,10 @@ area_active : bool;
|
||||
|
||||
handle_tool_click :: () {
|
||||
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 {
|
||||
push_undo();
|
||||
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;
|
||||
r.y += r.h;
|
||||
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;
|
||||
r.y += r.h;
|
||||
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) {
|
||||
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);
|
||||
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.
|
||||
@ -436,6 +472,16 @@ draw_meta_tab :: (theme: *GR.Overall_Theme, area: GR.Rect) {
|
||||
r.y += row_h;
|
||||
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;
|
||||
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.h = row_h;
|
||||
|
||||
@ -48,6 +48,7 @@ Editor_Action :: enum {
|
||||
// Trile editor — undo/redo
|
||||
TRIXEL_UNDO;
|
||||
TRIXEL_REDO;
|
||||
TRIXEL_TOOL_PICK;
|
||||
}
|
||||
|
||||
#scope_file;
|
||||
@ -217,4 +218,5 @@ set_default_bindings :: () {
|
||||
set(.TRIXEL_TILING_ROTATE, cast(Key_Code) #char "R");
|
||||
set(.TRIXEL_UNDO, cast(Key_Code) #char "Z", ctrl = true);
|
||||
set(.TRIXEL_REDO, cast(Key_Code) #char "Y", ctrl = true);
|
||||
set(.TRIXEL_TOOL_PICK, cast(Key_Code) #char "K");
|
||||
}
|
||||
|
||||
@ -140,7 +140,9 @@ init_after_core :: () {
|
||||
ltriles();
|
||||
tt := get_trile_table_ptr();
|
||||
if tt.count == 0 {
|
||||
set_trile("test", Trile.{});
|
||||
test : Trile;
|
||||
trile_fill(*test, first_used_swatch());
|
||||
set_trile("test", test);
|
||||
} else {
|
||||
name : string;
|
||||
for v : tt {
|
||||
|
||||
@ -19,6 +19,7 @@ Particle_Emitter_Config :: struct {
|
||||
color_start : Vector4 = .{1, 1, 1, 1};
|
||||
color_end : Vector4 = .{1, 1, 1, 0};
|
||||
gravity : float = 2.0;
|
||||
glow : float; // 0..1, like swatch emittance
|
||||
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,
|
||||
},
|
||||
color = lerp_color(def.color_start, def.color_end, t),
|
||||
glow = def.glow,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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_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.{
|
||||
enabled = true,
|
||||
@ -7,61 +11,123 @@ ALPHA_BLEND :: sg_blend_state.{
|
||||
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 {
|
||||
SCREEN;
|
||||
VIEW;
|
||||
VIEW_OPAQUE;
|
||||
VIEW_TRANSPARENT;
|
||||
SHADOW;
|
||||
POST; // colour only, view format
|
||||
}
|
||||
|
||||
samplers : struct {
|
||||
nearest_clamp : sg_sampler;
|
||||
linear_clamp : sg_sampler;
|
||||
linear_repeat : sg_sampler;
|
||||
shadow : sg_sampler;
|
||||
}
|
||||
|
||||
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.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.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.
|
||||
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;
|
||||
desc : sg_pipeline_desc;
|
||||
desc.shader = sg_make_shader(*shader_desc);
|
||||
if blend desc.colors[0].blend = ALPHA_BLEND;
|
||||
if target == .VIEW {
|
||||
desc.colors[0].blend = blend;
|
||||
if target != .SCREEN {
|
||||
desc.colors[0].pixel_format = VIEW_COLOR_FORMAT;
|
||||
desc.depth.pixel_format = VIEW_DEPTH_FORMAT;
|
||||
desc.depth.compare = .LESS_EQUAL;
|
||||
desc.depth.write_enabled = depth_write;
|
||||
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;
|
||||
}
|
||||
|
||||
Pass_Kind :: enum {
|
||||
MAIN;
|
||||
REFLECTION;
|
||||
SHADOW;
|
||||
}
|
||||
|
||||
Pass :: struct {
|
||||
scene : *Scene;
|
||||
view_index : int;
|
||||
view : View;
|
||||
reflection : bool;
|
||||
kind : Pass_Kind;
|
||||
params : View_Params;
|
||||
shadow : Shadow_Params;
|
||||
}
|
||||
|
||||
make_pass :: (scene: *Scene, view_index: int, reflection: bool) -> Pass {
|
||||
pass := Pass.{ scene = scene, view_index = view_index, view = scene.views[view_index], reflection = reflection };
|
||||
height := ifx pass.view.environment then pass.view.environment.reflection_height else 0;
|
||||
make_pass :: (scene: *Scene, view_index: int, kind: Pass_Kind) -> Pass {
|
||||
pass := Pass.{ scene = scene, view_index = view_index, view = scene.views[view_index], kind = kind };
|
||||
env := view_environment(pass.view);
|
||||
height := env.reflection_height;
|
||||
|
||||
camera := pass.view.camera.position;
|
||||
view_proj := scene.view_proj[view_index];
|
||||
pass.params.clip = .[0, 0, 0, 1];
|
||||
if reflection {
|
||||
if kind == .REFLECTION {
|
||||
camera.y = 2 * height - camera.y;
|
||||
view_proj = scene.reflection_view_proj[view_index];
|
||||
pass.params.clip = .[0, 1, 0, -height];
|
||||
}
|
||||
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.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;
|
||||
}
|
||||
|
||||
@ -133,3 +199,14 @@ batch_instances :: (view_index: int, draws: []Keyed($I), batches: *[..]Batch, in
|
||||
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 .{};
|
||||
}
|
||||
|
||||
49
src/renderer_sokol/lines.jai
Normal file
49
src/renderer_sokol/lines.jai
Normal 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;
|
||||
}
|
||||
107
src/renderer_sokol/probes.jai
Normal file
107
src/renderer_sokol/probes.jai
Normal 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);
|
||||
}
|
||||
@ -6,14 +6,21 @@
|
||||
#load "gpu.jai";
|
||||
#load "ui.jai";
|
||||
#load "triles.jai";
|
||||
#load "probes.jai";
|
||||
#load "views.jai";
|
||||
#load "water.jai";
|
||||
#load "billboards.jai";
|
||||
#load "sprites.jai";
|
||||
#load "sky.jai";
|
||||
#load "lines.jai";
|
||||
|
||||
renderer_init :: (orientations: [ORIENTATION_COUNT]Matrix3) {
|
||||
sg_setup(*(sg_desc.{
|
||||
environment = cast,force(sg_environment) sglue_environment(),
|
||||
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];
|
||||
@ -22,9 +29,12 @@ renderer_init :: (orientations: [ORIENTATION_COUNT]Matrix3) {
|
||||
samplers_init();
|
||||
ui_init();
|
||||
triles_init(orientations);
|
||||
probes_init();
|
||||
views_init();
|
||||
water_init();
|
||||
billboards_init();
|
||||
sprites_init();
|
||||
sky_init();
|
||||
lines_init();
|
||||
}
|
||||
|
||||
renderer_shutdown :: () {
|
||||
@ -54,24 +64,42 @@ renderer_texture_destroy :: (id: u32) {
|
||||
|
||||
renderer_render :: (scene: *Scene, width: s32, height: s32) {
|
||||
triles_upload(scene);
|
||||
billboards_upload(scene);
|
||||
probes_upload();
|
||||
sprites_upload(scene);
|
||||
lines_upload(scene);
|
||||
ui_upload(scene);
|
||||
|
||||
for view, view_index: scene.views {
|
||||
for reflection: bool.[true, false] {
|
||||
pass := make_pass(scene, view_index, reflection);
|
||||
view_begin_pass(view_index, width, height, reflection);
|
||||
shadow := make_pass(scene, view_index, .SHADOW);
|
||||
view_begin_shadow_pass(view_index);
|
||||
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);
|
||||
billboards_draw(*pass);
|
||||
sprites_draw(*pass, transparent = false);
|
||||
sg_end_pass();
|
||||
|
||||
view_begin_pass(view_index, width, height, kind, opaque = false);
|
||||
water_draw(*pass);
|
||||
sprites_draw(*pass, transparent = true);
|
||||
lines_draw(*pass, width, height);
|
||||
sg_end_pass();
|
||||
}
|
||||
view_bloom(view_index);
|
||||
}
|
||||
|
||||
action : sg_pass_action;
|
||||
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() }));
|
||||
for scene.views view_composite(it_index);
|
||||
for scene.views view_composite(it_index, view_environment(it));
|
||||
ui_draw(scene, width, height);
|
||||
sg_end_pass();
|
||||
sg_commit();
|
||||
|
||||
@ -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
|
||||
@ -1,8 +1,12 @@
|
||||
@block view
|
||||
layout(binding=0) uniform view_params {
|
||||
mat4 view_proj;
|
||||
mat4 inv_view_proj;
|
||||
vec4 camera; // xyz = position, w = time
|
||||
vec4 clip; // plane; geometry on its negative side is dropped
|
||||
vec4 sun_direction;
|
||||
vec4 sun_color;
|
||||
vec4 shade_color;
|
||||
};
|
||||
@end
|
||||
|
||||
@ -12,3 +16,61 @@ vec2 quad_corner(int vertex_index) {
|
||||
return corners[vertex_index];
|
||||
}
|
||||
@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
|
||||
|
||||
@ -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
41
src/renderer_sokol/shaders/line.glsl
Normal file
41
src/renderer_sokol/shaders/line.glsl
Normal 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
|
||||
90
src/renderer_sokol/shaders/post.glsl
Normal file
90
src/renderer_sokol/shaders/post.glsl
Normal 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
|
||||
@ -1,6 +1,8 @@
|
||||
@include common.glsl
|
||||
@include ui.glsl
|
||||
@include composite.glsl
|
||||
@include post.glsl
|
||||
@include trile.glsl
|
||||
@include water.glsl
|
||||
@include billboard.glsl
|
||||
@include sprite.glsl
|
||||
@include sky.glsl
|
||||
@include line.glsl
|
||||
|
||||
53
src/renderer_sokol/shaders/sky.glsl
Normal file
53
src/renderer_sokol/shaders/sky.glsl
Normal 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
|
||||
97
src/renderer_sokol/shaders/sprite.glsl
Normal file
97
src/renderer_sokol/shaders/sprite.glsl
Normal 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
|
||||
@ -3,51 +3,223 @@
|
||||
|
||||
layout(binding=1) uniform trile_params {
|
||||
vec4 orientations[72];
|
||||
float show_hidden; // shadow and reflection passes still draw cut triles
|
||||
};
|
||||
|
||||
in vec3 position;
|
||||
in float swatch;
|
||||
in float ao;
|
||||
in vec4 data_n; // bytes / 255: x = swatch, y = ao (0..3), z = normal index
|
||||
in vec4 instance; // xyz = position, w = orientation
|
||||
in float tint;
|
||||
in float cut;
|
||||
|
||||
out float v_clip;
|
||||
out vec3 v_to_camera;
|
||||
out vec3 v_world;
|
||||
out vec3 v_normal;
|
||||
out float v_swatch;
|
||||
out float v_facing;
|
||||
out float v_ao;
|
||||
out vec3 v_shade;
|
||||
out vec3 v_sun;
|
||||
out float v_tint;
|
||||
out float v_cut;
|
||||
|
||||
void main() {
|
||||
vec4 data = data_n * 255.0;
|
||||
int ori = int(instance.w + 0.5) * 3;
|
||||
vec3 p = position - 0.5;
|
||||
vec3 world = vec3(dot(orientations[ori].xyz, p),
|
||||
dot(orientations[ori + 1].xyz, p),
|
||||
dot(orientations[ori + 2].xyz, p)) + 0.5 + instance.xyz;
|
||||
mat3 rotation = transpose(mat3(orientations[ori].xyz, orientations[ori + 1].xyz, orientations[ori + 2].xyz));
|
||||
vec3 world = rotation * (position - 0.5) + 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);
|
||||
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_swatch = swatch;
|
||||
v_ao = ao;
|
||||
v_swatch = data.x;
|
||||
v_tint = tint;
|
||||
}
|
||||
@end
|
||||
|
||||
@fs fs_trile
|
||||
@block trile_in
|
||||
in float v_clip;
|
||||
in vec3 v_to_camera;
|
||||
in vec3 v_world;
|
||||
in vec3 v_normal;
|
||||
in float v_swatch;
|
||||
in float v_facing;
|
||||
in float v_ao;
|
||||
in vec3 v_shade;
|
||||
in vec3 v_sun;
|
||||
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 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() {
|
||||
if (v_clip < 0.0) discard;
|
||||
vec3 albedo = texelFetch(sampler2D(palette, palette_smp), ivec2(int(v_swatch + 0.5), 0), 0).rgb;
|
||||
albedo *= mix(0.4, 1.0, v_ao);
|
||||
if (v_clip < 0.0 || v_cut > dither(gl_FragCoord.xy)) discard;
|
||||
int id = int(v_swatch + 0.5);
|
||||
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);
|
||||
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
|
||||
|
||||
@program trile vs_trile fs_trile
|
||||
@program trile_shadow vs_trile fs_trile_shadow
|
||||
@program trile_glass_shadow vs_trile fs_trile_glass_shadow
|
||||
|
||||
@ -8,57 +8,80 @@ layout(binding=1) uniform water_vs_params {
|
||||
};
|
||||
|
||||
out vec3 v_to_camera;
|
||||
out vec4 v_normal_uv;
|
||||
out vec3 v_world;
|
||||
out vec2 v_xz;
|
||||
|
||||
void main() {
|
||||
vec2 xz = camera.xz + (quad_corner(gl_VertexIndex) * 2.0 - 1.0) * half_size;
|
||||
vec3 world = vec3(xz.x, height, xz.y);
|
||||
v_xz = camera.xz + (quad_corner(gl_VertexIndex) * 2.0 - 1.0) * half_size;
|
||||
vec3 world = vec3(v_xz.x, height, v_xz.y);
|
||||
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_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
|
||||
|
||||
@fs fs_water
|
||||
@include_block shadow
|
||||
@include_block transparent_out
|
||||
in vec3 v_to_camera;
|
||||
in vec4 v_normal_uv;
|
||||
out vec4 frag_color;
|
||||
in vec3 v_world;
|
||||
in vec2 v_xz;
|
||||
|
||||
layout(binding=2) uniform water_fs_params {
|
||||
vec3 color;
|
||||
float surface;
|
||||
vec3 water_color;
|
||||
vec3 sun_direction; float shininess;
|
||||
vec3 sun_color; float time;
|
||||
float reflectivity;
|
||||
float distortion;
|
||||
};
|
||||
|
||||
layout(binding=0) uniform texture2D reflection;
|
||||
layout(binding=1) uniform texture2D normals;
|
||||
layout(binding=2) uniform texture2D reflection_glow;
|
||||
layout(binding=0) uniform sampler reflection_smp;
|
||||
layout(binding=1) uniform sampler normals_smp;
|
||||
|
||||
void main() {
|
||||
if (surface < 0.5) {
|
||||
frag_color = vec4(color, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
vec2 flow = time * vec2(-0.005, -0.012);
|
||||
vec3 n1 = texture(sampler2D(normals, normals_smp), v_xz * 0.4 + flow * 1.5).xzy * 2.0 - 1.0;
|
||||
vec3 n2 = texture(sampler2D(normals, normals_smp), v_xz * 0.1 + flow * 1.7).xzy * 2.0 - 1.0;
|
||||
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);
|
||||
|
||||
vec2 uv = gl_FragCoord.xy / vec2(textureSize(sampler2D(reflection, reflection_smp), 0));
|
||||
uv.y = 1.0 - uv.y;
|
||||
vec3 reflected = texture(sampler2D(reflection, reflection_smp), uv + normal.xz * distortion).rgb;
|
||||
vec2 screen = gl_FragCoord.xy / vec2(textureSize(sampler2D(reflection, reflection_smp), 0));
|
||||
vec2 reflect_uv = vec2(screen.x, 1.0 - screen.y) + normal.xz * distortion;
|
||||
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);
|
||||
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
|
||||
|
||||
@program water_floor vs_water fs_water_floor
|
||||
@program water vs_water fs_water
|
||||
|
||||
29
src/renderer_sokol/sky.jai
Normal file
29
src/renderer_sokol/sky.jai
Normal 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;
|
||||
}
|
||||
101
src/renderer_sokol/sprites.jai
Normal file
101
src/renderer_sokol/sprites.jai
Normal 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;
|
||||
}
|
||||
@ -1,16 +1,32 @@
|
||||
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.attrs[ATTR_trile_position] = .{ format = .FLOAT3 };
|
||||
desc.layout.attrs[ATTR_trile_swatch] = .{ format = .FLOAT };
|
||||
desc.layout.attrs[ATTR_trile_ao] = .{ format = .FLOAT };
|
||||
desc.layout.attrs[ATTR_trile_data_n] = .{ format = .UBYTE4N };
|
||||
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_cut] = .{ format = .FLOAT, buffer_index = 1 };
|
||||
desc.index_type = .UINT16;
|
||||
desc.cull_mode = .BACK;
|
||||
desc.face_winding = .CCW;
|
||||
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 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);
|
||||
}
|
||||
|
||||
renderer_swatch_set :: (id: u8, color: [3]u8, roughness: u8, metallic: u8, emittance: u8) {
|
||||
for 0..2 triles.palette[id][it] = color[it];
|
||||
triles.palette[id][3] = 255;
|
||||
renderer_swatch_set :: (id: u8, color: [3]u8, reflectivity: u8, gloss: u8, emittance: u8, glass: bool) {
|
||||
for 0..2 triles.palette[0][id][it] = color[it];
|
||||
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;
|
||||
}
|
||||
|
||||
@ -44,7 +63,7 @@ triles_upload :: (scene: *Scene) {
|
||||
if triles.palette_dirty {
|
||||
triles.palette_dirty = false;
|
||||
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)) };
|
||||
triles.palette_image = sg_make_image(*desc);
|
||||
}
|
||||
@ -59,26 +78,62 @@ triles_upload :: (scene: *Scene) {
|
||||
if view.draw_world {
|
||||
for chunk: scene.chunks for group: chunk.groups for group.instances {
|
||||
position := chunk.origin + Vector3.{xx it.x, xx it.y, xx it.z};
|
||||
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 {
|
||||
if it.view.index != view_index continue;
|
||||
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);
|
||||
}
|
||||
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) {
|
||||
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_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 {
|
||||
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));
|
||||
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.images[IMG_palette] = triles.palette_image;
|
||||
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_draw(0, mesh.index_count, it.count);
|
||||
}
|
||||
@ -106,16 +168,19 @@ Instance :: struct {
|
||||
position : Vector3;
|
||||
orientation : float;
|
||||
tint : float;
|
||||
cut : float; // 0..1 dissolved away from the camera, still casts shadows
|
||||
}
|
||||
|
||||
PREVIEW_KEY :: 1 << 32;
|
||||
|
||||
triles : struct {
|
||||
pipeline : sg_pipeline;
|
||||
shadow_pipeline : sg_pipeline;
|
||||
glass_shadow_pipeline : sg_pipeline;
|
||||
params : Trile_Params;
|
||||
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_dirty : bool;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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_uv] = .{ format = .FLOAT2 };
|
||||
desc.layout.attrs[ATTR_ui_color0] = .{ format = .FLOAT4 };
|
||||
|
||||
@ -1,40 +1,140 @@
|
||||
BLOOM_LEVELS :: 5;
|
||||
BLOOM_SCATTER :: 0.7;
|
||||
|
||||
views_init :: () {
|
||||
desc := pipeline_desc(composite_shader_desc(sg_query_backend()), .SCREEN);
|
||||
views.composite = sg_make_pipeline(*desc);
|
||||
backend := sg_query_backend();
|
||||
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 {
|
||||
for views.targets {
|
||||
destroy_target(it.main);
|
||||
destroy_target(it.reflection);
|
||||
for level: it.bloom {
|
||||
sg_destroy_attachments(level.attachments);
|
||||
sg_destroy_image(level.image);
|
||||
}
|
||||
}
|
||||
array_reset_keeping_memory(*views.targets);
|
||||
views.width = width;
|
||||
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];
|
||||
|
||||
target := ifx kind == .REFLECTION then targets.reflection else targets.main;
|
||||
action : sg_pass_action;
|
||||
if opaque {
|
||||
action.colors[0] = .{ load_action = .CLEAR, clear_value = .{0, 0, 0, 1} };
|
||||
action.colors[1] = .{ load_action = .CLEAR, clear_value = .{0, 0, 0, 0} };
|
||||
action.colors[2] = .{ load_action = .CLEAR, clear_value = .{1.0e4, 0, 0, 0} };
|
||||
action.depth = .{ load_action = .CLEAR, clear_value = 1 };
|
||||
target := ifx reflection then targets.reflection else targets.main;
|
||||
sg_begin_pass(*(sg_pass.{ action = action, attachments = target.attachments }));
|
||||
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 {
|
||||
return views.targets[index].reflection.color;
|
||||
}
|
||||
|
||||
view_composite :: (index: int) {
|
||||
sg_apply_pipeline(views.composite);
|
||||
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);
|
||||
view_reflection_glow_image :: (index: int) -> sg_image {
|
||||
return views.targets[index].reflection.glow;
|
||||
}
|
||||
|
||||
// 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.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_draw(0, 3, 1);
|
||||
}
|
||||
@ -43,13 +143,20 @@ view_composite :: (index: int) {
|
||||
|
||||
View_Target :: struct {
|
||||
color : sg_image;
|
||||
glow : sg_image;
|
||||
scene : sg_image;
|
||||
depth : sg_image;
|
||||
attachments : sg_attachments;
|
||||
opaque : sg_attachments;
|
||||
transparent : sg_attachments;
|
||||
}
|
||||
|
||||
views : struct {
|
||||
composite : sg_pipeline;
|
||||
targets : [..]struct { main, reflection: View_Target; };
|
||||
post : sg_pipeline;
|
||||
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;
|
||||
height : s32;
|
||||
}
|
||||
@ -57,16 +164,24 @@ views : struct {
|
||||
make_target :: (width: s32, height: s32) -> 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.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 }));
|
||||
desc : sg_attachments_desc;
|
||||
desc.colors[0].image = t.color;
|
||||
desc.colors[1].image = t.glow;
|
||||
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;
|
||||
}
|
||||
|
||||
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.glow);
|
||||
sg_destroy_image(t.scene);
|
||||
sg_destroy_image(t.depth);
|
||||
}
|
||||
|
||||
@ -1,42 +1,62 @@
|
||||
WATER_HALF_SIZE :: 1000.0;
|
||||
|
||||
water_init :: () {
|
||||
desc := pipeline_desc(water_shader_desc(sg_query_backend()), .VIEW, blend = true);
|
||||
water.pipeline = sg_make_pipeline(*desc);
|
||||
backend := sg_query_backend();
|
||||
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) {
|
||||
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_shadow_params, *pass.shadow);
|
||||
|
||||
bind : sg_bindings;
|
||||
bind.images[IMG_reflection] = view_reflection_image(pass.view_index);
|
||||
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_normals_smp] = samplers.linear_repeat;
|
||||
bind_shadow(pass, *bind);
|
||||
sg_apply_bindings(*bind);
|
||||
|
||||
draw_plane :: (height: float, color: Vector3, surface: bool) #expand {
|
||||
vs := Water_Vs_Params.{ height = height, half_size = WATER_HALF_SIZE };
|
||||
vs := Water_Vs_Params.{ height = env.reflection_height, half_size = WATER_HALF_SIZE };
|
||||
fs := Water_Fs_Params.{
|
||||
color = color.component,
|
||||
surface = xx surface,
|
||||
water_color = env.water_color.component,
|
||||
sun_direction = env.sun_direction.component,
|
||||
shininess = env.water_shininess,
|
||||
sun_color = env.sun_color.component,
|
||||
time = pass.scene.time,
|
||||
reflectivity = env.water_reflectivity,
|
||||
distortion = env.water_distortion,
|
||||
};
|
||||
apply_uniforms(UB_water_vs_params, *vs);
|
||||
apply_uniforms(UB_water_fs_params, *fs);
|
||||
sg_draw(0, 6, 1);
|
||||
}
|
||||
draw_plane(0, env.deep_color, false);
|
||||
draw_plane(env.reflection_height, env.water_color, true);
|
||||
}
|
||||
|
||||
#scope_file
|
||||
|
||||
water : struct {
|
||||
pipeline : sg_pipeline;
|
||||
floor : sg_pipeline;
|
||||
surface : sg_pipeline;
|
||||
}
|
||||
|
||||
@ -41,3 +41,26 @@ create_lookat :: (camera: *Camera) -> Matrix4 {
|
||||
create_viewproj :: (camera: *Camera) -> Matrix4 {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -16,6 +16,8 @@
|
||||
|
||||
render :: () {
|
||||
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();
|
||||
scene_end_frame(w, h, xx get_time());
|
||||
g_debug_line_count = 0;
|
||||
|
||||
@ -11,19 +11,55 @@ View_Id :: struct { index: s32; }
|
||||
Environment :: struct {
|
||||
reflection_height : float;
|
||||
water : bool;
|
||||
sky : bool = true; // off: black outside the level
|
||||
water_color : Vector3 = .{1, 1, 1};
|
||||
deep_color : Vector3 = .{1, 1, 1};
|
||||
water_reflectivity : float;
|
||||
water_distortion : float = 0.007;
|
||||
water_shininess : float = 64;
|
||||
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 {
|
||||
camera : Camera;
|
||||
environment : *Environment;
|
||||
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 {
|
||||
NONE;
|
||||
ADD_PREVIEW;
|
||||
@ -43,6 +79,7 @@ Particle_Instance :: struct {
|
||||
size : float;
|
||||
uv_rect : Vector4; // x, y, w, h, 0..1
|
||||
color : Vector4;
|
||||
glow : float; // 0..1, like swatch emittance
|
||||
}
|
||||
|
||||
// Pixels from top left.
|
||||
@ -54,20 +91,24 @@ Ui_Vertex :: struct {
|
||||
|
||||
trile_mesh_create :: (trile: *Trile) -> Trile_Mesh {
|
||||
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) {
|
||||
vertices, indices := mesh_trile(trile);
|
||||
renderer_trile_mesh_update(mesh.id, vertices, indices);
|
||||
set_mesh_cut(mesh, trile.cut);
|
||||
}
|
||||
|
||||
trile_mesh_destroy :: (mesh: Trile_Mesh) {
|
||||
renderer_trile_mesh_destroy(mesh.id);
|
||||
set_mesh_cut(mesh, .NONE);
|
||||
}
|
||||
|
||||
swatch_set :: (id: u8, color: [3]u8, roughness: u8, metallic: u8, emittance: u8) {
|
||||
renderer_swatch_set(id, color, roughness, metallic, emittance);
|
||||
swatch_set :: (id: u8, color: [3]u8, reflectivity: u8, gloss: u8, emittance: u8, glass: bool) {
|
||||
renderer_swatch_set(id, color, reflectivity, gloss, emittance, glass);
|
||||
}
|
||||
|
||||
// Pixels are RGBA8.
|
||||
@ -88,6 +129,16 @@ World_Group :: struct {
|
||||
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) {
|
||||
chunk := find_or_add(*scene.chunks, key);
|
||||
chunk.origin = origin;
|
||||
@ -114,6 +165,14 @@ world_clear :: () {
|
||||
array_free(chunk.groups);
|
||||
}
|
||||
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 {
|
||||
@ -121,13 +180,45 @@ add_view :: (view: View) -> View_Id {
|
||||
camera := view.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.target.y = 2 * height - camera.target.y;
|
||||
array_add(*scene.reflection_view_proj, create_viewproj(*camera));
|
||||
array_add(*scene.view_cuts, make_view_cut(view));
|
||||
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) {
|
||||
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)});
|
||||
}
|
||||
|
||||
// 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.
|
||||
ui_triangles :: (vertices: []Ui_Vertex, texture: Texture) {
|
||||
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});
|
||||
}
|
||||
|
||||
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 {
|
||||
origin : Vector3;
|
||||
groups : [..]World_Group;
|
||||
@ -193,21 +299,31 @@ Scene_Ui :: struct {
|
||||
|
||||
Trile_Vertex :: struct {
|
||||
position : Vector3;
|
||||
swatch : float;
|
||||
ao : float; // 0 = fully occluded, 1 = open
|
||||
swatch : u8;
|
||||
ao : u8; // 0 = fully occluded, 3 = open
|
||||
normal : u8; // axis * 2, +1 when facing the negative direction
|
||||
_ : u8;
|
||||
}
|
||||
|
||||
Scene :: struct {
|
||||
chunks : Table(Chunk_Key, Scene_Chunk, chunk_key_hash, chunk_key_compare);
|
||||
mesh_cuts : [..]Trile_Cut; // by mesh id
|
||||
|
||||
views : [..]View;
|
||||
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;
|
||||
triles : [..]Scene_Trile;
|
||||
billboards : [..]Scene_Billboard;
|
||||
particles : [..]Scene_Particles;
|
||||
ui : [..]Scene_Ui;
|
||||
|
||||
debug_lines : []Debug_Line;
|
||||
overlay_lines : []Debug_Line;
|
||||
}
|
||||
|
||||
scene_init :: () {
|
||||
@ -226,15 +342,52 @@ scene_shutdown :: () {
|
||||
scene_end_frame :: (width: s32, height: s32, time: float) {
|
||||
scene.time = time;
|
||||
renderer_render(*scene, width, height);
|
||||
scene.cut_time = time;
|
||||
array_reset_keeping_memory(*scene.views);
|
||||
array_reset_keeping_memory(*scene.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.billboards);
|
||||
array_reset_keeping_memory(*scene.particles);
|
||||
array_reset_keeping_memory(*scene.ui);
|
||||
scene.debug_lines = .[];
|
||||
scene.overlay_lines = .[];
|
||||
}
|
||||
|
||||
#scope_file
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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] = .{};
|
||||
|
||||
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 {
|
||||
p : Vector3;
|
||||
p.component[`axis] = `plane * TRIXEL_SIZE;
|
||||
p.component[`u_axis] = u * 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.[
|
||||
corner(u0, v0, face.ao[0]),
|
||||
|
||||
@ -147,17 +147,18 @@ Swatch :: struct {
|
||||
used : bool;
|
||||
name : string;
|
||||
color : [3]u8;
|
||||
roughness : u8 = 4;
|
||||
metallic : u8;
|
||||
reflectivity : u8; // how much of the floor reflection shows, 0..255
|
||||
gloss : u8; // strength and sharpness of the sun highlight, 0..255
|
||||
emittance : u8;
|
||||
glass : bool; // lets sunlight through, but can't be seen through
|
||||
}
|
||||
|
||||
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 {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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 {
|
||||
@ -175,6 +187,19 @@ swatch_color :: (id: u8) -> Vector3 {
|
||||
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 {
|
||||
for trile: trile_table {
|
||||
for x: 0..15 for y: 0..15 for z: 0..15 {
|
||||
@ -188,9 +213,10 @@ Swatch_Serialize :: struct {
|
||||
id : int;
|
||||
name : string;
|
||||
color : [3]u8;
|
||||
roughness : u8;
|
||||
metallic : u8;
|
||||
reflectivity : u8;
|
||||
gloss : u8;
|
||||
emittance : u8;
|
||||
glass : bool;
|
||||
}
|
||||
|
||||
sswatches :: () {
|
||||
@ -199,7 +225,7 @@ sswatches :: () {
|
||||
list.allocator = temp;
|
||||
for g_swatches {
|
||||
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 {
|
||||
file :: #import "File";
|
||||
@ -216,7 +242,7 @@ lswatches :: () {
|
||||
_, list := Jaison.json_parse_string(xx data, [..]Swatch_Serialize,, temp);
|
||||
for list {
|
||||
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);
|
||||
} @Command
|
||||
@ -226,9 +252,6 @@ Palette_Entry :: struct {
|
||||
r : float = 1.0;
|
||||
g : float = 0.0;
|
||||
b : float = 1.0;
|
||||
roughness : int = 4;
|
||||
metallic : int = 0;
|
||||
emittance : int = 0;
|
||||
}
|
||||
|
||||
Loaded_Palette :: struct {
|
||||
@ -238,6 +261,30 @@ Loaded_Palette :: struct {
|
||||
|
||||
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 {
|
||||
name : string = "test";
|
||||
is_opaque : bool = true;
|
||||
@ -252,6 +299,8 @@ Trile :: struct {
|
||||
// in the Y axis is generated for it.
|
||||
is_stairs : bool;
|
||||
heightmap : [16][16]u8;
|
||||
|
||||
cut : Trile_Cut; // what the camera may cut away indoors
|
||||
};
|
||||
|
||||
TrileSerialize :: struct {
|
||||
@ -259,6 +308,7 @@ TrileSerialize :: struct {
|
||||
is_opaque : u8 = 1;
|
||||
is_stairs : u8 = 0;
|
||||
skip_collision : u8 = 0;
|
||||
cut : u8 = 0;
|
||||
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_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,
|
||||
cut = xx t.cut,
|
||||
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_stairs = ts.is_stairs != 0;
|
||||
t.skip_collision = ts.skip_collision != 0;
|
||||
t.cut = xx ts.cut;
|
||||
ids, ok := base64_decode(ts.trixels,, temp);
|
||||
if ok && ids.count == size_of(type_of(t.trixels)) {
|
||||
memcpy(*t.trixels, ids.data, ids.count);
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
|
||||
Pool :: #import "Pool";
|
||||
|
||||
CHUNK_SIZE :: 32;
|
||||
|
||||
Current_World :: struct {
|
||||
world : World;
|
||||
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
|
||||
|
||||
CHUNK_SIZE :: 32;
|
||||
|
||||
Chunk_Key :: struct {
|
||||
x: s32;
|
||||
y: s32;
|
||||
@ -346,6 +346,11 @@ World_Json_Config :: struct {
|
||||
waterReflectivity : float = 0.0;
|
||||
waterDistortion : float = 0.007;
|
||||
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 {
|
||||
@ -502,6 +507,11 @@ world_config_to_json :: (conf: *World_Config) -> World_Json_Config {
|
||||
jc.waterReflectivity = conf.waterReflectivity;
|
||||
jc.waterDistortion = conf.waterDistortion;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -525,6 +535,11 @@ world_config_from_json :: (jc: *World_Json_Config) -> World_Config {
|
||||
conf.waterReflectivity = jc.waterReflectivity;
|
||||
conf.waterDistortion = jc.waterDistortion;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -687,6 +702,49 @@ load_world_from_json :: (json_str: string, chunk_bin: []u8) -> (World, bool) {
|
||||
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 {
|
||||
// All of the @Notes are for the autoedit functionality.
|
||||
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
|
||||
sunHalo : 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});
|
||||
sunIntensity : float = 1.0; @Slider,0,4,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
|
||||
waterDistortion : float = 0.007; @Slider,0,0.05,0.001
|
||||
hsv_lighting : s32 = 1; @Slider,0,1,1
|
||||
// Indoor levels: no sun shadows (faces toward the sun still light up), a
|
||||
// flat skyBase instead of the sky, and no water. The reflection plane moves
|
||||
// to the top of the floor layer (y = 1) for floor reflections.
|
||||
// Indoor levels float in black: no sky and no water. Sunlight gets in through
|
||||
// glass. The reflection plane moves to the top of the floor layer (y = 1) for
|
||||
// floor reflections.
|
||||
indoor : s32 = 0; @Slider,0,1,1
|
||||
|
||||
// ambientColor : Vector3 = .{1.0, 1.0, 1.0}; @Color
|
||||
@ -726,11 +790,26 @@ world_environment :: (world: *World) -> *Environment {
|
||||
conf := *world.conf;
|
||||
env.reflection_height = effective_plane_height(conf);
|
||||
env.water = !conf.indoor;
|
||||
env.sky = !conf.indoor;
|
||||
env.water_color = conf.waterColor;
|
||||
env.deep_color = conf.deepColor;
|
||||
env.water_reflectivity = conf.waterReflectivity;
|
||||
env.water_distortion = conf.waterDistortion;
|
||||
env.water_shininess = conf.waterShininess;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user