This commit is contained in:
Tuomas Katajisto 2026-04-23 11:41:44 +03:00
parent 3bcece4bc3
commit 5e30f3f2ab
43 changed files with 12179 additions and 8761 deletions

16
ENGINE_TODO.md Normal file
View File

@ -0,0 +1,16 @@
# Todo for engine feature polish
## Particles
- Particle counts quite limited, increase.
## Post-processing
- DOF and bloom are both doing this thing where they draw to a small resolution buffer.
- It's performant now compared to old approach, but is bad. We need to do the blur thing
in a shader X and Y separately for both.
## Trile rendering
- Too many triangles in meshgen.
- Shadowmap should move with camera.
- Reflection trile rendering is taking forever compared to gbuffer pass even though it
shouldn't really be doing much more.
-

View File

@ -47,6 +47,46 @@ build_options_from_args :: (args: []string) -> Trueno_Build_Options {
return opts;
}
// Scans `dir` recursively and returns the size of the largest file whose
// short name ends with `ext`. Returns `floor` when no matching file exists.
max_asset_file_size :: (dir: string, ext: string, floor: s64 = 0) -> s64 {
Ctx :: struct { ext: string; max: s64; }
ctx: Ctx;
ctx.ext = ext;
visit_files(dir, true, *ctx, (info: *File_Visit_Info, c: *Ctx) {
if info.is_directory return;
if !ends_with(info.short_name, c.ext) return;
f, ok := file_open(info.full_name);
defer if ok file_close(*f);
if !ok return;
size, ok2 := file_length(f);
if ok2 && size > c.max then c.max = size;
});
return max(floor, ctx.max);
}
add_asset_buffer_sizes_to_compiler_strings :: (trueno_opts: Trueno_Build_Options, w: Workspace) {
pack_dir := ifx trueno_opts.test_exe_engine || trueno_opts.test_exe_game then "./test_packs" else "./packs";
game_resources_dir := ifx trueno_opts.test_exe_engine || trueno_opts.test_exe_game then "./test_game/resources" else "./game/resources";
sizes: [6]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, ".shgrid", 1 * 1024 * 1024);
sizes[4] = max_asset_file_size(game_resources_dir, "rdm_atlas.rdm", 16 * 1024 * 1024);
sizes[5] = max_asset_file_size(game_resources_dir, "rdm_manifest.json", 256 * 1024);
names :: string.["MAX_PACK_SIZE", "MAX_WORLD_SIZE", "MAX_CHUNKS_SIZE",
"MAX_SHGRID_SIZE", "MAX_RDM_ATLAS_SIZE", "MAX_RDM_MANIFEST_SIZE"];
for i: 0..5 {
add_build_string(tprint("% :: %;\n", names[i], sizes[i]), w);
}
}
add_trueno_opts_to_compiler_strings :: (trueno_opts : Trueno_Build_Options, w: Workspace) {
#import "String";
bool_to_string :: (b: bool) -> string {
@ -127,6 +167,7 @@ native_build :: (opts: Build_Options, trueno_opts: Trueno_Build_Options) {
compiler_begin_intercept(w);
add_trueno_opts_to_compiler_strings(trueno_opts, w);
add_asset_buffer_sizes_to_compiler_strings(trueno_opts, w);
add_build_file("src/platform_specific/main_native.jai", w);
add_shaders_to_workspace(w);
@ -178,6 +219,7 @@ wasm_build :: (opts: Build_Options, trueno_opts: Trueno_Build_Options) {
compiler_begin_intercept(w);
add_trueno_opts_to_compiler_strings(trueno_opts, w);
add_asset_buffer_sizes_to_compiler_strings(trueno_opts, w);
add_build_file("src/platform_specific/main_web.jai", w);
add_shaders_to_workspace(w);
@ -225,4 +267,6 @@ wasm_build :: (opts: Build_Options, trueno_opts: Trueno_Build_Options) {
#import "Compiler";
#import "Process";
#import "File";
#import "String";
#import "File_Utilities";

View File

@ -0,0 +1,27 @@
_______________
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: 1.8MB -> 0.6MB (1.2MB saved, 65.9% smaller)

20
perf Normal file
View File

@ -0,0 +1,20 @@
FRAME PRE OPTIMIZATION!
| Pass | Targets | Duration (µs) | What it does |
|--------------|---------|---------------|--------------|
| Colour Pass #1 | 1T+D | 257.64 | Draws all triles and billboards with the shadowmap shader into a shadowmap|
| Colour Pass #2 | 1T+D | 2226.80 | Draws triles and billboard for planar reflection, does viewport culling, uses mostly same shaders as main pass |
| Colour Pass #3 | 2T+D | 886.92 | Gbuffer pass. All triles and billboards. Special gbuffer shader. |
| Colour Pass #4 | 1T+D | 727.08 | SSAO texture draw pass. Output 4k |
| Colour Pass #5 | 1T+D | 500.60 | SSAO blur, output 1920x1066 |
| Colour Pass #6 | 1T+D | 4201.48 | Main pass, draws water (284us) draws billboards, samples previous buffers, draws particles (don't seem to be heavy, max 21us) draws triles draws billboards |
| Colour Pass #7 | 1T+D | 842.84 | Extracts bright parts and blurs? For bloom |
| Colour Pass #8 | 1T+D | 1069.52 | Applies bloom |
| Colour Pass #9 | 1T+D | 1648.36 | Blurs whole image for DOF purposes |
| Colour Pass #10 | 1T+D | 888.84 | I think this is dilution for the blurred image but not totally sure. |
| Colour Pass #11 | 1T+D | 1105.04 | Mixes blurred and original for DOF effect |
| Colour Pass #12 | 1T+D | 288.28 | Final pass, does color correction and some effects |
| **Total** | | **14643.40** | |

View File

@ -1,4 +1,4 @@
master_volume 0.475357
music_volume 0.522385
master_volume 0.364372
music_volume 1
sfx_volume 1
fullscreen 0
fullscreen 1

View File

@ -6,54 +6,57 @@ Pool :: #import "Pool";
#load "loaders.jai";
MAIN_CHUNK_BUFFER_SIZE : s64 : 256 * 1024;
RDM_CHUNK_BUFFER_SIZE : s64 : 64 * 1024 * 1024;
// MAX_PACK_SIZE, MAX_WORLD_SIZE, MAX_CHUNKS_SIZE, MAX_SHGRID_SIZE,
// MAX_RDM_ATLAS_SIZE, MAX_RDM_MANIFEST_SIZE are injected as build strings
// by first.jai after scanning the actual asset files at compile time.
Active_Fetch :: struct {
occupied : bool;
req : Fetch_Request;
accumulated : [..]u8;
chunk_buf : []u8;
}
MAX_FETCH_SLOTS :: 16;
#scope_export
CHANNEL_MAIN : u32 : 0;
CHANNEL_RDM : u32 : 1;
// Replaces the old should_block / should_block_engine bool pair.
Load_Priority :: enum u8 {
BLOCK_ENGINE; // must finish before the first game frame
LOADING_SCREEN; // show loading screen while pending
BACKGROUND; // stream silently; game runs normally
}
Fetch_Type :: enum {
PACK;
WORLD;
WORLD_CHUNKS;
RDM_ATLAS;
RDM_LOOKUP;
RDM_MANIFEST;
SHGRID;
}
Fetch_Request :: struct {
type : Fetch_Type;
path : string;
priority : Load_Priority;
// Pack
// PACK
pack_name : string;
should_block : bool;
should_block_engine : bool;
// World / RDM
// WORLD / RDM / SH
world_name : string;
chunk_key : Chunk_Key;
// Atlas GPU image held between RDM_ATLAS and its paired RDM_LOOKUP fetch.
rdm_pending_atlas : sg_image;
// Copy of world.json carried between WORLD and WORLD_CHUNKS fetches.
// WORLD_CHUNKS: JSON carried forward from the WORLD fetch
world_json_data : []u8;
}
// One in-flight sfetch request. `buf` is allocated when dispatched and
// freed (or handed to Loaded_Pack) when the callback fires.
Fetch_Slot :: struct {
occupied : bool;
req : Fetch_Request;
buf : []u8;
}
Asset_Manager :: struct {
active : [2]Active_Fetch;
main_queue : [..]Fetch_Request;
rdm_queue : [..]Fetch_Request;
slots : [MAX_FETCH_SLOTS]Fetch_Slot;
queue : [..]Fetch_Request;
loadedPacks : [..]Loaded_Pack;
pending_hot_reload : bool;
hot_reload_unpause_pending : bool;
@ -66,49 +69,48 @@ g_asset_manager : Asset_Manager;
#load "rdm_loader.jai";
#load "sh_loader.jai";
fetch_type_buffer_size :: (type: Fetch_Type) -> s64 {
if #complete type == {
case .PACK; return MAX_PACK_SIZE;
case .WORLD; return MAX_WORLD_SIZE;
case .WORLD_CHUNKS; return MAX_CHUNKS_SIZE;
case .RDM_ATLAS; return MAX_RDM_ATLAS_SIZE;
case .RDM_MANIFEST; return MAX_RDM_MANIFEST_SIZE;
case .SHGRID; return MAX_SHGRID_SIZE;
}
}
fetch_callback :: (res: *sfetch_response_t) #c_call {
push_context,defer_pop default_context;
af := *g_asset_manager.active[res.channel];
if res.fetched {
chunk_count := res.data.size.(s64);
needed := af.accumulated.count + chunk_count;
if af.accumulated.allocated < needed {
new_cap := max(needed, max(af.accumulated.allocated * 2, 1024 * 1024));
array_reserve(*af.accumulated, new_cap);
}
memcpy(af.accumulated.data + af.accumulated.count, res.data.ptr, chunk_count);
af.accumulated.count = needed;
}
// sokol copied the slot index into res.user_data when we sent the request.
slot_idx := (cast(*u32) res.user_data).*;
slot := *g_asset_manager.slots[slot_idx];
if !res.finished then return;
// Mark channel free before processing so that enqueued follow-up requests
// (WORLD -> WORLD_CHUNKS, RDM_ATLAS -> RDM_LOOKUP) are visible on the next tick.
af.occupied = false;
req := af.req;
slot.occupied = false;
req := slot.req;
if res.failed {
handle_fetch_failed(*req);
array_reset(*af.accumulated);
free(slot.buf.data);
slot.buf = .{};
return;
}
data : []u8;
data.data = af.accumulated.data;
data.count = af.accumulated.count;
data: []u8;
data.data = cast(*u8) res.data.ptr;
data.count = cast(s64) res.data.size;
process_completed_fetch(*req, data);
if req.type == .PACK {
// process_completed_fetch either stored data in Loaded_Pack.data_buffer or
// freed it on parse failure. Either way, zero af.accumulated without freeing.
af.accumulated.data = null;
af.accumulated.count = 0;
af.accumulated.allocated = 0;
// process_completed_fetch transferred ownership to Loaded_Pack.data_buffer.
slot.buf = .{};
} else {
array_reset(*af.accumulated);
free(slot.buf.data);
slot.buf = .{};
}
}
@ -120,12 +122,12 @@ handle_fetch_failed :: (req: *Fetch_Request) {
case .WORLD;
if ends_with(req.path, "world.json") {
fallback_req : Fetch_Request;
fallback_req.type = .WORLD;
fallback_req.world_name = req.world_name;
fallback_req.path = sprint("%/worlds/%/index.world", GAME_RESOURCES_DIR, req.world_name);
fallback_req.should_block = true;
array_add(*g_asset_manager.main_queue, fallback_req);
fallback: Fetch_Request;
fallback.type = .WORLD;
fallback.priority = req.priority;
fallback.world_name = req.world_name;
fallback.path = sprint("%/worlds/%/index.world", GAME_RESOURCES_DIR, req.world_name);
array_add(*g_asset_manager.queue, fallback);
return;
}
log_error("Failed to load world '%'", req.world_name);
@ -135,11 +137,10 @@ handle_fetch_failed :: (req: *Fetch_Request) {
log_error("Failed to load chunks.bin for world '%'", req.world_name);
case .RDM_ATLAS;
log_error("RDM: failed to load atlas for chunk %", req.chunk_key);
log_info("RDM: no global atlas for world '%' (ok if none baked yet)", req.world_name);
case .RDM_LOOKUP;
if req.rdm_pending_atlas.id != 0 then sg_destroy_image(req.rdm_pending_atlas);
log_error("RDM: failed to load lookup for chunk %", req.chunk_key);
case .RDM_MANIFEST;
log_info("RDM: no manifest for world '%' (ok if none baked yet)", req.world_name);
case .SHGRID;
sh_loader_handle_failed(req);
@ -171,13 +172,13 @@ process_completed_fetch :: (req: *Fetch_Request, data: []u8) {
if is_json {
json_copy := NewArray(data.count, u8, false);
memcpy(json_copy.data, data.data, data.count);
chunks_req : Fetch_Request;
chunks_req: Fetch_Request;
chunks_req.type = .WORLD_CHUNKS;
chunks_req.priority = req.priority;
chunks_req.world_name = req.world_name;
chunks_req.path = sprint("%/worlds/%/chunks.bin", GAME_RESOURCES_DIR, req.world_name);
chunks_req.should_block = true;
chunks_req.world_json_data = json_copy;
array_add(*g_asset_manager.main_queue, chunks_req);
array_add(*g_asset_manager.queue, chunks_req);
} else {
world, ok := load_world_from_data(data);
if ok {
@ -214,88 +215,44 @@ process_completed_fetch :: (req: *Fetch_Request, data: []u8) {
header_size := cast(s64) size_of(RDM_File_Header);
if data.count < header_size {
log_error("RDM: atlas too small for chunk %", req.chunk_key);
log_error("RDM: global atlas too small (world %)", req.world_name);
return;
}
header := cast(*RDM_File_Header) data.data;
if header.magic != RDM_FILE_MAGIC {
log_error("RDM: bad atlas magic for chunk %", req.chunk_key);
log_error("RDM: bad atlas magic (world %)", req.world_name);
return;
}
atlas_pixel_bytes := cast(u64) header.width * cast(u64) header.height * 4 * size_of(float);
atlas_imgdata : sg_image_data;
atlas_pixel_bytes := cast(u64) header.width * cast(u64) header.height * 4 * size_of(u16);
atlas_imgdata: sg_image_data;
atlas_imgdata.subimage[0][0] = .{ data.data + header_size, atlas_pixel_bytes };
atlas_desc : sg_image_desc = .{
atlas_desc: sg_image_desc = .{
render_target = false,
width = header.width,
height = header.height,
pixel_format = sg_pixel_format.RGBA32F,
pixel_format = sg_pixel_format.RGBA16F,
sample_count = 1,
data = atlas_imgdata,
};
chunk_ptr := table_find_pointer(*curworld.world.chunks, req.chunk_key);
lookup_path: string;
if chunk_ptr != null && chunk_ptr.rdm_lookup_path.count > 0 {
lookup_path = sprint("%/worlds/%/%", GAME_RESOURCES_DIR, req.world_name, chunk_ptr.rdm_lookup_path);
} else {
lookup_path = rdm_chunk_filename(req.world_name, req.chunk_key, "rdm_lookup");
}
lookup_req : Fetch_Request;
lookup_req.type = .RDM_LOOKUP;
lookup_req.world_name = req.world_name;
lookup_req.chunk_key = req.chunk_key;
lookup_req.path = lookup_path;
lookup_req.rdm_pending_atlas = sg_make_image(*atlas_desc);
array_add(*g_asset_manager.rdm_queue, lookup_req);
if g_rdm_atlas.id != 0 then sg_destroy_image(g_rdm_atlas);
g_rdm_atlas = sg_make_image(*atlas_desc);
log_debug("RDM: loaded global atlas (%x%)", header.width, header.height);
case .RDM_LOOKUP;
case .RDM_MANIFEST;
curworld := get_current_world();
world_ok := curworld.valid && curworld.world.name == req.world_name;
if !curworld.valid || curworld.world.name != req.world_name then return;
if !world_ok {
if req.rdm_pending_atlas.id != 0 then sg_destroy_image(req.rdm_pending_atlas);
json_str : string;
json_str.data = data.data;
json_str.count = data.count;
ok, entries := Jaison.json_parse_string(json_str, [..]Rdm_Atlas_Entry,, temp);
if !ok {
log_error("RDM: failed to parse manifest for world '%'", req.world_name);
return;
}
header_size := cast(s64) size_of(RDM_File_Header);
if data.count < header_size {
log_error("RDM: lookup too small for chunk %", req.chunk_key);
sg_destroy_image(req.rdm_pending_atlas);
return;
}
header := cast(*RDM_File_Header) data.data;
if header.magic != RDM_FILE_MAGIC {
log_error("RDM: bad lookup magic for chunk %", req.chunk_key);
sg_destroy_image(req.rdm_pending_atlas);
return;
}
lookup_pixel_bytes := cast(u64) header.width * cast(u64) header.height * 4 * size_of(float);
lookup_imgdata : sg_image_data;
lookup_imgdata.subimage[0][0] = .{ data.data + header_size, lookup_pixel_bytes };
lookup_desc : sg_image_desc = .{
render_target = false,
width = header.width,
height = header.height,
pixel_format = sg_pixel_format.RGBA32F,
sample_count = 1,
data = lookup_imgdata,
};
chunk := table_find_pointer(*curworld.world.chunks, req.chunk_key);
if chunk != null {
chunk.rdm_atlas = req.rdm_pending_atlas;
chunk.rdm_lookup = sg_make_image(*lookup_desc);
#if !FLAG_RELEASE_BUILD {
chunk.rdm_lookup_w = header.width;
chunk.rdm_lookup_h = header.height;
num_floats := cast(s64)(header.width * header.height * 4);
cpu_copy := NewArray(num_floats, float);
memcpy(cpu_copy.data, data.data + header_size, cast(s64)lookup_pixel_bytes);
chunk.rdm_lookup_cpu = cpu_copy;
}
chunk.rdm_valid = true;
log_debug("RDM: loaded chunk %", req.chunk_key);
} else {
sg_destroy_image(req.rdm_pending_atlas);
}
array_reset_keeping_memory(*curworld.world.rdm_lookup);
for e: entries array_add(*curworld.world.rdm_lookup, e);
log_debug("RDM: loaded manifest (% entries)", entries.count);
case .SHGRID;
sh_loader_handle_completed(req, data);
@ -384,15 +341,12 @@ add_resources_from_pack :: (pack: *Loaded_Pack) {
array_add(*g_palettes, pal);
log_info("Loaded % colors from hex palette(%)", pal.entries.count, v.name);
case "ttf";
// Load into a font. Add to free list.
case;
log_warn("File(%) in pack(%) has unknown format", v.name, pack.name);
}
log_debug("% -> %", v.name, extension);
}
// Properly initialize animations from the pack now that we have the images
// and the JSON files both combined.
for qsheet : sheets_to_init {
for qsheet.sheet.meta.frameTags {
anim : Animation;
@ -427,29 +381,51 @@ find_pack_by_name :: (name: string) -> (bool, Loaded_Pack) {
return false, .{};
}
dispatch_channel :: (queue: *[..]Fetch_Request, channel: u32) {
af := *g_asset_manager.active[channel];
if af.occupied || queue.count == 0 then return;
req := queue.data[0];
for i: 0..queue.count - 2 {
queue.data[i] = queue.data[i + 1];
// Removes the highest-priority item from the queue (lowest enum value = most urgent).
// Within equal priority, the earliest-queued item wins (stable).
dequeue_highest_priority :: (queue: *[..]Fetch_Request) -> (Fetch_Request, bool) {
if queue.count == 0 return .{}, false;
best := 0;
for i: 1..queue.count - 1 {
if queue.*[i].priority < queue.*[best].priority then best = i;
}
queue.count -= 1;
req := queue.*[best];
array_ordered_remove_by_index(queue, best);
return req, true;
}
af.occupied = true;
af.req = req;
af.accumulated.count = 0;
dispatch_slots :: () {
for i: 0..MAX_FETCH_SLOTS - 1 {
slot := *g_asset_manager.slots[i];
if slot.occupied continue;
if g_asset_manager.queue.count == 0 break;
req, found := dequeue_highest_priority(*g_asset_manager.queue);
if !found break;
slot.occupied = true;
slot.req = req;
slot.buf = NewArray(fetch_type_buffer_size(req.type), u8, false);
// sokol copies user_data bytes immediately on sfetch_send, so a
// stack-local index is safe here.
slot_idx : u32 = cast(u32) i;
// Route BACKGROUND requests to channel 1 so they don't starve
// LOADING_SCREEN / BLOCK_ENGINE work on native (two IO threads).
channel : u32 = ifx req.priority == .BACKGROUND then cast(u32)1 else cast(u32)0;
path_c := to_c_string(req.path);
defer free(path_c);
sfetch_send(*(sfetch_request_t.{
channel = channel,
path = path_c,
callback = fetch_callback,
chunk_size = cast(u32) af.chunk_buf.count,
buffer = .{ ptr = af.chunk_buf.data, size = cast(u64) af.chunk_buf.count },
buffer = .{ ptr = slot.buf.data, size = cast(u64) slot.buf.count },
user_data = .{ ptr = *slot_idx, size = cast(u64) size_of(u32) },
}));
}
}
#scope_export
@ -467,43 +443,35 @@ free_resources_from_pack :: (pack: *Loaded_Pack) {
}
asset_manager_init :: () {
g_asset_manager.active[CHANNEL_MAIN].chunk_buf = NewArray(MAIN_CHUNK_BUFFER_SIZE, u8, false);
g_asset_manager.active[CHANNEL_RDM].chunk_buf = NewArray(RDM_CHUNK_BUFFER_SIZE, u8, false);
// No upfront allocations; buffers are allocated per-request in dispatch_slots.
}
mandatory_loads_done :: () -> bool {
for channel: 0..1 {
af := *g_asset_manager.active[channel];
if af.occupied && af.req.should_block_engine then return false;
for g_asset_manager.slots {
if it.occupied && it.req.priority == .BLOCK_ENGINE then return false;
}
for g_asset_manager.main_queue {
if it.should_block_engine then return false;
}
for g_asset_manager.rdm_queue {
if it.should_block_engine then return false;
for g_asset_manager.queue {
if it.priority == .BLOCK_ENGINE then return false;
}
return true;
}
show_loading_screen :: () -> bool {
for channel: 0..1 {
af := *g_asset_manager.active[channel];
if af.occupied && af.req.should_block then return true;
for g_asset_manager.slots {
if it.occupied && it.req.priority != .BACKGROUND then return true;
}
for g_asset_manager.main_queue {
if it.should_block then return true;
}
for g_asset_manager.rdm_queue {
if it.should_block then return true;
for g_asset_manager.queue {
if it.priority != .BACKGROUND then return true;
}
return false;
}
asset_manager_tick :: () {
#if !FLAG_RELEASE_BUILD && OS != .WASM {
if g_asset_manager.pending_hot_reload
&& !g_asset_manager.active[CHANNEL_MAIN].occupied
&& g_asset_manager.main_queue.count == 0 {
all_idle := true;
for g_asset_manager.slots if it.occupied { all_idle = false; break; }
if g_asset_manager.pending_hot_reload && all_idle && g_asset_manager.queue.count == 0 {
g_asset_manager.pending_hot_reload = false;
hot_reload_all_packs();
}
@ -513,33 +481,30 @@ asset_manager_tick :: () {
}
}
dispatch_channel(*g_asset_manager.main_queue, CHANNEL_MAIN);
dispatch_channel(*g_asset_manager.rdm_queue, CHANNEL_RDM);
sfetch_dowork();
dispatch_slots();
}
load_world :: (name: string) {
unload_current_world();
req : Fetch_Request;
req: Fetch_Request;
req.type = .WORLD;
req.priority = .LOADING_SCREEN;
req.world_name = sprint("%", name);
req.path = sprint("%/worlds/%/world.json", GAME_RESOURCES_DIR, name);
req.should_block = true;
array_add(*g_asset_manager.main_queue, req);
array_add(*g_asset_manager.queue, req);
}
load_pack :: (name: string, shouldBlock: bool = true, shouldBlockEngine: bool = false) {
req : Fetch_Request;
load_pack :: (name: string, priority: Load_Priority = .LOADING_SCREEN) {
req: Fetch_Request;
req.type = .PACK;
req.priority = priority;
req.pack_name = sprint("%", name);
req.path = sprint("%/%.pack", PACK_DIR, name);
req.should_block = shouldBlock;
req.should_block_engine = shouldBlockEngine;
array_add(*g_asset_manager.main_queue, req);
array_add(*g_asset_manager.queue, req);
}
// Asset management:
// Asset accessors:
#scope_file
#scope_export
@ -549,7 +514,6 @@ get_texture_from_pack :: (pack: string, path: string) -> (sg_image) {
invalid_img : sg_image;
if !found {
// find_pack_by_name already logs this
return invalid_img;
}

View File

@ -1,63 +1,58 @@
// RDM streaming helpers.
// The unified fetch queue lives in asset_manager.jai; these functions
// add and remove RDM entries from that shared queue.
// enqueue and cancel the world-level global RDM atlas + manifest.
#scope_export
rdm_loader_enqueue_world :: (world: *World) {
for *chunk: world.chunks {
if chunk.rdm_valid then continue;
already_atlas := false;
already_manifest := false;
// Skip if this chunk is already in-flight on the RDM channel.
af_rdm := *g_asset_manager.active[CHANNEL_RDM];
if af_rdm.occupied {
cf := af_rdm.req;
if (cf.type == .RDM_ATLAS || cf.type == .RDM_LOOKUP) &&
cf.world_name == world.name && cf.chunk_key == chunk.coord then continue;
for g_asset_manager.slots {
if !it.occupied continue;
if it.req.world_name != world.name continue;
if it.req.type == .RDM_ATLAS then already_atlas = true;
if it.req.type == .RDM_MANIFEST then already_manifest = true;
}
for g_asset_manager.queue {
if it.world_name != world.name continue;
if it.type == .RDM_ATLAS then already_atlas = true;
if it.type == .RDM_MANIFEST then already_manifest = true;
}
// Skip if already queued (either as atlas or its follow-up lookup).
already_queued := false;
for g_asset_manager.rdm_queue {
if (it.type == .RDM_ATLAS || it.type == .RDM_LOOKUP) &&
it.world_name == world.name && it.chunk_key == chunk.coord {
already_queued = true;
break;
}
}
if already_queued then continue;
if !already_atlas {
req : Fetch_Request;
req.type = .RDM_ATLAS;
req.priority = .LOADING_SCREEN;
req.world_name = world.name;
req.chunk_key = chunk.coord;
if chunk.rdm_atlas_path.count > 0 {
req.path = sprint("%/worlds/%/%", GAME_RESOURCES_DIR, world.name, chunk.rdm_atlas_path);
} else {
req.path = rdm_chunk_filename(world.name, chunk.coord, "rdm_atlas");
req.path = rdm_global_atlas_filename(world.name);
array_add(*g_asset_manager.queue, req);
}
array_add(*g_asset_manager.rdm_queue, req);
if !already_manifest {
req : Fetch_Request;
req.type = .RDM_MANIFEST;
req.priority = .LOADING_SCREEN;
req.world_name = world.name;
req.path = rdm_global_manifest_filename(world.name);
array_add(*g_asset_manager.queue, req);
}
}
// Remove all pending RDM fetches from the queue and invalidate any in-flight
// RDM fetch so its callback discards the result.
// Drain all pending RDM work.
rdm_loader_cancel_all :: () {
// Destroy any atlas images stored in queued RDM_LOOKUP requests, then drain.
for i: 0..g_asset_manager.rdm_queue.count - 1 {
item := g_asset_manager.rdm_queue[i];
if item.rdm_pending_atlas.id != 0 then sg_destroy_image(item.rdm_pending_atlas);
new_count := 0;
for i: 0..g_asset_manager.queue.count - 1 {
item := g_asset_manager.queue[i];
if item.type != .RDM_ATLAS && item.type != .RDM_MANIFEST {
g_asset_manager.queue[new_count] = item;
new_count += 1;
}
array_reset(*g_asset_manager.rdm_queue);
}
g_asset_manager.queue.count = new_count;
// Blank the world name on the in-flight RDM request so its callback discards.
af := *g_asset_manager.active[CHANNEL_RDM];
if af.occupied &&
(af.req.type == .RDM_ATLAS || af.req.type == .RDM_LOOKUP) {
if af.req.rdm_pending_atlas.id != 0 {
sg_destroy_image(af.req.rdm_pending_atlas);
af.req.rdm_pending_atlas = .{};
}
af.req.world_name = "";
for *slot: g_asset_manager.slots {
if !slot.occupied then continue;
if slot.req.type != .RDM_ATLAS && slot.req.type != .RDM_MANIFEST then continue;
slot.req.world_name = "";
}
}

View File

@ -8,26 +8,33 @@ shgrid_loader_enqueue_world :: (world: *World) {
for *chunk: world.chunks {
if chunk.sh_valid then continue;
af := *g_asset_manager.active[CHANNEL_RDM];
if af.occupied && af.req.type == .SHGRID
&& af.req.world_name == world.name && af.req.chunk_key == chunk.coord
then continue;
already_queued := false;
for g_asset_manager.rdm_queue {
if it.type == .SHGRID && it.world_name == world.name && it.chunk_key == chunk.coord {
already_queued = true;
already := false;
for g_asset_manager.slots {
if !it.occupied then continue;
if it.req.type == .SHGRID &&
it.req.world_name == world.name && it.req.chunk_key == chunk.coord {
already = true;
break;
}
}
if already_queued then continue;
if already then continue;
for g_asset_manager.queue {
if it.type == .SHGRID &&
it.world_name == world.name && it.chunk_key == chunk.coord {
already = true;
break;
}
}
if already then continue;
req : Fetch_Request;
req.type = .SHGRID;
req.priority = .LOADING_SCREEN;
req.world_name = world.name;
req.chunk_key = chunk.coord;
req.path = shgrid_filename(world.name, chunk.coord);
array_add(*g_asset_manager.rdm_queue, req);
array_add(*g_asset_manager.queue, req);
}
}
@ -50,32 +57,40 @@ sh_loader_handle_completed :: (req: *Fetch_Request, data: []u8) {
return;
}
n := sh_header.probe_n;
expected_floats := cast(s64) n * n * n * 12;
if data.count < header_size + expected_floats * size_of(float) {
total_values := cast(s64) n * n * n * 12;
bytes_per_val := ifx sh_header.version >= 2 then size_of(u16) else size_of(float);
if data.count < header_size + total_values * bytes_per_val {
log_error("SH: probe grid data too short for chunk %", req.chunk_key);
return;
}
// Pack flat float[n^3 * 12] → 3-RGBA16F-texels-per-probe 2D texture layout.
// Pack flat [n^3 * 12] coefficients → 3-RGBA16F-texels-per-probe 2D texture layout.
// Texture dims: (n*3) x (n*n). Probe (px,py,pz) at row (pz*n+py), col (px*3+k).
// Coefficient layout in 3 texels (12 slots):
// t0: R.c0-3 t1: G.c0-3 t2: B.c0-3
tex_w := n * 3;
n_tex := cast(s64) tex_w * n * n;
packed := NewArray(n_tex * 4, u16);
src := cast(*float) (data.data + header_size);
for pz: 0..n-1 {
for py: 0..n-1 {
for px: 0..n-1 {
if sh_header.version >= 2 {
src := cast(*u16) (data.data + header_size);
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.data + tex_idx * 4;
for ch: 0..3 {
d[ch] = f32_to_f16((s + k * 4 + ch).*);
for ch: 0..3 d[ch] = (s + k * 4 + ch).*;
}
}
} else {
src := cast(*float) (data.data + header_size);
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.data + tex_idx * 4;
for ch: 0..3 d[ch] = f32_to_f16((s + k * 4 + ch).*);
}
}
}

View File

@ -2,6 +2,8 @@
#load "load.jai";
#load "mixer.jai";
g_audio_sample_rate : s32 = 44100;
audio_init :: () {
// load_wav_file();
}

View File

@ -23,6 +23,8 @@ Mixer_Play_Task :: struct {
startTime : float64 = 0;
delay : float64 = 0;
silenceBeforeRepeat : float64 = 0;
queued_next : *Mixer_Play_Task; // if set, starts when this ONESHOT task finishes
}
Mixer_Config :: struct {
@ -71,6 +73,22 @@ mixer_add_task :: (audio: *Audio_Data, bus: Mixer_Bus, mode: Play_Mode) {
});
}
// Queue `next_task` to start as soon as the task currently playing `current_audio` finishes.
// If `current_audio` isn't playing, `next_task` starts immediately.
mixer_queue_next :: (current_audio: *Audio_Data, next_task: Mixer_Play_Task) {
mixer_lock(*g_mixer);
defer mixer_unlock(*g_mixer);
for *g_mixer.tasks {
if it.audio == current_audio {
if it.queued_next then free(it.queued_next);
it.queued_next = New(Mixer_Play_Task);
it.queued_next.* = next_task;
return;
}
}
array_add(*g_mixer.tasks, next_task);
}
mixer_get_samples :: (buffer: *float, frame_count: s32, channel_count: s32) {
mixer_lock(*g_mixer);
defer mixer_unlock(*g_mixer);
@ -101,7 +119,7 @@ mixer_get_samples :: (buffer: *float, frame_count: s32, channel_count: s32) {
source_data := task.audio.data;
source_channels := cast(s64) task.audio.channels;
src_rate := cast(s64) task.audio.sample_rate;
out_rate := cast(s64) saudio_sample_rate();
out_rate := cast(s64) g_audio_sample_rate;
num_src_frames := source_data.count / source_channels;
task_finished := false;
@ -145,9 +163,15 @@ mixer_get_samples :: (buffer: *float, frame_count: s32, channel_count: s32) {
}
if task_finished {
if task.queued_next {
next := task.queued_next.*;
free(task.queued_next);
g_mixer.tasks[i] = next;
} else {
array_unordered_remove_by_index(*g_mixer.tasks, i);
}
}
}
}
mixer_lock :: (mixer: *Mixer) {

View File

@ -79,9 +79,7 @@ set_dist :: (dist: float) {
tacomaSamples : s32 = 100;
tacomaResolution : s32 = 500;
tacomaExposure : float = 1.0;
tacomaContrast : float = 1.0;
tacomaSaturation : float = 1.0;
tacoma_tab_scroll : float = 0;
lastInputTime : float64;
@ -237,33 +235,20 @@ tick_level_editor_camera :: () {
draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
curworld := get_current_world();
r := total_r;
r.h = ui_h(3,0);
#if FLAG_TACOMA_ENABLED {
region, inside := GR.begin_scrollable_region(total_r, *theme.scrollable_region_theme);
r := inside;
r.y -= tacoma_tab_scroll;
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);
}
r.y += r.h;
if GR.button(r, "Render a RDM", *theme.button_theme) {
gen_rdm(tacomaSamples, true, 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);
}
r.y += r.h;
dirty_count := 0;
if curworld.valid for chunk: curworld.world.chunks if chunk.rdm_dirty dirty_count += 1;
if GR.button(r, tprint("Bake dirty chunks (%)", dirty_count), *t_button_selectable(theme, dirty_count > 0)) {
if curworld.valid && dirty_count > 0 {
dirty_keys : [..]Chunk_Key;
dirty_keys.allocator = temp;
for chunk, key: curworld.world.chunks if chunk.rdm_dirty array_add(*dirty_keys, key);
rdm_bake_chunks(dirty_keys, curworld.world, tacomaSamples, true);
}
}
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);
}
@ -303,18 +288,6 @@ 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, "Exposure", *t_label_left(theme));
r.y += r.h;
GR.slider(r, *tacomaExposure, 0.5, 3.0, 0.1, *theme.slider_theme);
r.y += r.h;
GR.label(r, "Contrast", *t_label_left(theme));
r.y += r.h;
GR.slider(r, *tacomaContrast, 0.5, 3.0, 0.1, *theme.slider_theme);
r.y += r.h;
GR.label(r, "Saturation", *t_label_left(theme));
r.y += r.h;
GR.slider(r, *tacomaSaturation, 0.5, 3.0, 0.1, *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));
r.y += r.h;
GR.slider(r, *bake_sky_scale, 0.0, 5.0, 0.05, *theme.slider_theme);
@ -324,8 +297,10 @@ draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
GR.slider(r, *bake_sky_desaturation, 0.0, 1.0, 0.05, *theme.slider_theme);
r.y += r.h;
GR.end_scrollable_region(region, r.x + r.w, r.y, *tacoma_tab_scroll);
} else {
r := total_r;
r.h = ui_h(3,0);
GR.label(r, "Tacoma is not enabled in this build.", *theme.label_theme);
}
}
@ -500,7 +475,6 @@ add_trile :: (name: string, x: s32, y: s32, z: s32, orientation: u8 = 0) {
group.trile_name = sprint("%", name);
array_add(*group.instances, inst);
array_add(*chunk.groups, group);
chunk.rdm_dirty = true;
} @Command
remove_trile :: (x: s32, y: s32, z: s32) {
@ -516,7 +490,6 @@ remove_trile :: (x: s32, y: s32, z: s32) {
for inst, idx: group.instances {
if inst.x == lx && inst.y == ly && inst.z == lz {
array_unordered_remove_by_index(*group.instances, idx);
chunk.rdm_dirty = true;
return;
}
}
@ -540,14 +513,6 @@ tick_level_editor :: () {
tick_level_editor_camera();
tick_particles(cast(float)delta_time);
if is_action_start(Editor_Action.SAVE) then sworld();
if is_action_start(Editor_Action.LEVEL_TOOL_POINT) then current_tool_mode = .POINT;
if is_action_start(Editor_Action.LEVEL_TOOL_BRUSH) then current_tool_mode = .BRUSH;
if is_action_start(Editor_Action.LEVEL_TOOL_AREA) { current_tool_mode = .AREA; area_active = false; }
if is_action_start(Editor_Action.LEVEL_TOOL_LINE) { current_tool_mode = .LINE; line_active = false; }
if is_action_start(Editor_Action.LEVEL_TOOL_INSPECTOR) then current_tool_mode = .INSPECTOR;
if is_action_start(Editor_Action.LEVEL_TOOL_VIEWER) then current_tool_mode = .VIEWER;
if is_action_start(Editor_Action.LEVEL_TWIST_CCW) {
lastInputTime = get_time();
current_orientation_twist = (current_orientation_twist + 1) % 4;
@ -795,21 +760,35 @@ draw_inspector_panel :: (r: *GR.Rect, theme: *GR.Overall_Theme) {
if has_trile {
GR.label(r.*, tprint("Trile: % orient: %", trile_name, orientation), *t_label_left(theme));
r.y += r.h;
#if FLAG_TACOMA_ENABLED {
r.h = ui_h(3, 2);
GR.label(r.*, "-- Bake Config --", *t_label_left(theme));
GR.label(r.*, "-- RDM --", *t_label_left(theme));
r.y += r.h;
r.h = ui_h(4, 0);
size_ov, qual_ov := get_rdm_instance_override(world, inspector_x, inspector_y, inspector_z);
GR.label(r.*, tprint("Size override (0=default): %", size_ov), *t_label_left(theme));
rdm_on := is_rdm_instance_enabled(world, inspector_x, inspector_y, inspector_z);
label := ifx rdm_on then "RDM: ON (sharp specular)" else "RDM: OFF (sky reflection)";
if GR.button(r.*, label, *theme.button_theme, 300) {
set_rdm_instance_enabled(world, inspector_x, inspector_y, inspector_z, !rdm_on);
}
r.y += r.h;
GR.slider(r.*, *size_ov, 0, 512, 1, *theme.slider_theme);
if rdm_on {
cur_size := get_rdm_instance_size(world, inspector_x, inspector_y, inspector_z);
tile_w := 2 * cur_size;
tile_h := 3 * cur_size;
size_label := tprint("Size: % (% x % px)", cur_size, tile_w, tile_h);
GR.label(r.*, size_label, *t_label_left(theme));
r.y += r.h;
GR.label(r.*, tprint("Samples override (0=global): %", qual_ov), *t_label_left(theme));
r2 := r.*;
r2.w = r.w / 2;
if GR.button(r2, "- size", *theme.button_theme, 304) {
set_rdm_instance_size(world, inspector_x, inspector_y, inspector_z, rdm_cycle_size(cur_size, -1));
}
r2.x += r2.w;
if GR.button(r2, "+ size", *theme.button_theme, 305) {
set_rdm_instance_size(world, inspector_x, inspector_y, inspector_z, rdm_cycle_size(cur_size, +1));
}
r.y += r.h;
GR.slider(r.*, *qual_ov, 0, 10000, 10, *theme.slider_theme);
r.y += r.h;
set_rdm_instance_override(world, inspector_x, inspector_y, inspector_z, size_ov, qual_ov);
}
} else {
GR.label(r.*, "Trile: (empty)", *t_label_left(theme));
@ -884,8 +863,9 @@ draw_inspector_panel :: (r: *GR.Rect, theme: *GR.Overall_Theme) {
if has_note {
note := *world.notes[note_idx];
r.h = ui_h(4, 0);
a, new_text, _ := GR.text_input(r.*, note.text, *theme.text_input_theme, 500, input_action = note_input_action);
if (a & .TEXT_MODIFIED) || (a & .ENTERED) note.text = copy_string(new_text);
note_id := cast(s64)inspector_x * 1000003 + cast(s64)inspector_y * 1009 + cast(s64)inspector_z;
a, _, note_state := GR.text_input(r.*, note.text, *theme.text_input_theme, note_id, input_action = note_input_action);
if (a & .TEXT_MODIFIED) || (a & .ENTERED) note.text = copy_string(note_state.text);
r.y += r.h;
if GR.button(r.*, "Remove Note", *theme.button_theme, 302) {
array_ordered_remove_by_index(*world.notes, note_idx);
@ -908,77 +888,16 @@ draw_inspector_panel :: (r: *GR.Rect, theme: *GR.Overall_Theme) {
GR.label(r.*, "-- RDM --", *t_label_left(theme));
r.y += r.h;
chunk_key := world_to_chunk_coord(inspector_x, inspector_y, inspector_z);
chunk := table_find_pointer(*world.chunks, chunk_key);
if chunk != null && chunk.rdm_valid {
GR.label(r.*, "RDM: baked", *t_label_left(theme));
r.y += r.h;
GR.label(r.*, tprint("Chunk: (%, %, %)", chunk_key.x, chunk_key.y, chunk_key.z), *t_label_left(theme));
rdm_flagged := is_rdm_instance_enabled(world, inspector_x, inspector_y, inspector_z);
if rdm_flagged {
GR.label(r.*, "RDM: flagged", *t_label_left(theme));
r.y += r.h;
roughness_mask : u8 = 0;
if has_trile {
roughness_mask = get_trile_roughness_set(trile_name);
}
r.h = ui_h(4, 0);
GR.slider(r.*, *inspector_rdm_roughness, 0, 7, 1, *theme.slider_theme);
r.y += r.h;
r.h = ui_h(3, 2);
has_roughness := (roughness_mask & (1 << cast(u8)inspector_rdm_roughness)) != 0;
if has_roughness {
GR.label(r.*, tprint("Roughness %: present", inspector_rdm_roughness), *t_label_left(theme));
} else {
GR.label(r.*, tprint("Roughness %: not baked", inspector_rdm_roughness), *t_label_left(theme));
}
rect, has_rect := rdm_get_atlas_rect(world, inspector_x, inspector_y, inspector_z);
if has_rect {
GR.label(r.*, tprint("Rect: %.2,%.2 %.2x%.2", rect.x, rect.y, rect.z, rect.w), *t_label_left(theme));
r.y += r.h;
available: String_Builder;
available.allocator = temp;
append(*available, "Available: ");
for i: 0..7 {
if roughness_mask & (1 << cast(u8)i) then print_to_builder(*available, "% ", i);
}
GR.label(r.*, builder_to_string(*available,, temp), *t_label_left(theme));
r.y += r.h;
lx, ly, lz := world_to_local(inspector_x, inspector_y, inspector_z);
lookup_idx := cast(s32)lx + cast(s32)ly * 32 + cast(s32)lz * 1024 + cast(s32)inspector_rdm_roughness * 32768;
uv0 := Vector2.{0, 0};
uv1 := Vector2.{1, 0};
uv2 := Vector2.{1, 1};
uv3 := Vector2.{0, 1};
has_rect := false;
if chunk.rdm_lookup_cpu.data != null {
tx := lookup_idx % chunk.rdm_lookup_w;
ty := lookup_idx / chunk.rdm_lookup_w;
if tx >= 0 && ty >= 0 && tx < chunk.rdm_lookup_w && ty < chunk.rdm_lookup_h {
pixel_offset := (ty * chunk.rdm_lookup_w + tx) * 4;
rect_x := chunk.rdm_lookup_cpu[pixel_offset + 0];
rect_y := chunk.rdm_lookup_cpu[pixel_offset + 1];
rect_w := chunk.rdm_lookup_cpu[pixel_offset + 2];
rect_h := chunk.rdm_lookup_cpu[pixel_offset + 3];
if rect_w > 0 && rect_h > 0 {
has_rect = true;
fy0 := 1.0 - rect_y;
fy1 := 1.0 - (rect_y + rect_h);
uv0 = .{rect_x, fy0};
uv1 = .{rect_x + rect_w, fy0};
uv2 = .{rect_x + rect_w, fy1};
uv3 = .{rect_x, fy1};
GR.label(r.*, tprint("Rect: %.2,%.2 %.2x%.2", rect_x, rect_y, rect_w, rect_h), *t_label_left(theme));
r.y += r.h;
}
}
}
if !has_rect {
GR.label(r.*, "No RDM entry for this slot/roughness", *t_label_left(theme));
r.y += r.h;
} else {
r.y += r.h * 0.5;
tex_size := r.w * 0.9;
tex_r : GR.Rect;
@ -987,8 +906,15 @@ draw_inspector_panel :: (r: *GR.Rect, theme: *GR.Overall_Theme) {
tex_r.w = tex_size;
tex_r.h = tex_size * 1.5;
fy0 := 1.0 - rect.y;
fy1 := 1.0 - (rect.y + rect.w);
uv0 := Vector2.{rect.x, fy0};
uv1 := Vector2.{rect.x + rect.z, fy0};
uv2 := Vector2.{rect.x + rect.z, fy1};
uv3 := Vector2.{rect.x, fy1};
uiTex := New(Ui_Texture,, temp);
uiTex.tex = chunk.rdm_atlas;
uiTex.tex = g_rdm_atlas;
if uiTex.tex.id != INVALID_ID {
set_shader_for_images(uiTex);
immediate_quad(
@ -1003,9 +929,12 @@ draw_inspector_panel :: (r: *GR.Rect, theme: *GR.Overall_Theme) {
immediate_flush();
}
r.y += tex_r.h + r.h * 0.5;
} else {
GR.label(r.*, "RDM: not yet baked", *t_label_left(theme));
r.y += r.h;
}
} else {
GR.label(r.*, "RDM: not baked", *t_label_left(theme));
GR.label(r.*, "RDM: not flagged", *t_label_left(theme));
r.y += r.h;
}
}
@ -1039,8 +968,9 @@ draw_level_editor :: () {
curworld := get_current_world();
if !curworld.valid then return;
cam := get_level_editor_camera();
create_set_cam_rendering_task(cam, effective_plane_height(*curworld.world.conf));
create_world_rendering_tasks(*curworld.world, cam);
ph := effective_plane_height(*curworld.world.conf);
create_set_cam_rendering_task(cam, ph);
create_world_rendering_tasks(*curworld.world, cam, ph);
if show_trile_preview && !trile_preview_disabled {
create_level_editor_preview_tasks();
}

View File

@ -140,7 +140,7 @@ draw_particle_editor_ui :: (theme: *GR.Overall_Theme) {
}
er.y += er.h;
pe_float(*er, tprint("Emission Rate: %", def.emission_rate), *def.emission_rate, 0, 200, 1, theme);
pe_float(*er, tprint("Emission Rate: %", def.emission_rate), *def.emission_rate, 0, 2000, 1, theme);
pe_float(*er, tprint("Lifetime Min: %", def.lifetime_min), *def.lifetime_min, 0, 10, 0.1, theme);
pe_float(*er, tprint("Lifetime Max: %", def.lifetime_max), *def.lifetime_max, 0, 10, 0.1, theme);
pe_vec3(*er, "Velocity", *def.velocity, -50, 50, theme);

View File

@ -9,8 +9,12 @@ RDM_File_Header :: struct {
RDM_FILE_MAGIC :: u32.[0x4D445254][0]; // "TRDM" as little-endian u32
rdm_chunk_filename :: (world_name: string, chunk_key: Chunk_Key, suffix: string) -> string {
return sprint("%/worlds/%/%_%_%.%", GAME_RESOURCES_DIR, world_name, chunk_key.x, chunk_key.y, chunk_key.z, suffix);
rdm_global_atlas_filename :: (world_name: string) -> string {
return sprint("%/worlds/%/rdm_atlas.rdm", GAME_RESOURCES_DIR, world_name);
}
rdm_global_manifest_filename :: (world_name: string) -> string {
return sprint("%/worlds/%/rdm_manifest.json", GAME_RESOURCES_DIR, world_name);
}
// SH probe grid (2 probes per trile per axis = 64x64x64 per 32x32x32 chunk)

View File

@ -14,44 +14,31 @@ Tacoma_Screenshot :: struct {
current_screenshot : Tacoma_Screenshot;
RDM_Atlas_Entry :: struct {
// Single global atlas entry: stored on the World once baked.
RDM_Atlas_Entry_Bake :: struct {
world_pos: Vector3;
roughness: s32;
x, y: s32; // position in atlas (pixels)
w, h: s32; // size of this RDM
x, y: s32; // top-left in atlas pixels
w, h: s32; // pixel size of this RDM (= 2*size, 3*size)
}
// Per-chunk atlas bake state (CPU-side, used during baking only).
RDM_Chunk_Bake :: struct {
data: *float; // CPU atlas buffer (RGBA32F)
width: s32;
height: s32;
cursor_x: s32;
cursor_y: s32;
row_height: s32;
entries: [..]RDM_Atlas_Entry;
}
RDM_ATLAS_SIZE :: 4096 * 2;
RDM_LOOKUP_SIZE :: 512; // 512x512 = 32*32*32*8 = 262144 texels
rdm_chunk_bakes : Table(Chunk_Key, RDM_Chunk_Bake, chunk_key_hash, chunk_key_compare);
RDM_ATLAS_MAX_SIZE :: 16384; // GPU 2D texture ceiling.
ctx : *Tacoma.Tacoma_Context;
// --- Chunk RDM bake queue ---
// Default RDM sizes per roughness level (0=sharpest, 7=diffuse).
// w = 2*size, h = 3*size. Tacoma accepts size directly.
g_rdm_default_sizes : [8]s32 = .[256, 128, 64, 32, 16, 8, 4, 2];
// A single RDM render job: one world_trile at one roughness level.
RDM_Bake_Job :: struct {
world_trile_index: s32;
roughness: s32;
world_pos: Vector3;
size_override: s32; // 0 = use g_rdm_default_sizes[roughness]
quality_override: s32; // 0 = use rdm_bake.quality
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)
width : s32;
height : s32;
cursor_x : s32;
cursor_y : s32;
row_height : s32;
entries : [..]RDM_Atlas_Entry_Bake;
}
RDM_Bake_State :: struct {
@ -59,6 +46,7 @@ RDM_Bake_State :: struct {
quality : s32 = 100;
jobs : [..]RDM_Bake_Job;
current_job : s32 = 0;
atlas : RDM_Global_Bake;
}
rdm_bake : RDM_Bake_State;
@ -73,72 +61,62 @@ SH_Bake_State :: struct {
sh_bake : SH_Bake_State;
rdm_job_size :: (job: RDM_Bake_Job) -> s32 {
if job.size_override > 0 then return job.size_override;
return g_rdm_default_sizes[job.roughness];
}
rdm_job_quality :: (job: RDM_Bake_Job) -> s32 {
if job.quality_override > 0 then return job.quality_override;
return rdm_bake.quality;
}
// Pixel dimensions of a single RDM entry. w = 2*size, h = 3*size.
rdm_entry_predicted_size :: (roughness: s32) -> (w: s32, h: s32) {
size := g_rdm_default_sizes[roughness];
return 2 * size, 3 * size;
}
// Simulate shelf-pack of jobs into a canvas of given width; return total height used.
rdm_simulate_pack_height :: (jobs: []RDM_Bake_Job, canvas_w: s32) -> s32 {
cx : s32 = 0;
cy : s32 = 0;
rh : s32 = 0;
for job: jobs {
s := rdm_job_size(job);
w := 2 * s;
h := 3 * s;
if cx + w > canvas_w { cy += rh; cx = 0; rh = 0; }
cx += w;
if h > rh rh = h;
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;
}
jobs.data[j + 1] = key;
}
cy += rh;
return cy;
}
// Simulate shelf-packing for a chunk's sorted job list and return minimum
// power-of-2 atlas dimensions, capped at RDM_ATLAS_SIZE.
rdm_calc_chunk_atlas_size :: (jobs: []RDM_Bake_Job) -> (s32, s32) {
// 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;
// Estimate canvas width from sqrt(total pixel area).
total_area : s64 = 0;
for job: jobs {
s := rdm_job_size(job);
total_area += cast(s64)(2 * s) * cast(s64)(3 * s);
target : s32 = 16;
while target < RDM_ATLAS_MAX_SIZE {
if rdm_simulate_shelf_pack(jobs, target) return target, target;
target *= 2;
}
target_w : s32 = 16;
sqrt_area := cast(s32) sqrt(cast(float) total_area) + 1;
while target_w < sqrt_area target_w *= 2;
// Widen until the packed height fits within the width (roughly square).
while target_w < RDM_ATLAS_SIZE {
if rdm_simulate_pack_height(jobs, target_w) <= target_w break;
target_w *= 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);
}
// Round height up to next power of 2.
final_height := rdm_simulate_pack_height(jobs, target_w);
target_h : s32 = 16;
while target_h < final_height target_h *= 2;
if target_w > RDM_ATLAS_SIZE target_w = RDM_ATLAS_SIZE;
if target_h > RDM_ATLAS_SIZE target_h = RDM_ATLAS_SIZE;
return target_w, target_h;
return RDM_ATLAS_MAX_SIZE, RDM_ATLAS_MAX_SIZE;
}
// Build the Tacoma scene from the world, and emit bake jobs for selected chunks.
// If chunk_keys is null, all chunks are queued.
// 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;
@ -150,27 +128,7 @@ rdm_bake_start :: (world: World, quality: s32, include_water: bool, chunk_keys:
trile_name_to_index: Table(string, s32);
trile_name_to_index.allocator = temp;
// Per world_trile: which roughnesses to bake.
world_trile_roughnesses : [..]u8;
world_trile_roughnesses.allocator = temp;
// Cache roughness sets per trile type.
roughness_cache: Table(string, u8);
roughness_cache.allocator = temp;
bake_all := chunk_keys.count == 0;
// Build a set of chunk keys to bake if specific ones were requested.
chunk_key_set: Table(Chunk_Key, bool, chunk_key_hash, chunk_key_compare);
chunk_key_set.allocator = temp;
if !bake_all {
for key: chunk_keys {
table_set(*chunk_key_set, key, true);
}
}
for chunk: world.chunks {
should_bake := bake_all || table_contains(*chunk_key_set, chunk.coord);
for group: chunk.groups {
success, idx := table_find(*trile_name_to_index, group.trile_name);
if !success {
@ -196,40 +154,23 @@ rdm_bake_start :: (world: World, quality: s32, include_water: bool, chunk_keys:
table_set(*trile_name_to_index, group.trile_name, idx);
}
// Get roughness set for this trile type (cached).
roughness_mask : u8;
found_r, cached_mask := table_find(*roughness_cache, group.trile_name);
if found_r {
roughness_mask = cached_mask;
} else {
roughness_mask = get_trile_roughness_set(group.trile_name);
table_set(*roughness_cache, group.trile_name, roughness_mask);
}
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});
array_add(*world_trile_roughnesses, roughness_mask);
if should_bake {
size_ov, qual_ov := get_rdm_instance_override(*world, wx, wy, wz);
for r: 0..7 {
if roughness_mask & cast(u8)(1 << r) {
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,
roughness = cast(s32) r,
world_pos = wpos,
size_override = size_ov,
quality_override = qual_ov,
size = inst_size,
});
}
}
}
}
}
}
sky : Tacoma.Sky_Config = world_to_sky_config(world);
blases : Tacoma.Trile_Set = .{trile_list.data, cast(s32) trile_list.count};
@ -237,69 +178,18 @@ rdm_bake_start :: (world: World, quality: s32, include_water: bool, chunk_keys:
ctx = Tacoma.tacoma_init("./modules/Tacoma/");
Tacoma.tacoma_load_scene(ctx, sky, blases, tlas, cast(s32) include_water);
// Sort jobs by roughness ascending (lowest roughness = biggest images first).
// Simple insertion sort, N is small.
if rdm_bake.jobs.count > 1 {
for i: 1..cast(s32)(rdm_bake.jobs.count - 1) {
key := rdm_bake.jobs[i];
j := i - 1;
while j >= 0 && rdm_bake.jobs[j].roughness > key.roughness {
rdm_bake.jobs[j + 1] = rdm_bake.jobs[j];
j -= 1;
}
rdm_bake.jobs[j + 1] = key;
}
}
// Sort jobs tallest-first for a tighter shelf pack.
rdm_sort_jobs_by_height_desc(*rdm_bake.jobs);
// Clean up any previous per-chunk bake data.
rdm_cleanup_chunk_bakes();
// Clear RDM results only for chunks that are being re-baked.
curworld := get_current_world();
for *chunk: curworld.world.chunks {
if chunk.rdm_valid && (bake_all || table_contains(*chunk_key_set, chunk.coord)) {
sg_destroy_image(chunk.rdm_atlas);
sg_destroy_image(chunk.rdm_lookup);
chunk.rdm_valid = false;
}
}
// Pre-allocate per-chunk atlas CPU buffers with optimal (minimum) sizes.
{
// Collect unique chunk keys.
unique_chunk_keys : [..]Chunk_Key;
unique_chunk_keys.allocator = temp;
for job: rdm_bake.jobs {
ck := world_to_chunk_coord(cast(s32) job.world_pos.x, cast(s32) job.world_pos.y, cast(s32) job.world_pos.z);
already := false;
for unique_chunk_keys { if it == ck { already = true; break; } }
if !already array_add(*unique_chunk_keys, ck);
}
for chunk_key: unique_chunk_keys {
// Collect this chunk's jobs (already sorted by roughness ascending).
this_jobs : [..]RDM_Bake_Job;
this_jobs.allocator = temp;
for job: rdm_bake.jobs {
ck := world_to_chunk_coord(cast(s32) job.world_pos.x, cast(s32) job.world_pos.y, cast(s32) job.world_pos.z);
if ck == chunk_key array_add(*this_jobs, job);
}
atlas_w, atlas_h := rdm_calc_chunk_atlas_size(this_jobs);
log_info("RDM atlas for chunk %: %x% (%.1f MB, was % MB)",
chunk_key, atlas_w, atlas_h,
cast(float)(cast(s64) atlas_w * atlas_h * 4 * size_of(float)) / (1024.0 * 1024.0),
RDM_ATLAS_SIZE * RDM_ATLAS_SIZE * 4 * size_of(float) / (1024 * 1024));
atlas_bytes := cast(s64) atlas_w * cast(s64) atlas_h * 4 * size_of(float);
bake : RDM_Chunk_Bake;
bake.width = atlas_w;
bake.height = atlas_h;
bake.data = cast(*float) alloc(atlas_bytes);
memset(bake.data, 0, atlas_bytes);
table_set(*rdm_chunk_bakes, chunk_key, bake);
}
}
// 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;
@ -309,17 +199,16 @@ rdm_bake_start :: (world: World, quality: s32, include_water: bool, chunk_keys:
}
}
// Queue all chunks for RDM baking.
// 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);
}
// Queue specific chunks for RDM baking.
rdm_bake_chunks :: (chunk_keys: []Chunk_Key, world: World, quality: s32, include_water: bool) {
rdm_bake_start(world, quality, include_water, chunk_keys);
}
// Called once per frame to process at most one RDM.
// 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 {
@ -329,17 +218,13 @@ rdm_bake_tick :: () {
job := rdm_bake.jobs[rdm_bake.current_job];
size := rdm_job_size(job);
size : s32 = job.size;
w := 2 * size;
h := 3 * size;
ptr := Tacoma.tacoma_render_rdm(ctx, job.world_trile_index, job.roughness, rdm_job_quality(job), size);
ptr := Tacoma.tacoma_render_rdm(ctx, job.world_trile_index, 0, rdm_job_quality(job), size);
// Find this job's per-chunk bake state.
chunk_key := world_to_chunk_coord(cast(s32) job.world_pos.x, cast(s32) job.world_pos.y, cast(s32) job.world_pos.z);
bake := table_find_pointer(*rdm_chunk_bakes, chunk_key);
bake := *rdm_bake.atlas;
if bake != null {
// Shelf-pack this RDM into the chunk's atlas.
if bake.cursor_x + w > bake.width {
bake.cursor_y += bake.row_height;
bake.cursor_x = 0;
@ -350,7 +235,6 @@ rdm_bake_tick :: () {
ay := bake.cursor_y;
if ay + h <= bake.height {
// Copy pixels row-by-row into the chunk's atlas CPU buffer.
src := cast(*u8) ptr;
row_bytes := cast(s64) w * 4 * size_of(float);
for row: 0..h-1 {
@ -359,25 +243,21 @@ rdm_bake_tick :: () {
memcpy(cast(*u8) bake.data + dst_offset, src + src_offset, row_bytes);
}
entry : RDM_Atlas_Entry;
entry : RDM_Atlas_Entry_Bake;
entry.world_pos = job.world_pos;
entry.roughness = job.roughness;
entry.x = ax;
entry.y = ay;
entry.w = w;
entry.h = h;
array_add(*bake.entries, entry);
} else {
log_warn("RDM atlas overflow for chunk %, skipping (pos=%, roughness=%)", chunk_key, job.world_pos, job.roughness);
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;
}
// Still update the preview screenshot.
tacoma_handle_result(ptr, w, h);
rdm_bake.current_job += 1;
}
@ -385,115 +265,68 @@ rdm_bake_finish :: () {
if ctx != null then tacoma_stop();
curworld := get_current_world();
total_entries : s64 = 0;
chunk_count : s64 = 0;
bake := *rdm_bake.atlas;
// Collect unique chunk keys from jobs.
bake_chunk_keys : [..]Chunk_Key;
bake_chunk_keys.allocator = temp;
for job: rdm_bake.jobs {
ck := world_to_chunk_coord(cast(s32) job.world_pos.x, cast(s32) job.world_pos.y, cast(s32) job.world_pos.z);
already := false;
for bake_chunk_keys { if it == ck { already = true; break; } }
if !already then array_add(*bake_chunk_keys, ck);
}
lookup_texels :: RDM_LOOKUP_SIZE * RDM_LOOKUP_SIZE;
lookup_bytes :: lookup_texels * 4 * size_of(float);
for chunk_key: bake_chunk_keys {
print("Handling chunk key: %\n", chunk_key);
bake := table_find_pointer(*rdm_chunk_bakes, chunk_key);
if bake == null || bake.entries.count == 0 then continue;
// a) Upload per-chunk atlas to GPU.
atlas_imgdata : sg_image_data;
atlas_byte_size := cast(u64) bake.width * cast(u64) bake.height * 4 * size_of(float);
atlas_imgdata.subimage[0][0] = .{bake.data, atlas_byte_size};
atlas_desc : sg_image_desc = .{
render_target = false,
width = bake.width,
height = bake.height,
pixel_format = sg_pixel_format.RGBA32F,
sample_count = 1,
data = atlas_imgdata
};
atlas_image := sg_make_image(*atlas_desc);
// b) Generate lookup texture (512x512 RGBA32F).
lookup_data := cast(*float) alloc(lookup_bytes);
memset(lookup_data, 0, lookup_bytes);
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 {
lx, ly, lz := world_to_local(cast(s32) entry.world_pos.x, cast(s32) entry.world_pos.y, cast(s32) entry.world_pos.z);
index := cast(s32) lx + cast(s32) ly * 32 + cast(s32) lz * 32 * 32 + entry.roughness * 32 * 32 * 32;
tx := index % RDM_LOOKUP_SIZE;
ty := index / RDM_LOOKUP_SIZE;
pixel_offset := (ty * RDM_LOOKUP_SIZE + tx) * 4;
lookup_data[pixel_offset + 0] = cast(float) entry.x / atlas_w;
lookup_data[pixel_offset + 1] = cast(float) entry.y / atlas_h;
lookup_data[pixel_offset + 2] = cast(float) entry.w / atlas_w;
lookup_data[pixel_offset + 3] = cast(float) entry.h / atlas_h;
}
lookup_imgdata : sg_image_data;
lookup_imgdata.subimage[0][0] = .{lookup_data, lookup_bytes};
lookup_desc : sg_image_desc = .{
render_target = false,
width = RDM_LOOKUP_SIZE,
height = RDM_LOOKUP_SIZE,
pixel_format = sg_pixel_format.RGBA32F,
sample_count = 1,
data = lookup_imgdata
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,
};
lookup_image := sg_make_image(*lookup_desc);
free(lookup_data);
// c) Store in Chunk.
chunk := table_find_pointer(*curworld.world.chunks, chunk_key);
if chunk != null {
if chunk.rdm_valid {
sg_destroy_image(chunk.rdm_atlas);
sg_destroy_image(chunk.rdm_lookup);
array_add(*curworld.world.rdm_lookup, e);
}
chunk.rdm_atlas = atlas_image;
chunk.rdm_lookup = lookup_image;
chunk.rdm_valid = true;
chunk.rdm_dirty = false;
}
total_entries += cast(s64) bake.entries.count;
chunk_count += 1;
// 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: % chunks, % total entries", chunk_count, total_entries);
// Save baked RDM data to disk.
rdm_save_all_chunks_to_disk();
// Clean up CPU bake data.
rdm_cleanup_chunk_bakes();
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 = .{};
}
rdm_cleanup_chunk_bakes :: () {
for *bake: rdm_chunk_bakes {
if bake.data != null then free(bake.data);
array_free(bake.entries);
}
deinit(*rdm_chunk_bakes);
rdm_chunk_bakes = .{};
// Convert RGBA32F CPU atlas to RGBA16F and upload to g_rdm_atlas.
upload_global_atlas_image :: (bake: *RDM_Global_Bake) {
pixel_count := cast(s64) bake.width * cast(s64) bake.height * 4;
halves := cast(*u16) alloc(pixel_count * size_of(u16));
defer free(halves);
for i: 0..pixel_count - 1 halves[i] = f32_to_f16(bake.data[i]);
imgdata : sg_image_data;
imgdata.subimage[0][0] = .{halves, cast(u64)(pixel_count * size_of(u16))};
desc : sg_image_desc = .{
render_target = false,
width = bake.width,
height = bake.height,
pixel_format = sg_pixel_format.RGBA16F,
sample_count = 1,
data = imgdata,
};
if g_rdm_atlas.id != 0 then sg_destroy_image(g_rdm_atlas);
g_rdm_atlas = sg_make_image(*desc);
}
bake_sky_scale : float = 1.0;
bake_sky_desaturation : float = 0.0;
bake_sky_desaturation : float = 1.0;
world_to_sky_config :: (world: World) -> Tacoma.Sky_Config {
sky : Tacoma.Sky_Config;
@ -604,14 +437,6 @@ gen_reference :: (w: s32, h: s32, eye: Vector3, target: Vector3, quality: s32, i
tacoma_stop();
}
gen_rdm :: (quality: s32, include_water: bool, world: World) {
tacoma_init_scene(world, include_water);
size := g_rdm_default_sizes[0];
ptr := Tacoma.tacoma_render_rdm(ctx, 0, 0, quality, size);
tacoma_handle_result(ptr, 2 * size, 3 * size);
tacoma_stop();
}
sh_bake_start :: (quality: s32 = 50, include_water: bool = false) {
if sh_bake.active then return;
curworld := get_current_world();
@ -648,9 +473,11 @@ sh_bake_tick :: () {
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, cast(float) oy, cast(float) oz,
n, 0.5, sh_bake.quality);
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));
@ -708,75 +535,49 @@ shgrid_save_to_disk :: (world_name: string, chunk_key: Chunk_Key, data: *float,
builder : String_Builder;
header := SH_Grid_File_Header.{
magic = SH_FILE_MAGIC,
version = 1,
version = 2,
probe_n = probe_n,
};
write_bytes(*builder, *header, size_of(SH_Grid_File_Header));
total_floats := cast(s64) probe_n * probe_n * probe_n * 12;
write_bytes(*builder, data, total_floats * size_of(float));
total_values := cast(s64) probe_n * probe_n * probe_n * 12;
halves := NewArray(total_values, u16,, temp);
for i: 0..total_values-1 halves[i] = f32_to_f16(data[i]);
write_bytes(*builder, halves.data, total_values * size_of(u16));
file.write_entire_file(path, builder_to_string(*builder));
}
}
// --- RDM disk persistence ---
// (RDM_File_Header, RDM_FILE_MAGIC, rdm_chunk_filename, rdm_load_from_disk
// are defined in rdm_disk.jai which is always loaded on non-WASM builds.)
rdm_save_image_to_file :: (path: string, data: *float, width: s32, height: s32) {
rdm_save_global_atlas_to_disk :: (bake: *RDM_Global_Bake) {
#if OS != .WASM {
file :: #import "File";
builder: String_Builder;
header := RDM_File_Header.{
magic = RDM_FILE_MAGIC,
width = width,
height = height,
};
write_bytes(*builder, *header, size_of(RDM_File_Header));
pixel_bytes := cast(s64) width * cast(s64) height * 4 * size_of(float);
write_bytes(*builder, data, pixel_bytes);
file.write_entire_file(path, builder_to_string(*builder));
}
}
rdm_save_all_chunks_to_disk :: () {
#if OS != .WASM {
curworld := get_current_world();
if !curworld.valid then return;
world_name := curworld.world.name;
path := rdm_global_atlas_filename(curworld.world.name);
for *bake, chunk_key: rdm_chunk_bakes {
print("Processing chunk....\n");
if bake.entries.count == 0 then continue;
// Save atlas.
atlas_path := rdm_chunk_filename(world_name, chunk_key, "rdm_atlas");
rdm_save_image_to_file(atlas_path, bake.data, bake.width, bake.height);
// Regenerate and save lookup.
lookup_texels :: RDM_LOOKUP_SIZE * RDM_LOOKUP_SIZE;
lookup_floats :: lookup_texels * 4;
lookup_data : [lookup_floats]float;
memset(lookup_data.data, 0, size_of(type_of(lookup_data)));
atlas_w := cast(float) bake.width;
atlas_h := cast(float) bake.height;
for entry: bake.entries {
lx, ly, lz := world_to_local(cast(s32) entry.world_pos.x, cast(s32) entry.world_pos.y, cast(s32) entry.world_pos.z);
index := cast(s32) lx + cast(s32) ly * 32 + cast(s32) lz * 32 * 32 + entry.roughness * 32 * 32 * 32;
tx := index % RDM_LOOKUP_SIZE;
ty := index / RDM_LOOKUP_SIZE;
pixel_offset := (ty * RDM_LOOKUP_SIZE + tx) * 4;
lookup_data[pixel_offset + 0] = cast(float) entry.x / atlas_w;
lookup_data[pixel_offset + 1] = cast(float) entry.y / atlas_h;
lookup_data[pixel_offset + 2] = cast(float) entry.w / atlas_w;
lookup_data[pixel_offset + 3] = cast(float) entry.h / atlas_h;
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);
}
}
lookup_path := rdm_chunk_filename(world_name, chunk_key, "rdm_lookup");
rdm_save_image_to_file(lookup_path, lookup_data.data, RDM_LOOKUP_SIZE, RDM_LOOKUP_SIZE);
log_info("Saved RDM data for chunk % to disk", chunk_key);
}
rdm_save_global_manifest_to_disk :: (world_name: string, entries: []Rdm_Atlas_Entry) {
#if OS != .WASM {
file :: #import "File";
path := rdm_global_manifest_filename(world_name);
s := Jaison.json_write_string(entries);
file.write_entire_file(path, s);
log_info("Saved RDM manifest: % (% entries)", path, entries.count);
}
}

View File

@ -13,7 +13,7 @@ theme_ptr : GR.Overall_Theme;
current_pipeline : s32 = 0;
current_slot : s32 = 0;
pipeline_names : []string = .["shadowmap", "reflection", "main", "position", "normal", "ssao", "bloom", "dof", "rdm_atlas (chunk)", "rdm_lookup (chunk)"];
pipeline_names : []string = .["shadowmap", "reflection", "main", "position", "normal", "ssao", "bloom", "dof", "rdm_atlas"];
draw_subwindow_texture_debug :: (state: *GR.Subwindow_State, r: GR.Rect, data: *void) {
r2 := r;
@ -38,16 +38,7 @@ draw_subwindow_texture_debug :: (state: *GR.Subwindow_State, r: GR.Rect, data: *
case 5; image = g_ssaobuf;
case 6; image = g_bloom_tex;
case 7; image = g_dof_tex;
case 8; #through;
case 9;
cw := get_current_world();
for chunk: cw.world.chunks {
if chunk.rdm_valid {
if current_pipeline == 6 then image = chunk.rdm_atlas;
else image = chunk.rdm_lookup;
break;
}
}
case 8; image = g_rdm_atlas;
}
uiTex.tex = image;

View File

@ -545,20 +545,6 @@ draw_trile_editor :: () {
}
trile_editor_shortcuts :: () {
// Tool/mode buttons are only rendered in the Toolset tab — handle shortcuts here so they work from any tab.
if is_action_start(Editor_Action.TRIXEL_TOOL_PAINT) then current_tool = .PAINT;
if is_action_start(Editor_Action.TRIXEL_TOOL_ADD) then current_tool = .ADD;
if is_action_start(Editor_Action.TRIXEL_TOOL_REMOVE) then current_tool = .REMOVE;
if is_action_start(Editor_Action.TRIXEL_MODE_POINT) then current_mode = .POINT;
if is_action_start(Editor_Action.TRIXEL_MODE_AREA) then current_mode = .AREA;
if is_action_start(Editor_Action.TRIXEL_MODE_BRUSH) then current_mode = .BRUSH;
// Save is in the Toolset tab only.
if is_action_start(Editor_Action.SAVE) {
set_trile_gfx(editor_current_trile.name, generate_trile_gfx_matias(editor_current_trile));
striles();
update_trile_thumbnail(editor_current_trile.name);
}
if is_action_start(Editor_Action.TRIXEL_VIEW_TILE) then tiling_preview_active = !tiling_preview_active;
if is_action_start(Editor_Action.TRIXEL_UNDO) then do_undo();
if is_action_start(Editor_Action.TRIXEL_REDO) then do_redo();
}

View File

@ -97,9 +97,9 @@ init :: () {
logger = .{ func = slog_func },
}));
sfetch_setup(*(sfetch_desc_t.{
max_requests = 64,
max_requests = 128,
num_channels = 2,
num_lanes = 1,
num_lanes = 8,
logger = .{ func = slog_func },
}));
asset_manager_init();
@ -109,6 +109,7 @@ init :: () {
logger = .{ func = slog_func },
stream_cb = sokol_audio_callback,
}));
g_audio_sample_rate = saudio_sample_rate();
stm_setup();
state.dpi_scale = sapp_dpi_scale();
atlas_dim := round_pow2(1024.0 * state.dpi_scale);
@ -122,13 +123,16 @@ init :: () {
state.pass_action_clear.colors[0] = .{ load_action = .CLEAR, clear_value = .{ r = 0, g = 0, b = 0, a = 0 } };
state.pass_action_clear_gbuf.colors[0] = .{ load_action = .CLEAR, clear_value = .{ r = 0, g = 0, b = 1000, a = 0 } };
state.pass_action_clear_gbuf.colors[2] = .{ load_action = .CLEAR, clear_value = .{ r = 0, g = 0, b = 0, a = 0 } };
state.pass_action.colors[0] = .{ load_action = .LOAD };
g_mixer.paused = true;
useless_mem : [1]u8;
debug_font = get_font_at_size(useless_mem, 15);
load_pack("boot", true, true);
load_pack("core", true, false);
load_pack("game_core", true, false);
load_pack("boot", .BLOCK_ENGINE);
load_pack("core");
load_pack("game_core");
#if FLAG_TEST_EXE_ENGINE {
engine_exe_tests_add();
@ -211,6 +215,10 @@ frame :: () {
init_after_core_done = true;
}
if g_mixer.paused && !in_editor_view && !show_loading_screen() {
g_mixer.paused = false;
}
fonsClearState(state.fons);
add_frame_profiling_point("After loading logic");

View File

@ -51,7 +51,7 @@ hot_reload_all_packs :: () {
array_add(*pack_names, sprint("%", pack.name));
}
array_reset(*g_asset_manager.main_queue);
array_reset(*g_asset_manager.queue);
g_mixer.paused = true;
g_asset_manager.hot_reload_unpause_pending = true;
@ -76,7 +76,7 @@ hot_reload_all_packs :: () {
recreate_packs_on_disk();
for name: pack_names {
load_pack(name, true, false);
load_pack(name);
}
log_info("Hot-reload: queued % pack(s) for reload", pack_names.count);
}

View File

@ -41,9 +41,9 @@ Particle :: struct {
definition : *Particle_Emitter_Config;
}
MAX_PARTICLES : s32 : 2048;
MAX_PARTICLES :: 16384;
g_particles : [2048] Particle;
g_particles : [MAX_PARTICLES] Particle;
g_emitter_defs : [..] Particle_Emitter_Config;
clear_particles :: () {
@ -102,8 +102,11 @@ add_particle_render_tasks :: () {
anim_name : string;
blend_mode : Particle_Blend_Mode;
anim : *Animation;
task : *Rendering_Task_Particles;
sheet : sg_image;
count : s32;
pos_size : *[MAX_PARTICLES]Vector4;
uv_rects : *[MAX_PARTICLES]Vector4;
colors : *[MAX_PARTICLES]Vector4;
}
batches : [..] Batch;
@ -111,7 +114,6 @@ add_particle_render_tasks :: () {
for *p: g_particles {
if !p.alive then continue;
if p.definition == null then continue;
def := p.definition;
if def.animation_name.count == 0 then continue;
@ -127,10 +129,17 @@ add_particle_render_tasks :: () {
if batch == null {
anim := get_animation_from_string(def.animation_name);
if anim == null then continue;
array_add(*batches, .{ anim_name = def.animation_name, blend_mode = def.blend_mode, anim = anim, task = New(Rendering_Task_Particles,, temp) });
if anim.frames.count == 0 then continue;
array_add(*batches, .{
anim_name = def.animation_name,
blend_mode = def.blend_mode,
anim = anim,
sheet = anim.sheet,
pos_size = New([MAX_PARTICLES]Vector4,, temp),
uv_rects = New([MAX_PARTICLES]Vector4,, temp),
colors = New([MAX_PARTICLES]Vector4,, temp),
});
batch = *batches[batches.count - 1];
batch.task.sheet = anim.sheet;
batch.task.blend_mode = def.blend_mode;
}
if batch.count >= MAX_PARTICLES then continue;
@ -140,31 +149,48 @@ add_particle_render_tasks :: () {
col := lerp_color(def.color_start, def.color_end, t);
idx := batch.count;
batch.task.pos_size[idx] = .{p.position.x, p.position.y, p.position.z, size};
batch.pos_size.*[idx] = .{p.position.x, p.position.y, p.position.z, size};
anim := batch.anim;
frame_count := anim.frames.count;
if frame_count == 0 then continue;
frame := cast(s32)(t * cast(float)frame_count);
if frame < 0 then frame = 0;
if frame >= frame_count then frame = cast(s32)(frame_count - 1);
f := anim.frames[frame];
batch.task.uv_rects[idx] = .{
batch.uv_rects.*[idx] = .{
cast(float)f.x / cast(float)anim.sheet_w,
cast(float)f.y / cast(float)anim.sheet_h,
cast(float)f.w / cast(float)anim.sheet_w,
cast(float)f.h / cast(float)anim.sheet_h,
};
batch.task.colors[idx] = col;
batch.colors.*[idx] = col;
batch.count += 1;
}
buf_task := New(Rendering_Task_Particles_Buffer,, temp);
total_count : s32 = 0;
for *b: batches {
if b.count > 0 {
b.task.count = b.count;
add_rendering_task(b.task.*);
if b.count <= 0 then continue;
instance_offset := total_count;
memcpy(*buf_task.pos_size[instance_offset], cast(*void)b.pos_size, b.count * size_of(Vector4));
memcpy(*buf_task.uv_rects[instance_offset], cast(*void)b.uv_rects, b.count * size_of(Vector4));
memcpy(*buf_task.colors[instance_offset], cast(*void)b.colors, b.count * size_of(Vector4));
total_count += b.count;
draw_task : Rendering_Task_Particles;
draw_task.count = b.count;
draw_task.instance_offset = instance_offset;
draw_task.blend_mode = b.blend_mode;
draw_task.sheet = b.sheet;
add_rendering_task(draw_task);
}
if total_count > 0 {
buf_task.total_count = total_count;
add_rendering_task(buf_task.*);
}
}

View File

@ -1,13 +1,13 @@
#import,dir "../../modules/sokol-jai/sokol/app"(DEBUG = FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/gfx"(DEBUG = FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/gl"(DEBUG = FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/glue"(DEBUG = FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/shape"(DEBUG = FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/app"(DEBUG = !FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/gfx"(DEBUG = !FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/gl"(DEBUG = !FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/glue"(DEBUG = !FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/shape"(DEBUG = !FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/fontstash";
#import,dir "../../modules/sokol-jai/sokol/log"(DEBUG = FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/time"(DEBUG = FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/fetch"(DEBUG = FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/audio"(DEBUG = FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/log"(DEBUG = !FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/time"(DEBUG = !FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/fetch"(DEBUG = !FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/audio"(DEBUG = !FLAG_RELEASE_BUILD);
#load "../main.jai";

View File

@ -18,10 +18,13 @@ Render_Command_Type :: enum {
DRAW_GROUND;
ADD_TRILE_POSITIONS;
DRAW_TRILE_POSITIONS;
ADD_TRILE_RDM_POSITION;
DRAW_TRILE_RDM;
UPDATE_TRIXELS;
DRAW_TRIXELS;
SET_LIGHT;
DRAW_BILLBOARD;
UPDATE_PARTICLES;
DRAW_PARTICLES;
}
@ -58,6 +61,21 @@ Render_Command_Draw_Trile_Positions :: struct {
offset_index : s32 = 0; // index into trile_offsets, assigned at task-conversion time
}
Render_Command_Add_Trile_RDM_Position :: struct {
#as using c : Render_Command;
c.type = .ADD_TRILE_RDM_POSITION;
position : Vector4;
}
Render_Command_Draw_Trile_RDM :: struct {
#as using c : Render_Command;
c.type = .DRAW_TRILE_RDM;
trile : string;
conf : *World_Config;
atlas_rect : Vector4;
offset_index : s32 = 0;
}
Render_Command_Update_Trixels :: struct {
#as using c : Render_Command;
c.type = .UPDATE_TRIXELS;
@ -86,15 +104,22 @@ Render_Command_Draw_Trixels :: struct {
is_secondary : bool;
}
Render_Command_Update_Particles :: struct {
#as using c : Render_Command;
c.type = .UPDATE_PARTICLES;
total_count : s32;
pos_size : [MAX_PARTICLES]Vector4;
uv_rects : [MAX_PARTICLES]Vector4;
colors : [MAX_PARTICLES]Vector4;
}
Render_Command_Draw_Particles :: struct {
#as using c : Render_Command;
c.type = .DRAW_PARTICLES;
count : s32;
instance_offset : s32;
blend_mode : Particle_Blend_Mode;
sheet : sg_image;
pos_size : [2048]Vector4;
uv_rects : [2048]Vector4;
colors : [2048]Vector4;
}
Render_Command_Draw_Ground :: struct {

View File

@ -9,6 +9,7 @@ bypass_shadows : bool = false;
bypass_reflections : bool = false;
trile_offsets : [..]s32;
trile_rdm_offsets : [..]s32;
current_world_config : *World_Config = null;
in_shadowmap_pass : bool = false;
@ -24,6 +25,12 @@ backend_handle_command :: (cmd: *Render_Command) {
case .DRAW_TRILE_POSITIONS;
draw_command := cast(*Render_Command_Draw_Trile_Positions)cmd;
backend_draw_trile_positions(draw_command.trile, draw_command.amount, draw_command.conf, draw_command.chunk_key, draw_command.preview_mode, draw_command.offset_index);
case .ADD_TRILE_RDM_POSITION;
add_command := cast(*Render_Command_Add_Trile_RDM_Position)cmd;
backend_add_trile_rdm_position(add_command.position);
case .DRAW_TRILE_RDM;
draw_command := cast(*Render_Command_Draw_Trile_RDM)cmd;
backend_draw_trile_rdm(draw_command.trile, draw_command.conf, draw_command.atlas_rect, draw_command.offset_index);
case .DRAW_SKY;
sky_command := cast(*Render_Command_Sky)cmd;
backend_draw_sky(sky_command.worldConfig);
@ -53,6 +60,9 @@ backend_handle_command :: (cmd: *Render_Command) {
} else {
backend_gbuffer_draw_billboard(command.position, command.animation, command.frame, command.flipX, command.faceDir);
}
case .UPDATE_PARTICLES;
update_cmd := cast(*Render_Command_Update_Particles)cmd;
backend_update_particle_buffers(update_cmd);
case .DRAW_PARTICLES;
particles_cmd := cast(*Render_Command_Draw_Particles)cmd;
backend_draw_particles(particles_cmd);
@ -215,27 +225,12 @@ backend_draw_trile_positions_main :: (trile : string, amount : s32, worldConf: *
bindings.images[1] = g_ssaobuf;
bindings.samplers[2] = g_shadowmap_sampler;
bindings.images[2] = g_shadowmap;
bindings.images[3] = g_brdf_lut;
bindings.images[4] = g_sh_irradiance;
bindings.samplers[3] = gPipelines.trile.bind.samplers[3];
// Bind RDM textures for this chunk. Fall back to a 1x1 black image when
// no baked data is available so sokol doesn't drop the draw call.
// The shader gates all RDM sampling on atlas_rect.z > 0, which the
// fallback texture returns as 0, so the ambient fallback path is taken.
curworld := get_current_world();
chunk := table_find_pointer(*curworld.world.chunks, chunk_key);
if chunk != null && chunk.rdm_valid {
bindings.images[3] = chunk.rdm_lookup;
bindings.images[4] = chunk.rdm_atlas;
} else {
bindings.images[3] = g_rdm_fallback;
bindings.images[4] = g_rdm_fallback;
}
bindings.images[5] = g_brdf_lut;
if chunk != null && chunk.sh_valid {
bindings.images[6] = chunk.sh_probe_grid;
} else {
bindings.images[6] = g_sh_fallback;
}
bindings.samplers[3] = gPipelines.trile.bind.samplers[3];
fs_params : Trile_Fs_Params;
fs_params.mvp_shadow = shadow_mvp.floats;
@ -244,15 +239,13 @@ backend_draw_trile_positions_main :: (trile : string, amount : s32, worldConf: *
fs_params.screen_w = w;
fs_params.screen_h = h;
lc := *current_lighting_config;
fs_params.rdm_enabled = ifx in_reflection_pass then 0 else lc.rdm_enabled;
fs_params.ambient_intensity = lc.ambient_intensity;
fs_params.emissive_scale = lc.emissive_scale;
fs_params.rdm_diff_scale = lc.rdm_diff_scale;
fs_params.rdm_spec_scale = lc.rdm_spec_scale;
fs_params.indirect_diff_scale = lc.indirect_diff_scale;
fs_params.indirect_spec_scale = lc.indirect_spec_scale;
fs_params.ambient_color = lc.ambient_color.component;
fs_params.is_preview = preview_mode;
fs_params.rdm_tint = lc.rdm_tint.component;
fs_params.rdm_diff_saturation = worldConf.rdmDiffSaturation;
fs_params.indirect_tint = lc.indirect_tint.component;
fs_params.sh_enabled = ifx (chunk != null && chunk.sh_valid && !in_reflection_pass) then cast(s32)1 else cast(s32)0;
sg_apply_bindings(*bindings);
@ -263,6 +256,73 @@ backend_draw_trile_positions_main :: (trile : string, amount : s32, worldConf: *
sg_draw(0, cast(s32) trilegfx.vertex_count, amount);
end_frame_profiling_group("Draw trile positions");
}
backend_add_trile_rdm_position :: (position: Vector4) {
pos := position;
offset := sg_append_buffer(gPipelines.trile_rdm.bind.vertex_buffers[3], *(sg_range.{
ptr = *pos,
size = size_of(Vector4),
}));
array_add(*trile_rdm_offsets, offset);
}
backend_draw_trile_rdm :: (trile: string, worldConf: *World_Config, atlas_rect: Vector4, offset_index: s32) {
if offset_index >= trile_rdm_offsets.count then return;
trilegfx := get_trile_gfx(trile);
offset := trile_rdm_offsets[offset_index];
mvp := create_viewproj(*camera);
vs_params : Trile_Rdm_Vs_Params;
vs_params.mvp = mvp.floats;
vs_params.mvp_shadow = shadow_mvp.floats;
vs_params.camera = camera.position.component;
sg_apply_pipeline(gPipelines.trile_rdm.pipeline);
world_conf : Trile_Rdm_World_Config;
world_config_to_shader_type(worldConf, *world_conf);
world_conf.planeHeight = effective_plane_height(worldConf);
bindings : sg_bindings;
bindings.vertex_buffers[0] = trilegfx.vertex_buffer;
bindings.vertex_buffers[1] = trilegfx.normal_buffer;
bindings.vertex_buffers[2] = trilegfx.centre_buffer;
bindings.vertex_buffers[3] = gPipelines.trile_rdm.bind.vertex_buffers[3];
bindings.vertex_buffer_offsets[3] = offset;
bindings.samplers[SMP_rdm_trilesmp] = gPipelines.trile_rdm.bind.samplers[SMP_rdm_trilesmp];
bindings.samplers[SMP_rdm_shadowsmp] = g_shadowmap_sampler;
bindings.samplers[SMP_rdm_linsmp] = gPipelines.trile_rdm.bind.samplers[SMP_rdm_linsmp];
bindings.images[IMG_rdm_triletex] = trilegfx.trixel_colors;
bindings.images[IMG_rdm_ssaotex] = g_ssaobuf;
bindings.images[IMG_rdm_shadowtex] = g_shadowmap;
bindings.images[IMG_rdm_brdflut] = g_brdf_lut;
bindings.images[IMG_rdm_shirradiance] = g_sh_irradiance;
bindings.images[IMG_rdm_atlas] = g_rdm_atlas;
curworld := get_current_world();
fs_params : Trile_Rdm_Fs_Params;
fs_params.mvp_shadow = shadow_mvp.floats;
fs_params.is_reflection = 0;
w, h := get_render_size();
fs_params.screen_w = w;
fs_params.screen_h = h;
lc := *current_lighting_config;
fs_params.ambient_intensity = lc.ambient_intensity;
fs_params.emissive_scale = lc.emissive_scale;
fs_params.indirect_diff_scale = lc.indirect_diff_scale;
fs_params.indirect_spec_scale = lc.indirect_spec_scale;
fs_params.ambient_color = lc.ambient_color.component;
fs_params.is_preview = 0;
fs_params.indirect_tint = lc.indirect_tint.component;
fs_params.sh_enabled = ifx curworld.valid then cast(s32)1 else cast(s32)0;
fs_params.atlas_rect = atlas_rect.component;
sg_apply_bindings(*bindings);
sg_apply_uniforms(UB_trile_rdm_fs_params, *(sg_range.{ ptr = *fs_params, size = size_of(type_of(fs_params)) }));
sg_apply_uniforms(UB_trile_rdm_vs_params, *(sg_range.{ ptr = *vs_params, size = size_of(type_of(vs_params)) }));
sg_apply_uniforms(UB_trile_rdm_world_config, *(sg_range.{ ptr = *world_conf, size = size_of(type_of(world_conf)) }));
sg_draw(0, cast(s32) trilegfx.vertex_count, 1);
}
backend_draw_sky :: (wc: *World_Config) {
mvp := create_viewproj(*camera);
vs_params : Sky_Vs_Params;
@ -370,23 +430,27 @@ backend_gbuffer_draw_billboard :: (position: Vector3, anim: *Animation, frame_id
sg_draw(0, 6, 1);
}
backend_update_particle_buffers :: (cmd: *Render_Command_Update_Particles) {
if cmd.total_count <= 0 then return;
byte_size := cast(u64)(cmd.total_count * size_of(Vector4));
inst_buf1 := gPipelines.particle_additive.bind.vertex_buffers[1];
inst_buf2 := gPipelines.particle_additive.bind.vertex_buffers[2];
inst_buf3 := gPipelines.particle_additive.bind.vertex_buffers[3];
sg_update_buffer(inst_buf1, *(sg_range.{ ptr = cmd.pos_size.data, size = byte_size }));
sg_update_buffer(inst_buf2, *(sg_range.{ ptr = cmd.uv_rects.data, size = byte_size }));
sg_update_buffer(inst_buf3, *(sg_range.{ ptr = cmd.colors.data, size = byte_size }));
}
backend_draw_particles :: (cmd: *Render_Command_Draw_Particles) {
if cmd.count <= 0 then return;
pip := ifx cmd.blend_mode == .ALPHA then *gPipelines.particle_alpha else *gPipelines.particle_additive;
sg_update_buffer(pip.bind.vertex_buffers[1], *(sg_range.{
ptr = cmd.pos_size.data,
size = cast(u64)(cmd.count * size_of(Vector4)),
}));
sg_update_buffer(pip.bind.vertex_buffers[2], *(sg_range.{
ptr = cmd.uv_rects.data,
size = cast(u64)(cmd.count * size_of(Vector4)),
}));
sg_update_buffer(pip.bind.vertex_buffers[3], *(sg_range.{
ptr = cmd.colors.data,
size = cast(u64)(cmd.count * size_of(Vector4)),
}));
byte_offset := cmd.instance_offset * size_of(Vector4);
bind := pip.bind;
bind.vertex_buffer_offsets[1] = byte_offset;
bind.vertex_buffer_offsets[2] = byte_offset;
bind.vertex_buffer_offsets[3] = byte_offset;
mvp := create_viewproj(*camera);
vs_params : Particle_Vs_Params;
@ -394,8 +458,8 @@ backend_draw_particles :: (cmd: *Render_Command_Draw_Particles) {
vs_params.cam = camera.position.component;
sg_apply_pipeline(pip.pipeline);
pip.bind.images[IMG_particle_sprite] = cmd.sheet;
sg_apply_bindings(*pip.bind);
bind.images[IMG_particle_sprite] = cmd.sheet;
sg_apply_bindings(*bind);
sg_apply_uniforms(UB_particle_vs_params, *(sg_range.{ ptr = *vs_params, size = size_of(type_of(vs_params)) }));
sg_draw(0, 6, cmd.count);
}
@ -421,6 +485,39 @@ backend_draw_ground_gbuf :: (wc: *World_Config) {
sg_draw(0, 6, 1);
}
draw_sh_irradiance_pass :: () {
curworld := get_current_world();
if !curworld.valid then return;
sg_begin_pass(*(sg_pass.{ action = state.pass_action_clear, attachments = g_sh_irradiance_attach }));
sg_apply_pipeline(gPipelines.sh_irradiance.pipeline);
view := create_lookat(*camera);
inv_view := isometry_inverse(view);
lc := *current_lighting_config;
params : Sh_Deferred_Params;
params.inv_view = inv_view.floats;
params.ambient = .[lc.ambient_color.x, lc.ambient_color.y, lc.ambient_color.z, lc.ambient_intensity];
for *chunk: curworld.world.chunks {
if !chunk.sh_valid then continue;
gPipelines.sh_irradiance.bind.images[0] = g_gbuf_worldpos;
gPipelines.sh_irradiance.bind.images[1] = g_gbuf_normal;
gPipelines.sh_irradiance.bind.images[2] = chunk.sh_probe_grid;
sg_apply_bindings(*gPipelines.sh_irradiance.bind);
ck := chunk.coord;
params.chunk_origin = .[cast(float)(ck.x * 32), cast(float)(ck.y * 32), cast(float)(ck.z * 32), 0];
sg_apply_uniforms(UB_sh_deferred_params, *(sg_range.{ ptr = *params, size = size_of(type_of(params)) }));
sg_draw(0, 6, 1);
}
sg_end_pass();
}
backend_process_command_buckets :: () {
// 1. Set up textures and buffers.
start_frame_profiling_group("Setup");
@ -486,6 +583,10 @@ backend_process_command_buckets :: () {
sg_end_pass();
end_frame_profiling_group("SSAO pass");
start_frame_profiling_group("SH irradiance pass");
draw_sh_irradiance_pass();
end_frame_profiling_group("SH irradiance pass");
// 5. Main pass
start_frame_profiling_group("Main pass");
sg_begin_pass(*(sg_pass.{ action = state.pass_action_clear, attachments = g_rendertex_attachments}));
@ -554,5 +655,6 @@ backend_process_command_buckets :: () {
array_reset_keeping_memory(*render_command_buckets.gbuffer);
array_reset_keeping_memory(*render_command_buckets.ui);
array_reset_keeping_memory(*trile_offsets);
array_reset_keeping_memory(*trile_rdm_offsets);
current_world_config = null;
}

View File

@ -18,6 +18,11 @@ Gathered_Positions :: struct {
positions: [..]Vector4;
}
Gathered_Rdm_Position :: struct {
name: string;
position: Vector4;
}
extract_frustum_planes :: (mvp: Matrix4) -> [6]Vector4 {
planes : [6]Vector4;
m := mvp;
@ -40,7 +45,7 @@ aabb_in_frustum :: (planes: [6]Vector4, bmin: Vector3, bmax: Vector3) -> bool {
return true;
}
create_world_rendering_tasks :: (world: *World, camera: Camera) {
create_world_rendering_tasks :: (world: *World, camera: Camera, plane_height: float = 0) {
create_sky_rendering_task(*world.conf);
create_set_light_rendering_task(*world.conf);
@ -49,12 +54,24 @@ create_world_rendering_tasks :: (world: *World, camera: Camera) {
cam_planes := extract_frustum_planes(cam_mvp);
shadow_planes := extract_frustum_planes(shadow_mvp);
reflect_cam := camera;
reflect_cam.position *= .{1, -1, 1};
reflect_cam.position.y += plane_height * 2;
reflect_cam.target *= .{1, -1, 1};
reflect_cam.target.y += plane_height * 2;
reflect_mvp := create_viewproj(*reflect_cam);
reflect_planes := extract_frustum_planes(reflect_mvp);
// Gather positions for camera-visible instances (all passes) and
// shadow-only instances (chunks visible from sun but not camera).
gathered : [..]Gathered_Positions;
gathered.allocator = temp;
shad_gathered : [..]Gathered_Positions;
shad_gathered.allocator = temp;
rdm_extra : [..]Gathered_Positions; // RDM-flagged instances visible to camera; shadow/gbuffer/reflection use base pipeline
rdm_extra.allocator = temp;
rdm_main : [..]Gathered_Rdm_Position; // one entry per RDM-flagged instance for the main pass
rdm_main.allocator = temp;
find_or_create :: (list: *[..]Gathered_Positions, name: string, chunk_key: Chunk_Key) -> *Gathered_Positions {
for *g: list.* {
@ -71,8 +88,9 @@ create_world_rendering_tasks :: (world: *World, camera: Camera) {
bmax := bmin + .{32, 32, 32};
in_cam := aabb_in_frustum(cam_planes, bmin, bmax);
in_reflect := aabb_in_frustum(reflect_planes, bmin, bmax);
in_shad := aabb_in_frustum(shadow_planes, bmin, bmax);
if !in_cam && !in_shad continue;
if !in_cam && !in_reflect && !in_shad continue;
for group: chunk.groups {
for inst: group.instances {
@ -81,13 +99,23 @@ create_world_rendering_tasks :: (world: *World, camera: Camera) {
imax := imin + .{1, 1, 1};
inst_cam := in_cam && aabb_in_frustum(cam_planes, imin, imax);
inst_reflect := in_reflect && aabb_in_frustum(reflect_planes, imin, imax);
inst_shad := in_shad && aabb_in_frustum(shadow_planes, imin, imax);
if !inst_cam && !inst_shad continue;
if !inst_cam && !inst_reflect && !inst_shad continue;
pos := Vector4.{cast(float)wx, cast(float)wy, cast(float)wz, cast(float)inst.orientation};
is_rdm := is_rdm_instance_enabled(world, wx, wy, wz);
if inst_cam || inst_reflect {
if is_rdm {
target := find_or_create(*rdm_extra, group.trile_name, chunk.coord);
array_add(*target.positions, pos);
if inst_cam {
array_add(*rdm_main, .{name = group.trile_name, position = pos});
}
} else {
target := find_or_create(*gathered, group.trile_name, chunk.coord);
array_add(*target.positions, pos);
}
} else {
target := find_or_create(*shad_gathered, group.trile_name, chunk.coord);
array_add(*target.positions, pos);
@ -115,6 +143,26 @@ create_world_rendering_tasks :: (world: *World, camera: Camera) {
triletask.shadow_only = true;
add_rendering_task(triletask);
}
for g: rdm_extra {
if g.positions.count < 1 continue;
triletask : Rendering_Task_Trile;
triletask.trile = g.name;
triletask.chunk_key = g.chunk_key;
triletask.positions = g.positions;
triletask.worldConf = *world.conf;
triletask.skip_main = true;
add_rendering_task(triletask);
}
for r: rdm_main {
rect, found := rdm_get_atlas_rect(world, cast(s32) r.position.x, cast(s32) r.position.y, cast(s32) r.position.z);
if !found then continue;
rdmtask : Rendering_Task_Trile_RDM;
rdmtask.trile = r.name;
rdmtask.position = r.position;
rdmtask.atlas_rect = rect;
rdmtask.worldConf = *world.conf;
add_rendering_task(rdmtask);
}
create_ground_rendering_task(world);
}

View File

@ -0,0 +1,34 @@
Trile_Side :: enum_flags u8 {
TOP :: 0x1; // Larger Y
LEFT :: 0x2; // Larger Z
RIGHT :: 0x4; // Smaller Z
FRONT :: 0x8; // Larger X
BACK :: 0x10; // Smaller X
BOTTOM :: 0x20; // Smaller Ys
}
trileSideValues : [6]Trile_Side = .[.TOP, .LEFT, .RIGHT, .FRONT, .BACK, .BOTTOM];
swap_xyz_for_side :: (x: int, y: int, z: int, Trile_Side) -> (int, int, int) {
}
sides_with_air_exposure :: (trileptr: *Trile, x: int, y: int, z: int) -> u8 {
res : u8 = 0;
if trileptr.trixels[x][y][z].empty then return res;
if y == 15 || trileptr.trixels[x][y+1][z].empty then res |= xx Trile_Side.TOP;
if y == 0 || trileptr.trixels[x][y-1][z].empty then res |= xx Trile_Side.BOTTOM;
if z == 15 || trileptr.trixels[x][y][z+1].empty then res |= xx Trile_Side.LEFT;
if z == 0 || trileptr.trixels[x][y][z-1].empty then res |= xx Trile_Side.RIGHT;
if x == 15 || trileptr.trixels[x+1][y][z].empty then res |= xx Trile_Side.FRONT;
if x == 0 || trileptr.trixels[x-1][y][z].empty then res |= xx Trile_Side.BACK;
return res;
}
meshgen :: (trile: *Trile) {
for trileSideValues {
}
}

View File

@ -1,4 +1,4 @@
SHADOWMAP_SIZE :: 1000;
SHADOWMAP_SIZE :: 1500;
Pipeline_Binding :: struct {
pipeline : sg_pipeline;
@ -10,6 +10,7 @@ Pipeline_Binding :: struct {
g_specular_lut : sg_image;
g_brdf_lut : sg_image;
g_rdm_fallback : sg_image; // 1x1 black image used when a chunk has no baked RDM data
g_rdm_atlas : sg_image; // global RGBA16F atlas holding RDM hemispheres for flagged instances (populated in Steps 5-6)
g_sh_fallback : sg_image; // 1x1 black 2D image used when a chunk has no SH probe grid
g_shadowmap : sg_image;
@ -23,6 +24,7 @@ g_rendertex_attachments : sg_attachments;
g_gbuf_position : sg_image;
g_gbuf_normal : sg_image;
g_gbuf_worldpos : sg_image;
g_gbuf_depth : sg_image;
g_gbuf_attachments : sg_attachments;
@ -31,6 +33,9 @@ g_ssao_noise_buf : sg_image;
g_ssaobuf_depth : sg_image;
g_ssao_attachments : sg_attachments;
g_sh_irradiance : sg_image;
g_sh_irradiance_attach : sg_attachments;
g_postprocess_a : sg_image;
g_postprocess_b : sg_image;
g_postprocess_a_depth : sg_image;
@ -63,6 +68,9 @@ gPipelines : struct {
// Renders sets of triles
trile : Pipeline_Binding;
// Per-instance variant for triles flagged with sharp-specular RDM lookup
trile_rdm : Pipeline_Binding;
// Depth-only shadow pass for triles (no lighting/RDM)
trile_shadow : Pipeline_Binding;
@ -88,6 +96,8 @@ gPipelines : struct {
ssao: Pipeline_Binding;
sh_irradiance: Pipeline_Binding;
debugline : Pipeline_Binding;
}
@ -102,7 +112,7 @@ create_final_image :: () {
img_desc := sg_image_desc.{
width = w,
height = h,
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
render_target = true,
};
depth_desc := sg_image_desc.{
@ -137,7 +147,7 @@ create_shadowmap_image :: () {
img_desc := sg_image_desc.{
width = w,
height = h,
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
render_target = true,
};
g_shadowmap = sg_make_image(*depth_desc);
@ -157,6 +167,7 @@ create_pipelines :: () {
create_arbtri_pipeline();
create_trixel_pipeline();
create_trile_pipeline();
create_trile_rdm_pipeline();
create_trile_shadow_pipeline();
create_sky_pipeline();
create_plane_pipeline();
@ -174,6 +185,8 @@ create_pipelines :: () {
create_shadowmap_image();
create_final_image();
create_ssao_images();
create_sh_irradiance_pipeline();
create_sh_irradiance_image();
create_gbuffer_impostors();
}
@ -182,6 +195,7 @@ create_gbuffer_images :: () {
if g_gbuf_position.id != INVALID_ID then sg_destroy_image(g_gbuf_position);
if g_gbuf_normal.id != INVALID_ID then sg_destroy_image(g_gbuf_normal);
if g_gbuf_worldpos.id != INVALID_ID then sg_destroy_image(g_gbuf_worldpos);
if g_gbuf_depth.id != INVALID_ID then sg_destroy_image(g_gbuf_depth);
img_desc := sg_image_desc.{
@ -199,12 +213,14 @@ create_gbuffer_images :: () {
g_gbuf_position = sg_make_image(*img_desc);
g_gbuf_normal = sg_make_image(*img_desc);
g_gbuf_depth = sg_make_image(*depth_desc);
g_gbuf_worldpos = sg_make_image(*img_desc);
g_gbuf_depth = sg_make_image(*depth_desc);
attachmentsDesc : sg_attachments_desc;
attachmentsDesc = .{
colors[0].image = g_gbuf_position,
colors[1].image = g_gbuf_normal,
colors[2].image = g_gbuf_worldpos,
depth_stencil.image = g_gbuf_depth,
};
sg_destroy_attachments(g_gbuf_attachments);
@ -241,7 +257,7 @@ create_trixel_pipeline :: () {
};
color_state := sg_color_target_state.{
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
blend = .{
enabled = true,
src_factor_rgb = .SRC_ALPHA,
@ -369,9 +385,10 @@ create_gbuffer_pipeline :: () {
};
pipeline.color_count = 2;
pipeline.color_count = 3;
pipeline.colors[0] = color_state_pos;
pipeline.colors[1] = color_state_normal;
pipeline.colors[2] = .{ pixel_format = .RGBA16F };
gPipelines.gbuffer.pipeline = sg_make_pipeline(*pipeline);
gPipelines.gbuffer.bind.samplers[0] = sg_make_sampler(*(sg_sampler_desc.{
@ -405,7 +422,7 @@ create_trile_pipeline :: () {
};
color_state := sg_color_target_state.{
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
blend = .{
enabled = true,
src_factor_rgb = .SRC_ALPHA,
@ -433,6 +450,52 @@ create_trile_pipeline :: () {
}));
}
create_trile_rdm_pipeline :: () {
pipeline: sg_pipeline_desc;
shader_desc := trile_rdm_shader_desc(sg_query_backend());
pipeline.shader = sg_make_shader(*shader_desc);
pipeline.layout.buffers[0].stride = 4*3;
pipeline.layout.buffers[1].stride = 4*3;
pipeline.layout.buffers[3].step_func = .PER_INSTANCE;
instance_buffer := sg_buffer_desc.{ usage = .STREAM, size = 16 * 64 };
pipeline.layout.attrs[ATTR_trile_rdm_position] = .{ format = .FLOAT3, buffer_index = 0 };
pipeline.layout.attrs[ATTR_trile_rdm_normal] = .{ format = .FLOAT3, buffer_index = 1 };
pipeline.layout.attrs[ATTR_trile_rdm_centre] = .{ format = .FLOAT3, buffer_index = 2 };
pipeline.layout.attrs[ATTR_trile_rdm_instance] = .{ format = .FLOAT4, buffer_index = 3 };
pipeline.depth = .{
write_enabled = true,
compare = .LESS_EQUAL,
pixel_format = .DEPTH,
};
pipeline.color_count = 1;
pipeline.colors[0] = sg_color_target_state.{
pixel_format = .RGBA16F,
blend = .{
enabled = true,
src_factor_rgb = .SRC_ALPHA,
dst_factor_rgb = .ONE_MINUS_SRC_ALPHA,
},
};
gPipelines.trile_rdm.pipeline = sg_make_pipeline(*pipeline);
gPipelines.trile_rdm.bind.samplers[0] = sg_make_sampler(*(sg_sampler_desc.{
wrap_u = .CLAMP_TO_EDGE,
wrap_v = .CLAMP_TO_EDGE,
min_filter = .NEAREST,
mag_filter = .NEAREST,
}));
gPipelines.trile_rdm.bind.vertex_buffers[3] = sg_make_buffer(*instance_buffer);
gPipelines.trile_rdm.bind.samplers[3] = sg_make_sampler(*(sg_sampler_desc.{
wrap_u = .CLAMP_TO_EDGE,
wrap_v = .CLAMP_TO_EDGE,
min_filter = .LINEAR,
mag_filter = .LINEAR,
}));
}
create_trile_shadow_pipeline :: () {
pipeline: sg_pipeline_desc;
shader_desc := trile_shadow_shader_desc(sg_query_backend());
@ -451,7 +514,7 @@ create_trile_shadow_pipeline :: () {
pixel_format = .DEPTH,
};
pipeline.color_count = 1;
pipeline.colors[0].pixel_format = .RGBA32F;
pipeline.colors[0].pixel_format = .RGBA16F;
gPipelines.trile_shadow.pipeline = sg_make_pipeline(*pipeline);
}
@ -470,7 +533,7 @@ create_sky_pipeline :: () {
};
color_state := sg_color_target_state.{
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
blend = .{
enabled = true,
src_factor_rgb = .SRC_ALPHA,
@ -545,7 +608,7 @@ create_plane_pipeline_reflection_images :: () {
img_desc := sg_image_desc.{
width = w/3,
height = h/3,
pixel_format = .RGBA8,
pixel_format = .RGBA16F,
render_target = true,
};
depth_desc := sg_image_desc.{
@ -585,7 +648,7 @@ create_plane_pipeline :: () {
};
color_state := sg_color_target_state.{
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
blend = .{
enabled = true,
src_factor_rgb = .SRC_ALPHA,
@ -785,7 +848,7 @@ create_op_pipeline :: () {
src_factor_rgb = .SRC_ALPHA,
dst_factor_rgb = .ONE_MINUS_SRC_ALPHA
},
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
};
pipeline.depth = .{
write_enabled = true,
@ -845,7 +908,7 @@ create_billboard_pipeline :: () {
};
color_state := sg_color_target_state.{
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
blend = .{
enabled = true,
src_factor_rgb = .SRC_ALPHA,
@ -907,9 +970,10 @@ create_gbuffer_billboard_pipeline :: () {
};
pipeline.color_count = 2;
pipeline.color_count = 3;
pipeline.colors[0] = color_state_pos;
pipeline.colors[1] = color_state_normal;
pipeline.colors[2] = .{ pixel_format = .RGBA16F };
vertices: [4]Vector3 = .[
.{ 0.0, 0.0, 0.0},
@ -923,10 +987,6 @@ create_gbuffer_billboard_pipeline :: () {
0, 2, 3,
];
pipeline.color_count = 2;
pipeline.colors[0] = color_state_pos;
pipeline.colors[1] = color_state_normal;
gPipelines.gbuffer_billboard.pipeline = sg_make_pipeline(*pipeline);
ibuffer := sg_buffer_desc.{ type = .INDEXBUFFER, data = .{ ptr = indices.data, size = 6 * 2 } };
@ -960,7 +1020,7 @@ create_mix_pipeline :: () {
src_factor_rgb = .SRC_ALPHA,
dst_factor_rgb = .ONE_MINUS_SRC_ALPHA
},
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
};
pipeline.depth = .{
write_enabled = true,
@ -1014,9 +1074,10 @@ create_bloom_pipeline :: () {
pipeline.layout.attrs[ATTR_bloom_position] = .{ format = .FLOAT2 };
pipeline.layout.attrs[ATTR_bloom_uv] = .{ format = .FLOAT2 };
pipeline.index_type = .UINT16;
pipeline.depth.pixel_format = .NONE;
pipeline.color_count = 1;
pipeline.colors[0] = .{ pixel_format = .RGBA32F };
pipeline.colors[0] = .{ pixel_format = .RGBA16F };
gPipelines.bloom.pipeline = sg_make_pipeline(*pipeline);
@ -1060,9 +1121,10 @@ create_dof_pipeline :: () {
pipeline.layout.attrs[ATTR_dof_position] = .{ format = .FLOAT2 };
pipeline.layout.attrs[ATTR_dof_uv] = .{ format = .FLOAT2 };
pipeline.index_type = .UINT16;
pipeline.depth.pixel_format = .NONE;
pipeline.color_count = 1;
pipeline.colors[0] = .{ pixel_format = .RGBA32F };
pipeline.colors[0] = .{ pixel_format = .RGBA16F };
gPipelines.dof.pipeline = sg_make_pipeline(*pipeline);
@ -1097,6 +1159,53 @@ create_dof_pipeline :: () {
}));
}
create_sh_irradiance_image :: () {
w, h := get_render_size();
if g_sh_irradiance.id != INVALID_ID then sg_destroy_image(g_sh_irradiance);
g_sh_irradiance = sg_make_image(*(sg_image_desc.{
width = w / 2,
height = h / 2,
pixel_format = .RGBA16F,
render_target = true,
sample_count = 1,
}));
sg_destroy_attachments(g_sh_irradiance_attach);
g_sh_irradiance_attach = sg_make_attachments(*(sg_attachments_desc.{
colors[0].image = g_sh_irradiance,
}));
}
create_sh_irradiance_pipeline :: () {
pipeline: sg_pipeline_desc;
shader_desc := sh_deferred_shader_desc(sg_query_backend());
pipeline.shader = sg_make_shader(*shader_desc);
pipeline.layout.attrs[ATTR_sh_deferred_position] = .{ format = .FLOAT2 };
pipeline.layout.attrs[ATTR_sh_deferred_uv] = .{ format = .FLOAT2 };
pipeline.index_type = .UINT16;
pipeline.depth.pixel_format = .NONE;
pipeline.color_count = 1;
pipeline.colors[0] = .{ pixel_format = .RGBA16F };
gPipelines.sh_irradiance.pipeline = sg_make_pipeline(*pipeline);
quad_vertices : [16]float = .[
-1.0, 1.0, 0.0, flip_if_plat(1.0),
-1.0, -1.0, 0.0, flip_if_plat(0.0),
1.0, -1.0, 1.0, flip_if_plat(0.0),
1.0, 1.0, 1.0, flip_if_plat(1.0),
];
quad_indices : [6]u16 = .[ 0, 1, 2, 0, 2, 3 ];
vbuffer := sg_buffer_desc.{ size = size_of(float) * 16, data = .{ ptr = quad_vertices.data, size = 16 * 4 }};
ibuffer := sg_buffer_desc.{ size = size_of(u16) * 6, data = .{ ptr = quad_indices.data, size = 6 * 2 }, type = .INDEXBUFFER };
gPipelines.sh_irradiance.bind.vertex_buffers[0] = sg_make_buffer(*vbuffer);
gPipelines.sh_irradiance.bind.index_buffer = sg_make_buffer(*ibuffer);
gPipelines.sh_irradiance.bind.samplers[0] = sg_make_sampler(*(sg_sampler_desc.{
wrap_u = .CLAMP_TO_EDGE,
wrap_v = .CLAMP_TO_EDGE,
min_filter = .NEAREST,
mag_filter = .NEAREST,
}));
}
create_ssao_images :: () {
if g_ssaobuf.id != INVALID_ID then sg_destroy_image(g_ssaobuf);
if g_ssaobuf_depth.id != INVALID_ID then sg_destroy_image(g_ssaobuf_depth);
@ -1109,7 +1218,7 @@ create_ssao_images :: () {
width = w/2,
height = h/2,
render_target = true,
pixel_format = .RGBA8
pixel_format = .RGBA16F
};
img_desc.sample_count = 1;
g_ssaobuf = sg_make_image(*img_desc);
@ -1152,7 +1261,7 @@ create_ssao_images :: () {
bloom_img_desc := sg_image_desc.{
width = w/8,
height = h/8,
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
render_target = true,
sample_count = 1,
};
@ -1167,7 +1276,7 @@ create_ssao_images :: () {
dof_img_desc := sg_image_desc.{
width = cast(s32)((cast(float)w)/1.5),
height = cast(s32)((cast(float)h)/1.5),
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
render_target = true,
sample_count = 1,
};
@ -1196,7 +1305,7 @@ create_ssao_pipeline :: () {
src_factor_rgb = .SRC_ALPHA,
dst_factor_rgb = .ONE_MINUS_SRC_ALPHA
},
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
};
pipeline.color_count = 1;
@ -1273,9 +1382,6 @@ init_brdf_lut :: () {
g_brdf_lut = sg_make_image(*desc);
}
// 1x1 black image for RDM slots when no baked data is present.
// The lookup texture returning all zeros makes atlas_rect.z == 0,
// so the shader's fallback ambient path is taken.
{
pixels : [4]u8 = .[0, 0, 0, 0];
imgdata : sg_image_data;
@ -1288,7 +1394,6 @@ init_brdf_lut :: () {
};
g_rdm_fallback = sg_make_image(*desc);
// 1x1 RGBA16F 2D texture — fallback when a chunk has no SH probe grid.
zero_sh : [4]u16 = .[0, 0, 0, 0];
sh_imgdata : sg_image_data;
sh_imgdata.subimage[0][0] = .{ zero_sh.data, size_of(type_of(zero_sh)) };
@ -1299,6 +1404,17 @@ init_brdf_lut :: () {
data = sh_imgdata,
};
g_sh_fallback = sg_make_image(*sh_desc);
zero_atlas : [4]u16 = .[0, 0, 0, 0];
atlas_imgdata : sg_image_data;
atlas_imgdata.subimage[0][0] = .{ zero_atlas.data, size_of(type_of(zero_atlas)) };
atlas_desc := sg_image_desc.{
width = 1,
height = 1,
pixel_format = .RGBA16F,
data = atlas_imgdata,
};
g_rdm_atlas = sg_make_image(*atlas_desc);
}
}
@ -1383,7 +1499,7 @@ create_particle_pipeline :: () {
pipeline.color_count = 1;
pipeline.colors[0] = .{
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
blend = .{
enabled = true,
src_factor_rgb = .SRC_ALPHA,
@ -1460,7 +1576,7 @@ create_debugline_pipeline :: () {
pixel_format = .DEPTH,
};
color_state := sg_color_target_state.{
pixel_format = .RGBA32F,
pixel_format = .RGBA16F,
};
pipeline.color_count = 1;
pipeline.colors[0] = color_state;

View File

@ -1,7 +1,7 @@
Post_Process :: struct {
exposure : float = 1.0; @Slider,0,3,0.1;
contrast : float = 1.0; @Slider,0,6,0.1;
saturation : float = 1.0; @Slider,0.0,2.0,0.1;
contrast : float = 1.0; @Slider,0,3,0.1;
saturation : float = 1.0; @Slider,0.0,5.0,0.1;
gamma : float = 1.0; @Slider,0.3,3.0,0.1;
tonemap : float = 0.0; @Slider,0,1,1;
ssao : float = 0.0; @Slider,0,5,0.1;
@ -32,13 +32,12 @@ Post_Process :: struct {
current_post_process : Post_Process;
Lighting_Config :: struct {
rdm_enabled : s32 = 1; @Slider,0,1,1
ambient_intensity : float = 0.35; @Slider,0,2,0.05
emissive_scale : float = 5.0; @Slider,0,20,0.5
rdm_diff_scale : float = 1.0; @Slider,0,3,0.1
rdm_spec_scale : float = 1.0; @Slider,0,3,0.1
indirect_diff_scale : float = 1.0; @Slider,0,3,0.1
indirect_spec_scale : float = 1.0; @Slider,0,3,0.1
ambient_color : Vector3 = .{0.3,0.3,0.4}; @Color
rdm_tint : Vector3 = .{1.05,1.0,0.9}; @Color
indirect_tint : Vector3 = .{1.05,1.0,0.9}; @Color
}
current_lighting_config : Lighting_Config;

View File

@ -32,6 +32,7 @@ on_window_resize :: () {
create_plane_pipeline_reflection_images();
create_final_image();
create_ssao_images();
create_sh_irradiance_image();
// Reset glyph cache so stale sizes from resizing don't fill the atlas.
w, h : s32;
fonsGetAtlasSize(state.fons, *w, *h);

View File

@ -58,7 +58,7 @@ create_shadow_viewproj :: (cam: *Camera, conf: *World_Config) -> Matrix4 {
B.z, C.z, A.z, 0,
-dot(B, sunCameraPosition), -dot(C, sunCameraPosition), -dot(A, sunCameraPosition), 1
};
proj := matrix_ortho(-60, 60, -60, 60, 0, 100);
proj := matrix_ortho(-30, 30, -30, 30, 0, 100);
return view*proj;
}

View File

@ -7,9 +7,11 @@ Rendering_Task_Type :: enum {
SET_CAMERA;
SET_LIGHT;
TRILE; // We need to add an ability to invalidate buffer instead of updating it constantly. Also probably have a buffer for static world triles and one for moving ones.
TRILE_RDM;
TRIXELS;
BILLBOARD;
PARTICLES;
PARTICLES_BUFFER;
};
Rendering_Task :: struct {
@ -59,6 +61,16 @@ Rendering_Task_Trile :: struct {
worldConf : *World_Config;
preview_mode : s32 = 0; // 0=normal, 1=add preview (blue), 2=delete preview (red)
shadow_only : bool = false; // only submit to shadow bucket (frustum-culled from camera)
skip_main : bool = false; // RDM-flagged instances still cast shadows / write gbuffer / reflect, but main pass uses the RDM pipeline
}
Rendering_Task_Trile_RDM :: struct {
#as using t : Rendering_Task;
t.type = .TRILE_RDM;
trile : string;
position : Vector4; // xyz=world position, w=orientation
atlas_rect : Vector4; // global atlas UV rect (zeros until Step 6 wires the manifest)
worldConf : *World_Config;
}
Rendering_Task_Trixels :: struct {
@ -76,11 +88,18 @@ Rendering_Task_Particles :: struct {
#as using t : Rendering_Task;
t.type = .PARTICLES;
count : s32;
instance_offset : s32;
blend_mode : Particle_Blend_Mode;
sheet : sg_image;
pos_size : [2048]Vector4;
uv_rects : [2048]Vector4;
colors : [2048]Vector4;
}
Rendering_Task_Particles_Buffer :: struct {
#as using t : Rendering_Task;
t.type = .PARTICLES_BUFFER;
total_count : s32;
pos_size : [MAX_PARTICLES]Vector4;
uv_rects : [MAX_PARTICLES]Vector4;
colors : [MAX_PARTICLES]Vector4;
}
Rendering_Task_Set_Camera :: struct {
@ -103,6 +122,7 @@ rendering_tasklist : [..]*Rendering_Task;
tasks_to_commands :: () {
trile_add_counter: s32 = 0;
trile_rdm_add_counter: s32 = 0;
for rendering_tasklist {
if it.type == {
case .SET_LIGHT;
@ -144,12 +164,28 @@ tasks_to_commands :: () {
array_add(*render_command_buckets.shadow, drawPositionsCmd);
} else if trileTask.preview_mode != 0 {
array_add(*render_command_buckets.main, drawPositionsCmd);
} else if trileTask.skip_main {
array_add(*render_command_buckets.reflection, drawPositionsCmd);
array_add(*render_command_buckets.gbuffer, drawPositionsCmd);
array_add(*render_command_buckets.shadow, drawPositionsCmd);
} else {
array_add(*render_command_buckets.reflection, drawPositionsCmd);
array_add(*render_command_buckets.main, drawPositionsCmd);
array_add(*render_command_buckets.gbuffer, drawPositionsCmd);
array_add(*render_command_buckets.shadow, drawPositionsCmd);
}
case .TRILE_RDM;
rdmTask := (cast(*Rendering_Task_Trile_RDM)it);
addCmd := New(Render_Command_Add_Trile_RDM_Position,, temp);
addCmd.position = rdmTask.position;
array_add(*render_command_buckets.setup, addCmd);
drawCmd := New(Render_Command_Draw_Trile_RDM,, temp);
drawCmd.trile = rdmTask.trile;
drawCmd.conf = rdmTask.worldConf;
drawCmd.atlas_rect = rdmTask.atlas_rect;
drawCmd.offset_index = trile_rdm_add_counter;
trile_rdm_add_counter += 1;
array_add(*render_command_buckets.main, drawCmd);
case .SKY;
command := New(Render_Command_Sky,, temp);
command.worldConfig = (cast(*Rendering_Task_Sky)it).worldConfig;
@ -173,15 +209,21 @@ tasks_to_commands :: () {
array_add(*render_command_buckets.main, commandDrawBillboard);
array_add(*render_command_buckets.shadow, commandDrawBillboard);
array_add(*render_command_buckets.reflection, commandDrawBillboard);
case .PARTICLES_BUFFER;
bufTask := cast(*Rendering_Task_Particles_Buffer)it;
uploadCmd := New(Render_Command_Update_Particles,, temp);
uploadCmd.total_count = bufTask.total_count;
memcpy(uploadCmd.pos_size.data, bufTask.pos_size.data, bufTask.total_count * size_of(Vector4));
memcpy(uploadCmd.uv_rects.data, bufTask.uv_rects.data, bufTask.total_count * size_of(Vector4));
memcpy(uploadCmd.colors.data, bufTask.colors.data, bufTask.total_count * size_of(Vector4));
array_add(*render_command_buckets.setup, uploadCmd);
case .PARTICLES;
particleTask := (cast(*Rendering_Task_Particles)it);
particleTask := cast(*Rendering_Task_Particles)it;
drawCmd := New(Render_Command_Draw_Particles,, temp);
drawCmd.count = particleTask.count;
drawCmd.instance_offset = particleTask.instance_offset;
drawCmd.blend_mode = particleTask.blend_mode;
drawCmd.sheet = particleTask.sheet;
memcpy(drawCmd.pos_size.data, particleTask.pos_size.data, particleTask.count * size_of(Vector4));
memcpy(drawCmd.uv_rects.data, particleTask.uv_rects.data, particleTask.count * size_of(Vector4));
memcpy(drawCmd.colors.data, particleTask.colors.data, particleTask.count * size_of(Vector4));
array_add(*render_command_buckets.main, drawCmd);
array_add(*render_command_buckets.reflection, drawCmd);
case .SET_CAMERA;

View File

@ -45,10 +45,18 @@ Blob :: struct {
g_settings : Settings_State;
g_settings_config : Settings_Menu_Config;
MAIN_ITEMS :: string.["Resume", "Settings", "Exit"];
#if OS == .WASM {
MAIN_ITEMS :: string.["Resume", "Settings"];
} else {
MAIN_ITEMS :: string.["Resume", "Settings", "Exit"];
}
SETTINGS_ITEMS :: string.["Audio", "Graphics"];
AUDIO_LABELS :: string.["Master Volume", "Music Volume", "Sound Effects"];
GRAPHICS_ITEMS :: string.["Fullscreen"];
#if OS == .WASM {
GRAPHICS_ITEMS :: string.[];
} else {
GRAPHICS_ITEMS :: string.["Fullscreen"];
}
page_items :: () -> []string {
if g_settings.page == .MAIN return MAIN_ITEMS;
@ -223,7 +231,7 @@ handle_enter :: (index: s32 = -1) {
if i == 0 then navigate_to(.AUDIO);
if i == 1 then navigate_to(.GRAPHICS);
case .GRAPHICS;
if i == 0 then sapp_toggle_fullscreen();
#if OS != .WASM { if i == 0 then sapp_toggle_fullscreen(); }
}
}
@ -422,8 +430,10 @@ draw_settings_menu :: () {
bt.label_theme.alignment = GR.Text_Alignment.Center;
label := items[i];
#if OS != .WASM {
if g_settings.page == .GRAPHICS && i == 0
label = tprint("Fullscreen: %", ifx sapp_is_fullscreen() then "On" else "Off");
}
pressed, _, _ := GR.button(r, label, *bt, identifier = cast(s64)i);
if pressed then handle_enter(cast(s32)i);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@ in vec4 instance;
out vec3 view_space_pos;
out vec3 view_space_normal;
out vec3 v_world_pos;
mat3 gbuf_rot_x(float a) { float c=cos(a),s=sin(a); return mat3(1,0,0, 0,c,-s, 0,s,c); }
mat3 gbuf_rot_z(float a) { float c=cos(a),s=sin(a); return mat3(c,-s,0, s,c,0, 0,0,1); }
@ -39,6 +40,7 @@ void main() {
gl_Position = mvp * world_pos;
view_space_pos = view_pos_4.xyz;
view_space_normal = mat3(view_matrix) * normal.xyz;
v_world_pos = world_pos.xyz;
} else {
int ori = int(round(instance.w));
mat3 rot = gbuf_get_orientation_matrix(ori);
@ -49,6 +51,7 @@ void main() {
gl_Position = mvp * world_pos;
view_space_pos = view_pos_4.xyz;
view_space_normal = mat3(view_matrix) * (rot * normal.xyz);
v_world_pos = world_pos.xyz;
}
}
@end
@ -57,13 +60,16 @@ void main() {
in vec3 view_space_pos;
in vec3 view_space_normal;
in vec3 v_world_pos;
layout(location=0) out vec4 out_position;
layout(location=1) out vec4 out_normal;
layout(location=2) out vec4 out_worldpos;
void main() {
out_position = vec4(view_space_pos, 1.0);
out_normal = vec4(normalize(view_space_normal), 1.0);
out_worldpos = vec4(v_world_pos, 1.0);
}
@end

View File

@ -0,0 +1,102 @@
@vs vs_sh_deferred
in vec2 position;
in vec2 uv;
out vec2 quad_uv;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
quad_uv = uv;
}
@end
@fs fs_sh_deferred
layout(binding=0) uniform texture2D gbuf_worldpos;
layout(binding=1) uniform texture2D gbuf_norm;
layout(binding=2) uniform texture2D sh_chunk;
layout(binding=0) uniform sampler sh_smp;
layout(binding=0) uniform sh_deferred_params {
mat4 inv_view;
vec4 chunk_origin;
vec4 ambient; // rgb = ambient color, a = ambient intensity
};
in vec2 quad_uv;
out vec4 frag_color;
const float PI = 3.14159265359;
vec3 sh_eval(ivec3 probe, vec3 N) {
int base = probe.x * 3;
int row = probe.z * 64 + probe.y;
vec4 t0 = texelFetch(sampler2D(sh_chunk, sh_smp), ivec2(base, row), 0);
vec4 t1 = texelFetch(sampler2D(sh_chunk, sh_smp), ivec2(base+1, row), 0);
vec4 t2 = texelFetch(sampler2D(sh_chunk, sh_smp), ivec2(base+2, row), 0);
float x = N.x, y = N.y, z = N.z;
float r = 0.886227*t0.x + 1.023327*(t0.w*x + t0.y*y + t0.z*z);
float g = 0.886227*t1.x + 1.023327*(t1.w*x + t1.y*y + t1.z*z);
float b = 0.886227*t2.x + 1.023327*(t2.w*x + t2.y*y + t2.z*z);
return max(vec3(r, g, b) / PI, vec3(0.0));
}
float sh_probe_energy(ivec3 probe) {
int base = probe.x * 3;
int row = probe.z * 64 + probe.y;
vec4 t0 = texelFetch(sampler2D(sh_chunk, sh_smp), ivec2(base, row), 0);
vec4 t1 = texelFetch(sampler2D(sh_chunk, sh_smp), ivec2(base+1, row), 0);
vec4 t2 = texelFetch(sampler2D(sh_chunk, sh_smp), ivec2(base+2, row), 0);
return max(0.886227 * (t0.x + t1.x + t2.x), 0.0);
}
vec3 sh_eval_trilinear(ivec3 p0, ivec3 p1, vec3 t, vec3 N) {
float wx[2] = float[2](1.0 - t.x, t.x);
float wy[2] = float[2](1.0 - t.y, t.y);
float wz[2] = float[2](1.0 - t.z, t.z);
vec3 result = vec3(0.0);
vec3 unweighted = vec3(0.0);
float total_w = 0.0;
for (int iz = 0; iz < 2; iz++) {
for (int iy = 0; iy < 2; iy++) {
for (int ix = 0; ix < 2; ix++) {
ivec3 probe = ivec3(
ix == 0 ? p0.x : p1.x,
iy == 0 ? p0.y : p1.y,
iz == 0 ? p0.z : p1.z
);
vec3 sh = sh_eval(probe, N);
float triw = wx[ix] * wy[iy] * wz[iz];
float w = triw * sh_probe_energy(probe);
result += sh * w;
unweighted += sh * triw;
total_w += w;
}
}
}
vec3 amb = ambient.rgb * ambient.a;
return total_w > 0.001 ? result / total_w : max(unweighted, amb);
}
void main() {
vec4 wp_sample = texture(sampler2D(gbuf_worldpos, sh_smp), quad_uv);
if (wp_sample.a < 0.5) discard;
vec3 world_pos = wp_sample.xyz;
vec3 cmin = chunk_origin.xyz;
vec3 cmax = cmin + vec3(32.0);
if (any(lessThan(world_pos, cmin)) || any(greaterThanEqual(world_pos, cmax))) discard;
vec3 view_norm = normalize(texture(sampler2D(gbuf_norm, sh_smp), quad_uv).xyz);
vec3 world_norm = normalize(mat3(inv_view) * view_norm);
const float SH_PAD = 2.0;
const float SH_SPACING = (32.0 + 2.0 * SH_PAD) / 64.0;
vec3 probe_f = clamp((world_pos - (cmin - vec3(SH_PAD))) / SH_SPACING, vec3(0.0), vec3(63.0));
ivec3 p0 = ivec3(floor(probe_f));
ivec3 p1 = min(p0 + ivec3(1), ivec3(63));
frag_color = vec4(sh_eval_trilinear(p0, p1, fract(probe_f), world_norm), 1.0);
}
@end
@program sh_deferred vs_sh_deferred fs_sh_deferred

View File

@ -17,7 +17,6 @@ out vec3 vpos;
out vec3 ipos;
out vec4 fnormal;
out vec3 orig_normal;
out vec3 trileCenter;
out vec3 cv;
mat3 rot_x(float a) { float c=cos(a),s=sin(a); return mat3(1,0,0, 0,c,-s, 0,s,c); }
@ -52,7 +51,6 @@ void main() {
ipos = position.xyz;
cam = camera;
cv = normalize(camera - vpos);
trileCenter = instance.xyz + vec3(0.5);
}
@end
@ -86,7 +84,6 @@ in vec3 vpos;
in vec3 ipos;
in vec4 fnormal;
in vec3 orig_normal;
in vec3 trileCenter;
in vec3 cv;
out vec4 frag_color;
@ -95,36 +92,27 @@ layout(binding=3) uniform trile_fs_params {
int is_reflection;
int screen_h;
int screen_w;
int rdm_enabled;
float ambient_intensity;
float emissive_scale;
float rdm_diff_scale;
float rdm_spec_scale;
float indirect_diff_scale;
float indirect_spec_scale;
vec3 ambient_color;
int is_preview;
vec3 rdm_tint;
float rdm_diff_saturation;
vec3 indirect_tint;
int sh_enabled;
};
layout(binding = 0) uniform texture2D triletex;
layout(binding = 0) uniform sampler trilesmp;
layout(binding = 1) uniform texture2D ssaotex;
layout(binding = 1) uniform sampler ssaosmp;
layout(binding = 2) uniform texture2D shadowtex;
layout(binding = 2) uniform sampler shadowsmp;
layout(binding = 3) uniform texture2D rdm_lookup;
layout(binding = 4) uniform texture2D rdm_atlas;
layout(binding = 5) uniform texture2D brdf_lut;
layout(binding = 6) uniform texture2D sh_chunk;
layout(binding = 3) uniform sampler rdmsmp;
layout(binding = 3) uniform texture2D brdf_lut;
layout(binding = 4) uniform texture2D sh_irradiance;
layout(binding = 3) uniform sampler linsmp;
const float PI = 3.1415927;
const float ROUGHNESS_RAYMARCH_MAX = 0.2; // Below this roughness, actually try to get sharp reflection from RDM.
const float ROUGHNESS_SPEC_CUTOFF = 0.7; // Above this roughness, disregard RDM specular lighting entirely.
// ---- SKY ----
const float ROUGHNESS_SPEC_CUTOFF = 0.7;
const float cirrus = 0.5;
@ -164,8 +152,6 @@ vec3 sky_reflect(vec3 R, vec3 sunpos) {
return sky(R, sunpos);
}
// ---- PBR ----
float DistributionGGX(vec3 N, vec3 H, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
@ -192,222 +178,9 @@ vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
// ---- RDM HELPERS ----
// Hemioct encoding (Cigolle2014)
vec2 rdm_hemioct(vec3 v, int face) {
vec3 vc = v;
if (face / 2 == 0) { vc.z = v.y; vc.y = v.z; }
if (face / 2 == 2) { vc.z = v.x; vc.x = v.z; }
if (face % 2 == 1) { vc.z *= -1.0; }
vec2 p = vc.xy * (1.0 / (abs(vc.x) + abs(vc.y) + vc.z));
return vec2(p.x + p.y, p.x - p.y) * 0.5 + 0.5;
}
int rdm_face_from_normal(vec3 N) {
vec3 a = abs(N);
if (a.y >= a.x && a.y >= a.z) return N.y >= 0.0 ? 0 : 1;
if (a.z >= a.x && a.z >= a.y) return N.z >= 0.0 ? 2 : 3;
return N.x >= 0.0 ? 4 : 5;
}
vec4 rdm_atlas_rect(ivec3 local_pos, int roughness) {
int rdm_index = local_pos.x + local_pos.y * 32 + local_pos.z * 1024 + roughness * 32768;
return texelFetch(sampler2D(rdm_lookup, trilesmp), ivec2(rdm_index % 512, rdm_index / 512), 0);
}
ivec2 rdm_face_offset(vec4 rect, int face, int rdmSize, ivec2 atlasSize) {
int col = face % 2;
int row = face / 2;
return ivec2(int(rect.x * float(atlasSize.x)) + col * rdmSize,
int(rect.y * float(atlasSize.y)) + row * rdmSize);
}
vec2 rdm_face_uv(int face) {
if (face <= 1) return vec2(ipos.x, ipos.z);
if (face <= 3) return vec2(ipos.x, ipos.y);
return vec2(ipos.z, ipos.y);
}
// ---- RDM SPECULAR ----
vec3 rdm_spec_raymarch(vec3 N, vec3 V, vec3 diff, int face, ivec2 faceOffset, int rdmSize, vec2 atlasInvSize) {
vec3 reflected = reflect(V, N);
float maxDist = 20.0;
int steps = 40;
float stepSize = maxDist / float(steps);
for (int i = 0; i < steps; i++) {
float t = stepSize * float(i + 1);
vec3 samplePos = diff + t * reflected;
if (dot(samplePos, N) < 0.0) continue;
vec3 dir = normalize(samplePos);
vec2 hemiUV = rdm_hemioct(dir, face);
vec2 texCoord = (vec2(faceOffset) + hemiUV * float(rdmSize)) * atlasInvSize;
vec4 s = texture(sampler2D(rdm_atlas, rdmsmp), texCoord, 0);
float dist = length(samplePos);
if (s.a > 0.0 && s.a < dist && s.a + stepSize > dist)
return s.rgb;
}
return sky_reflect(reflected, sunPosition);
}
vec3 rdm_spec_single(vec3 N, vec3 V, vec3 diff, int face, ivec2 faceOffset, int rdmSize, vec2 atlasInvSize) {
vec3 reflected = reflect(V, N);
vec3 sampleDir = normalize(diff + 2.0 * reflected);
vec2 hemiUV = rdm_hemioct(sampleDir, face);
vec2 texCoord = (vec2(faceOffset) + hemiUV * float(rdmSize)) * atlasInvSize;
return texture(sampler2D(rdm_atlas, rdmsmp), texCoord).rgb;
}
vec3 rdm_sample_diff_probe(vec3 N, ivec3 local_pos, vec3 fallback) {
vec4 rect = rdm_atlas_rect(local_pos, 7);
if (rect.z <= 0.0) return fallback;
int face = rdm_face_from_normal(N);
int rdmSize = int(pow(2.0, float((7 - 7) + 1)));
ivec2 atlasSize = textureSize(sampler2D(rdm_atlas, rdmsmp), 0);
ivec2 fOff = rdm_face_offset(rect, face, rdmSize, atlasSize);
vec2 pos = rdm_hemioct(N, face);
return texelFetch(sampler2D(rdm_atlas, rdmsmp),
ivec2(fOff.x + int(pos.x * float(rdmSize)),
fOff.y + int(pos.y * float(rdmSize))), 0).rgb;
}
int isign(float f) { return f < 0.0 ? -1 : 1; }
vec3 smix(vec3 a, vec3 b, float t) {
float power = 1.6;
float st = pow(t, power) / (pow(t, power) + pow(1.0 - t, power));
return mix(a, b, st);
}
vec3 rdm_indirect_diffuse(vec3 N, vec3 diff, ivec3 local_pos) {
int face = rdm_face_from_normal(N);
vec3 ambient = vec3(0.3, 0.3, 0.4);
vec2 delta;
if (face <= 1) delta = vec2(diff.x, diff.z);
else if (face <= 3) delta = vec2(diff.x, diff.y);
else delta = vec2(diff.z, diff.y);
ivec3 s1, s2, s3;
if (face <= 1) {
s1 = ivec3(isign(delta.x), 0, 0);
s2 = ivec3(0, 0, isign(delta.y));
s3 = ivec3(isign(delta.x), 0, isign(delta.y));
} else if (face <= 3) {
s1 = ivec3(isign(delta.x), 0, 0);
s2 = ivec3(0, isign(delta.y), 0);
s3 = ivec3(isign(delta.x), isign(delta.y), 0);
} else {
s1 = ivec3(0, 0, isign(delta.x));
s2 = ivec3(0, isign(delta.y), 0);
s3 = ivec3(0, isign(delta.y), isign(delta.x));
}
vec3 p0 = rdm_sample_diff_probe(N, clamp(local_pos, ivec3(0), ivec3(31)), ambient);
vec3 p1 = rdm_sample_diff_probe(N, clamp(local_pos + s1, ivec3(0), ivec3(31)), ambient);
vec3 p2 = rdm_sample_diff_probe(N, clamp(local_pos + s2, ivec3(0), ivec3(31)), ambient);
vec3 p3 = rdm_sample_diff_probe(N, clamp(local_pos + s1 + s2,ivec3(0), ivec3(31)), ambient);
return smix(smix(p0, p1, abs(delta.x)),
smix(p2, p3, abs(delta.x)),
abs(delta.y));
}
// ---- SH PROBE GRID ----
// Each probe stores 27 L2 SH coefficients (9 per RGB channel), packed into
// 3 RGBA16F texels per probe along the X axis of a 192x4096 2D texture.
// Row = probe.z * 64 + probe.y, col = probe.x * 3 + k.
// Texel layout per probe (px,py,pz):
// t0: R.c0-3 t1: G.c0-3 t2: B.c0-3
// Probe index from chunk-local world position p (0..32 range):
// ivec3(floor(p * 2.0)) clamped to [0,63]
// SH evaluation: Lambertian irradiance (L1 only), A0=PI, A1=2PI/3.
vec3 sh_eval(ivec3 probe, vec3 N) {
int base = probe.x * 3;
int row = probe.z * 64 + probe.y;
vec4 t0 = texelFetch(sampler2D(sh_chunk, rdmsmp), ivec2(base, row), 0);
vec4 t1 = texelFetch(sampler2D(sh_chunk, rdmsmp), ivec2(base+1, row), 0);
vec4 t2 = texelFetch(sampler2D(sh_chunk, rdmsmp), ivec2(base+2, row), 0);
float x = N.x, y = N.y, z = N.z;
float r = 0.886227*t0.x + 1.023327*(t0.w*x + t0.y*y + t0.z*z);
float g = 0.886227*t1.x + 1.023327*(t1.w*x + t1.y*y + t1.z*z);
float b = 0.886227*t2.x + 1.023327*(t2.w*x + t2.y*y + t2.z*z);
return max(vec3(r, g, b) / PI, vec3(0.0));
}
// Sum of L0 irradiance across RGB — proxy for total incoming energy.
// Near-zero means the probe is buried inside solid geometry.
float sh_probe_energy(ivec3 probe) {
int base = probe.x * 3;
int row = probe.z * 64 + probe.y;
vec4 t0 = texelFetch(sampler2D(sh_chunk, rdmsmp), ivec2(base, row), 0);
vec4 t1 = texelFetch(sampler2D(sh_chunk, rdmsmp), ivec2(base+1, row), 0);
vec4 t2 = texelFetch(sampler2D(sh_chunk, rdmsmp), ivec2(base+2, row), 0);
return max(0.886227 * (t0.x + t1.x + t2.x), 0.0);
}
// Trilinear SH evaluation with confidence weighting.
// Probes with near-zero energy (buried in geometry) are downweighted
// so they don't pull the result toward black.
vec3 sh_eval_trilinear(ivec3 p0, ivec3 p1, vec3 t, vec3 N) {
float wx[2] = float[2](1.0 - t.x, t.x);
float wy[2] = float[2](1.0 - t.y, t.y);
float wz[2] = float[2](1.0 - t.z, t.z);
vec3 result = vec3(0.0);
float total_w = 0.0;
for (int iz = 0; iz < 2; iz++) {
for (int iy = 0; iy < 2; iy++) {
for (int ix = 0; ix < 2; ix++) {
ivec3 probe = ivec3(
ix == 0 ? p0.x : p1.x,
iy == 0 ? p0.y : p1.y,
iz == 0 ? p0.z : p1.z
);
float w = wx[ix] * wy[iy] * wz[iz] * sh_probe_energy(probe);
result += sh_eval(probe, N) * w;
total_w += w;
}
}
}
return total_w > 0.001 ? result / total_w : vec3(0.0);
}
// ---- HSV ----
vec3 rgb2hsv(vec3 c) {
vec4 K = vec4(0.0, -1.0/3.0, 2.0/3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0/3.0, 1.0/3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
// ---- MAIN ----
void main() {
if (vpos.y < planeHeight - 0.01 && is_reflection == 1) discard;
// Get trixel material.
vec3 sample_pos = ipos - orig_normal * 0.02;
vec4 trixel_material;
int maxSteps = is_reflection == 1 ? 1 : 3;
@ -438,14 +211,12 @@ void main() {
metallic = float((packed >> 3) & 0x3) / 3.0;
}
// Avoid noise in normals which appears for some reason.
vec3 absN = abs(fnormal.xyz);
vec3 N;
if (absN.x >= absN.y && absN.x >= absN.z) N = vec3(sign(fnormal.x), 0.0, 0.0);
else if (absN.y >= absN.x && absN.y >= absN.z) N = vec3(0.0, sign(fnormal.y), 0.0);
else N = vec3(0.0, 0.0, sign(fnormal.z));
// Simplified lighting evaluation for planar reflection.
if (is_reflection == 1) {
vec3 L = normalize(sunPosition);
float NdotL = max(dot(N, L), 0.0);
@ -453,7 +224,6 @@ void main() {
return;
}
// ---- 1. VIEW / LIGHT VECTORS ----
vec3 V = normalize(cam - vpos);
vec3 L = normalize(sunPosition);
vec3 H = normalize(V + L);
@ -461,14 +231,12 @@ void main() {
float NdotV = max(dot(N, V), 0.0);
float HdotV = max(dot(H, V), 0.0);
// ---- 2. PBR TERMS ----
vec3 F0 = mix(vec3(0.04), albedo, metallic);
vec3 F = fresnelSchlick(HdotV, F0);
float NDF = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
vec3 kD = (1.0 - F) * (1.0 - metallic);
// ---- 3. DIRECT LIGHT (sun + shadow) ----
vec4 light_proj = mvp_shadow * vec4(floor(vpos * 16.0) / 16.0, 1.0);
vec3 light_ndc = light_proj.xyz / light_proj.w * 0.5 + 0.5;
light_ndc.z -= 0.001;
@ -477,82 +245,30 @@ void main() {
vec3 direct_specular = (NDF * G * F) / (4.0 * NdotV * NdotL + 0.0001);
vec3 light = shadow * (kD * albedo / PI + direct_specular) * NdotL * sunLightColor * sunIntensity;
// ---- 4. INDIRECT LIGHT (RDM / SH / ambient fallback) ----
ivec3 local = ivec3(mod(floor(trileCenter), 32.0));
vec4 atlas_rect = rdm_atlas_rect(local, roughnessInt);
float ssao = texture(sampler2D(ssaotex, rdmsmp),
float ssao = texture(sampler2D(ssaotex, linsmp),
gl_FragCoord.xy / vec2(float(screen_w), float(screen_h))).r;
vec3 emissive = albedo * emittance * emissive_scale;
if (rdm_enabled == 1) {
vec3 Frough = FresnelSchlickRoughness(NdotV, F0, roughness);
vec3 hemispherePos = trileCenter + N * 0.49;
vec3 diff = vpos - hemispherePos;
// 4a. Indirect specular.
// roughnessInt 0-1 with a baked RDM: ray-march or single-sample into the atlas.
// roughnessInt 2+ without RDM data: sky reflection.
// roughnessInt > ROUGHNESS_SPEC_CUTOFF: skip specular entirely.
if (roughnessInt <= 1 && atlas_rect.z > 0.0) {
int face = rdm_face_from_normal(N);
ivec2 atlasSize = textureSize(sampler2D(rdm_atlas, rdmsmp), 0);
vec2 atlasInvSz = 1.0 / vec2(atlasSize);
int rdmSize = int(atlas_rect.z * float(atlasSize.x)) / 2;
ivec2 fOff = rdm_face_offset(atlas_rect, face, rdmSize, atlasSize);
vec3 indirectSpec = roughness < ROUGHNESS_RAYMARCH_MAX
? rdm_spec_raymarch(N, -cv, diff, face, fOff, rdmSize, atlasInvSz)
: rdm_spec_single (N, -cv, diff, face, fOff, rdmSize, atlasInvSz);
indirectSpec *= rdm_tint;
float specLum = dot(indirectSpec, vec3(0.2126, 0.7152, 0.0722));
indirectSpec = mix(indirectSpec, vec3(specLum), metallic);
vec2 envBRDF = texture(sampler2D(brdf_lut, rdmsmp), vec2(NdotV, roughness)).rg;
float roughnessBell = 1.0 - 0.7 * sin(roughness * PI);
float grazingSuppr = 1.0 - 0.9 * roughness * sin(roughness * PI) * pow(1.0 - NdotV, 2.0);
float specRoughFade = 1.0 - clamp((roughness - 0.5) / 0.3, 0.0, 1.0);
light += indirectSpec * (Frough * envBRDF.x + envBRDF.y)
* rdm_spec_scale * roughnessBell * grazingSuppr * specRoughFade;
} else if (roughness < ROUGHNESS_SPEC_CUTOFF) {
if (roughness < ROUGHNESS_SPEC_CUTOFF) {
vec3 R = reflect(-V, N);
vec2 envBRDF = texture(sampler2D(brdf_lut, rdmsmp), vec2(NdotV, roughness)).rg;
vec2 envBRDF = texture(sampler2D(brdf_lut, linsmp), vec2(NdotV, roughness)).rg;
float specRoughFd = 1.0 - clamp((roughness - 0.5) / 0.3, 0.0, 1.0);
light += sky_reflect(R, sunPosition) * (Frough * envBRDF.x + envBRDF.y)
* rdm_spec_scale * specRoughFd;
* indirect_spec_scale * specRoughFd;
}
// 4b. Indirect diffuse.
// SH probe grid when available (trilinear, confidence-weighted).
// Falls back to RDM level-7 diffuse probes.
vec3 indirectDiff;
if (sh_enabled == 1) {
vec3 trile_origin = floor(trileCenter);
vec3 local_frag = vec3(local) + (vpos - trile_origin);
vec3 probe_f = clamp(local_frag * 2.0, vec3(0.0), vec3(63.0));
ivec3 p0 = ivec3(floor(probe_f));
ivec3 p1 = min(p0 + ivec3(1), ivec3(63));
indirectDiff = sh_eval_trilinear(p0, p1, fract(probe_f), N) * rdm_tint;
vec2 sh_uv = gl_FragCoord.xy / vec2(float(screen_w), float(screen_h));
indirectDiff = texture(sampler2D(sh_irradiance, linsmp), sh_uv).rgb * indirect_tint;
} else {
indirectDiff = rdm_indirect_diffuse(N, diff, local) * rdm_tint;
}
float diffLuma = dot(indirectDiff, vec3(0.2126, 0.7152, 0.0722));
indirectDiff = mix(vec3(diffLuma), indirectDiff, rdm_diff_saturation);
light += (1.0 - Frough) * (1.0 - metallic) * indirectDiff / PI * albedo * ssao * rdm_diff_scale;
// 4c. Ambient floor — kicks in when indirect light is below the configured minimum.
if (rdm_diff_scale < 0.001 || length(light) < ambient_intensity)
light += ambient_color * max(ambient_intensity - length(light), 0.0) * albedo * ssao;
} else {
// No baked data: flat ambient + sky specular.
light += ambient_color * ambient_intensity * albedo * ssao;
light += F * sky_reflect(reflect(-V, N), sunPosition) * 0.1;
indirectDiff = ambient_color * ambient_intensity;
}
// ---- 5. FINAL COMPOSITE ----
light += (1.0 - Frough) * (1.0 - metallic) * indirectDiff / PI * albedo * ssao * indirect_diff_scale;
vec3 final_color = light + emissive;
frag_color = vec4(mix(deepColor, final_color, smoothstep(0.0, planeHeight, vpos.y)), 1.0);

View File

@ -0,0 +1,346 @@
@vs vs_trile_rdm
in vec4 position;
in vec4 normal;
in vec4 centre;
in vec4 instance;
layout(binding=0) uniform trile_rdm_vs_params {
mat4 mvp;
mat4 mvp_shadow;
vec3 camera;
};
out vec3 cam;
out vec3 to_center;
out vec3 vpos;
out vec3 ipos;
out vec4 fnormal;
out vec3 orig_normal;
out vec3 trileCenter;
out vec3 cv;
mat3 rot_x(float a) { float c=cos(a),s=sin(a); return mat3(1,0,0, 0,c,-s, 0,s,c); }
mat3 rot_y(float a) { float c=cos(a),s=sin(a); return mat3(c,0,s, 0,1,0, -s,0,c); }
mat3 rot_z(float a) { float c=cos(a),s=sin(a); return mat3(c,-s,0, s,c,0, 0,0,1); }
mat3 get_orientation_matrix(int ori) {
int face = ori / 4;
int twist = ori % 4;
float PI = 3.1415927;
mat3 base;
if (face == 0) base = mat3(1.0);
else if (face == 1) base = rot_x(PI);
else if (face == 2) base = rot_z(-PI*0.5);
else if (face == 3) base = rot_z( PI*0.5);
else if (face == 4) base = rot_x( PI*0.5);
else base = rot_x(-PI*0.5);
return base * rot_y(float(twist) * PI * 0.5);
}
void main() {
int ori = int(round(instance.w));
mat3 rot = get_orientation_matrix(ori);
vec3 local = position.xyz - 0.5;
vec3 rotated = rot * local + 0.5;
gl_Position = mvp * vec4(rotated + instance.xyz, 1.0);
fnormal = vec4(rot * normal.xyz, 0.0);
orig_normal = normal.xyz;
to_center = centre.xyz - position.xyz;
vpos = rotated + instance.xyz;
ipos = position.xyz;
cam = camera;
cv = normalize(camera - vpos);
trileCenter = instance.xyz + vec3(0.5);
}
@end
@fs fs_trile_rdm
layout(binding=1) uniform trile_rdm_world_config {
vec3 skyBase;
vec3 skyTop;
vec3 sunDisk;
vec3 horizonHalo;
vec3 sunHalo;
vec3 sunLightColor;
vec3 sunPosition;
float sunIntensity;
float skyIntensity;
int hasClouds;
float planeHeight;
int animatePlaneHeight;
vec3 waterColor;
vec3 deepColor;
float time;
int hsv_lighting;
};
in vec3 cam;
in vec3 to_center;
in vec3 vpos;
in vec3 ipos;
in vec4 fnormal;
in vec3 orig_normal;
in vec3 trileCenter;
in vec3 cv;
out vec4 frag_color;
layout(binding=3) uniform trile_rdm_fs_params {
mat4 mvp_shadow;
int is_reflection;
int screen_h;
int screen_w;
float ambient_intensity;
float emissive_scale;
float indirect_diff_scale;
float indirect_spec_scale;
vec3 ambient_color;
int is_preview;
vec3 indirect_tint;
int sh_enabled;
vec4 atlas_rect;
};
layout(binding = 0) uniform texture2D rdm_triletex;
layout(binding = 0) uniform sampler rdm_trilesmp;
layout(binding = 1) uniform texture2D rdm_ssaotex;
layout(binding = 2) uniform texture2D rdm_shadowtex;
layout(binding = 2) uniform sampler rdm_shadowsmp;
layout(binding = 3) uniform texture2D rdm_brdflut;
layout(binding = 4) uniform texture2D rdm_shirradiance;
layout(binding = 5) uniform texture2D rdm_atlas;
layout(binding = 3) uniform sampler rdm_linsmp;
const float PI = 3.1415927;
const float ROUGHNESS_SPEC_CUTOFF = 0.7;
const float ROUGHNESS_RAYMARCH_MAX = 0.2;
vec3 sky(vec3 skypos, vec3 sunpos) {
vec3 npos = normalize(skypos);
float sDist = dot(npos, normalize(sunpos));
vec3 skyGradient = mix(skyBase, skyTop, clamp(npos.y * 2.0, 0.0, 0.7));
vec3 result = skyGradient;
result += sunHalo * clamp((sDist - 0.95) * 10.0, 0.0, 0.8) * 0.2;
if (sDist > 0.9999)
result = sunDisk;
result += mix(horizonHalo, vec3(0.0), clamp(abs(npos.y) * 80.0, 0.0, 1.0)) * 0.1;
return result;
}
vec3 sky_reflect(vec3 R, vec3 sunpos) {
if (R.y < 0.0) R = reflect(R, vec3(0.0, 1.0, 0.0));
return sky(R, sunpos);
}
float DistributionGGX(vec3 N, vec3 H, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
float NdotH = max(dot(N, H), 0.0);
float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
return a2 / (PI * denom * denom);
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
float r = roughness + 1.0;
float k = (r * r) / 8.0;
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx1 = NdotL / (NdotL * (1.0 - k) + k);
float ggx2 = NdotV / (NdotV * (1.0 - k) + k);
return ggx1 * ggx2;
}
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
vec2 rdm_hemioct(vec3 v, int face) {
vec3 vc = v;
if (face / 2 == 0) { vc.z = v.y; vc.y = v.z; }
if (face / 2 == 2) { vc.z = v.x; vc.x = v.z; }
if (face % 2 == 1) { vc.z *= -1.0; }
vec2 p = vc.xy * (1.0 / (abs(vc.x) + abs(vc.y) + vc.z));
return vec2(p.x + p.y, p.x - p.y) * 0.5 + 0.5;
}
int rdm_face_from_normal(vec3 N) {
vec3 a = abs(N);
if (a.y >= a.x && a.y >= a.z) return N.y >= 0.0 ? 0 : 1;
if (a.z >= a.x && a.z >= a.y) return N.z >= 0.0 ? 2 : 3;
return N.x >= 0.0 ? 4 : 5;
}
ivec2 rdm_face_offset(vec4 rect, int face, int rdmSize, ivec2 atlasSize) {
int col = face % 2;
int row = face / 2;
return ivec2(int(rect.x * float(atlasSize.x)) + col * rdmSize,
int(rect.y * float(atlasSize.y)) + row * rdmSize);
}
vec3 rdm_spec_raymarch(vec3 N, vec3 V, vec3 diff, int face, ivec2 faceOffset, int rdmSize, vec2 atlasInvSize) {
vec3 reflected = reflect(V, N);
float maxDist = 20.0;
int steps = 40;
float stepSize = maxDist / float(steps);
for (int i = 0; i < steps; i++) {
float t = stepSize * float(i + 1);
vec3 samplePos = diff + t * reflected;
if (dot(samplePos, N) < 0.0) continue;
vec3 dir = normalize(samplePos);
vec2 hemiUV = rdm_hemioct(dir, face);
vec2 texCoord = (vec2(faceOffset) + hemiUV * float(rdmSize)) * atlasInvSize;
vec4 s = texture(sampler2D(rdm_atlas, rdm_linsmp), texCoord, 0);
float dist = length(samplePos);
if (s.a > 0.0 && s.a < dist && s.a + stepSize > dist)
return s.rgb;
}
return sky_reflect(reflected, sunPosition);
}
vec3 rdm_spec_single(vec3 N, vec3 V, vec3 diff, int face, ivec2 faceOffset, int rdmSize, vec2 atlasInvSize) {
vec3 reflected = reflect(V, N);
vec3 sampleDir = normalize(diff + 2.0 * reflected);
vec2 hemiUV = rdm_hemioct(sampleDir, face);
vec2 texCoord = (vec2(faceOffset) + hemiUV * float(rdmSize)) * atlasInvSize;
return texture(sampler2D(rdm_atlas, rdm_linsmp), texCoord).rgb;
}
void main() {
if (vpos.y < planeHeight - 0.01 && is_reflection == 1) discard;
vec3 sample_pos = ipos - orig_normal * 0.02;
vec4 trixel_material;
int maxSteps = is_reflection == 1 ? 1 : 3;
for (int i = 0; i < maxSteps; i++) {
ivec2 texel = ivec2(
int(clamp(sample_pos.z, 0.0001, 0.99999) * 16.0),
int(clamp(sample_pos.y, 0.0001, 0.99999) * 16.0) +
int(clamp(sample_pos.x, 0.0001, 0.99999) * 16.0) * 16
);
trixel_material = texelFetch(sampler2D(rdm_triletex, rdm_trilesmp), texel, 0);
if (dot(trixel_material, trixel_material) > 0.0001) break;
sample_pos += to_center * 0.1;
}
vec3 albedo = trixel_material.xyz;
int packed = int(round(trixel_material.w * 255.0));
float emittance = 0.0;
int roughnessInt = 0;
float roughness = 0.0;
float metallic = 0.0;
if ((packed & 0x1) != 0) {
emittance = float((packed >> 1) & 0x7F) / 127.0;
} else {
roughnessInt = (packed >> 5) & 0x7;
roughness = max(float(roughnessInt) / 7.0, 0.05);
metallic = float((packed >> 3) & 0x3) / 3.0;
}
vec3 absN = abs(fnormal.xyz);
vec3 N;
if (absN.x >= absN.y && absN.x >= absN.z) N = vec3(sign(fnormal.x), 0.0, 0.0);
else if (absN.y >= absN.x && absN.y >= absN.z) N = vec3(0.0, sign(fnormal.y), 0.0);
else N = vec3(0.0, 0.0, sign(fnormal.z));
if (is_reflection == 1) {
vec3 L = normalize(sunPosition);
float NdotL = max(dot(N, L), 0.0);
frag_color = vec4(albedo * (NdotL * sunLightColor * sunIntensity + 0.1), 1.0);
return;
}
vec3 V = normalize(cam - vpos);
vec3 L = normalize(sunPosition);
vec3 H = normalize(V + L);
float NdotL = max(dot(N, L), 0.0);
float NdotV = max(dot(N, V), 0.0);
float HdotV = max(dot(H, V), 0.0);
vec3 F0 = mix(vec3(0.04), albedo, metallic);
vec3 F = fresnelSchlick(HdotV, F0);
float NDF = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
vec3 kD = (1.0 - F) * (1.0 - metallic);
vec4 light_proj = mvp_shadow * vec4(floor(vpos * 16.0) / 16.0, 1.0);
vec3 light_ndc = light_proj.xyz / light_proj.w * 0.5 + 0.5;
light_ndc.z -= 0.001;
float shadow = texture(sampler2DShadow(rdm_shadowtex, rdm_shadowsmp), light_ndc);
vec3 direct_specular = (NDF * G * F) / (4.0 * NdotV * NdotL + 0.0001);
vec3 light = shadow * (kD * albedo / PI + direct_specular) * NdotL * sunLightColor * sunIntensity;
float ssao = texture(sampler2D(rdm_ssaotex, rdm_linsmp),
gl_FragCoord.xy / vec2(float(screen_w), float(screen_h))).r;
vec3 emissive = albedo * emittance * emissive_scale;
vec3 Frough = FresnelSchlickRoughness(NdotV, F0, roughness);
vec3 hemispherePos = trileCenter + N * 0.49;
vec3 diff = vpos - hemispherePos;
if (roughnessInt <= 1) {
int face = rdm_face_from_normal(N);
ivec2 atlasSize = textureSize(sampler2D(rdm_atlas, rdm_linsmp), 0);
vec2 atlasInvSz = 1.0 / vec2(atlasSize);
int rdmSize = int(atlas_rect.z * float(atlasSize.x)) / 2;
ivec2 fOff = rdm_face_offset(atlas_rect, face, rdmSize, atlasSize);
vec3 indirectSpec = roughness < ROUGHNESS_RAYMARCH_MAX
? rdm_spec_raymarch(N, -cv, diff, face, fOff, rdmSize, atlasInvSz)
: rdm_spec_single (N, -cv, diff, face, fOff, rdmSize, atlasInvSz);
vec2 envBRDF = texture(sampler2D(rdm_brdflut, rdm_linsmp), vec2(NdotV, roughness)).rg;
float roughnessBell = 1.0 - 0.7 * sin(roughness * PI);
float grazingSuppr = 1.0 - 0.9 * roughness * sin(roughness * PI) * pow(1.0 - NdotV, 2.0);
float specRoughFade = 1.0 - clamp((roughness - 0.5) / 0.3, 0.0, 1.0);
light += indirectSpec * (Frough * envBRDF.x + envBRDF.y)
* indirect_spec_scale * roughnessBell * grazingSuppr * specRoughFade;
} else if (roughness < ROUGHNESS_SPEC_CUTOFF) {
vec3 R = reflect(-V, N);
vec2 envBRDF = texture(sampler2D(rdm_brdflut, rdm_linsmp), vec2(NdotV, roughness)).rg;
float specRoughFd = 1.0 - clamp((roughness - 0.5) / 0.3, 0.0, 1.0);
light += sky_reflect(R, sunPosition) * (Frough * envBRDF.x + envBRDF.y)
* indirect_spec_scale * specRoughFd;
}
vec3 indirectDiff;
if (sh_enabled == 1) {
vec2 sh_uv = gl_FragCoord.xy / vec2(float(screen_w), float(screen_h));
indirectDiff = texture(sampler2D(rdm_shirradiance, rdm_linsmp), sh_uv).rgb * indirect_tint;
} else {
indirectDiff = ambient_color * ambient_intensity;
}
light += (1.0 - Frough) * (1.0 - metallic) * indirectDiff / PI * albedo * ssao * indirect_diff_scale;
vec3 final_color = light + emissive;
frag_color = vec4(mix(deepColor, final_color, smoothstep(0.0, planeHeight, vpos.y)), 1.0);
if (is_preview == 1) frag_color.rgb = mix(frag_color.rgb, vec3(0.3, 0.7, 1.0), 0.5);
else if (is_preview == 2) frag_color.rgb = mix(frag_color.rgb, vec3(1.0, 0.3, 0.2), 0.5);
}
@end
@program trile_rdm vs_trile_rdm fs_trile_rdm

View File

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

View File

@ -17,15 +17,15 @@ demo_toggle_float :: (val: *float, saved: *float, default: float) {
draw_demo_ui :: (theme: *GR.Overall_Theme) {
r := GR.get_rect(ui_w(86,0), ui_h(1,0), ui_w(13,0), ui_h(3,0));
rdm_diff_on := current_lighting_config.rdm_diff_scale > 0;
if GR.button(r, ifx rdm_diff_on then "RDM Diffuse: ON" else "RDM Diffuse: OFF") {
demo_toggle_float(*current_lighting_config.rdm_diff_scale, *demo_saved_rdm_diff, 1.0);
diff_on := current_lighting_config.indirect_diff_scale > 0;
if GR.button(r, ifx diff_on then "Indirect Diffuse: ON" else "Indirect Diffuse: OFF") {
demo_toggle_float(*current_lighting_config.indirect_diff_scale, *demo_saved_rdm_diff, 1.0);
}
r.y += r.h * 1.3;
rdm_spec_on := current_lighting_config.rdm_spec_scale > 0;
if GR.button(r, ifx rdm_spec_on then "RDM Specular: ON" else "RDM Specular: OFF") {
demo_toggle_float(*current_lighting_config.rdm_spec_scale, *demo_saved_rdm_spec, 1.0);
spec_on := current_lighting_config.indirect_spec_scale > 0;
if GR.button(r, ifx spec_on then "Indirect Specular: ON" else "Indirect Specular: OFF") {
demo_toggle_float(*current_lighting_config.indirect_spec_scale, *demo_saved_rdm_spec, 1.0);
}
r.y += r.h * 1.3;

View File

@ -429,14 +429,15 @@ font_boundary :: () {
render_ui :: () {
voxel_theme := get_voxel_theme();
GR.set_default_theme(voxel_theme);
loading := show_loading_screen();
#if !FLAG_RELEASE_BUILD {
draw_editor_ui(*voxel_theme);
if !in_editor_view then game_ui(*voxel_theme);
if !in_editor_view && !loading then game_ui(*voxel_theme);
draw_console(*voxel_theme);
} else {
game_ui(*voxel_theme);
if !loading then game_ui(*voxel_theme);
}
#if FLAG_DEMO_BUILD { if !in_editor_view then draw_demo_ui(*voxel_theme); }
#if FLAG_DEMO_BUILD { if !in_editor_view && !loading then draw_demo_ui(*voxel_theme); }
draw_settings_menu();
}

View File

@ -92,6 +92,8 @@ get_voxel_theme :: () -> GR.Overall_Theme {
t.slider_theme.background.frame_color = cfg.frame_color;
t.slider_theme.background.frame_color_over = cfg.frame_color;
t.slider_theme.foreground.label_theme.text_color = .{1, 1, 1, 1};
apply_voxel_btn(*t.slider_theme.spinbox_theme, cfg, true);
t.slider_theme.surface_style = .EXTEND_FROM_LEFT;

View File

@ -52,21 +52,9 @@ Chunk :: struct {
coord: Chunk_Key;
groups: [..]Chunk_Trile_Group;
rdm_atlas: sg_image;
rdm_lookup: sg_image;
rdm_valid: bool;
rdm_dirty: bool;
rdm_atlas_path: string;
rdm_lookup_path: string;
sh_probe_grid: sg_image; // 192x4096 RGBA16F 2D texture (2 probes/trile/axis)
sh_valid: bool;
sh_dirty: bool;
#if !FLAG_RELEASE_BUILD {
rdm_lookup_cpu: []float;
rdm_lookup_w: s32;
rdm_lookup_h: s32;
}
}
Editor_Note :: struct {
@ -74,12 +62,20 @@ Editor_Note :: struct {
text : string;
}
RDM_DEFAULT_SIZE :: 128;
Rdm_Instance_Override :: struct {
x : s32;
y : s32;
z : s32;
size_override : s32;
quality_override : s32;
rdm_enabled : bool;
rdm_size : s32; // side in px; 0 means legacy — treat as RDM_DEFAULT_SIZE.
}
// Per-world entry of the global RDM atlas. atlas_rect is normalized UV (x, y, w, h).
Rdm_Atlas_Entry :: struct {
x, y, z : s32;
atlas_rect : Vector4;
}
World :: struct {
@ -89,32 +85,72 @@ World :: struct {
emitter_instances : [..]Particle_Emitter_Instance;
notes : [..]Editor_Note;
rdm_overrides : [..]Rdm_Instance_Override;
rdm_lookup : [..]Rdm_Atlas_Entry; // populated by bake (Step 5) and by loader (Step 6)
}
get_rdm_instance_override :: (world: *World, x: s32, y: s32, z: s32) -> (size_override: s32, quality_override: s32) {
for world.rdm_overrides {
if it.x == x && it.y == y && it.z == z then return it.size_override, it.quality_override;
rdm_get_atlas_rect :: (world: *World, x: s32, y: s32, z: s32) -> (Vector4, bool) {
for world.rdm_lookup {
if it.x == x && it.y == y && it.z == z then return it.atlas_rect, true;
}
return 0, 0;
return .{}, false;
}
set_rdm_instance_override :: (world: *World, x: s32, y: s32, z: s32, size_override: s32, quality_override: s32) {
is_rdm_instance_enabled :: (world: *World, x: s32, y: s32, z: s32) -> bool {
for world.rdm_overrides {
if it.x == x && it.y == y && it.z == z then return it.rdm_enabled;
}
return false;
}
set_rdm_instance_enabled :: (world: *World, x: s32, y: s32, z: s32, enabled: bool) {
for *world.rdm_overrides {
if it.x == x && it.y == y && it.z == z {
if size_override == 0 && quality_override == 0 {
if !enabled {
array_ordered_remove_by_index(*world.rdm_overrides, it_index);
} else {
it.size_override = size_override;
it.quality_override = quality_override;
it.rdm_enabled = true;
if it.rdm_size == 0 then it.rdm_size = RDM_DEFAULT_SIZE;
}
return;
}
}
if size_override != 0 || quality_override != 0 {
array_add(*world.rdm_overrides, .{x=x, y=y, z=z, size_override=size_override, quality_override=quality_override});
if enabled {
array_add(*world.rdm_overrides, .{x=x, y=y, z=z, rdm_enabled=true, rdm_size=RDM_DEFAULT_SIZE});
}
}
get_rdm_instance_size :: (world: *World, x: s32, y: s32, z: s32) -> s32 {
for world.rdm_overrides {
if it.x == x && it.y == y && it.z == z {
if !it.rdm_enabled then return 0;
return ifx it.rdm_size > 0 then it.rdm_size else RDM_DEFAULT_SIZE;
}
}
return 0;
}
set_rdm_instance_size :: (world: *World, x: s32, y: s32, z: s32, size: s32) {
for *world.rdm_overrides {
if it.x == x && it.y == y && it.z == z {
it.rdm_size = size;
return;
}
}
}
RDM_SIZE_LADDER :: s32.[32, 64, 128, 256, 512, 1024, 2048, 4096];
rdm_cycle_size :: (cur: s32, dir: s32) -> s32 {
idx : s32 = 2;
for RDM_SIZE_LADDER {
if it == cur { idx = cast(s32) it_index; break; }
}
idx += dir;
if idx < 0 then idx = 0;
if idx >= cast(s32) RDM_SIZE_LADDER.count then idx = cast(s32) RDM_SIZE_LADDER.count - 1;
return RDM_SIZE_LADDER[idx];
}
// Convert a world-space integer position to chunk coordinate.
world_to_chunk_coord :: (wx: s32, wy: s32, wz: s32) -> Chunk_Key {
return .{
@ -172,23 +208,13 @@ unload_current_world :: () {
if !current_world.valid then return;
rdm_loader_cancel_all();
for *chunk: current_world.world.chunks {
if chunk.rdm_valid {
sg_destroy_image(chunk.rdm_atlas);
sg_destroy_image(chunk.rdm_lookup);
#if !FLAG_RELEASE_BUILD {
if chunk.rdm_lookup_cpu.data then free(chunk.rdm_lookup_cpu.data);
chunk.rdm_lookup_cpu = .{};
}
chunk.rdm_atlas = .{};
chunk.rdm_lookup = .{};
chunk.rdm_valid = false;
}
for *group: chunk.groups {
array_free(group.instances);
}
array_free(chunk.groups);
}
deinit(*current_world.world.chunks);
array_free(current_world.world.rdm_lookup);
Pool.reset(*current_world.pool);
current_world.valid = false;
}
@ -253,7 +279,6 @@ World_Json_Config :: struct {
waterColor : [3]float;
deepColor : [3]float;
waterShininess : float;
rdmDiffSaturation : float;
}
World_Json_Chunk :: struct {
@ -262,8 +287,6 @@ World_Json_Chunk :: struct {
z : s32;
offset : s32;
size : s32;
rdm_atlas : string;
rdm_lookup : string;
}
World_Json_Emitter :: struct {
@ -281,8 +304,8 @@ World_Json_Rdm_Override :: struct {
x : s32;
y : s32;
z : s32;
size_override : s32;
quality_override : s32;
rdm_enabled : bool;
rdm_size : s32;
}
// World_Config serialized as a fixed-size binary blob.
@ -303,7 +326,6 @@ World_Config_Binary :: struct {
water_color: [3]float;
deep_color: [3]float;
water_shininess: float;
rdm_diff_saturation: float;
}
world_config_to_binary :: (conf: *World_Config) -> World_Config_Binary {
@ -323,7 +345,6 @@ world_config_to_binary :: (conf: *World_Config) -> World_Config_Binary {
b.water_color = conf.waterColor.component;
b.deep_color = conf.deepColor.component;
b.water_shininess = conf.waterShininess;
b.rdm_diff_saturation = conf.rdmDiffSaturation;
return b;
}
@ -344,7 +365,6 @@ world_config_from_binary :: (b: *World_Config_Binary) -> World_Config {
conf.waterColor.component = b.water_color;
conf.deepColor.component = b.deep_color;
conf.waterShininess = b.water_shininess;
conf.rdmDiffSaturation = b.rdm_diff_saturation;
return conf;
}
@ -409,7 +429,6 @@ world_config_to_json :: (conf: *World_Config) -> World_Json_Config {
jc.waterColor = conf.waterColor.component;
jc.deepColor = conf.deepColor.component;
jc.waterShininess = conf.waterShininess;
jc.rdmDiffSaturation = conf.rdmDiffSaturation;
return jc;
}
@ -430,7 +449,6 @@ world_config_from_json :: (jc: *World_Json_Config) -> World_Config {
conf.waterColor.component = jc.waterColor;
conf.deepColor.component = jc.deepColor;
conf.waterShininess = ifx jc.waterShininess > 0 then jc.waterShininess else 64.0;
conf.rdmDiffSaturation = ifx jc.rdmDiffSaturation > 0 then jc.rdmDiffSaturation else 1.0;
return conf;
}
@ -441,8 +459,6 @@ save_world :: (world: *World) -> (json: string, chunks_bin: string) {
coord: Chunk_Key;
offset: s32;
size: s32;
rdm_atlas_path: string;
rdm_lookup_path: string;
}
chunk_entries: [..]Chunk_Save_Entry;
chunk_entries.allocator = temp;
@ -469,19 +485,10 @@ save_world :: (world: *World) -> (json: string, chunks_bin: string) {
chunk_data := builder_to_string(*chunk_builder,, temp);
data_size := cast(s32) chunk_data.count;
atlas_path := chunk.rdm_atlas_path;
lookup_path := chunk.rdm_lookup_path;
if !atlas_path.count {
atlas_path = tprint("%_%_%.rdm_atlas", chunk.coord.x, chunk.coord.y, chunk.coord.z);
lookup_path = tprint("%_%_%.rdm_lookup", chunk.coord.x, chunk.coord.y, chunk.coord.z);
}
array_add(*chunk_entries, .{
coord = chunk.coord,
offset = running_offset,
size = data_size,
rdm_atlas_path = atlas_path,
rdm_lookup_path = lookup_path,
});
append(*bin_builder, chunk_data);
running_offset += data_size;
@ -499,8 +506,6 @@ save_world :: (world: *World) -> (json: string, chunks_bin: string) {
jc.z = entry.coord.z;
jc.offset = entry.offset;
jc.size = entry.size;
jc.rdm_atlas = entry.rdm_atlas_path;
jc.rdm_lookup = entry.rdm_lookup_path;
array_add(*wj.chunks, jc);
}
@ -520,7 +525,8 @@ save_world :: (world: *World) -> (json: string, chunks_bin: string) {
}
for ov: world.rdm_overrides {
array_add(*wj.rdm_overrides, .{x=ov.x, y=ov.y, z=ov.z, size_override=ov.size_override, quality_override=ov.quality_override});
size := ifx ov.rdm_size > 0 then ov.rdm_size else RDM_DEFAULT_SIZE;
array_add(*wj.rdm_overrides, .{x=ov.x, y=ov.y, z=ov.z, rdm_enabled=ov.rdm_enabled, rdm_size=size});
}
json_str := Jaison.json_write_string(wj, " ");
@ -542,8 +548,6 @@ load_world_from_json :: (json_str: string, chunk_bin: []u8) -> (World, bool) {
for jc: wj.chunks {
chunk: Chunk;
chunk.coord = .{x = jc.x, y = jc.y, z = jc.z};
chunk.rdm_atlas_path = sprint("%", jc.rdm_atlas);
chunk.rdm_lookup_path = sprint("%", jc.rdm_lookup);
offset := cast(s64) jc.offset;
size := cast(s64) jc.size;
@ -589,7 +593,8 @@ load_world_from_json :: (json_str: string, chunk_bin: []u8) -> (World, bool) {
}
for jov: wj.rdm_overrides {
array_add(*world.rdm_overrides, .{x=jov.x, y=jov.y, z=jov.z, size_override=jov.size_override, quality_override=jov.quality_override});
size := ifx jov.rdm_size > 0 then jov.rdm_size else RDM_DEFAULT_SIZE;
array_add(*world.rdm_overrides, .{x=jov.x, y=jov.y, z=jov.z, rdm_enabled=jov.rdm_enabled, rdm_size=size});
}
return world, true;
@ -721,7 +726,6 @@ World_Config :: struct {
waterColor : Vector3 = .{1.0, 1.0, 1.0}; @Color // @ToDo: sensible default values.
deepColor : Vector3 = .{1.0, 1.0, 1.0}; @Color // @ToDo: sensible default values.
waterShininess : float = 64.0; @Slider,1,512,8
rdmDiffSaturation : float = 1.0; @Slider,0,2,0.05
hsv_lighting : s32 = 1; @Slider,0,1,1
// ambientColor : Vector3 = .{1.0, 1.0, 1.0}; @Color