Compare commits

..

1 Commits

Author SHA1 Message Date
3bff0f9501 upload wip work from desktop to take with me on laptop 2026-05-07 15:11:38 +03:00
196 changed files with 153460 additions and 39559 deletions

View File

@ -1,7 +1,7 @@
Copyright 2026 Tuomas Katajisto
Copyright 2025 Tuomas Katajisto
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,8 +1,20 @@
Engine todo list:
- Engine testing + game testing framework is a mess.
- A lot of the graphics pipeline stuff is really bloated and repeats itself.
- Sokol should be fully under a platform abstraction layer but it's not,
currently it's fine but it would be really nice to be able to have a metal, vulkan
webgpu backends at some point for the longevity of the engine.
- Backend sokol has a global named camera taking up the name which is fucking crazy.
DONE:
- Implement idle orbit thing using the rotation in the level editor camera. (Cool transition effect?)
- Level editor camera movement complete.
TODO:
- Level format (just a hash map?). Store a matrix so that we can rotate and maybe scale blocks. Scaling will cause problems with RDM, so figure that out.
- Trile color textures.
- Sun lighting and shadowmap for levels.
- A ground plane with customizable material (ground or water or maybe a reflective floor. Think about what part of this should be done with triles and what with textures.)
- Sky editor and passing to shader.
- Level saving
- Trile atlas saving? JSON?
PLANNED:
- Render distance, sky and fog?
- Load RDMs. Load into the Trile_GFX structure?
- Render RDMs.
- Start working on game port for proof of concept.
- Maybe port tacoma in as a part of the engine in plain Vulkan without nvpro. Would also allow for cool ability to render levels as path traced for screenshots and references.
- Improve the bz4x format, or figure out a pre-existing format.

View File

@ -1,4 +1,8 @@
#!/bin/bash
set -e
cd src/shaders/
./compile_shaders.sh
cd ..
cd ..
jai -x64 first.jai
./first

View File

@ -124,7 +124,8 @@ beachgame/
│ │ ├── helpers.jai create_world_rendering_tasks(), uniform helpers
│ │ ├── arbtri.jai Immediate-mode triangle batch renderer (UI)
│ │ ├── post_processing.jai Post-process config, LUT management
│ │ └── sky.jai Sky rendering helpers
│ │ ├── sky.jai Sky rendering helpers
│ │ └── ssao.jai SSAO kernel/noise generation
│ │
│ ├── editor/
│ │ ├── editor.jai Editor root: three views, F3 toggle
@ -248,8 +249,10 @@ Sokol GFX draw calls
| Setup | GPU position/uniform buffers | Uploads trile instance stream data |
| Shadow | 1000×1000 depth texture | Orthographic sun directional light; PCF soft edges (Gaussian-weighted sampling) |
| Reflection | Half-res RGBA32F texture | Planar water reflection: camera Y-flipped around water plane; handles dynamic objects that RDMs cannot |
| G-Buffer | Position + Normal RGBA16F | Used by SH irradiance and deferred effects |
| Main | RGBA32F HDR target | Forward render: albedo-first trile shading with trixel AO and sky fog; RDM triles keep PBR |
| G-Buffer | Position + Normal RGBA16F | Used by SSAO and deferred effects |
| SSAO | Single-channel AO texture | Random sample kernel, 64 samples |
| SSAO Blur | Blurred AO texture | Separable blur |
| Main | RGBA32F HDR target | Forward render: sun direct + RDM indirect (specular + diffuse) + SSAO |
| Post chain | Bloom → DoF ping-pong | Between postprocess_a / postprocess_b |
| Final | Swapchain | Tonemap, LUT grade, vignette, grain |
@ -368,31 +371,12 @@ handles dynamic objects (players, ball) that do not appear in the static RDMs, a
sharp mirror-like reflections correct for large flat surfaces. Post-processing can add ripple
distortion to the reflection texture.
#### Trixel AO
#### SSAO
Ambient occlusion is worked out per pixel in the trile shaders, voxel style, from the trile's
16³ trixel grid: each corner of a trixel face is darkened by the trixels in front of it, and the
four corners are blended across the face. It darkens all of a face's lighting, squared for crisp
edges and scaled by `trixel_ao_strength` in the lighting config.
To carry across trile boundaries, every instance knows which of its 26 surrounding cells hold an
opaque trile (`Chunk_Trile_Group.neighbours`, computed alongside `is_buried`). Those cells count
as solid throughout. The mask is in the trile's own unrotated frame and is packed together with
the orientation into the instance's `w` (`trile_instance_w`), so every shader that reads the
orientation decodes it first.
#### Trile shading
Opaque triles are shaded albedo-first, after Fez: a sunlit face shows its colour, a shaded face
that colour times `shadow_tint`, slightly more saturated. Top, x sides, z sides and bottom each
get a fixed brightness (`face_shade_*`). Trixel colours are converted from sRGB to linear before
lighting. Specular only appears on smooth materials: roughness 0..7 is read as gloss
`1 - roughness/7`, so roughness 7 never shines. Fog takes the sky colour in the view direction,
climbs to `fog_max` between `fog_start` and `fog_end`, then to fully fogged by the LOD cull
distance so nothing pops. All of it lives in `Lighting_Config`, saved to
`game_core/lighting.json` next to `postprocess.json`; `lighting_debug` shows colour × face shade
(1), AO (2) or light only (3). Post adds `tonemap = 2`, a hue-preserving soft clip, and applies
LUTs after gamma encoding.
Screen-space ambient occlusion darkens cavities and contact areas. A geometry pass outputs
world-space position and normal (RGBA16F G-buffer), then the SSAO pass samples 64 random
offsets to estimate local occlusion. The result is blurred with a separable filter and applied to
the ambient/diffuse contribution.
#### Pre-bake pipeline

View File

@ -1,5 +1,4 @@
#load "./src/meta/meta.jai";
#load "./src/renderer_sokol/shaders/build.jai";
Iprof :: #import "Iprof"(IMPORT_MODE = .METAPROGRAM);
#import "File_Utilities";
@ -9,28 +8,17 @@ Trueno_Build_Options :: struct {
release_build : bool;
tacoma_enabled : bool;
iprof_enabled : bool;
demo_build : bool;
test_engine : bool;
test_game : bool;
test_exe_engine : bool;
test_exe_game : bool;
renderer : string = "sokol";
}
// Engine tests build against test_game/ so they work without the game/ directory;
// game tests build against the real game and its real resources. Kept in step with
// FLAG_USE_TEST_GAME in src/main.jai.
uses_test_game :: (opts: Trueno_Build_Options) -> bool {
return opts.test_engine || opts.test_exe_engine;
}
build_options_from_args :: (args: []string) -> Trueno_Build_Options {
opts : Trueno_Build_Options;
for arg : args {
if begins_with(arg, "renderer=") {
opts.renderer = slice(arg, 9, arg.count - 9);
continue;
}
if arg == {
case "wasm";
opts.wasm_build = true;
@ -40,6 +28,8 @@ build_options_from_args :: (args: []string) -> Trueno_Build_Options {
opts.release_build = true;
case "iprof";
opts.iprof_enabled = true;
case "demo";
opts.demo_build = true;
case "test";
opts.test_game = true;
opts.test_engine = true;
@ -76,21 +66,23 @@ max_asset_file_size :: (dir: string, ext: string, floor: s64 = 0) -> s64 {
}
add_asset_buffer_sizes_to_compiler_strings :: (trueno_opts: Trueno_Build_Options, w: Workspace) {
use_test_game := uses_test_game(trueno_opts);
pack_dir := ifx use_test_game then "./test_packs" else "./packs";
game_resources_dir := ifx use_test_game then "./test_game/resources" else "./game/resources";
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: [4]s64;
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, "probes.bin", 4 * 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_PROBES_SIZE"];
for i: 0..3 {
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);
}
}
@ -103,13 +95,22 @@ add_trueno_opts_to_compiler_strings :: (trueno_opts : Trueno_Build_Options, w: W
trueno_opts_info := type_info(Trueno_Build_Options);
for trueno_opts_info.members {
if it.type.type != .BOOL continue;
optPtr := (cast(*bool)(*trueno_opts)) + it.offset_in_bytes;
name := tprint("%", it.name);
to_upper_in_place(name);
add_build_string(tprint("FLAG_% :: %;\n", name, bool_to_string(optPtr.*)), w);
}
add_build_string(tprint("RENDERER :: \"%\";\n", trueno_opts.renderer), w);
}
compile_shaders :: () {
process_result, output, error := run_command("bash", ifx OS == .MACOS then "./compile_shaders_mac.sh" else "./compile_shaders.sh", working_directory=tprint("%/src/shaders", #filepath));
if process_result.exit_code != 0 {
log_error("Shader compilation failed.");
if output {
log_error(output,, logger = runtime_support_default_logger);
}
exit(1);
}
}
wasm_copy_assets :: () {
@ -146,8 +147,7 @@ native_build :: (opts: Build_Options, trueno_opts: Trueno_Build_Options) {
opts.cpu_target = root_opts.cpu_target;
opts.os_target = root_opts.os_target;
// opts.backend = ifx (trueno_opts.release_build || OS == .MACOS) then .LLVM else .X64;
opts.backend = .LLVM; // rad debugger doesn't currently work with the x64 backend.
opts.backend = ifx (trueno_opts.release_build || OS == .MACOS) then .LLVM else .X64;
opts.output_executable_name = root_opts.output_executable_name;
set_build_options(opts, w);
@ -170,6 +170,7 @@ native_build :: (opts: Build_Options, trueno_opts: Trueno_Build_Options) {
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);
metaprogramming_loop(trueno_opts, *w, iprof_plugin);
@ -221,14 +222,14 @@ wasm_build :: (opts: Build_Options, trueno_opts: Trueno_Build_Options) {
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);
metaprogramming_loop(trueno_opts, *w);
args := string.[
"emcc",
"src/platform_specific/main.c", "dist/main.o", "modules/sokol-jai/sokol/gfx/sokol_gfx_wasm_gl_release.a", "modules/sokol-jai/sokol/log/sokol_log_wasm_gl_release.a", "modules/sokol-jai/sokol/audio/sokol_audio_wasm_gl_release.a", "modules/sokol-jai/sokol/time/sokol_time_wasm_gl_release.a", "modules/sokol-jai/sokol/app/sokol_app_wasm_gl_release.a", "modules/sokol-jai/sokol/glue/sokol_glue_wasm_gl_release.a", "modules/sokol-jai/sokol/fetch/sokol_fetch_wasm_gl_release.a", "modules/Fontstash/wasm/fontstash.a",
"src/platform_specific/main.c", "dist/main.o", "modules/sokol-jai/sokol/gfx/sokol_gfx_wasm_gl_release.a", "modules/sokol-jai/sokol/log/sokol_log_wasm_gl_release.a", "modules/sokol-jai/sokol/audio/sokol_audio_wasm_gl_release.a", "modules/sokol-jai/sokol/time/sokol_time_wasm_gl_release.a", "modules/sokol-jai/sokol/app/sokol_app_wasm_gl_release.a", "modules/sokol-jai/sokol/glue/sokol_glue_wasm_gl_release.a", "modules/sokol-jai/sokol/fetch/sokol_fetch_wasm_gl_release.a", "modules/sokol-jai/sokol/gl/sokol_gl_wasm_gl_release.a", "modules/sokol-jai/sokol/fontstash/sokol_fontstash_wasm_gl_release.a",
"modules/sokol-jai/sokol/stbi/stb_image.a",
"modules/stb_vorbis/wasm/stb_vorbis.a",
"-o", "dist/index.html",
"-sSTACK_SIZE=10MB",
"-sALLOW_MEMORY_GROWTH",
@ -252,10 +253,9 @@ wasm_build :: (opts: Build_Options, trueno_opts: Trueno_Build_Options) {
#run {
opt := get_build_options();
trueno_opts := build_options_from_args(opt.compile_time_command_line);
use_test_game := uses_test_game(trueno_opts);
create_pack(use_test_game, ifx use_test_game then "./test_packs" else "./packs");
if trueno_opts.renderer == "sokol" && !compile_sokol_shaders() exit(1);
compile_shaders();
is_test_exe := trueno_opts.test_exe_engine || trueno_opts.test_exe_game;
create_pack(is_test_exe, ifx is_test_exe then "./test_packs" else "./packs");
if trueno_opts.wasm_build {
wasm_build(opt, trueno_opts);

BIN
first.rdi

Binary file not shown.

Binary file not shown.

View File

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

4
mise.toml Normal file
View File

@ -0,0 +1,4 @@
[tasks]
build_web = "$HOME/bin/jai/bin/jai-linux first.jai - wasm release && rm -rf dist/packs/ && cp -r packs/ dist/ && rm dist/main.o"
ship_web = "./game/ship_web.sh"
start = "$HOME/bin/jai/bin/jai-linux first.jai && ./first"

View File

@ -1,13 +0,0 @@
#!/bin/bash
# Builds fontstash (with stb_truetype) as a plain static library, without the
# sokol glue the sokol-jai version comes with. Run on each platform:
# ./build.sh linux | macos | wasm
set -e
cd "$(dirname "$0")"
case "$1" in
linux) cc -c -O2 -fPIC source/fontstash.c -o fontstash.o && mkdir -p linux && ar rcs linux/fontstash.a fontstash.o ;;
macos) cc -c -O2 -arch arm64 -arch x86_64 source/fontstash.c -o fontstash.o && mkdir -p macos && ar rcs macos/fontstash.a fontstash.o ;;
wasm) emcc -c -O2 -sMEMORY64 source/fontstash.c -o fontstash.o && mkdir -p wasm && emar rcs wasm/fontstash.a fontstash.o ;;
*) echo "usage: $0 linux|macos|wasm"; exit 1 ;;
esac
rm fontstash.o

Binary file not shown.

View File

@ -1,43 +0,0 @@
// Fontstash with stb_truetype, as a plain C library: no rendering backend,
// glyphs reach the caller through FONSparams' render callbacks. Only the parts
// the engine uses are bound. Libraries are built with build.sh.
FONScontext :: struct {}
FONS_ZERO_TOPLEFT :: 1;
FONSparams :: struct {
width, height : s32;
flags : u8;
userPtr : *void;
renderCreate : (uptr: *void, width: s32, height: s32) -> s32 #c_call;
renderResize : (uptr: *void, width: s32, height: s32) -> s32 #c_call;
renderUpdate : (uptr: *void, rect: *s32, data: *u8) #c_call;
renderDraw : (uptr: *void, verts: *float, tcoords: *float, colors: *u32, nverts: s32) #c_call;
renderDelete : (uptr: *void) #c_call;
}
fonsCreateInternal :: (params: *FONSparams) -> *FONScontext #foreign fontstash;
fonsResetAtlas :: (s: *FONScontext, width: s32, height: s32) -> s32 #foreign fontstash;
fonsAddFontMem :: (s: *FONScontext, name: *u8, data: *u8, ndata: s32, free_data: s32) -> s32 #foreign fontstash;
fonsClearState :: (s: *FONScontext) #foreign fontstash;
fonsSetSize :: (s: *FONScontext, size: float) #foreign fontstash;
fonsSetColor :: (s: *FONScontext, color: u32) #foreign fontstash;
fonsSetFont :: (s: *FONScontext, font: s32) #foreign fontstash;
fonsDrawText :: (s: *FONScontext, x: float, y: float, str: *u8, end: *u8) -> float #foreign fontstash;
fonsTextBounds :: (s: *FONScontext, x: float, y: float, str: *u8, end: *u8, bounds: *float) -> float #foreign fontstash;
fonsVertMetrics :: (s: *FONScontext, ascender: *float, descender: *float, line_height: *float) #foreign fontstash;
fonsGetTextureData :: (s: *FONScontext, width: *s32, height: *s32) -> *u8 #foreign fontstash;
#scope_file
#if OS == .LINUX {
fontstash :: #library,no_dll "linux/fontstash";
#library,system,link_always "libm";
} else #if OS == .MACOS {
fontstash :: #library,no_dll "macos/fontstash";
} else #if OS == .WASM {
fontstash :: #library,no_dll "wasm/fontstash";
} else {
#assert false "Fontstash has no library for this OS; see build.sh.";
}

View File

@ -1,4 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#define FONTSTASH_IMPLEMENTATION
#include "fontstash.h"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

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

View File

@ -7,10 +7,6 @@ input_mouse_y : float;
set_custom_cursor_handling :: (is_custom: bool) {
}
plat_set_cursor_lock :: (locked: bool) {
sapp_lock_mouse(locked);
}
get_key_code :: (key: sapp_keycode) -> Key_Code {
using Key_Code;
// non-ascii ordered as in ./module.jai/Key_Code
@ -131,9 +127,7 @@ handle_sokol_event :: (event: *sapp_event) {
new_event.key_code = mapped;
new_event.key_pressed = 1;
add_modifier_info(event, *new_event);
// OS key-repeat must not re-trigger START
if event.key_repeat input_button_states[mapped] |= .DOWN;
else input_button_states[mapped] = .START | .DOWN;
input_button_states[mapped] = .START | .DOWN;
case .KEY_UP;
mapped := get_key_code(event.key_code);
new_event.type = .KEYBOARD;
@ -166,10 +160,6 @@ handle_sokol_event :: (event: *sapp_event) {
case .MOUSE_MOVE;
input_mouse_x = event.mouse_x;
input_mouse_y = event.mouse_y;
mouse_delta_x = cast(s32)event.mouse_dx;
mouse_delta_y = cast(s32)event.mouse_dy;
return;
case .MOUSE_SCROLL;
mouse_delta_z = cast(int) event.scroll_y;

View File

@ -332,6 +332,7 @@ parse_object :: (str: string) -> result: JSON_Object, remainder: string, success
return result, remainder, true;
}
init(*result, 32);
while true {
if remainder[0] != #char "\"" return result, remainder, false;

View File

@ -540,6 +540,7 @@ parse_object :: (str: string, slot: *u8, info: *Type_Info_Struct, ignore_unknown
// @Speed: Building this table every time is pretty silly.
// We should probably either not build it at all or cache it somewhere.
member_table: Table(string, Member_Offset);
init(*member_table);
defer deinit(*member_table);
if info fill_member_table(*member_table, info, rename);

Binary file not shown.

View File

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

View File

@ -32,7 +32,6 @@ build_lib_wasm_release sokol_fetch fetch/sokol_fetch_wasm_gl_release SOKOL
build_lib_wasm_release sokol_audio audio/sokol_audio_wasm_gl_release SOKOL_GLES3
build_lib_wasm_release sokol_gl gl/sokol_gl_wasm_gl_release SOKOL_GLES3
build_lib_wasm_release stb_image stbi/stb_image SOKOL_GLES3
build_lib_wasm_release stb_vorbis stbv/stb_vorbis SOKOL_GLES3
# wasm + GL + Debug
build_lib_wasm_debug sokol_log log/sokol_log_wasm_gl_debug SOKOL_GLES3
@ -48,6 +47,5 @@ build_lib_wasm_debug sokol_fetch fetch/sokol_fetch_wasm_gl_debug SOKOL_G
build_lib_wasm_debug sokol_audio audio/sokol_audio_wasm_gl_debug SOKOL_GLES3
build_lib_wasm_debug sokol_gl gl/sokol_gl_wasm_gl_debug SOKOL_GLES3
build_lib_wasm_debug stb_image stbi/stb_image SOKOL_GLES3
build_lib_wasm_debug stb_vorbis stbv/stb_vorbis SOKOL_GLES3
rm *.o

View File

@ -1,9 +0,0 @@
#ifdef WIN32
#define __EXPORT __declspec(dllexport)
#else
#define __EXPORT
#endif
#define extern extern __EXPORT
#include "stb_vorbis_impl.c"

File diff suppressed because it is too large Load Diff

View File

@ -1,273 +0,0 @@
//
// This file was auto-generated using the following command:
//
// jai modules/stb_vorbis/generate.jai
//
// normally stb_vorbis uses malloc() to allocate memory at startup,
// and alloca() to allocate temporary memory during a frame on the
// stack. (Memory consumption will depend on the amount of setup
// data in the file and how you set the compile flags for speed
// vs. size. In my test files the maximal-size usage is ~150KB.)
//
// You can modify the wrapper functions in the source (setup_malloc,
// setup_temp_malloc, temp_malloc) to change this behavior, or you
// can use a simpler allocation model: you pass in a buffer from
// which stb_vorbis will allocate _all_ its memory (including the
// temp memory). "open" may fail with a VORBIS_outofmem if you
// do not pass in enough data; there is no way to determine how
// much you do need except to succeed (at which point you can
// query get_info to find the exact amount required. yes I know
// this is lame).
//
// If you pass in a non-NULL buffer of the type below, allocation
// will occur from it as described above. Otherwise just pass NULL
// to use malloc()/alloca()
stb_vorbis_alloc :: struct {
alloc_buffer: *u8;
alloc_buffer_length_in_bytes: s32;
}
stb_vorbis :: struct {}
stb_vorbis_info :: struct {
sample_rate: u32;
channels: s32;
setup_memory_required: u32;
setup_temp_memory_required: u32;
temp_memory_required: u32;
max_frame_size: s32;
}
stb_vorbis_comment :: struct {
vendor: *u8;
comment_list_length: s32;
comment_list: **u8;
}
// get general information about the file
stb_vorbis_get_info :: (f: *stb_vorbis) -> stb_vorbis_info #foreign lib_stb_vorbis;
// get ogg comments
stb_vorbis_get_comment :: (f: *stb_vorbis) -> stb_vorbis_comment #foreign lib_stb_vorbis;
// get the last error detected (clears it, too)
stb_vorbis_get_error :: (f: *stb_vorbis) -> s32 #foreign lib_stb_vorbis;
// close an ogg vorbis file and free all memory in use
stb_vorbis_close :: (f: *stb_vorbis) -> void #foreign lib_stb_vorbis;
// this function returns the offset (in samples) from the beginning of the
// file that will be returned by the next decode, if it is known, or -1
// otherwise. after a flush_pushdata() call, this may take a while before
// it becomes valid again.
// NOT WORKING YET after a seek with PULLDATA API
stb_vorbis_get_sample_offset :: (f: *stb_vorbis) -> s32 #foreign lib_stb_vorbis;
// returns the current seek point within the file, or offset from the beginning
// of the memory buffer. In pushdata mode it returns 0.
stb_vorbis_get_file_offset :: (f: *stb_vorbis) -> u32 #foreign lib_stb_vorbis;
// this API allows you to get blocks of data from any source and hand
// them to stb_vorbis. you have to buffer them; stb_vorbis will tell
// you how much it used, and you have to give it the rest next time;
// and stb_vorbis may not have enough data to work with and you will
// need to give it the same data again PLUS more. Note that the Vorbis
// specification does not bound the size of an individual frame.
stb_vorbis_open_pushdata :: (datablock: *u8, datablock_length_in_bytes: s32, datablock_memory_consumed_in_bytes: *s32, error: *s32, alloc_buffer: *stb_vorbis_alloc) -> *stb_vorbis #foreign lib_stb_vorbis;
// create a vorbis decoder by passing in the initial data block containing
// the ogg&vorbis headers (you don't need to do parse them, just provide
// the first N bytes of the file--you're told if it's not enough, see below)
// on success, returns an stb_vorbis *, does not set error, returns the amount of
// data parsed/consumed on this call in *datablock_memory_consumed_in_bytes;
// on failure, returns NULL on error and sets *error, does not change *datablock_memory_consumed
// if returns NULL and *error is VORBIS_need_more_data, then the input block was
// incomplete and you need to pass in a larger block from the start of the file
stb_vorbis_decode_frame_pushdata :: (f: *stb_vorbis, datablock: *u8, datablock_length_in_bytes: s32, channels: *s32, output: ***float, samples: *s32) -> s32 #foreign lib_stb_vorbis;
// decode a frame of audio sample data if possible from the passed-in data block
//
// return value: number of bytes we used from datablock
//
// possible cases:
// 0 bytes used, 0 samples output (need more data)
// N bytes used, 0 samples output (resynching the stream, keep going)
// N bytes used, M samples output (one frame of data)
// note that after opening a file, you will ALWAYS get one N-bytes,0-sample
// frame, because Vorbis always "discards" the first frame.
//
// Note that on resynch, stb_vorbis will rarely consume all of the buffer,
// instead only datablock_length_in_bytes-3 or less. This is because it wants
// to avoid missing parts of a page header if they cross a datablock boundary,
// without writing state-machiney code to record a partial detection.
//
// The number of channels returned are stored in *channels (which can be
// NULL--it is always the same as the number of channels reported by
// get_info). *output will contain an array of float* buffers, one per
// channel. In other words, (*output)[0][0] contains the first sample from
// the first channel, and (*output)[1][0] contains the first sample from
// the second channel.
//
// *output points into stb_vorbis's internal output buffer storage; these
// buffers are owned by stb_vorbis and application code should not free
// them or modify their contents. They are transient and will be overwritten
// once you ask for more data to get decoded, so be sure to grab any data
// you need before then.
stb_vorbis_flush_pushdata :: (f: *stb_vorbis) -> void #foreign lib_stb_vorbis;
stb_vorbis_decode_filename :: (filename: *u8, channels: *s32, sample_rate: *s32, output: **s16) -> s32 #foreign lib_stb_vorbis;
stb_vorbis_decode_memory :: (mem: *u8, len: s32, channels: *s32, sample_rate: *s32, output: **s16) -> s32 #foreign lib_stb_vorbis;
// decode an entire file and output the data interleaved into a malloc()ed
// buffer stored in *output. The return value is the number of samples
// decoded, or -1 if the file could not be opened or was not an ogg vorbis file.
// When you're done with it, just free() the pointer returned in *output.
stb_vorbis_open_memory :: (data: *u8, len: s32, error: *s32, alloc_buffer: *stb_vorbis_alloc) -> *stb_vorbis #foreign lib_stb_vorbis;
stb_vorbis_open_filename :: (filename: *u8, error: *s32, alloc_buffer: *stb_vorbis_alloc) -> *stb_vorbis #foreign lib_stb_vorbis;
// create an ogg vorbis decoder from a filename via fopen(). on failure,
// returns NULL and sets *error (possibly to VORBIS_file_open_failure).
stb_vorbis_open_file :: (f: *FILE, close_handle_on_close: s32, error: *s32, alloc_buffer: *stb_vorbis_alloc) -> *stb_vorbis #foreign lib_stb_vorbis;
// create an ogg vorbis decoder from an open FILE *, looking for a stream at
// the _current_ seek point (ftell). on failure, returns NULL and sets *error.
// note that stb_vorbis must "own" this stream; if you seek it in between
// calls to stb_vorbis, it will become confused. Moreover, if you attempt to
// perform stb_vorbis_seek_*() operations on this file, it will assume it
// owns the _entire_ rest of the file after the start point. Use the next
// function, stb_vorbis_open_file_section(), to limit it.
stb_vorbis_open_file_section :: (f: *FILE, close_handle_on_close: s32, error: *s32, alloc_buffer: *stb_vorbis_alloc, len: u32) -> *stb_vorbis #foreign lib_stb_vorbis;
stb_vorbis_seek_frame :: (f: *stb_vorbis, sample_number: u32) -> s32 #foreign lib_stb_vorbis;
stb_vorbis_seek :: (f: *stb_vorbis, sample_number: u32) -> s32 #foreign lib_stb_vorbis;
// these functions seek in the Vorbis file to (approximately) 'sample_number'.
// after calling seek_frame(), the next call to get_frame_*() will include
// the specified sample. after calling stb_vorbis_seek(), the next call to
// stb_vorbis_get_samples_* will start with the specified sample. If you
// do not need to seek to EXACTLY the target sample when using get_samples_*,
// you can also use seek_frame().
stb_vorbis_seek_start :: (f: *stb_vorbis) -> s32 #foreign lib_stb_vorbis;
// this function is equivalent to stb_vorbis_seek(f,0)
stb_vorbis_stream_length_in_samples :: (f: *stb_vorbis) -> u32 #foreign lib_stb_vorbis;
stb_vorbis_stream_length_in_seconds :: (f: *stb_vorbis) -> float #foreign lib_stb_vorbis;
// these functions return the total length of the vorbis stream
stb_vorbis_get_frame_float :: (f: *stb_vorbis, channels: *s32, output: ***float) -> s32 #foreign lib_stb_vorbis;
stb_vorbis_get_frame_short_interleaved :: (f: *stb_vorbis, num_c: s32, buffer: *s16, num_shorts: s32) -> s32 #foreign lib_stb_vorbis;
stb_vorbis_get_frame_short :: (f: *stb_vorbis, num_c: s32, buffer: **s16, num_samples: s32) -> s32 #foreign lib_stb_vorbis;
// Channel coercion rules:
// Let M be the number of channels requested, and N the number of channels present,
// and Cn be the nth channel; let stereo L be the sum of all L and center channels,
// and stereo R be the sum of all R and center channels (channel assignment from the
// vorbis spec).
// M N output
// 1 k sum(Ck) for all k
// 2 * stereo L, stereo R
// k l k > l, the first l channels, then 0s
// k l k <= l, the first k channels
// Note that this is not _good_ surround etc. mixing at all! It's just so
// you get something useful.
stb_vorbis_get_samples_float_interleaved :: (f: *stb_vorbis, channels: s32, buffer: *float, num_floats: s32) -> s32 #foreign lib_stb_vorbis;
stb_vorbis_get_samples_float :: (f: *stb_vorbis, channels: s32, buffer: **float, num_samples: s32) -> s32 #foreign lib_stb_vorbis;
stb_vorbis_get_samples_short_interleaved :: (f: *stb_vorbis, channels: s32, buffer: *s16, num_shorts: s32) -> s32 #foreign lib_stb_vorbis;
stb_vorbis_get_samples_short :: (f: *stb_vorbis, channels: s32, buffer: **s16, num_samples: s32) -> s32 #foreign lib_stb_vorbis;
//////// ERROR CODES
STBVorbisError :: enum u32 {
no_error :: 0;
need_more_data :: 1;
invalid_api_mixing :: 2;
outofmem :: 3;
feature_not_supported :: 4;
too_many_channels :: 5;
file_open_failure :: 6;
seek_without_length :: 7;
unexpected_eof :: 10;
seek_invalid :: 11;
invalid_setup :: 20;
invalid_stream :: 21;
missing_capture_pattern :: 30;
invalid_stream_structure_version :: 31;
continued_packet_flag_invalid :: 32;
incorrect_stream_serial_number :: 33;
invalid_first_page :: 34;
bad_packet_type :: 35;
cant_find_last_page :: 36;
seek_failed :: 37;
ogg_skeleton_not_supported :: 38;
VORBIS__no_error :: no_error;
VORBIS_need_more_data :: need_more_data;
VORBIS_invalid_api_mixing :: invalid_api_mixing;
VORBIS_outofmem :: outofmem;
VORBIS_feature_not_supported :: feature_not_supported;
VORBIS_too_many_channels :: too_many_channels;
VORBIS_file_open_failure :: file_open_failure;
VORBIS_seek_without_length :: seek_without_length;
VORBIS_unexpected_eof :: unexpected_eof;
VORBIS_seek_invalid :: seek_invalid;
VORBIS_invalid_setup :: invalid_setup;
VORBIS_invalid_stream :: invalid_stream;
VORBIS_missing_capture_pattern :: missing_capture_pattern;
VORBIS_invalid_stream_structure_version :: invalid_stream_structure_version;
VORBIS_continued_packet_flag_invalid :: continued_packet_flag_invalid;
VORBIS_incorrect_stream_serial_number :: incorrect_stream_serial_number;
VORBIS_invalid_first_page :: invalid_first_page;
VORBIS_bad_packet_type :: bad_packet_type;
VORBIS_cant_find_last_page :: cant_find_last_page;
VORBIS_seek_failed :: seek_failed;
VORBIS_ogg_skeleton_not_supported :: ogg_skeleton_not_supported;
}
#scope_file
#if OS == .WINDOWS {
lib_stb_vorbis :: #library "windows/stb_vorbis";
} else #if OS == .LINUX {
lib_stb_vorbis :: #library "linux/stb_vorbis";
} else #if OS == .MACOS {
lib_stb_vorbis :: #library "macos/stb_vorbis";
} else #if OS == .ANDROID {
#if CPU == .X64 {
lib_stb_vorbis :: #library "android/x64/stb_vorbis";
} else #if CPU == .ARM64 {
lib_stb_vorbis :: #library "android/arm64/stb_vorbis";
}
} else #if OS == .PS5 {
lib_stb_vorbis :: #library "ps5/stb_vorbis";
} else #if OS == .NN_SWITCH2 {
#if CPU == .X64 {
lib_stb_vorbis :: #library "windows/stb_vorbis";
} else #if CPU == .ARM64 {
lib_stb_vorbis :: #library "switch2/stb_vorbis";
}
} else #if OS == .WASM {
lib_stb_vorbis :: #library,no_dll "wasm/stb_vorbis";
} else {
#assert false;
}

View File

@ -1,182 +0,0 @@
AT_COMPILE_TIME :: true;
PS5_SUPPORT :: false;
NINTENDO_SUPPORT :: #run to_string(getenv("NINTENDO_SDK_ROOT")) != "";
SOURCE_PATH :: "source";
LIB_BASE_NAME :: "stb_vorbis";
LIB_DECLARATION_NAME :: "lib_stb_vorbis";
DECLARATIONS_TO_OMIT :: string.[
"STB_VORBIS_HEADER_ONLY", // No reason to export this.
];
#if AT_COMPILE_TIME {
#run,stallable {
set_build_options_dc(.{do_output=false});
options := get_build_options();
args := options.compile_time_command_line;
if !generate_bindings(args, options.minimum_os_version) {
compiler_set_workspace_status(.FAILED);
}
}
} else {
#import "System";
main :: () {
set_working_directory(path_strip_filename(get_path_of_running_executable()));
if !generate_bindings(get_command_line_arguments(), #run get_build_options().minimum_os_version) {
exit(1);
}
}
}
generate_bindings :: (args: [] string, minimum_os_version: type_of(Build_Options.minimum_os_version)) -> bool {
using BG;
target_android := array_find(args, "-android");
target_switch2 := array_find(args, "-switch2");
target_ps5 := array_find(args, "-ps5");
target_x64 := array_find(args, "-x64");
target_arm := array_find(args, "-arm64");
compile := array_find(args, "-compile");
compile_debug := array_find(args, "-debug");
os_target := OS;
cpu_target := CPU;
if target_android os_target = .ANDROID;
if target_switch2 os_target = .NN_SWITCH2;
if target_ps5 os_target = .PS5;
if target_x64 cpu_target = .X64;
if target_arm cpu_target = .ARM64;
build_dynamic_lib := true;
lib_directory: string;
if os_target == {
case .WINDOWS;
lib_directory = "windows";
case .LINUX;
lib_directory = "linux";
case .MACOS;
lib_directory = "macos";
case .ANDROID;
lib_directory = ifx cpu_target == .X64 then "android/x64" else "android/arm64";
case .PS5;
build_dynamic_lib = false;
lib_directory = "ps5";
case .NN_SWITCH2;
build_dynamic_lib = false;
lib_directory = "switch2";
case;
assert(false);
}
if compile {
source_file := tprint("%/stb_vorbis_custom.c", SOURCE_PATH);
make_directory_if_it_does_not_exist(lib_directory, recursive = true);
lib_path := tprint("%/%", lib_directory, LIB_BASE_NAME);
success := true;
if os_target == .MACOS {
success &&= Build_Cpp.build_universal_macos_binary(lib_path, source_file, type = .DYNAMIC_LIBRARY, debug = compile_debug);
success &&= Build_Cpp.build_universal_macos_binary(lib_path, source_file, type = .STATIC_LIBRARY, debug = compile_debug);
} else {
extra: [..] string;
if os_target == .ANDROID {
_, target_triple_with_sdk := get_android_target_triple(cpu_target);
array_add(*extra, "-target", target_triple_with_sdk);
}
if os_target != .WINDOWS {
array_add(*extra, "-fPIC");
}
if build_dynamic_lib {
success &&= Build_Cpp.build_cpp_dynamic_lib(lib_path, source_file, target = os_target, debug = compile_debug, extra = extra);
}
success &&= Build_Cpp.build_cpp_static_lib(lib_path, source_file, target = os_target, debug = compile_debug, extra = extra);
}
if !success return false;
}
options: Generate_Bindings_Options;
options.os = os_target;
options.cpu = cpu_target;
{
using options;
array_add(*extra_clang_arguments, "-DSTB_VORBIS_HEADER_ONLY");
array_add(*library_search_paths, lib_directory);
array_add(*libraries, .{filename=LIB_BASE_NAME, identifier=LIB_DECLARATION_NAME});
array_add(*source_files, tprint("%/stb_vorbis.c", SOURCE_PATH));
generate_library_declarations = false;
footer = tprint(FOOTER_TEMPLATE, LIB_BASE_NAME, LIB_DECLARATION_NAME);
auto_detect_enum_prefixes = true;
log_stripped_declarations = false;
generate_compile_time_struct_checks = false;
visitor = stb_visitor;
}
output_filename := "bindings.jai";
return generate_bindings(options, output_filename);
}
FOOTER_TEMPLATE :: #string END
#if OS == .WINDOWS {
%2 :: #library "windows/%1";
} else #if OS == .LINUX {
%2 :: #library "linux/%1";
} else #if OS == .MACOS {
%2 :: #library "macos/%1";
} else #if OS == .ANDROID {
#if CPU == .X64 {
%2 :: #library "android/x64/%1";
} else #if CPU == .ARM64 {
%2 :: #library "android/arm64/%1";
}
} else #if OS == .PS5 {
%2 :: #library "ps5/%1";
} else #if OS == .NN_SWITCH2 {
#if CPU == .X64 {
%2 :: #library "windows/%1";
} else #if CPU == .ARM64 {
%2 :: #library "switch2/%1";
}
} else #if OS == .WASM {
%2 :: #library,no_dll "wasm/%1";
} else {
#assert false;
}
END
stb_visitor :: (decl: *BG.Declaration, parent_decl: *BG.Declaration) -> BG.Declaration_Visit_Result {
if !parent_decl {
if array_find(DECLARATIONS_TO_OMIT, decl.name) {
decl.decl_flags |= .OMIT_FROM_OUTPUT;
return .STOP;
}
}
return .RECURSE;
}
#import "Basic";
BG :: #import "Bindings_Generator"(NINTENDO_SUPPORT = NINTENDO_SUPPORT);
Build_Cpp :: #import "BuildCpp"()(PS5_SUPPORT = PS5_SUPPORT, NINTENDO_SUPPORT = NINTENDO_SUPPORT);
#import "Compiler";
#import "File";
#import "Android/Toolchain";
#if OS == .WINDOWS {
#import "Windows";
} else {
#import "POSIX";
}

View File

@ -1,11 +0,0 @@
#load "bindings.jai";
#if OS == .WINDOWS || OS == .PS5 || OS == .NN_SWITCH2 || OS == .WASM {
#scope_module
FILE :: void;
} else #if OS_IS_UNIX {
#import "POSIX";
#library,system,link_always "libm";
} else {
#assert false;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
#ifdef WIN32
#define __EXPORT __declspec(dllexport)
#else
#define __EXPORT
#endif
#define extern extern __EXPORT
#include "stb_vorbis.c"

Binary file not shown.

View File

@ -1,142 +0,0 @@
#!/bin/bash
# Runs every test tier and exits nonzero if any of them fail.
#
# ./run_tests.sh # everything that can run here
# ./run_tests.sh unit # compile-time tests only (no display needed)
# ./run_tests.sh engine # engine tiers only
# ./run_tests.sh game # game tiers only
#
# Tiers:
# test_engine engine unit tests compile-time #run, builds against test_game/
# test_game game unit tests compile-time #run, builds against game/
# test_exe_engine engine exe tests builds then runs ./first against test_game/
# test_exe_game game exe tests builds then runs ./first against game/
#
# The unit tiers report through the compiler's exit code. The exe tiers run the
# real app, so they need a display and are wrapped in a hard timeout.
set -uo pipefail
cd "$(dirname "$0")"
JAI="${JAI:-$HOME/bin/jai/bin/jai-linux}"
EXE_TIMEOUT="${EXE_TIMEOUT:-180}" # seconds per exe-test binary, kills a true freeze
FILTER="${1:-all}"
if [ ! -x "$JAI" ]; then
echo "error: Jai compiler not found at '$JAI' (override with JAI=/path/to/jai)" >&2
exit 1
fi
# The game/ directory is not in the repo, so the game tiers are skipped rather
# than failed on a checkout that does not have it.
HAVE_GAME=0
[ -f game/game.jai ] && HAVE_GAME=1
# Exe tests open a real window. Prefer a live display, fall back to Xvfb.
XVFB=()
NO_DISPLAY=0
if [ -z "${DISPLAY:-}" ]; then
if command -v xvfb-run >/dev/null 2>&1; then
XVFB=(xvfb-run -a)
else
NO_DISPLAY=1
fi
fi
PASSED=(); FAILED=(); SKIPPED=()
want() {
case "$FILTER" in
all) return 0 ;;
unit) [[ "$1" != *exe* ]] ;;
engine) [[ "$1" == *engine* ]] ;;
game) [[ "$1" == *game* && "$1" != *engine* ]] ;;
*) echo "error: unknown filter '$FILTER' (use all|unit|engine|game)" >&2; exit 1 ;;
esac
}
run_unit_tier() {
local flag="$1"
echo ""
echo "=============================================================="
echo " $flag (compile-time)"
echo "=============================================================="
if "$JAI" first.jai - "$flag"; then
PASSED+=("$flag")
else
FAILED+=("$flag")
fi
}
run_exe_tier() {
local flag="$1"
echo ""
echo "=============================================================="
echo " $flag (builds, then runs the app)"
echo "=============================================================="
if ! "$JAI" first.jai - "$flag"; then
echo "[$flag] build failed"
FAILED+=("$flag (build)")
return
fi
timeout --foreground "$EXE_TIMEOUT" "${XVFB[@]}" ./first
local status=$?
if [ $status -eq 0 ]; then
PASSED+=("$flag")
elif [ $status -eq 124 ]; then
echo "[$flag] hung and was killed after ${EXE_TIMEOUT}s"
FAILED+=("$flag (hung)")
else
echo "[$flag] exited $status"
FAILED+=("$flag")
fi
}
for flag in test_engine test_game; do
want "$flag" || continue
if [ "$flag" = "test_game" ] && [ "$HAVE_GAME" -eq 0 ]; then
SKIPPED+=("$flag (no game/ directory)")
continue
fi
run_unit_tier "$flag"
done
for flag in test_exe_engine test_exe_game; do
want "$flag" || continue
if [ "$NO_DISPLAY" -eq 1 ]; then
SKIPPED+=("$flag (no DISPLAY and no xvfb-run)")
continue
fi
if [ "$flag" = "test_exe_game" ] && [ "$HAVE_GAME" -eq 0 ]; then
SKIPPED+=("$flag (no game/ directory)")
continue
fi
run_exe_tier "$flag"
done
echo ""
echo "=============================================================="
echo " Summary"
echo "=============================================================="
for t in ${PASSED[@]+"${PASSED[@]}"}; do echo " PASS $t"; done
for t in ${SKIPPED[@]+"${SKIPPED[@]}"}; do echo " SKIP $t"; done
for t in ${FAILED[@]+"${FAILED[@]}"}; do echo " FAIL $t"; done
if [ ${#FAILED[@]} -ne 0 ]; then
echo ""
echo "${#FAILED[@]} tier(s) failed."
exit 1
fi
# A run where everything was skipped is not a pass.
if [ ${#PASSED[@]} -eq 0 ]; then
echo ""
echo "Nothing ran."
exit 1
fi
echo ""
echo "All ${#PASSED[@]} tier(s) passed."

View File

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

View File

@ -6,7 +6,8 @@ Pool :: #import "Pool";
#load "loaders.jai";
// MAX_PACK_SIZE, MAX_WORLD_SIZE and MAX_CHUNKS_SIZE are injected as build strings
// 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.
MAX_FETCH_SLOTS :: 16;
@ -24,7 +25,9 @@ Fetch_Type :: enum {
PACK;
WORLD;
WORLD_CHUNKS;
WORLD_PROBES;
RDM_ATLAS;
RDM_MANIFEST;
SHGRID;
}
Fetch_Request :: struct {
@ -35,8 +38,9 @@ Fetch_Request :: struct {
// PACK
pack_name : string;
// WORLD
// WORLD / RDM / SH
world_name : string;
chunk_key : Chunk_Key;
// WORLD_CHUNKS: JSON carried forward from the WORLD fetch
world_json_data : []u8;
@ -62,12 +66,17 @@ g_asset_manager : Asset_Manager;
#scope_file
#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 .WORLD_PROBES; return MAX_PROBES_SIZE;
case .RDM_ATLAS; return MAX_RDM_ATLAS_SIZE;
case .RDM_MANIFEST; return MAX_RDM_MANIFEST_SIZE;
case .SHGRID; return MAX_SHGRID_SIZE;
}
}
@ -127,8 +136,14 @@ handle_fetch_failed :: (req: *Fetch_Request) {
free(req.world_json_data.data);
log_error("Failed to load chunks.bin for world '%'", req.world_name);
case .WORLD_PROBES;
log_info("No baked probes for world '%'", req.world_name);
case .RDM_ATLAS;
log_info("RDM: no global atlas for world '%' (ok if none baked yet)", req.world_name);
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);
}
}
@ -153,15 +168,28 @@ process_completed_fetch :: (req: *Fetch_Request, data: []u8) {
array_add(*g_asset_manager.loadedPacks, pack);
case .WORLD;
json_copy := NewArray(data.count, u8, false);
memcpy(json_copy.data, data.data, data.count);
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.world_json_data = json_copy;
array_add(*g_asset_manager.queue, chunks_req);
is_json := data.count > 0 && data[0] == #char "{";
if is_json {
json_copy := NewArray(data.count, u8, false);
memcpy(json_copy.data, data.data, data.count);
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.world_json_data = json_copy;
array_add(*g_asset_manager.queue, chunks_req);
} else {
world, ok := load_world_from_data(data);
if ok {
set_loaded_world(world);
rdm_loader_enqueue_world(*get_current_world().world);
shgrid_loader_enqueue_world(*get_current_world().world);
log_info("Loaded world (legacy): %", req.world_name);
} else {
log_error("Failed to parse world '%'", req.world_name);
}
}
case .WORLD_CHUNKS;
json_data := req.world_json_data;
@ -174,22 +202,60 @@ process_completed_fetch :: (req: *Fetch_Request, data: []u8) {
world, ok := load_world_from_json(json_str, data);
if ok {
set_loaded_world(world);
rdm_loader_enqueue_world(*get_current_world().world);
shgrid_loader_enqueue_world(*get_current_world().world);
log_info("Loaded world: %", req.world_name);
probes_req: Fetch_Request;
probes_req.type = .WORLD_PROBES;
probes_req.priority = .BACKGROUND;
probes_req.world_name = req.world_name;
probes_req.path = sprint("%/worlds/%/probes.bin", GAME_RESOURCES_DIR, req.world_name);
array_add(*g_asset_manager.queue, probes_req);
} else {
log_error("Failed to parse world '%'", req.world_name);
}
case .WORLD_PROBES;
// Another world may have been loaded while these were on the way.
case .RDM_ATLAS;
curworld := get_current_world();
if !curworld.valid || curworld.world.name != req.world_name return;
if !load_probes(data) log_error("Broken probes.bin for world '%'", req.world_name);
if !curworld.valid || curworld.world.name != req.world_name then return;
header_size := cast(s64) size_of(RDM_File_Header);
if data.count < header_size {
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 (world %)", req.world_name);
return;
}
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 = .{
render_target = false,
width = header.width,
height = header.height,
pixel_format = sg_pixel_format.RGBA16F,
sample_count = 1,
data = atlas_imgdata,
};
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_MANIFEST;
curworld := get_current_world();
if !curworld.valid || curworld.world.name != req.world_name then return;
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;
}
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);
}
}
@ -199,7 +265,7 @@ Loaded_Pack :: struct {
nameHash : u32 = 0;
data_buffer : []u8;
content : Load_Package;
textures : Table(string, Texture);
textures : Table(string, sg_image);
animations : Table(string, Animation);
audio : Table(string, Audio_Data);
pool : Pool.Pool;
@ -210,7 +276,7 @@ add_resources_from_pack :: (pack: *Loaded_Pack) {
Queued_Sheet_File :: struct {
name : string;
image : Texture;
image : sg_image;
sheet_w : s32;
sheet_h : s32;
sheet : Aseprite_Sheet;
@ -247,12 +313,12 @@ add_resources_from_pack :: (pack: *Loaded_Pack) {
} else {
queuedSheet.sheet = sheet;
}
case "colorgrade.png";
img, x, y := create_texture_from_memory(v.data);
add_image_to_lut_list(img, v.name);
case "wav";
audio := load_wav_from_memory(v.data);
table_set(*pack.audio, name, audio);
case "ogg";
audio := load_ogg_from_memory(v.data);
table_set(*pack.audio, name, audio);
case "json";
if name == "particles" {
s := create_string_from_memory(v.data);
@ -267,17 +333,12 @@ add_resources_from_pack :: (pack: *Loaded_Pack) {
}
log_info("Loaded % particle definitions from pack", g_emitter_defs.count);
}
} else if name == "foliage" {
load_foliage_defs_from_json(create_string_from_memory(v.data));
}
case "hex";
pal : Loaded_Palette;
pal.name = sprint("%", name);
pal.entries = load_hex_palette_from_memory(v.data);
// Pack hot reload brings the same palettes in again.
replaced := false;
for *g_palettes if it.name == pal.name { it.* = pal; replaced = true; }
if !replaced array_add(*g_palettes, pal);
array_add(*g_palettes, pal);
log_info("Loaded % colors from hex palette(%)", pal.entries.count, v.name);
case "ttf";
case;
@ -370,7 +431,7 @@ dispatch_slots :: () {
#scope_export
free_resources_from_pack :: (pack: *Loaded_Pack) {
for pack.textures texture_destroy(it);
for pack.textures sg_destroy_image(it);
table_reset(*pack.textures);
table_reset(*pack.audio);
table_reset(*pack.animations);
@ -448,9 +509,9 @@ load_pack :: (name: string, priority: Load_Priority = .LOADING_SCREEN) {
#scope_file
#scope_export
get_texture_from_pack :: (pack: string, path: string) -> (Texture) {
get_texture_from_pack :: (pack: string, path: string) -> (sg_image) {
found, pack := find_pack_by_name(pack);
invalid_img : Texture;
invalid_img : sg_image;
if !found {
return invalid_img;
@ -488,16 +549,7 @@ add_font_from_pack :: (pack: string, path: string) {
log_error("Failed to find font % from pack", path);
return;
}
state.font_default.face = font_face_add("sans", entry.data);
}
// For files that may legitimately be absent from a pack.
get_raw_file_from_pack :: (pack: string, path: string) -> ([]u8, found: bool) {
pack_ok, loaded := find_pack_by_name(pack);
if !pack_ok return .[], false;
ok, entry := table_find(*loaded.content.lookup, path);
if !ok return .[], false;
return entry.data, true;
state.font_default.fons_font = fonsAddFontMem(state.fons, "sans", entry.data.data, xx entry.data.count, 0);
}
load_string_from_pack :: (pack: string, path: string) -> string {

View File

@ -1,9 +1,26 @@
create_texture_from_memory :: (data: []u8) -> (Texture, s32, s32) {
create_texture_from_memory :: (data: []u8) -> (sg_image, s32, s32) {
x, y, channels : s32;
image := stbi.stbi_load_from_memory(data.data, xx data.count, *x, *y, *channels, 4);
texture := texture_create(x, y, .{x * y * 4, image});
subimg : [6][16]sg_range;
subimg[0][0] = .{
ptr = image,
size = xx (x * y * 4)
};
img := sg_alloc_image();
sg_init_image(img, *(sg_image_desc.{
width = x,
height = y,
pixel_format = sg_pixel_format.RGBA8,
data = .{
subimage = subimg
}
}));
stbi.stbi_image_free(image);
return texture, x, y;
return img, x, y;
}
create_string_from_memory :: (data: []u8) -> string {
@ -47,29 +64,6 @@ load_hex_palette_from_memory :: (data: []u8) -> [..]Palette_Entry {
return entries;
}
// Decoded in full up front, so the mixer sees the same float samples as from a wav;
// only the pack stays small.
load_ogg_from_memory :: (data: []u8) -> Audio_Data {
Vorbis :: #import "stb_vorbis";
audio : Audio_Data;
error : s32;
v := Vorbis.stb_vorbis_open_memory(data.data, cast(s32) data.count, *error, null);
if !v {
log_error("Failed to load ogg file! (stb_vorbis error %)", error);
return audio;
}
defer Vorbis.stb_vorbis_close(v);
info := Vorbis.stb_vorbis_get_info(v);
frames := cast(s64) Vorbis.stb_vorbis_stream_length_in_samples(v);
array_resize(*audio.data, frames * info.channels);
got := Vorbis.stb_vorbis_get_samples_float_interleaved(v, info.channels, audio.data.data, cast(s32) audio.data.count);
audio.data.count = got * info.channels;
audio.channels = cast(s16) info.channels;
audio.sample_rate = cast(s32) info.sample_rate;
return audio;
}
load_wav_from_memory :: (data: []u8) -> Audio_Data {
Wav :: #import "Wav_File";
audio : Audio_Data;

58
src/assets/rdm_loader.jai Normal file
View File

@ -0,0 +1,58 @@
// RDM streaming helpers.
// The unified fetch queue lives in asset_manager.jai; these functions
// enqueue and cancel the world-level global RDM atlas + manifest.
#scope_export
rdm_loader_enqueue_world :: (world: *World) {
already_atlas := false;
already_manifest := false;
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;
}
if !already_atlas {
req : Fetch_Request;
req.type = .RDM_ATLAS;
req.priority = .LOADING_SCREEN;
req.world_name = world.name;
req.path = rdm_global_atlas_filename(world.name);
array_add(*g_asset_manager.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);
}
}
// Drain all pending RDM work.
rdm_loader_cancel_all :: () {
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;
}
}
g_asset_manager.queue.count = new_count;
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 = "";
}
}

119
src/assets/sh_loader.jai Normal file
View File

@ -0,0 +1,119 @@
// SH probe grid streaming helpers.
// The unified fetch queue lives in asset_manager.jai; these functions
// add entries to that queue and handle completed/failed fetches.
#scope_export
shgrid_loader_enqueue_world :: (world: *World) {
for *chunk: world.chunks {
if chunk.sh_valid then continue;
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 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.queue, req);
}
}
sh_loader_handle_failed :: (req: *Fetch_Request) {
log_error("SH: failed to load probe grid for chunk %", req.chunk_key);
}
sh_loader_handle_completed :: (req: *Fetch_Request, data: []u8) {
curworld := get_current_world();
if !curworld.valid || curworld.world.name != req.world_name then return;
header_size := cast(s64) size_of(SH_Grid_File_Header);
if data.count < header_size {
log_error("SH: probe grid too small for chunk %", req.chunk_key);
return;
}
sh_header := cast(*SH_Grid_File_Header) data.data;
if sh_header.magic != SH_FILE_MAGIC {
log_error("SH: bad magic for chunk %", req.chunk_key);
return;
}
n := sh_header.probe_n;
if n != SH_PROBE_N {
log_error("SH: probe_n mismatch for chunk % (file=%, expected=%) — re-bake required", req.chunk_key, n, SH_PROBE_N);
return;
}
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 [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);
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] = (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).*);
}
}
}
sh_imgdata : sg_image_data;
sh_imgdata.subimage[0][0] = .{ packed.data, cast(u64)(n_tex * 4 * size_of(u16)) };
sh_desc : sg_image_desc = .{
width = tex_w,
height = n * n,
pixel_format = sg_pixel_format.RGBA16F,
sample_count = 1,
data = sh_imgdata,
};
chunk := table_find_pointer(*curworld.world.chunks, req.chunk_key);
if chunk != null {
if chunk.sh_probe_grid.id != 0 then sg_destroy_image(chunk.sh_probe_grid);
chunk.sh_probe_grid = sg_make_image(*sh_desc);
chunk.sh_valid = true;
log_debug("SH: loaded probe grid for chunk %", req.chunk_key);
}
free(packed.data);
}

View File

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

View File

@ -2,15 +2,13 @@
#load "iprof.jai";
#load "trile_thumbnails.jai";
#load "picker.jai";
#load "gizmo.jai";
#load "trile_editor.jai";
#load "level_editor.jai";
#load "particle_editor.jai";
#load "material_studio.jai";
#load "foliage_studio.jai";
}
#if FLAG_TACOMA_ENABLED { #load "tacoma.jai"; }
#load "console.jai";
#load "textureDebugger.jai";
#scope_file
@ -19,8 +17,6 @@ Editor_View :: enum {
Trile_Editor;
Level_Editor;
Particle_Editor;
Material_Editor;
Foliage_Editor;
};
current_editor_view : Editor_View = .Trile_Editor;
@ -29,10 +25,6 @@ current_editor_view : Editor_View = .Trile_Editor;
in_editor_view : bool = false;
editor_shows_water :: () -> bool {
return current_editor_view == .Level_Editor;
}
init_editor :: () {
#if !FLAG_RELEASE_BUILD {
#if OS != .WASM {
@ -65,10 +57,6 @@ draw_editor_ui :: (theme: *GR.Overall_Theme) {
if keybind_button(r, "Trile studio", .STUDIO_TRILE, *t_button_selectable(theme, current_editor_view == .Trile_Editor)) { clear_particles(); current_editor_view = .Trile_Editor; }
r.x -= r.w;
if keybind_button(r, "Particle studio", .STUDIO_MATERIAL, *t_button_selectable(theme, current_editor_view == .Particle_Editor)) { clear_particles(); current_editor_view = .Particle_Editor; }
r.x -= r.w;
if keybind_button(r, "Material studio", .STUDIO_SWATCHES, *t_button_selectable(theme, current_editor_view == .Material_Editor)) { clear_particles(); current_editor_view = .Material_Editor; }
r.x -= r.w;
if keybind_button(r, "Foliage studio", .STUDIO_FOLIAGE, *t_button_selectable(theme, current_editor_view == .Foliage_Editor)) { clear_particles(); current_editor_view = .Foliage_Editor; }
if current_editor_view == {
case .Trile_Editor;
@ -77,14 +65,13 @@ draw_editor_ui :: (theme: *GR.Overall_Theme) {
draw_level_editor_ui(theme);
case .Particle_Editor;
draw_particle_editor_ui(theme);
case .Material_Editor;
draw_material_studio_ui(theme);
case .Foliage_Editor;
draw_foliage_studio_ui(theme);
}
}
draw_profiler();
}
draw_texture_debugger(theme);
draw_postprocess_popup(theme);
draw_lighting_popup(theme);
GR.draw_popups();
immediate_flush();
}
@ -109,11 +96,6 @@ tick_editor_ui :: () {
in_editor_view = !in_editor_view;
g_mixer.paused = in_editor_view;
clear_particles();
if in_editor_view {
unlock_cursor();
} else {
lock_cursor_if_should();
}
}
#if !FLAG_RELEASE_BUILD && OS != .WASM {
@ -125,14 +107,14 @@ tick_editor_ui :: () {
#if OS != .WASM {
if in_editor_view {
if current_editor_view == {
case .Trile_Editor;
tick_trile_editor();
case .Level_Editor;
tick_level_editor();
case .Particle_Editor;
tick_particle_editor();
}
if current_editor_view == {
case .Trile_Editor;
tick_trile_editor();
case .Level_Editor;
tick_level_editor();
case .Particle_Editor;
tick_particle_editor();
}
}
}

View File

@ -1,155 +0,0 @@
#scope_file
fs_selected_index : s32 = -1;
fs_scroll : float = 0;
fs_name_input_start : string = "";
fs_anim_input_start : string = "";
fs_label :: (r: *GR.Rect, text: string, theme: *GR.Overall_Theme) {
GR.label(r.*, text, *t_label_left(theme));
r.y += r.h;
}
fs_float :: (r: *GR.Rect, label: string, value: *float, lo: float, hi: float, step: float, theme: *GR.Overall_Theme, loc := #caller_location) {
GR.label(r.*, label, *t_label_left(theme));
r.y += r.h;
GR.slider(r.*, value, lo, hi, step, *theme.slider_theme, loc = loc);
r.y += r.h;
}
fs_select_def :: (idx: s32) {
fs_selected_index = idx;
if idx >= 0 && idx < cast(s32) g_foliage_defs.count {
fs_name_input_start = g_foliage_defs[idx].name;
fs_anim_input_start = g_foliage_defs[idx].animation_name;
}
}
#scope_export
draw_foliage_studio_ui :: (theme: *GR.Overall_Theme) {
panel_w := ui_w(20, 20);
row_h := ui_h(3, 0);
list_r := GR.get_rect(ui_w(80, 20), ui_h(5,0), panel_w, ui_h(95, 0));
ui_add_mouse_occluder(list_r);
draw_bg_rectangle(list_r, theme);
r := list_r;
r.h = row_h;
half_w := r.w / 2;
orig_w := r.w;
r.w = half_w;
if GR.button(r, "New", *theme.button_theme) {
def : Foliage_Def;
def.name = sprint("foliage_%", g_foliage_defs.count);
array_add(*g_foliage_defs, def);
fs_select_def(cast(s32)(g_foliage_defs.count - 1));
}
r.x += half_w;
if GR.button(r, "Duplicate", *theme.button_theme) {
if fs_selected_index >= 0 && fs_selected_index < cast(s32) g_foliage_defs.count {
def := g_foliage_defs[fs_selected_index];
def.name = sprint("%_copy", def.name);
def.animation_name = copy_string(def.animation_name);
array_add(*g_foliage_defs, def);
fs_select_def(cast(s32)(g_foliage_defs.count - 1));
}
}
r.x -= half_w;
r.w = orig_w;
r.y += r.h;
for def, idx : g_foliage_defs {
if r.y > list_r.y + list_r.h - row_h * 3 then break;
selected := (cast(s32)idx == fs_selected_index);
if GR.button(r, def.name, *t_button_selectable(theme, selected), cast(s32)idx) {
fs_select_def(cast(s32)idx);
}
r.y += r.h;
}
r.y = list_r.y + list_r.h - row_h * 2;
r.w = half_w;
if keybind_button(r, "Save", .SAVE, *theme.button_theme) {
save_foliage_defs();
}
r.x += half_w;
if GR.button(r, "Delete", *theme.button_theme) {
if fs_selected_index >= 0 && fs_selected_index < cast(s32) g_foliage_defs.count {
array_ordered_remove_by_index(*g_foliage_defs, fs_selected_index);
if fs_selected_index >= cast(s32) g_foliage_defs.count then fs_selected_index -= 1;
fs_select_def(fs_selected_index);
}
}
r.w = orig_w;
if fs_selected_index < 0 || fs_selected_index >= cast(s32) g_foliage_defs.count then return;
def := *g_foliage_defs[fs_selected_index];
edit_r := GR.get_rect(0, ui_h(5,0), panel_w, ui_h(95, 0));
ui_add_mouse_occluder(edit_r);
draw_bg_rectangle(edit_r, theme);
region, inside := GR.begin_scrollable_region(edit_r, *theme.scrollable_region_theme);
er := inside;
er.y -= fs_scroll;
er.h = row_h;
fs_label(*er, "Name", theme);
a, new_name, _ := GR.text_input(er, fs_name_input_start, *theme.text_input_theme, 0);
if a & .ENTERED {
def.name = copy_string(new_name);
fs_name_input_start = def.name;
}
er.y += er.h;
fs_label(*er, "Animation (pack.anim)", theme);
a2, new_anim, _ := GR.text_input(er, fs_anim_input_start, *theme.text_input_theme, 1);
if a2 & .ENTERED {
def.animation_name = copy_string(new_anim);
fs_anim_input_start = def.animation_name;
}
er.y += er.h;
anim := get_animation_from_string(def.animation_name);
if anim != null {
fs_label(*er, tprint("Animation: % frames", anim.frames.count), theme);
} else {
fs_label(*er, tprint("Animation: not found ('%')", def.animation_name), theme);
}
orig_w2 := er.w;
er.w = orig_w2 / 2;
fs_label(*er, "Type", theme);
if GR.button(er, "Grass", *t_button_selectable(theme, def.type == .GRASS), 100) then def.type = .GRASS;
er.x += er.w;
if GR.button(er, "Tree", *t_button_selectable(theme, def.type == .TREE), 101) then def.type = .TREE;
er.x -= er.w;
er.y += er.h;
fs_label(*er, "Frames", theme);
if GR.button(er, "Variant", *t_button_selectable(theme, def.frame_mode == .VARIANT), 102) then def.frame_mode = .VARIANT;
er.x += er.w;
if GR.button(er, "Loop", *t_button_selectable(theme, def.frame_mode == .LOOP), 103) then def.frame_mode = .LOOP;
er.x -= er.w;
er.y += er.h;
er.w = orig_w2;
if def.type == .GRASS {
fs_float(*er, tprint("Density: %", def.density), *def.density, 0, 1, 0.05, theme);
fs_float(*er, tprint("Jitter: %", def.jitter), *def.jitter, 0, 0.5, 0.05, theme);
}
fs_float(*er, tprint("Scale Min: %", def.scale_min), *def.scale_min, 0.1, 4, 0.05, theme);
fs_float(*er, tprint("Scale Max: %", def.scale_max), *def.scale_max, 0.1, 4, 0.05, theme);
if def.scale_max < def.scale_min then def.scale_max = def.scale_min;
fs_float(*er, tprint("Sway Intensity: %", def.sway_intensity), *def.sway_intensity, 0, 1, 0.05, theme);
fs_float(*er, tprint("Sway Speed: %", def.sway_speed), *def.sway_speed, 0, 5, 0.1, theme);
GR.end_scrollable_region(region, er.x + er.w, er.y, *fs_scroll);
}

View File

@ -1,455 +0,0 @@
// Blender-style gizmos: a translate gizmo with three RGB axis arrows plus three
// plane handles for two-axis drags, and a rotate gizmo with three RGB rings.
// Drawn with depth-ignoring overlay lines so they stay visible through terrain,
// and picked in world space so the pick thresholds scale with the gizmo itself.
Gizmo_Handle :: enum {
NONE;
AXIS_X;
AXIS_Y;
AXIS_Z;
PLANE_YZ; // normal +X
PLANE_XZ; // normal +Y
PLANE_XY; // normal +Z
RING_X; // turns about +X
RING_Y;
RING_Z;
}
Gizmo_State :: struct {
hover : Gizmo_Handle;
active : Gizmo_Handle;
start_position : Vector3; // object position when the drag began
start_t : float; // axis drags: parameter along the axis at grab time
start_hit : Vector3; // plane drags: world point grabbed
start_angle : float; // ring drags: angle around the ring at grab time
applied_steps : int; // ring drags: quarter turns already handed to the caller
}
// Gizmo length as a fraction of the camera distance, so it keeps a constant
// on-screen size. Everything else is expressed in units of that length.
GIZMO_SCALE :: 0.14;
GIZMO_PLANE_INNER :: 0.22;
GIZMO_PLANE_OUTER :: 0.52;
GIZMO_PICK_RADIUS :: 0.08;
// Below this |dot(plane normal, view dir)| the plane handle is edge-on: too thin
// to aim at, so it is neither drawn nor picked.
GIZMO_PLANE_MIN_FACING :: 0.15;
// Ring radius, and how far off it a click can land, both in units of gizmo length.
GIZMO_RING_RADIUS :: 1.0;
GIZMO_RING_PICK :: 0.14;
GIZMO_RING_SEGMENTS :: 48;
// Line widths, in pixels.
GIZMO_LINE_WIDTH :: 3.0;
GIZMO_FILL_WIDTH :: 2.0;
// Runs one frame of the gizmo: hover, click-to-grab, drag, release, and drawing.
// Returns the dragged-to position; the caller owns snapping and writing it back.
gizmo_translate :: (using state: *Gizmo_State, position: Vector3, cam: Camera, ray: Ray) -> (position: Vector3, changed: bool) {
len := max(length(cam.position - position) * GIZMO_SCALE, 0.001);
pos := position;
changed := false;
mouse := get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT);
pressed := (mouse & .START) != .NONE;
held := (mouse & .DOWN) != .NONE;
if active == .NONE {
hover = gizmo_pick(position, len, cam, ray);
if hover != .NONE && pressed {
start_position = position;
if gizmo_is_plane(hover) {
ok, point := gizmo_plane_hit(position, hover, ray);
if ok {
start_hit = point;
active = hover;
}
} else {
t, _, _ := gizmo_closest_axis_t(ray, position, gizmo_axis(hover));
start_t = t;
active = hover;
}
}
} else if !held {
active = .NONE;
} else {
if gizmo_is_plane(active) {
ok, point := gizmo_plane_hit(start_position, active, ray);
if ok {
pos = start_position + (point - start_hit);
changed = true;
}
} else {
axis := gizmo_axis(active);
t, _, _ := gizmo_closest_axis_t(ray, start_position, axis);
pos = start_position + axis * (t - start_t);
changed = true;
}
}
// Drawn at the object's own position rather than the dragged-to one: the
// caller may snap what we hand back, and the gizmo has to sit on the object.
gizmo_draw(position, len, cam, ifx active != .NONE then active else hover);
return pos, changed;
}
// One frame of the rotate gizmo. Entities only hold the 24 axis-aligned cube
// rotations, so a drag reports whole quarter turns: the returned basis is the
// (u, v, n) of the ring being dragged and 'steps' is how many turns of u toward
// v to apply since the last frame, which is usually zero.
gizmo_rotate :: (using state: *Gizmo_State, position: Vector3, cam: Camera, ray: Ray) -> (u: Vector3, v: Vector3, n: Vector3, steps: int) {
len := max(length(cam.position - position) * GIZMO_SCALE, 0.001);
mouse := get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT);
pressed := (mouse & .START) != .NONE;
held := (mouse & .DOWN) != .NONE;
steps := 0;
if active == .NONE {
hover = gizmo_pick_ring(position, len, cam, ray);
if hover != .NONE && pressed {
ok, angle := gizmo_ring_angle(position, hover, ray);
if ok {
start_position = position;
start_angle = angle;
applied_steps = 0;
active = hover;
}
}
} else if !held {
active = .NONE;
} else {
ok, angle := gizmo_ring_angle(start_position, active, ray);
if ok {
// Wrap into (-180, 180] so passing the seam doesn't spin the object.
delta := angle - start_angle;
while delta > PI delta -= 2 * PI;
while delta < -PI delta += 2 * PI;
total := cast(int) floor(delta / (PI * 0.5) + 0.5);
steps = total - applied_steps;
applied_steps = total;
}
}
gizmo_draw_rings(position, len, cam, ifx active != .NONE then active else hover);
handle := ifx active != .NONE then active else Gizmo_Handle.RING_Y;
u, v := gizmo_plane_axes(handle);
return u, v, gizmo_axis(handle), steps;
}
gizmo_axis :: (h: Gizmo_Handle) -> Vector3 {
if h == {
case .AXIS_X; #through;
case .PLANE_YZ; #through;
case .RING_X; return .{1, 0, 0};
case .AXIS_Y; #through;
case .PLANE_XZ; #through;
case .RING_Y; return .{0, 1, 0};
case; return .{0, 0, 1};
}
}
gizmo_is_plane :: (h: Gizmo_Handle) -> bool {
return h == .PLANE_YZ || h == .PLANE_XZ || h == .PLANE_XY;
}
gizmo_is_ring :: (h: Gizmo_Handle) -> bool {
return h == .RING_X || h == .RING_Y || h == .RING_Z;
}
// The two in-plane axes of a plane or ring handle; gizmo_axis gives the third
// (the plane normal, or the axis the ring turns about).
gizmo_plane_axes :: (h: Gizmo_Handle) -> (u: Vector3, v: Vector3) {
if h == {
case .PLANE_YZ; #through;
case .RING_X; return .{0, 1, 0}, .{0, 0, 1};
case .PLANE_XZ; #through;
case .RING_Y; return .{1, 0, 0}, .{0, 0, 1};
case; return .{1, 0, 0}, .{0, 1, 0};
}
}
// Handles are colored by their axis; plane handles by the axis they are
// perpendicular to, which is what gizmo_axis returns for them.
gizmo_color :: (h: Gizmo_Handle, highlighted: bool) -> Vector4 {
if highlighted then return .{1.0, 0.85, 0.2, 1.0};
axis := gizmo_axis(h);
if axis.x != 0 then return .{0.95, 0.28, 0.30, 1.0};
if axis.y != 0 then return .{0.40, 0.90, 0.32, 1.0};
return .{0.30, 0.50, 0.98, 1.0};
}
#scope_file
// Parameter along the line (origin + axis*t) of the point closest to the ray,
// plus that point and the distance between the two lines at closest approach.
gizmo_closest_axis_t :: (ray: Ray, origin: Vector3, axis: Vector3) -> (t: float, point: Vector3, dist: float) {
r := origin - ray.origin;
a := dot(axis, axis);
b := dot(axis, ray.direction);
e := dot(ray.direction, ray.direction);
c := dot(axis, r);
f := dot(ray.direction, r);
denom := a*e - b*b;
if abs(denom) < 0.00001 then return 0, origin, 99999; // ray parallel to the axis
t := (b*f - c*e) / denom;
s := (a*f - c*b) / denom;
pa := origin + axis * t;
pb := ray.origin + ray.direction * s;
return t, pa, length(pa - pb);
}
gizmo_plane_hit :: (origin: Vector3, h: Gizmo_Handle, ray: Ray) -> (bool, Vector3) {
n := gizmo_axis(h);
denom := dot(n, ray.direction);
if abs(denom) < 0.0001 then return false, .{};
t := dot(n, origin - ray.origin) / denom;
if t < 0 then return false, .{}; // plane is behind the camera
return true, ray.origin + ray.direction * t;
}
// Which way each plane handle is offset from the origin: toward the camera, so
// the handles always sit in the octant facing the viewer.
gizmo_plane_signs :: (origin: Vector3, h: Gizmo_Handle, cam: Camera) -> (su: float, sv: float) {
u, v := gizmo_plane_axes(h);
to_cam := cam.position - origin;
su := ifx dot(u, to_cam) >= 0 then cast(float)1 else cast(float)-1;
sv := ifx dot(v, to_cam) >= 0 then cast(float)1 else cast(float)-1;
return su, sv;
}
gizmo_pick :: (origin: Vector3, len: float, cam: Camera, ray: Ray) -> Gizmo_Handle {
// Planes first: they sit closer to the origin and never overlap the arrows,
// but grabbing a plane is the more common intent when they are near.
for h: Gizmo_Handle.[.PLANE_YZ, .PLANE_XZ, .PLANE_XY] {
n := gizmo_axis(h);
if abs(dot(n, normalize(cam.position - origin))) < GIZMO_PLANE_MIN_FACING then continue;
ok, point := gizmo_plane_hit(origin, h, ray);
if !ok then continue;
u, v := gizmo_plane_axes(h);
su, sv := gizmo_plane_signs(origin, h, cam);
d := point - origin;
cu := dot(d, u) * su;
cv := dot(d, v) * sv;
if cu >= GIZMO_PLANE_INNER * len && cu <= GIZMO_PLANE_OUTER * len
&& cv >= GIZMO_PLANE_INNER * len && cv <= GIZMO_PLANE_OUTER * len {
return h;
}
}
best := Gizmo_Handle.NONE;
best_dist := GIZMO_PICK_RADIUS * len;
for h: Gizmo_Handle.[.AXIS_X, .AXIS_Y, .AXIS_Z] {
t, _, dist := gizmo_closest_axis_t(ray, origin, gizmo_axis(h));
if t < 0 || t > len then continue;
if dist < best_dist {
best_dist = dist;
best = h;
}
}
return best;
}
gizmo_draw :: (origin: Vector3, len: float, cam: Camera, highlighted: Gizmo_Handle) {
for h: Gizmo_Handle.[.PLANE_YZ, .PLANE_XZ, .PLANE_XY] {
n := gizmo_axis(h);
if abs(dot(n, normalize(cam.position - origin))) < GIZMO_PLANE_MIN_FACING then continue;
u, v := gizmo_plane_axes(h);
su, sv := gizmo_plane_signs(origin, h, cam);
col := gizmo_color(h, h == highlighted);
gizmo_draw_plane(origin, u * su, v * sv, GIZMO_PLANE_INNER * len, GIZMO_PLANE_OUTER * len, col, h == highlighted);
}
for h: Gizmo_Handle.[.AXIS_X, .AXIS_Y, .AXIS_Z] {
gizmo_draw_arrow(origin, gizmo_axis(h) * len, gizmo_color(h, h == highlighted));
}
}
gizmo_draw_arrow :: (origin: Vector3, vec: Vector3, col: Vector4) {
tip := origin + vec;
shaft := normalize(vec);
debug_line_overlay(origin, tip, col, GIZMO_LINE_WIDTH);
head := length(vec) * 0.18;
back := tip - shaft * head;
perp : Vector3;
if abs(shaft.x) < 0.9 {
perp = normalize(cross(shaft, .{1, 0, 0}));
} else {
perp = normalize(cross(shaft, .{0, 1, 0}));
}
perp2 := cross(shaft, perp);
w1 := perp * (head * 0.4);
w2 := perp2 * (head * 0.4);
// Four fins plus a ring, so the head reads as a cone from any angle.
debug_line_overlay(back + w1, tip, col, GIZMO_LINE_WIDTH);
debug_line_overlay(back - w1, tip, col, GIZMO_LINE_WIDTH);
debug_line_overlay(back + w2, tip, col, GIZMO_LINE_WIDTH);
debug_line_overlay(back - w2, tip, col, GIZMO_LINE_WIDTH);
debug_line_overlay(back + w1, back + w2, col, GIZMO_LINE_WIDTH);
debug_line_overlay(back + w2, back - w1, col, GIZMO_LINE_WIDTH);
debug_line_overlay(back - w1, back - w2, col, GIZMO_LINE_WIDTH);
debug_line_overlay(back - w2, back + w1, col, GIZMO_LINE_WIDTH);
}
// A square in the (u, v) plane between inner and outer, outlined at full alpha
// and hatched at low alpha so it reads as a translucent pane.
gizmo_draw_plane :: (origin: Vector3, u: Vector3, v: Vector3, inner: float, outer: float, col: Vector4, filled: bool) {
p00 := origin + u * inner + v * inner;
p10 := origin + u * outer + v * inner;
p11 := origin + u * outer + v * outer;
p01 := origin + u * inner + v * outer;
debug_line_overlay(p00, p10, col, GIZMO_LINE_WIDTH);
debug_line_overlay(p10, p11, col, GIZMO_LINE_WIDTH);
debug_line_overlay(p11, p01, col, GIZMO_LINE_WIDTH);
debug_line_overlay(p01, p00, col, GIZMO_LINE_WIDTH);
fill := col;
fill.w = ifx filled then 0.55 else 0.18;
// Enough hatch lines that the pane reads as filled rather than striped.
HATCH :: 14;
for i: 1..HATCH {
f := cast(float)i / cast(float)(HATCH + 1);
c := inner + (outer - inner) * f;
debug_line_overlay(origin + u * c + v * inner, origin + u * c + v * outer, fill, GIZMO_FILL_WIDTH);
}
}
// Angle of the mouse ray's hit on the ring's plane, measured from u toward v.
gizmo_ring_angle :: (origin: Vector3, h: Gizmo_Handle, ray: Ray) -> (ok: bool, angle: float) {
ok, point := gizmo_plane_hit(origin, h, ray);
if !ok then return false, 0;
u, v := gizmo_plane_axes(h);
d := point - origin;
return true, atan2(dot(d, v), dot(d, u));
}
gizmo_pick_ring :: (origin: Vector3, len: float, cam: Camera, ray: Ray) -> Gizmo_Handle {
best := Gizmo_Handle.NONE;
best_dist := GIZMO_RING_PICK * len;
for h: Gizmo_Handle.[.RING_X, .RING_Y, .RING_Z] {
if !gizmo_ring_faces_camera(origin, h, cam) then continue;
ok, point := gizmo_plane_hit(origin, h, ray);
if !ok then continue;
// Distance from the ring itself, not from its centre.
off := abs(length(point - origin) - GIZMO_RING_RADIUS * len);
if off < best_dist {
best_dist = off;
best = h;
}
}
return best;
}
// An edge-on ring is a line on screen: impossible to aim at, and its plane
// intersection shoots off to infinity, so skip it.
gizmo_ring_faces_camera :: (origin: Vector3, h: Gizmo_Handle, cam: Camera) -> bool {
return abs(dot(gizmo_axis(h), normalize(cam.position - origin))) >= GIZMO_PLANE_MIN_FACING;
}
gizmo_draw_rings :: (origin: Vector3, len: float, cam: Camera, highlighted: Gizmo_Handle) {
for h: Gizmo_Handle.[.RING_X, .RING_Y, .RING_Z] {
is_hot := h == highlighted;
if !gizmo_ring_faces_camera(origin, h, cam) && !is_hot then continue;
u, v := gizmo_plane_axes(h);
col := gizmo_color(h, is_hot);
r := GIZMO_RING_RADIUS * len;
prev := origin + u * r;
for i: 1..GIZMO_RING_SEGMENTS {
a := (cast(float)i / cast(float)GIZMO_RING_SEGMENTS) * 2 * PI;
next := origin + u * (cos(a) * r) + v * (sin(a) * r);
debug_line_overlay(prev, next, col, GIZMO_LINE_WIDTH);
prev = next;
}
}
}
#if FLAG_TEST_ENGINE {
gizmo_test_cam :: () -> Camera {
// Off the +X+Y+Z octant, so every plane handle faces the viewer and its
// signs are all positive.
cam : Camera;
cam.position = .{5, 5, 5};
cam.target = .{0, 0, 0};
return cam;
}
test_gizmo_closest_axis :: () {
s := begin_suite("gizmo closest point on axis");
// Vertical ray dropping onto the +X axis at x = 0.5.
ray := Ray.{ origin = .{0.5, 3, 0}, direction = .{0, -1, 0} };
t, point, dist := gizmo_closest_axis_t(ray, .{0, 0, 0}, .{1, 0, 0});
check(*s, "t is the distance along the axis", abs(t - 0.5) < 0.001);
check(*s, "closest point sits on the axis", abs(point.x - 0.5) < 0.001 && abs(point.y) < 0.001 && abs(point.z) < 0.001);
check(*s, "intersecting lines have 0 distance", dist < 0.001);
// A ray offset in Z never meets the axis; the gap is that offset.
off := Ray.{ origin = .{0.5, 3, 0.25}, direction = .{0, -1, 0} };
_, _, off_dist := gizmo_closest_axis_t(off, .{0, 0, 0}, .{1, 0, 0});
check(*s, "offset ray reports its offset as distance", abs(off_dist - 0.25) < 0.001);
// Parallel lines have no unique closest point.
par := Ray.{ origin = .{0, 3, 0}, direction = .{1, 0, 0} };
_, _, par_dist := gizmo_closest_axis_t(par, .{0, 0, 0}, .{1, 0, 0});
check(*s, "parallel ray is rejected", par_dist > 1000);
end_suite(s);
}
test_gizmo_pick :: () {
s := begin_suite("gizmo handle picking");
cam := gizmo_test_cam();
len : float : 1.0;
// Straight down onto the middle of the +X arrow.
axis_ray := Ray.{ origin = .{0.5, 3, 0}, direction = .{0, -1, 0} };
check(*s, "aiming at the X arrow picks AXIS_X", gizmo_pick(.{0,0,0}, len, cam, axis_ray) == .AXIS_X);
// Down onto the XZ pane, which spans 0.22..0.52 on both of its axes.
plane_ray := Ray.{ origin = .{0.35, 3, 0.35}, direction = .{0, -1, 0} };
check(*s, "aiming at the XZ pane picks PLANE_XZ", gizmo_pick(.{0,0,0}, len, cam, plane_ray) == .PLANE_XZ);
// Inside the pane's plane but short of its inner edge: no handle there.
gap_ray := Ray.{ origin = .{0.1, 3, 0.1}, direction = .{0, -1, 0} };
check(*s, "the gap inside the panes picks nothing", gizmo_pick(.{0,0,0}, len, cam, gap_ray) == .NONE);
// Past the end of every arrow.
miss_ray := Ray.{ origin = .{2, 3, 2}, direction = .{0, -1, 0} };
check(*s, "aiming past the gizmo picks nothing", gizmo_pick(.{0,0,0}, len, cam, miss_ray) == .NONE);
// Picking follows the gizmo, not the world origin.
moved := Vector3.{10, 4, -7};
moved_ray := Ray.{ origin = moved + Vector3.{0.5, 3, 0}, direction = .{0, -1, 0} };
check(*s, "picking is relative to the gizmo origin", gizmo_pick(moved, len, cam, moved_ray) == .AXIS_X);
end_suite(s);
}
test_gizmo_plane_hit :: () {
s := begin_suite("gizmo plane intersection");
// PLANE_XZ has a +Y normal, so it is the horizontal plane through the origin.
ray := Ray.{ origin = .{1, 4, 2}, direction = .{0, -1, 0} };
ok, point := gizmo_plane_hit(.{0, 0, 0}, .PLANE_XZ, ray);
check(*s, "downward ray hits the XZ pane's plane", ok);
check(*s, "hit lands at the ray's XZ", abs(point.x - 1) < 0.001 && abs(point.z - 2) < 0.001);
check(*s, "hit lands on the plane", abs(point.y) < 0.001);
// The plane passes through the gizmo origin, not the world origin.
shifted, sp := gizmo_plane_hit(.{0, 2.5, 0}, .PLANE_XZ, ray);
check(*s, "plane follows the gizmo origin", shifted && abs(sp.y - 2.5) < 0.001);
away := Ray.{ origin = .{1, 4, 2}, direction = .{0, 1, 0} };
no_hit, _ := gizmo_plane_hit(.{0, 0, 0}, .PLANE_XZ, away);
check(*s, "ray pointing away misses", !no_hit);
end_suite(s);
}
#run {
test_gizmo_closest_axis();
test_gizmo_pick();
test_gizmo_plane_hit();
}
}

View File

@ -36,7 +36,14 @@ draw_line :: (p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, color: Vector4
}
text_width :: (text: string) -> float {
return text_measure(state.font_default.face, xx iprof_font.character_height, text);
if text.count < 1 {
return 0;
}
fonsSetFont(state.fons, state.font_default.fons_font);
fonsSetSize(state.fons, xx iprof_font.character_height);
w := fonsTextBounds(state.fons, 0.0, 0.0, text.data, text.data + text.count, null);
return cast(float) w;
}
draw_rectangle_prof :: (x0: float, y0: float, x1: float, y1: float, color: Vector4) {

View File

@ -29,7 +29,6 @@ Level_Editor_Tool_Mode :: enum {
LINE;
INSPECTOR;
VIEWER;
ENTITY;
}
current_tool_mode : Level_Editor_Tool_Mode = .POINT;
@ -56,24 +55,10 @@ inspector_x : s32;
inspector_y : s32;
inspector_z : s32;
inspector_emitter_def : s32 = -1;
inspector_rdm_roughness : int = 0;
inspector_note_deactivate : bool;
inspector_note_activate_next : bool;
// Entity tool: click to select, drag the gizmo to move or turn, right-click to
// place. The selection is held by id rather than index so removing an entity
// can't silently reselect another.
Entity_Gizmo_Mode :: enum {
MOVE;
ROTATE;
}
entity_gizmo : Gizmo_State;
entity_gizmo_mode : Entity_Gizmo_Mode;
entity_selected_id : s64 = -1;
entity_add_type : s32 = -1;
entity_snap_enabled : bool = true;
entity_snap_step : float = 1.0;
editor_billboards_visible : bool = true;
get_current_orientation :: () -> u8 {
@ -130,7 +115,11 @@ get_level_editor_camera :: () -> Camera {
camera.position = camera.target;
camera.position += cameraDir * cameraDist;
if is_in_reflection_pass {
camera.position.y *= -1;
camera.target.y *= -1;
}
return camera;
}
@ -253,24 +242,37 @@ draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
r.h = ui_h(3,0);
if GR.button(r, "Render with Tacoma", *theme.button_theme) {
cam := get_level_editor_camera();
if curworld.valid gen_reference(tacomaResolution, tacomaResolution, cam.position, cam.target, tacomaSamples, *curworld.world);
gen_reference(tacomaResolution, tacomaResolution, cam.position, cam.target, tacomaSamples, true, curworld.world);
}
r.y += r.h;
if GR.button(r, "Bake probes", *t_button_selectable(theme, !probe_bake.active)) {
if curworld.valid && !probe_bake.active bake_probes(tacomaSamples);
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;
if probe_bake.active {
total := probe_bake.keys.count;
done := probe_bake.results.count;
GR.label(r, tprint("Baking probes: %/% chunks", done, total), *t_label_left(theme));
if GR.button(r, "Bake all chunk SH grids", *t_button_selectable(theme, !sh_bake.active)) {
if curworld.valid && !sh_bake.active sh_bake_start(tacomaSamples);
}
r.y += r.h;
if rdm_bake.active {
total := cast(s32) rdm_bake.jobs.count;
done := rdm_bake.current_job;
pct := ifx total > 0 then done * 100 / total else 0;
GR.label(r, tprint("Baking RDMs: %/% (\%%)", done, total, pct), *t_label_left(theme));
r.y += r.h;
}
if tacoma_preview.texture.id {
aspect := cast(float) tacoma_preview.width / cast(float) tacoma_preview.height;
if sh_bake.active {
total := cast(s32) sh_bake.chunk_keys.count;
done := sh_bake.current_chunk;
pct := ifx total > 0 then done * 100 / total else 0;
GR.label(r, tprint("Baking SH grids: %/% (\%%)", done, total, pct), *t_label_left(theme));
r.y += r.h;
}
if current_screenshot.valid {
aspect := cast(float)current_screenshot.width / cast(float)current_screenshot.height;
r.h = r.w / aspect;
uiTex := New(Ui_Texture,, temp);
uiTex.tex = tacoma_preview.texture;
uiTex.tex = current_screenshot.image;
set_shader_for_images(uiTex);
immediate_quad(.{r.x, r.y}, .{r.x + r.w, r.y}, .{r.x + r.w, r.y + r.h}, .{r.x, r.y + r.h});
set_shader_for_color();
@ -286,9 +288,13 @@ 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, "Emissive light (bake)", *t_label_left(theme));
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_emissive_light, 0.0, 50.0, 0.5, *theme.slider_theme);
GR.slider(r, *bake_sky_scale, 0.0, 5.0, 0.05, *theme.slider_theme);
r.y += r.h;
GR.label(r, tprint("Sky Desaturation (bake): %", formatFloat(bake_sky_desaturation, trailing_width=2)), *t_label_left(theme));
r.y += r.h;
GR.slider(r, *bake_sky_desaturation, 0.0, 1.0, 0.05, *theme.slider_theme);
r.y += r.h;
GR.end_scrollable_region(region, r.x + r.w, r.y, *tacoma_tab_scroll);
@ -314,6 +320,14 @@ draw_tools_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
r.h = ui_h(4, 0);
if keybind_button(r, "Save world", .SAVE, *theme.button_theme) then sworld();
r.y += r.h;
if GR.button(r, "Delete buried triles", *theme.button_theme, 400) {
curworld := get_current_world();
if curworld.valid {
removed := delete_buried_triles(*curworld.world);
log_info("Deleted % buried triles\n", removed);
}
}
r.y += r.h * 2;
// Tool mode buttons
@ -327,8 +341,6 @@ draw_tools_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
r.y += r.h;
if keybind_button(r, "Inspector", .LEVEL_TOOL_INSPECTOR, *t_button_selectable(theme, current_tool_mode == .INSPECTOR)) then current_tool_mode = .INSPECTOR;
r.y += r.h;
if keybind_button(r, "Entity", .LEVEL_TOOL_ENTITY, *t_button_selectable(theme, current_tool_mode == .ENTITY)) then current_tool_mode = .ENTITY;
r.y += r.h;
if keybind_button(r, "Viewer", .LEVEL_TOOL_VIEWER, *t_button_selectable(theme, current_tool_mode == .VIEWER)) then current_tool_mode = .VIEWER;
r.y += r.h;
@ -366,7 +378,7 @@ draw_tools_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
GR.label(r, "Click start of line", *t_label_left(theme));
}
r.y += r.h;
} else if current_tool_mode == .INSPECTOR || current_tool_mode == .ENTITY {
} else if current_tool_mode == .INSPECTOR {
r.h = ui_h(3, 2);
if GR.button(r, ifx editor_billboards_visible then "Hide Markers" else "Show Markers", *theme.button_theme, 199) {
editor_billboards_visible = !editor_billboards_visible;
@ -475,10 +487,6 @@ add_trile :: (name: string, x: s32, y: s32, z: s32, orientation: u8 = 0) {
array_add(*group.instances, inst);
array_add(*chunk.groups, group);
}
upload_chunk(chunk);
trile, ok := get_trile(name);
if !ok || trile.skip_collision then return;
chunk_set_occupancy_mask_at(chunk, lx, ly, lz, occupancy_state_for(trile, orientation));
} @Command
remove_trile :: (x: s32, y: s32, z: s32) {
@ -490,138 +498,18 @@ remove_trile :: (x: s32, y: s32, z: s32) {
lx, ly, lz := world_to_local(x, y, z);
removed := false;
for *group: chunk.groups {
for inst, idx: group.instances {
if inst.x == lx && inst.y == ly && inst.z == lz {
array_unordered_remove_by_index(*group.instances, idx);
removed = true;
break;
return;
}
}
if removed break;
}
if removed upload_chunk(chunk);
chunk_set_occupancy_mask_at(chunk, lx, ly, lz, .FREE);
} @Command
level_editor_clear_entity_selection :: () {
entity_selected_id = -1;
entity_gizmo.hover = .NONE;
entity_gizmo.active = .NONE;
}
get_selected_entity :: (world: *World) -> *Entity {
if entity_selected_id < 0 then return null;
for e: world.entities if cast(s64)e.id == entity_selected_id return e;
return null;
}
// World-space box covering an entity's cell plus every part it draws, used for
// click-picking and for the selection outline.
entity_bounds :: (e: *Entity) -> (mn: Vector3, mx: Vector3) {
mn := e.position;
mx := e.position + Vector3.{1, 1, 1};
for part: get_entity_parts(e.type) {
p, q : Vector3 = ---;
if part.kind == .TRILE {
p = entity_trile_point(e, part.offset);
q = p + Vector3.{1, 1, 1};
} else {
// Billboards and emitters are points; give them enough of a box to click.
c := entity_point(e, part.offset);
p = c - Vector3.{0.25, 0.25, 0.25};
q = c + Vector3.{0.25, 0.25, 0.25};
}
mn.x = min(mn.x, p.x); mn.y = min(mn.y, p.y); mn.z = min(mn.z, p.z);
mx.x = max(mx.x, q.x); mx.y = max(mx.y, q.y); mx.z = max(mx.z, q.z);
}
return mn, mx;
}
// The billboard that stands in for an entity in the editor: the type's declared
// EDITOR_MARKER, or a default one when the entity draws nothing you could aim
// at. Null for entities that are already visible on their own.
entity_marker_animation :: (e: *Entity) -> *Animation {
marker := get_entity_editor_marker(e.type);
if marker.count > 0 return get_animation_from_string(marker);
for part: get_entity_parts(e.type) {
if part.kind == .TRILE || part.kind == .BILLBOARD return null;
}
return get_animation_from_string("game_core.ball");
}
pick_entity :: (world: *World, ray: Ray) -> *Entity {
best : *Entity = null;
best_dist : float = FLOAT32_MAX;
for e: world.entities {
mn, mx := entity_bounds(e);
col := does_ray_hit_cube(ray, .{position = mn, size = mx - mn});
if !col.hit then continue;
d := abs(col.distance);
if d < best_dist {
best_dist = d;
best = e;
}
}
return best;
}
snap_to_step :: (v: float, step: float) -> float {
if step <= 0 then return v;
return floor(v / step + 0.5) * step;
}
tick_entity_tool :: (ray: Ray) {
curworld := get_current_world();
if !curworld.valid then return;
world := *curworld.world;
e := get_selected_entity(world);
if e == null {
entity_gizmo.hover = .NONE;
entity_gizmo.active = .NONE;
} else {
mn, mx := entity_bounds(e);
debug_aabb_3d_overlay(mn, mx, .{1.0, 0.75, 0.1, 0.8}, 2.0);
// The gizmo sits at the middle of the entity's cell, which is both what
// the entity's marker draws on and what it rotates about.
cam := get_level_editor_camera();
origin := e.position + ENTITY_PIVOT;
if entity_gizmo_mode == .MOVE {
moved, changed := gizmo_translate(*entity_gizmo, origin, cam, ray);
if changed {
p := moved - ENTITY_PIVOT;
if entity_snap_enabled {
p.x = snap_to_step(p.x, entity_snap_step);
p.y = snap_to_step(p.y, entity_snap_step);
p.z = snap_to_step(p.z, entity_snap_step);
}
e.position = p;
}
} else {
u, v, n, steps := gizmo_rotate(*entity_gizmo, origin, cam, ray);
if steps != 0 {
// A negative drag is the same turn taken the other way round.
turn := ifx steps > 0 then orientation_quarter_turn(u, v, n) else orientation_quarter_turn(v, u, n);
for 1..abs(steps) e.orientation = compose_orientations(turn, e.orientation);
}
}
for *inst: e.emitters inst.position = entity_point(e, inst.offset);
}
// A click that didn't land on the gizmo selects (or clears) the selection.
if entity_gizmo.active == .NONE && entity_gizmo.hover == .NONE {
if get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT) & .START {
picked := pick_entity(world, ray);
entity_selected_id = ifx picked then cast(s64)picked.id else cast(s64)-1;
}
}
}
editor_edit_y :: () -> float {
y := cast(float)editY;
curworld := get_current_world();
@ -633,13 +521,10 @@ editor_edit_y :: () -> float {
}
tick_level_editor :: () {
#if FLAG_TACOMA_ENABLED { probe_bake_tick(); }
#if FLAG_TACOMA_ENABLED { rdm_bake_tick(); sh_bake_tick(); }
tick_level_editor_camera();
tick_particles(cast(float)delta_time);
curworld := get_current_world();
if curworld.valid then tick_entity_emitters(*curworld.world, cast(float)delta_time);
if is_action_start(Editor_Action.LEVEL_TWIST_CCW) {
lastInputTime = get_time();
current_orientation_twist = (current_orientation_twist + 1) % 4;
@ -653,14 +538,9 @@ tick_level_editor :: () {
current_orientation_face = (current_orientation_face + 1) % 6;
}
cam := get_level_editor_camera();
ray := get_mouse_ray(*cam);
ray := get_mouse_ray(*get_level_editor_camera());
hit, point := ray_plane_collision_point(ray, editor_edit_y(), 20);
// The entity tool works off the entities themselves, not the edit plane, so
// it runs whether or not the cursor is over the plane.
if current_tool_mode == .ENTITY then tick_entity_tool(ray);
show_trile_preview = false;
if hit {
show_trile_preview = true;
@ -752,30 +632,31 @@ tick_level_editor :: () {
inspector_z = cast(s32)pz;
inspector_note_deactivate = true;
}
} else if current_tool_mode == .ENTITY {
// Right-click places the type picked in the panel, so the cursor never
// has to leave the spot you're aiming at.
if get_mouse_state(Key_Code.MOUSE_BUTTON_RIGHT) & .START {
add_entity_at(cast(s32)px, cast(s32)py, cast(s32)pz);
}
}
}
}
draw_level_editor_preview :: (view: View_Id) {
create_level_editor_preview_tasks :: () {
curworld := get_current_world();
if !curworld.valid then return;
px := trile_preview_x; py := trile_preview_y; pz := trile_preview_z;
// The entity tool never paints triles, so it shows no brush preview.
if current_tool_mode == .ENTITY then return;
if current_tool_mode == .INSPECTOR {
name, orientation, found := get_trile_at(*curworld.world, xx px, xx py, xx pz);
cursor_trile := ifx found then name else (ifx editor_current_trile then editor_current_trile.name else "");
mesh, ok := get_trile_mesh(cursor_trile);
if ok draw_trile(view, mesh, .{xx px, xx py, xx pz}, orientation, .ADD_PREVIEW);
if cursor_trile != "" {
positions: [..]Trile_Instance;
positions.allocator = temp;
array_add(*positions, Trile_Instance.{xx (px % 32), xx (py % 32), xx (pz % 32), xx orientation});
task: Rendering_Task_Trile;
task.trile = cursor_trile;
task.positions = positions;
task.worldConf = *curworld.world.conf;
task.preview_mode = 1;
task.chunk_key = world_to_chunk_coord(xx px, xx py, xx pz);
add_rendering_task(task);
}
return;
}
@ -783,8 +664,7 @@ draw_level_editor_preview :: (view: View_Id) {
positions: [..]Vector4;
positions.allocator = temp;
ori_u8 := get_current_orientation();
ori := cast(float) ori_u8;
ori := cast(float) get_current_orientation();
if current_tool_mode == .POINT {
array_add(*positions, .{cast(float)px, cast(float)py, cast(float)pz, ori});
@ -840,10 +720,12 @@ draw_level_editor_preview :: (view: View_Id) {
is_delete := (current_tool_mode == .AREA && area_active && area_delete)
|| (current_tool_mode == .LINE && line_active && line_delete);
mesh, ok := get_trile_mesh(editor_current_trile.name);
if !ok then return;
tint := ifx is_delete then Trile_Tint.DELETE_PREVIEW else .ADD_PREVIEW;
for positions draw_trile(view, mesh, it.xyz, ori_u8, tint);
task: Rendering_Task_Trile;
task.trile = editor_current_trile.name;
// task.positions = positions;
task.worldConf = *curworld.world.conf;
task.preview_mode = ifx is_delete then cast(s32)2 else cast(s32)1;
add_rendering_task(task);
}
get_trile_at :: (world: *World, wx: s32, wy: s32, wz: s32) -> (name: string, orientation: u8, found: bool) {
@ -891,6 +773,36 @@ 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;
r.h = ui_h(3, 2);
GR.label(r.*, "-- RDM --", *t_label_left(theme));
r.y += r.h;
r.h = ui_h(4, 0);
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;
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;
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;
}
} else {
GR.label(r.*, "Trile: (empty)", *t_label_left(theme));
r.y += r.h;
@ -984,152 +896,98 @@ draw_inspector_panel :: (r: *GR.Rect, theme: *GR.Overall_Theme) {
r.y += r.h;
}
}
draw_entity_tool_panel :: (r: *GR.Rect, theme: *GR.Overall_Theme) {
curworld := get_current_world();
if !curworld.valid then return;
world := *curworld.world;
r.h = ui_h(3, 2);
GR.label(r.*, "-- Entity Tool --", *t_label_left(theme));
r.y += r.h;
e := get_selected_entity(world);
if e != null {
GR.label(r.*, tprint("Type: %", entity_type_name(e.type)), *t_label_left(theme));
r.y += r.h;
GR.label(r.*, tprint("Pos: %, %, %",
formatFloat(e.position.x, trailing_width=2),
formatFloat(e.position.y, trailing_width=2),
formatFloat(e.position.z, trailing_width=2)), *t_label_left(theme));
r.y += r.h;
GR.label(r.*, tprint("Orientation: %", e.orientation), *t_label_left(theme));
r.y += r.h;
r.h = ui_h(4, 0);
mode_r := r.*;
mode_r.w = r.w / 2;
if GR.button(mode_r, "Move", *t_button_selectable(theme, entity_gizmo_mode == .MOVE), 524) {
entity_gizmo_mode = .MOVE;
entity_gizmo.active = .NONE;
}
mode_r.x += mode_r.w;
if GR.button(mode_r, "Rotate", *t_button_selectable(theme, entity_gizmo_mode == .ROTATE), 525) {
entity_gizmo_mode = .ROTATE;
entity_gizmo.active = .NONE;
}
r.y += r.h;
// Rotation is always in quarter turns, so snapping is a move-only setting.
if entity_gizmo_mode == .MOVE {
snap_label := ifx entity_snap_enabled then tprint("Snap: %", formatFloat(entity_snap_step, trailing_width=2)) else "Snap: off";
if GR.button(r.*, snap_label, *t_button_selectable(theme, entity_snap_enabled), 520) {
entity_snap_enabled = !entity_snap_enabled;
}
r.y += r.h;
if entity_snap_enabled {
GR.slider(r.*, *entity_snap_step, 0.125, 1.0, 0.125, *theme.slider_theme);
r.y += r.h;
}
}
fields_r := r.*;
fields_r.h = ui_h(30, 0);
entity_autoedit(fields_r, e, theme);
r.y += fields_r.h;
if GR.button(r.*, "Remove Entity", *theme.button_theme, 521) {
remove_entity(world, e);
level_editor_clear_entity_selection();
}
r.y += r.h;
if GR.button(r.*, "Deselect", *theme.button_theme, 522) {
level_editor_clear_entity_selection();
}
r.y += r.h;
} else {
GR.label(r.*, "Click an entity to select it.", *t_label_left(theme));
r.y += r.h;
GR.label(r.*, "Drag the gizmo to move or turn.", *t_label_left(theme));
r.y += r.h;
}
r.h = ui_h(3, 2);
r.y += r.h * 0.5;
GR.label(r.*, "-- Spawn --", *t_label_left(theme));
GR.label(r.*, "-- RDM --", *t_label_left(theme));
r.y += r.h;
r.h = ui_h(4, 0);
for info, idx: ENTITY_TYPE_TABLE {
selected := cast(s32)idx == entity_add_type;
if GR.button(r.*, info.name, *t_button_selectable(theme, selected), cast(s32)(500 + idx)) {
entity_add_type = cast(s32)idx;
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;
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;
r.y += r.h * 0.5;
tex_size := r.w * 0.9;
tex_r : GR.Rect;
tex_r.x = r.x + (r.w - tex_size) * 0.5;
tex_r.y = r.y;
tex_r.w = tex_size;
tex_r.h = tex_size * 1.5;
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 = g_rdm_atlas;
if uiTex.tex.id != INVALID_ID {
set_shader_for_images(uiTex);
immediate_quad(
.{tex_r.x, tex_r.y},
.{tex_r.x + tex_r.w, tex_r.y},
.{tex_r.x + tex_r.w, tex_r.y + tex_r.h},
.{tex_r.x, tex_r.y + tex_r.h},
.{1,1,1,1},
uv0, uv1, uv2, uv3
);
set_shader_for_color();
immediate_flush();
}
r.y += tex_r.h + r.h * 0.5;
} else {
GR.label(r.*, "RDM: not yet baked", *t_label_left(theme));
r.y += r.h;
}
} else {
GR.label(r.*, "RDM: not flagged", *t_label_left(theme));
r.y += r.h;
}
r.h = ui_h(3, 2);
if entity_add_type >= 0 && entity_add_type < cast(s32)ENTITY_TYPE_TABLE.count {
GR.label(r.*, "Right-click in the world to place.", *t_label_left(theme));
} else {
GR.label(r.*, "Pick a type to place.", *t_label_left(theme));
}
r.y += r.h;
}
// Places the type picked in the panel. Called from the right-click handler, so
// the position is whatever cell the cursor is over on the edit plane.
add_entity_at :: (x: s32, y: s32, z: s32) {
if entity_add_type < 0 || entity_add_type >= cast(s32)ENTITY_TYPE_TABLE.count then return;
curworld := get_current_world();
if !curworld.valid then return;
spawned := spawn_entity(*curworld.world, ENTITY_TYPE_TABLE[entity_add_type].name, .{cast(float)x, cast(float)y, cast(float)z});
if spawned then entity_selected_id = cast(s64)spawned.id;
}
// Runs autoedit on the entity's concrete type, so each type gets an editor
// generated from its own fields.
entity_autoedit :: (rect: GR.Rect, e: *Entity, theme: *GR.Overall_Theme) {
#insert #run,stallable gen_entity_dispatch("autoedit(rect, cast(*%)e, theme, cast(s32)e.id)");
}
draw_editor_billboards :: (view: View_Id) {
add_editor_billboards :: () {
if !editor_billboards_visible then return;
curworld := get_current_world();
if !curworld.valid then return;
// Entities are marked first, before the fallback animation is required, so a
// type with its own EDITOR_MARKER still shows up if "game_core.ball" is gone.
for e: curworld.world.entities {
marker := entity_marker_animation(e);
if marker == null then continue;
draw_animation_frame(view, marker, 0, e.position + ENTITY_PIVOT);
}
anim := get_animation_from_string("game_core.ball");
if anim == null then return;
for inst: curworld.world.emitter_instances {
draw_animation_frame(view, anim, 0, inst.position);
task : Rendering_Task_Billboard;
task.position = inst.position;
task.animation = anim;
task.frame = 0;
add_rendering_task(task);
}
for note: curworld.world.notes {
draw_animation_frame(view, anim, 0, .{cast(float)note.position.x + 0.5, cast(float)note.position.y + 0.5, cast(float)note.position.z + 0.5});
task : Rendering_Task_Billboard;
task.position = .{cast(float)note.position.x + 0.5, cast(float)note.position.y + 0.5, cast(float)note.position.z + 0.5};
task.animation = anim;
task.frame = 0;
add_rendering_task(task);
}
}
draw_level_editor :: () {
curworld := get_current_world();
if !curworld.valid then return;
camera := get_level_editor_camera();
view := add_view(.{camera = camera, environment = world_environment(*curworld.world), cut = true, focus = camera.target});
draw_entities(view, *curworld.world);
draw_live_particles(view);
cam := get_level_editor_camera();
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 {
draw_level_editor_preview(view);
create_level_editor_preview_tasks();
}
draw_editor_billboards(view);
add_editor_billboards();
debug_grid(floor(cameraCenter.x), editor_edit_y(), floor(cameraCenter.y), 20, .{0.3, 0.3, 0.35, 0.7});
}
@ -1157,14 +1015,12 @@ draw_level_editor_ui :: (theme: *GR.Overall_Theme) {
case .INFO;
if curworld.valid then autoedit(r, *curworld.world.conf, theme);
}
if current_tool_mode == .INSPECTOR || current_tool_mode == .ENTITY {
if current_tool_mode == .INSPECTOR {
rr := GR.get_rect(ui_w(85,0), ui_h(5,0), ui_w(15, 0), ui_h(95, 0));
draw_bg_rectangle(rr, theme);
ui_add_mouse_occluder(rr);
rr.y += ui_h(1, 0);
if current_tool_mode == .ENTITY {
draw_entity_tool_panel(*rr, theme);
} else if inspector_selected {
if inspector_selected {
draw_inspector_panel(*rr, theme);
} else {
rr.h = ui_h(3, 2);

View File

@ -1,126 +0,0 @@
#scope_file
ms_selected : u8;
ms_scroll : float;
ms_name_input : string;
#scope_export
draw_material_studio_ui :: (theme: *GR.Overall_Theme) {
panel := GR.get_rect(0, ui_h(5,0), ui_w(20, 20), ui_h(95, 0));
ui_add_mouse_occluder(panel);
draw_bg_rectangle(panel, theme);
row_h := ui_h(4, 0);
r := panel;
r.h = row_h;
if GR.button(r, "New material", *theme.button_theme) {
id, ok := swatch_add("new", u8.[255, 255, 255]);
if ok {
ms_selected = id;
ms_name_input = copy_string(g_swatches[id].name);
}
}
r.y += row_h;
// .hex palettes in a pack, one RRGGBB per line.
for g_palettes {
if GR.button(r, tprint("Import % (% colours)", it.name, it.entries.count), *theme.button_theme, it_index) {
log_info("Imported % new materials from %", swatch_import_palette(it), it.name);
}
r.y += row_h;
}
grid := r;
grid.h = ui_h(25, 0);
previous := ms_selected;
region, inside := GR.begin_scrollable_region(grid, *theme.scrollable_region_theme);
end_y := draw_swatch_grid(inside, *ms_scroll, *ms_selected, theme);
GR.end_scrollable_region(region, inside.x + inside.w, end_y, *ms_scroll);
if ms_selected != previous ms_name_input = copy_string(g_swatches[ms_selected].name);
r.y += grid.h + row_h * 0.5;
if ms_selected == 0 || !g_swatches[ms_selected].used return;
swatch := *g_swatches[ms_selected];
GR.label(r, tprint("Material %", ms_selected), *t_label_left(theme));
r.y += row_h;
a, new_text := GR.text_input(r, ms_name_input, *theme.text_input_theme, 128);
if a & .ENTERED {
ms_name_input = copy_string(new_text);
swatch.name = copy_string(new_text);
}
r.y += row_h;
r.h = ui_h(3, 2);
GR.label(r, "Reflectivity", *t_label_left(theme));
r.y += r.h;
GR.slider(r, *swatch.reflectivity, 0, 255, 1, *theme.slider_theme);
r.y += r.h;
GR.label(r, "Gloss", *t_label_left(theme));
r.y += r.h;
GR.slider(r, *swatch.gloss, 0, 255, 1, *theme.slider_theme);
r.y += r.h;
GR.label(r, "Emittance", *t_label_left(theme));
r.y += r.h;
GR.slider(r, *swatch.emittance, 0, 127, 1, *theme.slider_theme);
r.y += r.h;
if GR.base_checkbox(r, "Glass (lets sunlight through)", swatch.glass, *theme.checkbox_theme) swatch.glass = !swatch.glass;
r.y += r.h * 2;
// The colour gets its own panel on the right, so the picker has room.
color_panel := GR.get_rect(100*vw - panel.w, panel.y, panel.w, ui_h(45, 0));
ui_add_mouse_occluder(color_panel);
draw_bg_rectangle(color_panel, theme);
picker := color_panel;
picker.h = ui_h(40, 0);
color := swatch_color(ms_selected);
GR.color_picker(picker, *color, *theme.color_picker_theme);
for 0..2 swatch.color[it] = cast(u8) clamp(color.component[it] * 255 + 0.5, 0, 255);
picker.y += picker.h;
picker.h = row_h;
GR.label(picker, tprint("#%", hex_color(swatch.color)), *t_label_left(theme));
used_panel := GR.get_rect(color_panel.x, color_panel.y + color_panel.h, color_panel.w, ui_h(45, 0));
ui_add_mouse_occluder(used_panel);
draw_bg_rectangle(used_panel, theme);
line := used_panel;
line.h = row_h;
GR.label(line, "Used by", *t_label_left(theme));
line.y += row_h;
for trile: get_trile_table_ptr() {
count := trile_swatch_counts(*trile)[ms_selected];
if count == 0 continue;
GR.label(line, tprint("% (%)", trile.name, count), *t_label_left(theme));
line.y += row_h;
}
swatch_changed(ms_selected);
r.h = row_h;
if GR.button(r, "Save", *theme.button_theme) {
striles();
for trile: get_trile_table_ptr() update_trile_thumbnail(trile.name);
}
r.y += row_h;
if swatch_in_use(ms_selected) {
GR.label(r, "In use, can't delete", *t_label_left(theme));
} else if GR.button(r, "Delete", *theme.button_theme) {
swatch.* = .{};
swatch_changed(ms_selected);
ms_selected = 0;
}
}
#scope_file
hex_color :: (c: [3]u8) -> string {
digits :: "0123456789abcdef";
out := talloc_string(6);
for 0..2 {
out[it * 2] = digits[c[it] >> 4];
out[it * 2 + 1] = digits[c[it] & 0xf];
}
return out;
}

View File

@ -151,7 +151,6 @@ draw_particle_editor_ui :: (theme: *GR.Overall_Theme) {
pe_vec4(*er, "Color Start", *def.color_start, 0, 1, theme);
pe_vec4(*er, "Color End", *def.color_end, 0, 1, theme);
pe_float(*er, tprint("Gravity: %", def.gravity), *def.gravity, -20, 20, 0.1, theme);
pe_float(*er, tprint("Glow: %", def.glow), *def.glow, 0, 1, 0.05, theme);
pe_label(*er, tprint("Blend Mode: %", def.blend_mode), theme);
orig_w2 := er.w;
@ -165,11 +164,7 @@ draw_particle_editor_ui :: (theme: *GR.Overall_Theme) {
er.y += er.h;
alive_count : s32 = 0;
for p : g_particles {
if p.alive then alive_count += 1;
}
pe_label(*er, tprint("Alive particles: %", alive_count), theme);
pe_label(*er, tprint("Alive particles: %", count_alive_particles()), theme);
pe_label(*er, tprint("Emitter defs: %", g_emitter_defs.count), theme);
pe_label(*er, tprint("Preview active: %", pe_preview_emitter.active), theme);
pe_label(*er, tprint("Preview def: %", pe_preview_emitter.definition != null), theme);
@ -185,8 +180,7 @@ draw_particle_editor_ui :: (theme: *GR.Overall_Theme) {
}
draw_particle_editor :: () {
view := add_view(.{camera = pe_cam, environment = New(Environment,, temp), draw_world = false});
draw_live_particles(view);
create_set_cam_rendering_task(pe_cam, 0.0);
if pe_selected_index >= 0 && pe_selected_index < cast(s32)g_emitter_defs.count {
def := *g_emitter_defs[pe_selected_index];

View File

@ -59,7 +59,7 @@ draw_trile_picker :: (r_in: GR.Rect, theme: *GR.Overall_Theme) {
btn_theme.frame_color_down = .{0, 0, 0, 0};
btn_theme.frame_color_flash = .{0, 0, 0, 0};
atlas_valid := g_thumbnail_atlas.tex.id != 0;
atlas_valid := g_thumbnail_atlas.tex.id != INVALID_ID;
region, inside := GR.begin_scrollable_region(r, *theme.scrollable_region_theme);

48
src/editor/rdm_disk.jai Normal file
View File

@ -0,0 +1,48 @@
// RDM disk format helpers shared by the baking tool (tacoma) and the
// sfetch-based streaming loader (rdm_loader.jai).
RDM_File_Header :: struct {
magic: u32;
width: s32;
height: s32;
}
RDM_FILE_MAGIC :: u32.[0x4D445254][0]; // "TRDM" as little-endian u32
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 (1 probe per trile per axis = 32x32x32 per 32x32x32 chunk)
SH_FILE_MAGIC :: u32.[0x44475348][0]; // "SHGD" as little-endian u32
SH_PROBE_N :: 32; // probes per axis (1 per trile in a 32-trile chunk)
SH_TEX_W :: SH_PROBE_N * 3; // 96 — 3 RGBA texels per probe along X
SH_Grid_File_Header :: struct {
magic: u32;
version: u16;
probe_n: s32;
}
shgrid_filename :: (world_name: string, chunk_key: Chunk_Key) -> string {
return sprint("%/worlds/%/%_%_%.shgrid", GAME_RESOURCES_DIR, world_name, chunk_key.x, chunk_key.y, chunk_key.z);
}
f32_to_f16 :: (f: float) -> u16 {
b := (cast(*u32) *f).*;
sign := (b >> 31) & 0x1;
exp := cast(s32)((b >> 23) & 0xFF) - 127;
mant := b & 0x7FFFFF;
if exp >= 16 return cast(u16)((sign << 15) | 0x7C00); // clamp to inf
if exp >= -14 return cast(u16)((sign << 15) | (cast(u32)(exp + 15) << 10) | (mant >> 13));
if exp >= -24 {
shift := cast(u32)(-exp - 1);
return cast(u16)((sign << 15) | ((mant | 0x800000) >> shift));
}
return cast(u16)(sign << 15); // underflow → ±0
}

View File

@ -0,0 +1,35 @@
test_f32_to_f16 :: () {
s := begin_suite("f32_to_f16");
check(*s, "0.0 → 0x0000", f32_to_f16(0.0) == 0x0000);
check(*s, "-0.0 → 0x8000", f32_to_f16(-0.0) == 0x8000);
check(*s, "1.0 → 0x3C00", f32_to_f16(1.0) == 0x3C00);
check(*s, "-1.0 → 0xBC00", f32_to_f16(-1.0) == 0xBC00);
check(*s, "0.5 → 0x3800", f32_to_f16(0.5) == 0x3800);
check(*s, "2.0 → 0x4000", f32_to_f16(2.0) == 0x4000);
check(*s, "1.5 → 0x3E00", f32_to_f16(1.5) == 0x3E00);
// Max normal f16 = 65504
check(*s, "65504.0 → 0x7BFF", f32_to_f16(65504.0) == 0x7BFF);
// Min normal f16 = 2^-14
check(*s, "2^-14 → 0x0400", f32_to_f16(cast(float)(1.0 / 16384.0)) == 0x0400);
// Overflow clamps to infinity
check(*s, "65536.0 → 0x7C00 (inf)", f32_to_f16(65536.0) == 0x7C00);
check(*s, "1e10 → 0x7C00 (inf)", f32_to_f16(1.0e10) == 0x7C00);
check(*s, "-65536.0 → 0xFC00 (-inf)", f32_to_f16(-65536.0) == 0xFC00);
// Denormals: 2^-15 is the largest f16 denormal base
check(*s, "2^-15 → 0x0200", f32_to_f16(cast(float)(1.0 / 32768.0)) == 0x0200);
// Smallest representable f16 denormal = 2^-24
check(*s, "2^-24 → 0x0001", f32_to_f16(cast(float)(1.0 / 16777216.0)) == 0x0001);
// Underflow to zero
check(*s, "2^-25 → 0x0000", f32_to_f16(cast(float)(1.0 / 33554432.0)) == 0x0000);
check(*s, "-2^-25 → 0x8000", f32_to_f16(cast(float)(-1.0 / 33554432.0)) == 0x8000);
end_suite(s);
}
#run test_f32_to_f16();

View File

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

View File

@ -0,0 +1,145 @@
#scope_file
subwindow : GR.Subwindow_Info;
subwindow_initted : bool = false;
pp_subwindow : GR.Subwindow_Info;
pp_subwindow_initted : bool = false;
lighting_subwindow : GR.Subwindow_Info;
lighting_subwindow_initted : bool = false;
// @Hack: This is probably kinda a bad idea, I don't really know atm how else to get the theme into the subwindow.
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"];
draw_subwindow_texture_debug :: (state: *GR.Subwindow_State, r: GR.Rect, data: *void) {
r2 := r;
r2.h = ui_h(5,0);
r2.w = r.w;
GR.dropdown(r2, pipeline_names, *current_pipeline, *theme_ptr.dropdown_theme);
r2.y = r.y + r2.h;
r2.h = r.h - r2.h;
r2.x = r.x;
r2.w = r.w;
uiTex := New(Ui_Texture ,,temp);
image : sg_image;
if current_pipeline == {
case 0; image = g_shadowmap_img;
case 1; image = gPipelines.plane.bind.images[0];
case 2; image = g_rendertex;
case 3; image = g_gbuf_position;
case 4; image = g_gbuf_normal;
case 5; image = g_ssaobuf;
case 6; image = g_bloom_up_1;
case 7; image = g_dof_tex;
case 8; image = g_rdm_atlas;
}
uiTex.tex = image;
if uiTex.tex.id != INVALID_ID {
set_shader_for_images(uiTex);
immediate_quad(.{r2.x, r2.y}, .{r2.x + r2.w, r2.y}, .{r2.x + r2.w, r2.y + r2.h}, .{r2.x, r2.y + r2.h});
set_shader_for_color();
}
immediate_flush();
}
draw_subwindow_lighting :: (state: *GR.Subwindow_State, r: GR.Rect, data: *void) {
ui_add_mouse_occluder(r);
autoedit(r, *current_lighting_config, *theme_ptr);
}
draw_subwindow_post_process :: (state: *GR.Subwindow_State, r: GR.Rect, data: *void) {
r2 := r;
ui_add_mouse_occluder(r);
r3 := r;
r3.h = ui_h(5,0);
r2.h -= r3.h;
r2.y += r3.h;
if GR.button(r3, "Save postprocessing", *theme_ptr.button_theme) {
#if OS != .WASM {
file :: #import "File";
save_post_process();
}
}
r3.y += r3.h;
r2.h -= r3.h;
r2.y += r3.h;
GR.dropdown(r3, LUT_name_list, *g_current_lut_texture_index, *theme_ptr.dropdown_theme);
autoedit(r2, *current_post_process, *theme_ptr);
}
#scope_export
draw_texture_debug :: () {
}
toggle_texdebug :: () {
subwindow.open = !subwindow.open;
} @Command
toggle_pp :: () {
pp_subwindow.open = !pp_subwindow.open;
} @Command
toggle_lighting :: () {
lighting_subwindow.open = !lighting_subwindow.open;
} @Command
draw_postprocess_popup :: (theme: *GR.Overall_Theme) {
if !pp_subwindow.open then return;
if !pp_subwindow_initted {
theme_ptr = theme;
r := GR.get_rect(ui_w(40, 0), ui_w(40, 0), ui_h(50,0), ui_h(50,0));
pp_subwindow.rect = r;
pp_subwindow.draw = draw_subwindow_post_process;
pp_subwindow.title_text = "Post Process";
pp_subwindow.open = false;
pp_subwindow_initted = true;
}
if pp_subwindow.open { // The Subwindow starts open, but pressing the Close button can close it.
pp_subwindow_state := GR.add_subwindow(*pp_subwindow, *theme.subwindow_theme);
}
}
draw_lighting_popup :: (theme: *GR.Overall_Theme) {
if !lighting_subwindow.open then return;
if !lighting_subwindow_initted {
theme_ptr = theme;
r := GR.get_rect(ui_w(55, 0), ui_w(40, 0), ui_h(50,0), ui_h(40,0));
lighting_subwindow.rect = r;
lighting_subwindow.draw = draw_subwindow_lighting;
lighting_subwindow.title_text = "Lighting";
lighting_subwindow.open = false;
lighting_subwindow_initted = true;
}
if lighting_subwindow.open {
GR.add_subwindow(*lighting_subwindow, *theme.subwindow_theme);
}
}
draw_texture_debugger :: (theme: *GR.Overall_Theme) {
if !subwindow.open then return;
if !subwindow_initted {
theme_ptr = theme;
r := GR.get_rect(ui_w(40, 0), ui_w(40, 0), ui_h(50,0), ui_h(50,0));
subwindow.rect = r;
subwindow.draw = draw_subwindow_texture_debug;
subwindow.title_text = "Texture Debugger";
subwindow.open = false;
subwindow_initted = true;
}
if subwindow.open { // The Subwindow starts open, but pressing the Close button can close it.
subwindow_state := GR.add_subwindow(*subwindow, *theme.subwindow_theme);
}
}

View File

@ -5,6 +5,10 @@ zoom : float = 3.0;
brush_radius : int = 5;
roughness : u8 = 0;
metallic : u8 = 0;
emittance : u8 = 0;
hovered_trixel_x : int = -1;
hovered_trixel_y : int = -1;
hovered_trixel_z : int = -1;
@ -23,7 +27,6 @@ Trile_Editor_Tool :: enum {
PAINT;
ADD;
REMOVE;
PICK; // takes the clicked trixel's material
}
Trile_Editor_Tool_Mode :: enum {
@ -36,6 +39,7 @@ current_tab : Trile_Editor_Tab = .TOOLSET;
current_tool : Trile_Editor_Tool = .PAINT;
current_mode : Trile_Editor_Tool_Mode = .AREA;
colorMuls : [16][16][16]Vector3;
tiling_preview_active : bool;
tiling_axis : s32 = 0; // 0=+X, 1=+Z, 2=-X, 3=-Z
tiling_orientation : s32 = 0; // 0-23 = face(0..5)*4 + twist(0..3)
@ -43,10 +47,10 @@ tiling_neighbor_trile_name : string;
tiling_neighbor_trile_input : string;
UNDO_MAX :: 32;
undo_trixels : [UNDO_MAX][16][16][16]u8;
undo_trixels : [UNDO_MAX][16][16][16] Trixel;
undo_count : int = 0;
undo_head : int = 0;
redo_trixels : [UNDO_MAX][16][16][16]u8;
redo_trixels : [UNDO_MAX][16][16][16] Trixel;
redo_count : int = 0;
redo_head : int = 0;
@ -55,36 +59,30 @@ meta_name_input : string;
#scope_export
ntrile :: (name: string) {
trile_changed(editor_current_trile);
set_trile_gfx(editor_current_trile.name, generate_trile_gfx_matias(editor_current_trile));
newt : Trile;
newt.name = sprint("%", name);
swatch := current_swatch;
if !g_swatches[swatch].used swatch = first_used_swatch();
trile_fill(*newt, swatch);
set_trile(newt.name, newt);
editor_current_trile = get_trile(newt.name);
trile_gen_heightmap(editor_current_trile);
} @Command
cpytrile :: (trile: string, destination: string) {
trile_changed(editor_current_trile);
set_trile_gfx(editor_current_trile.name, generate_trile_gfx_matias(editor_current_trile));
newt : Trile;
newt.name = sprint("%", destination);
newt.trixels = get_trile(trile).trixels;
set_trile(newt.name, newt);
editor_current_trile = get_trile(newt.name);
trile_gen_heightmap(editor_current_trile);
} @Command
ltrile :: (name: string) {
trile_changed(editor_current_trile);
set_trile_gfx(editor_current_trile.name, generate_trile_gfx_matias(editor_current_trile));
nt := get_trile(name);
if !nt {
console_add_output_line("Failed to load a trile with that name...");
return;
}
editor_current_trile = nt;
trile_gen_heightmap(editor_current_trile);
} @Command
#scope_file
@ -106,8 +104,6 @@ do_undo :: () {
undo_head = (undo_head - 1 + UNDO_MAX) % UNDO_MAX;
undo_count -= 1;
editor_current_trile.trixels = undo_trixels[undo_head];
trile_gen_heightmap(editor_current_trile);
trile_changed(editor_current_trile);
}
do_redo :: () {
@ -118,16 +114,20 @@ do_redo :: () {
redo_head = (redo_head - 1 + UNDO_MAX) % UNDO_MAX;
redo_count -= 1;
editor_current_trile.trixels = redo_trixels[redo_head];
trile_gen_heightmap(editor_current_trile);
trile_changed(editor_current_trile);
}
apply_tool_to_trixel :: (x: s64, y: s64, z: s64) {
trixel := *editor_current_trile.trixels[x][y][z];
if current_tool == {
case .PAINT; if trixel.* != 0 trixel.* = current_swatch;
case .ADD; if trixel.* == 0 trixel.* = current_swatch;
case .REMOVE; trixel.* = 0;
if current_tool == .PAINT {
editor_current_trile.trixels[x][y][z].material.color = current_color;
editor_current_trile.trixels[x][y][z].material.metallic = metallic;
editor_current_trile.trixels[x][y][z].material.roughness = roughness;
editor_current_trile.trixels[x][y][z].material.emittance = emittance;
}
if current_tool == .ADD {
editor_current_trile.trixels[x][y][z].empty = false;
}
if current_tool == .REMOVE {
editor_current_trile.trixels[x][y][z].empty = true;
}
}
@ -138,10 +138,6 @@ area_active : bool;
handle_tool_click :: () {
if hovered_trixel_x < 0 && hovered_trixel_y < 0 && hovered_trixel_z < 0 then return;
if current_tool == .PICK {
current_swatch = editor_current_trile.trixels[hovered_trixel_x][hovered_trixel_y][hovered_trixel_z];
return;
}
if current_mode == .POINT {
push_undo();
if current_tool == .ADD {
@ -191,22 +187,21 @@ handle_tool_click :: () {
area_start_z = hovered_trixel_z;
}
}
trile_gen_heightmap(editor_current_trile);
}
#scope_export
current_swatch : u8 = 1;
current_color : Vector3 = .{1.0, 0.0, 0.0};
reset_trile :: () {
for x: 0..15 {
for y: 0..15 {
for z: 0..15 {
editor_current_trile.trixels[x][y][z] = current_swatch;
editor_current_trile.trixels[x][y][z].empty = false;
}
}
}
trile_gen_heightmap(editor_current_trile);
} @Command
trile_copy :: (from: string) {
@ -218,17 +213,15 @@ trile_copy :: (from: string) {
}
}
}
trile_gen_heightmap(editor_current_trile);
} @Command
tick_trile_editor :: () {
if console_open_ignore_input then return;
if !editor_current_trile then editor_current_trile = get_any_trile();
if !editor_current_trile then editor_current_trile = get_trile("test");
if input_button_states[Key_Code.MOUSE_BUTTON_LEFT] & .START {
handle_tool_click();
trile_changed(editor_current_trile);
}
mindist : float = 999999;
@ -239,7 +232,8 @@ tick_trile_editor :: () {
for x: 0..15 {
for y: 0..15 {
for z: 0..15 {
if editor_current_trile.trixels[x][y][z] == 0 then continue;
t := editor_current_trile.trixels[x][y][z];
if t.empty then continue;
cube : Collision_Cube;
cube.position = Vector3.{cast(float) x, cast(float) y, cast(float) z} * TRIXEL_SIZE;
@ -258,18 +252,25 @@ tick_trile_editor :: () {
}
}
if hovered_trixel_x >= 0 {
lo := Vector3.{xx hovered_trixel_x, xx hovered_trixel_y, xx hovered_trixel_z};
hi := lo;
if current_mode == .BRUSH {
lo -= Vector3.{1, 1, 1} * cast(float) brush_radius;
hi += Vector3.{1, 1, 1} * cast(float) brush_radius;
} else if current_mode == .AREA && area_active {
corner := Vector3.{xx area_start_x, xx area_start_y, xx area_start_z};
lo = min(lo, corner);
hi = max(hi, corner);
for x: 0..15 {
for y: 0..15 {
for z: 0..15 {
highlighted := false;
if hovered_trixel_x >= 0 {
if current_mode == .BRUSH {
center : Vector3 = .{xx hovered_trixel_x, xx hovered_trixel_y, xx hovered_trixel_z};
highlighted = length(Vector3.{xx x, xx y, xx z} - center) <= xx brush_radius;
} else if current_mode == .AREA && area_active {
highlighted = x >= min(area_start_x, hovered_trixel_x) && x <= max(area_start_x, hovered_trixel_x)
&& y >= min(area_start_y, hovered_trixel_y) && y <= max(area_start_y, hovered_trixel_y)
&& z >= min(area_start_z, hovered_trixel_z) && z <= max(area_start_z, hovered_trixel_z);
} else {
highlighted = x == hovered_trixel_x && y == hovered_trixel_y && z == hovered_trixel_z;
}
}
colorMuls[x][y][z] = ifx highlighted then Vector3.{0.6, 0.0, 0.0} else .{1.0, 1.0, 1.0};
}
}
debug_aabb_3d_overlay(lo * TRIXEL_SIZE, (hi + Vector3.{1, 1, 1}) * TRIXEL_SIZE, .{0.9, 0.1, 0.1, 1});
}
if !input_button_states[Key_Code.CTRL] & .DOWN {
@ -316,7 +317,10 @@ get_trile_editor_camera :: () -> Camera {
return camera;
}
swatch_scroll : float = 0.0;
global_palette_scroll : float = 0.0;
trile_palette_scroll : float = 0.0;
use_color_picker : bool = false;
selected_palette : s32 = 0;
draw_tool_tab :: (theme: *GR.Overall_Theme, area: GR.Rect) {
r := area;
@ -326,9 +330,7 @@ draw_tool_tab :: (theme: *GR.Overall_Theme, area: GR.Rect) {
if keybind_button(r, "Add", .TRIXEL_TOOL_ADD, *t_button_selectable(theme, current_tool == .ADD)) then current_tool = .ADD;
r.y += r.h;
if keybind_button(r, "Remove", .TRIXEL_TOOL_REMOVE, *t_button_selectable(theme, current_tool == .REMOVE)) then current_tool = .REMOVE;
r.y += r.h;
if keybind_button(r, "Pick material", .TRIXEL_TOOL_PICK, *t_button_selectable(theme, current_tool == .PICK)) then current_tool = .PICK;
r.y += r.h * 2;
r.y += r.h * 3;
if keybind_button(r, "Point", .TRIXEL_MODE_POINT, *t_button_selectable(theme, current_mode == .POINT)) then current_mode = .POINT;
r.y += r.h;
if keybind_button(r, "Area", .TRIXEL_MODE_AREA, *t_button_selectable(theme, current_mode == .AREA)) then current_mode = .AREA;
@ -339,60 +341,126 @@ draw_tool_tab :: (theme: *GR.Overall_Theme, area: GR.Rect) {
GR.slider(r, *brush_radius, 0, 12, 1, *theme.slider_theme);
r.y += r.h * 2;
if keybind_button(r, "Save and gen", .SAVE, *theme.button_theme) {
trile_changed(editor_current_trile);
set_trile_gfx(editor_current_trile.name, generate_trile_gfx_matias(editor_current_trile));
striles();
update_trile_thumbnail(editor_current_trile.name);
};
}
draw_swatches :: (entries: []Palette_Entry, inside: GR.Rect, block: float, scroll: *float, id_offset: s32, theme: *GR.Overall_Theme) -> (end_y: float) {
cols := max(1, cast(int)(inside.w / block));
for entry, idx: entries {
er : GR.Rect;
er.x = inside.x + (idx % cols) * block;
er.y = inside.y - scroll.* + (idx / cols) * block;
er.w = block; er.h = block;
btn := theme.button_theme;
btn.surface_color = .{entry.r, entry.g, entry.b, 1.0};
if GR.button(er, "", *btn, id_offset + cast(s32)idx) {
current_color = .{entry.r, entry.g, entry.b};
roughness = xx entry.roughness;
metallic = xx entry.metallic;
emittance = xx entry.emittance;
}
}
rows := ifx entries.count > 0 then (entries.count + cols - 1) / cols else 0;
return inside.y + rows * block;
}
draw_material_tab :: (theme: *GR.Overall_Theme, area: GR.Rect) {
row_h := ui_h(3, 2);
r := area;
r := area;
row_h := ui_h(4, 0);
block := ui_h(3, 2);
bottom := area.y + area.h;
sliders_h := ui_h(18, 0);
r.h = row_h;
GR.label(r, tprint("Selected %", swatch_label(current_swatch)), *t_label_left(theme));
half := r.w / 2;
r.w = half;
if GR.button(r, "Palette", *t_button_selectable(theme, !use_color_picker)) then use_color_picker = false;
r.x += half;
if GR.button(r, "Color Picker", *t_button_selectable(theme, use_color_picker)) then use_color_picker = true;
r.x = area.x; r.w = area.w;
r.y += row_h;
hovered := "-";
if editor_current_trile && hovered_trixel_x >= 0 {
hovered = swatch_label(editor_current_trile.trixels[hovered_trixel_x][hovered_trixel_y][hovered_trixel_z]);
}
GR.label(r, tprint("Hovered %", hovered), *t_label_left(theme));
r.y += row_h * 1.5;
grid := r;
grid.h = ui_h(30, 0);
region, inside := GR.begin_scrollable_region(grid, *theme.scrollable_region_theme);
end_y := draw_swatch_grid(inside, *swatch_scroll, *current_swatch, theme);
GR.end_scrollable_region(region, inside.x + inside.w, end_y, *swatch_scroll);
r.y += grid.h + row_h * 0.5;
available := bottom - r.y - sliders_h;
top_h := ifx use_color_picker then ui_h(42, 0) else available / 2;
bot_h := available - top_h;
if !editor_current_trile return;
GR.label(r, "In this trile (click to select)", *t_label_left(theme));
r.y += row_h;
counts := trile_swatch_counts(editor_current_trile);
for count, id: counts {
if count == 0 continue;
btn := t_button_selectable(theme, current_swatch == id);
if GR.button(r, tprint("% (%)", swatch_label(xx id), count), *btn, 1000 + id) current_swatch = xx id;
r.y += row_h;
r.h = top_h;
if use_color_picker {
GR.color_picker(r, *current_color, *theme.color_picker_theme);
} else {
if g_palettes.count > 1 {
r.h = row_h;
pal_w := r.w / cast(float)g_palettes.count;
for pal, idx: g_palettes {
pr := r;
pr.x = area.x + idx * pal_w;
pr.w = pal_w;
if GR.button(pr, pal.name, *t_button_selectable(theme, cast(s32)idx == selected_palette), cast(s32)idx + 20000) {
selected_palette = cast(s32)idx;
global_palette_scroll = 0;
}
}
r.y += row_h;
top_h -= row_h;
r.h = top_h;
}
entries : []Palette_Entry;
if g_palettes.count > 0 {
selected_palette = clamp(selected_palette, 0, cast(s32)g_palettes.count - 1);
entries = g_palettes[selected_palette].entries;
}
region, inside := GR.begin_scrollable_region(r, *theme.scrollable_region_theme);
end_y := draw_swatches(entries, inside, block, *global_palette_scroll, 0, theme);
GR.end_scrollable_region(region, inside.x + inside.w, end_y, *global_palette_scroll);
}
r.y += top_h;
unique_mats : [..]Material;
unique_mats.allocator = temp;
for x: 0..15 for y: 0..15 for z: 0..15 {
trix := editor_current_trile.trixels[x][y][z];
if trix.empty then continue;
mr, mg, mb, ma := material_to_rgba(trix.material);
key := (cast(u32)mr << 24) | (cast(u32)mg << 16) | (cast(u32)mb << 8) | cast(u32)ma;
already := false;
for um: unique_mats {
ur, ug, ub, ua := material_to_rgba(um);
if ((cast(u32)ur << 24) | (cast(u32)ug << 16) | (cast(u32)ub << 8) | cast(u32)ua) == key { already = true; break; }
}
if !already array_add(*unique_mats, trix.material);
}
as_entries : [..]Palette_Entry;
as_entries.allocator = temp;
for mat: unique_mats {
e : Palette_Entry;
e.r = mat.color.x; e.g = mat.color.y; e.b = mat.color.z;
e.roughness = mat.roughness; e.metallic = mat.metallic; e.emittance = mat.emittance;
array_add(*as_entries, e);
}
r.h = bot_h * 0.75;
region2, inside2 := GR.begin_scrollable_region(r, *theme.scrollable_region_theme);
end_y2 := draw_swatches(as_entries, inside2, block, *trile_palette_scroll, 10000, theme);
GR.end_scrollable_region(region2, inside2.x + inside2.w, end_y2, *trile_palette_scroll);
r.y += bot_h * 0.75;
r.h = ui_h(3, 2);
GR.label(r, "Roughness", *t_label_left(theme));
r.y += r.h;
GR.slider(r, *roughness, 0, 7, 1, *theme.slider_theme);
r.y += r.h;
GR.label(r, "Metallic", *t_label_left(theme));
r.y += r.h;
GR.slider(r, *metallic, 0, 3, 1, *theme.slider_theme);
r.y += r.h;
GR.label(r, "Emittance", *t_label_left(theme));
r.y += r.h;
GR.slider(r, *emittance, 0, 127, 1, *theme.slider_theme);
}
// Shared with the material studio. Returns the bottom of the grid.
draw_swatch_grid :: (inside: GR.Rect, scroll: *float, selected: *u8, theme: *GR.Overall_Theme) -> float {
block := ui_h(3, 2);
cols := max(1, cast(int)(inside.w / block));
n := 0;
for g_swatches {
if !it.used continue;
r := GR.Rect.{inside.x + (n % cols) * block, inside.y - scroll.* + (n / cols) * block, block, block};
btn := t_button_selectable(theme, selected.* == it_index);
c := swatch_color(cast(u8) it_index);
btn.surface_color = .{c.x, c.y, c.z, 1};
if GR.button(r, "", *btn, cast(s32) it_index) selected.* = cast(u8) it_index;
n += 1;
}
return inside.y + ((n + cols - 1) / cols) * block;
}
TILING_AXIS_NAMES :: string.["+ X", "+ Z", "- X", "- Z", "+ Y", "- Y"];
@ -410,6 +478,53 @@ get_tiling_camera_target :: () -> Vector3 {
return Vector3.{0.5, 0.5, 0.5} + off * 0.5;
}
// Build rotation matrices matching the same GLSL convention used in shader_trile.glsl.
// In Jai row-major passed as GLSL column-major: GLSL out[i] = sum_j M_jai[j][i]*v[j].
tiling_rot_x :: (a: float) -> Matrix4 {
c := cos(a); s := sin(a);
m : Matrix4;
m._11 = 1;
m._22 = c; m._23 = s;
m._32 = -s; m._33 = c;
m._44 = 1;
return m;
}
tiling_rot_y :: (a: float) -> Matrix4 {
c := cos(a); s := sin(a);
m : Matrix4;
m._11 = c; m._13 = s;
m._22 = 1;
m._31 = -s; m._33 = c;
m._44 = 1;
return m;
}
tiling_rot_z :: (a: float) -> Matrix4 {
c := cos(a); s := sin(a);
m : Matrix4;
m._11 = c; m._21 = s;
m._12 = -s; m._22 = c;
m._33 = 1;
m._44 = 1;
return m;
}
// Matches get_orientation_matrix(ori) from shader_trile.glsl.
// Jai A*B applies A_eff first in GLSL, so twist*base = twist applied second = same as GLSL base*rot_y(twist).
make_orientation_matrix :: (ori: s32) -> Matrix4 {
face := ori / 4;
twist := ori % 4;
twist_mat : Matrix4;
identity(*twist_mat);
if twist > 0 twist_mat = tiling_rot_y(cast(float)twist * PI / 2.0);
base_mat : Matrix4;
if face == 0 { identity(*base_mat); }
else if face == 1 { base_mat = tiling_rot_x(PI); }
else if face == 2 { base_mat = tiling_rot_z(-PI * 0.5); }
else if face == 3 { base_mat = tiling_rot_z( PI * 0.5); }
else if face == 4 { base_mat = tiling_rot_x( PI * 0.5); }
else { base_mat = tiling_rot_x(-PI * 0.5); }
return twist_mat * base_mat;
}
get_tiling_neighbor_trile :: () -> *Trile {
if tiling_neighbor_trile_name.count > 0 {
t := get_trile(tiling_neighbor_trile_name);
@ -419,14 +534,13 @@ get_tiling_neighbor_trile :: () -> *Trile {
}
draw_trile_editor :: () {
environment := New(Environment,, temp);
view := add_view(.{camera = get_trile_editor_camera(), environment = environment, draw_world = false});
mesh, ok := get_trile_mesh(editor_current_trile.name);
if ok draw_trile(view, mesh, .{}, 0);
create_set_cam_rendering_task(get_trile_editor_camera(), 0.0);
w := New(World,, temp);
create_sky_rendering_task(*w.conf);
create_trixel_rendering_task(editor_current_trile, *colorMuls);
if tiling_preview_active {
neighbor, neighbor_ok := get_trile_mesh(get_tiling_neighbor_trile().name);
if neighbor_ok draw_trile(view, neighbor, get_tiling_world_offset(), xx tiling_orientation);
neighbor_trile := get_tiling_neighbor_trile();
create_trixel_rendering_task(neighbor_trile, null, world_offset = get_tiling_world_offset(), brightness = 0.75, tile_rotation = make_orientation_matrix(tiling_orientation), is_secondary = true);
}
}
@ -463,22 +577,6 @@ draw_meta_tab :: (theme: *GR.Overall_Theme, area: GR.Rect) {
r.h = row_h;
changed := GR.base_checkbox(r, "Opaque (occludes neighbors)", editor_current_trile.is_opaque, *theme.checkbox_theme);
if changed editor_current_trile.is_opaque = !editor_current_trile.is_opaque;
r.y += row_h;
changed = GR.base_checkbox(r, "Skip collision", editor_current_trile.skip_collision, *theme.checkbox_theme);
if changed editor_current_trile.skip_collision = !editor_current_trile.skip_collision;
r.y += row_h;
changed = GR.base_checkbox(r, "Is stairs", editor_current_trile.is_stairs, *theme.checkbox_theme);
if changed editor_current_trile.is_stairs = !editor_current_trile.is_stairs;
r.y += row_h;
for cut: Trile_Cut.[.ROOF, .WALL] {
label := ifx cut == .ROOF then "Roof (hidden from below)" else "Wall (cut away toward camera)";
if GR.base_checkbox(r, label, editor_current_trile.cut == cut, *theme.checkbox_theme, xx cut) {
editor_current_trile.cut = ifx editor_current_trile.cut == cut then .NONE else cut;
trile_changed(editor_current_trile);
}
r.y += row_h;
}
r.y -= row_h;
r.y += row_h * 2;
r.h = row_h;
@ -532,7 +630,7 @@ draw_trile_editor_ui :: (theme: *GR.Overall_Theme) {
// View mode buttons — bottom center of the 3D viewport.
btn_w := 11.0 * vw;
btn_h := cast(float) ui_h(4, 0);
bar_w := btn_w * 3.0;
bar_w := btn_w * 5.0;
bar_x := 100.0 * vw / 2.0 - bar_w / 2.0;
bar_y := 100.0 * vh - btn_h - 1.5 * vh;
@ -542,6 +640,14 @@ draw_trile_editor_ui :: (theme: *GR.Overall_Theme) {
r2 : GR.Rect;
r2.x = bar_x; r2.y = bar_y; r2.w = btn_w; r2.h = btn_h;
if keybind_button(r2, "Lit", .TRIXEL_VIEW_LIT, *t_button_selectable(theme, trixel_view_mode == 0)) then trixel_view_mode = 0;
r2.x += btn_w;
if keybind_button(r2, "Normals", .TRIXEL_VIEW_NORMALS, *t_button_selectable(theme, trixel_view_mode == 1)) then trixel_view_mode = 1;
r2.x += btn_w;
if keybind_button(r2, "Albedo", .TRIXEL_VIEW_ALBEDO, *t_button_selectable(theme, trixel_view_mode == 2)) then trixel_view_mode = 2;
r2.x += btn_w;
if keybind_button(r2, "Mixed", .TRIXEL_VIEW_MIXED, *t_button_selectable(theme, trixel_view_mode == 3)) then trixel_view_mode = 3;
r2.x += btn_w;
if keybind_button(r2, "Tile", .TRIXEL_VIEW_TILE, *t_button_selectable(theme, tiling_preview_active)) {
tiling_preview_active = !tiling_preview_active;
}

View File

@ -65,9 +65,9 @@ render_thumbnail :: (trile: *Trile, out: *[THUMB_SIZE * THUMB_SIZE * 4] u8) {
for 0..199 {
if ix >= 0 && ix < 16 && iy >= 0 && iy < 16 && iz >= 0 && iz < 16 {
if trile.trixels[ix][iy][iz] != 0 {
if !trile.trixels[ix][iy][iz].empty {
hit = true;
hc = swatch_color(trile.trixels[ix][iy][iz]);
hc = trile.trixels[ix][iy][iz].material.color;
break;
}
}
@ -106,7 +106,9 @@ render_thumbnail :: (trile: *Trile, out: *[THUMB_SIZE * THUMB_SIZE * 4] u8) {
}
upload_atlas :: () {
texture_update(g_thumbnail_atlas.tex, g_thumbnail_atlas_buf);
imgdata : sg_image_data;
imgdata.subimage[0][0] = .{ g_thumbnail_atlas_buf.data, cast(u64)(ATLAS_SIZE * ATLAS_SIZE * 4) };
sg_update_image(g_thumbnail_atlas.tex, *imgdata);
}
#scope_export
@ -145,7 +147,7 @@ update_trile_thumbnail :: (name: string) {
render_thumbnail(trile, *thumb);
blit_slot(*thumb, slot);
if g_thumbnail_atlas.tex.id != 0 {
if g_thumbnail_atlas.tex.id != INVALID_ID {
upload_atlas();
}
}
@ -165,5 +167,12 @@ init_trile_thumbnails :: () {
blit_slot(*thumb, slot);
}
g_thumbnail_atlas.tex = texture_create(xx ATLAS_SIZE, xx ATLAS_SIZE, g_thumbnail_atlas_buf);
desc := sg_image_desc.{
width = xx ATLAS_SIZE,
height = xx ATLAS_SIZE,
pixel_format = .RGBA8,
usage = .DYNAMIC,
};
g_thumbnail_atlas.tex = sg_make_image(*desc);
upload_atlas();
}

View File

@ -1,373 +0,0 @@
// Entity types are plain structs declared in game code, registered with an
// @Entity note the way console commands register with @Command. The metaprogram
// (src/meta/entity_types.jai) collects them into ENTITY_TYPES, and spawning,
// dispatch, editor UI and saving all come from that. Marking a struct @Entity
// without embedding Entity, or embedding Entity without the note, is an error.
//
// Foo :: struct {
// #as using base : Entity;
// base.type = Foo;
// PARTS :: Entity_Part.[...]; // visuals, may be empty
// EDITOR_MARKER :: "pack.animation"; // optional, editor-only billboard
// } @Entity
// entity_tick :: (using foo: *Foo, dt: float) {} // optional
// entity_draw :: (foo: *Foo, view: View_Id) {} // optional
//
// Fields of type float, s32, bool, string or Vector3 are editable and saved;
// @Slider / @Color notes pick the editor widget.
//
// Each frame the game calls tick_entities, then tick_entity_emitters, then
// draw_entities. The level editor calls the last two only: it shows
// entities and their particles without running their game logic.
#scope_export
Entity :: struct {
type : Type;
id : u32;
position : Vector3;
orientation : u8; // 0..23, see orientation.jai
emitters : [..]Particle_Emitter_Instance; // built from PARTS, not saved
}
Entity_Part_Kind :: enum {
TRILE;
BILLBOARD;
EMITTER;
}
Entity_Part :: struct {
kind : Entity_Part_Kind;
trile : string; // TRILE
orientation : u8; // TRILE, 0..23
animation : string; // BILLBOARD, e.g. "game_core.ball"
emitter : string; // EMITTER, a particle definition name
offset : Vector3;
}
Entity_Type_Info :: struct {
name : string;
type : Type;
parts : []Entity_Part;
editor_marker : string;
create : () -> *Entity;
}
#insert #run,stallable gen_entity_type_table();
get_entity_type_info :: (type: Type) -> *Entity_Type_Info {
for * ENTITY_TYPE_TABLE if it.type == type return it;
return null;
}
get_entity_parts :: (type: Type) -> []Entity_Part {
info := get_entity_type_info(type);
return ifx info then info.parts else .[];
}
get_entity_editor_marker :: (type: Type) -> string {
info := get_entity_type_info(type);
return ifx info then info.editor_marker else "";
}
entity_type_name :: (type: Type) -> string {
return (cast(*Type_Info_Struct) type).name;
}
// Entities turn about the middle of their own cell, so a rotated entity stays
// on the block it was placed on.
ENTITY_PIVOT :: Vector3.{0.5, 0.5, 0.5};
entity_point :: (e: *Entity, offset: Vector3) -> Vector3 {
return e.position + ENTITY_PIVOT + rotate_by_orientation(e.orientation, offset - ENTITY_PIVOT);
}
// A trile part fills a cell rather than being a point, so the pivot cancels out.
entity_trile_point :: (e: *Entity, offset: Vector3) -> Vector3 {
return e.position + rotate_by_orientation(e.orientation, offset);
}
// Orientation is taken up front rather than assigned afterwards because the
// emitters are placed relative to it.
spawn_entity :: (world: *World, type_name: string, position: Vector3, orientation: u8 = 0) -> *Entity {
for ENTITY_TYPE_TABLE {
if it.name != type_name continue;
e := it.create();
e.position = position;
e.orientation = orientation;
e.id = world.next_entity_id;
world.next_entity_id += 1;
init_entity_emitters(e);
array_add(*world.entities, e);
return e;
}
log_error("Unknown entity type: %", type_name);
return null;
}
get_first_entity :: (world: *World, type: Type) -> *Entity {
for e: world.entities if e.type == type return e;
return null;
}
remove_entity :: (world: *World, e: *Entity) {
for other, i: world.entities {
if other != e continue;
array_ordered_remove_by_index(*world.entities, i);
free_entity(e);
return;
}
}
free_entity :: (e: *Entity) {
array_free(e.emitters);
free(e);
}
free_all_entities :: (world: *World) {
for e: world.entities free_entity(e);
array_reset(*world.entities);
}
// Concrete per-type overloads in game code take precedence over these.
entity_tick :: (e: *$T/Entity, dt: float) {}
entity_draw :: (e: *$T/Entity, view: View_Id) { draw_entity_parts(e, view); }
tick_entities :: (world: *World, dt: float) {
for e: world.entities entity_dispatch_tick(e, dt);
}
entity_dispatch_tick :: (e: *Entity, dt: float) {
#insert #run,stallable gen_entity_dispatch("entity_tick(cast(*%)e, dt)");
}
entity_dispatch_draw :: (e: *Entity, view: View_Id) {
#insert #run,stallable gen_entity_dispatch("entity_draw(cast(*%)e, view)");
}
// Dispatches a call on the entity's concrete type. '%' in 'call' becomes the type
// name. Also used by the editor to run autoedit on an entity's own fields.
gen_entity_dispatch :: (call: string) -> string {
builder : String_Builder;
for ENTITY_TYPES {
name := (cast(*Type_Info_Struct) it).name;
print_to_builder(*builder, "if e.type == % { %; return; }\n", name, tprint(call, name));
}
return builder_to_string(*builder);
}
init_entity_emitters :: (e: *Entity) {
for part: get_entity_parts(e.type) {
if part.kind != .EMITTER continue;
inst : Particle_Emitter_Instance;
inst.definition_name = part.emitter;
inst.definition = get_emitter_def(part.emitter);
inst.offset = part.offset;
inst.position = entity_point(e, part.offset);
inst.active = true;
array_add(*e.emitters, inst);
}
}
tick_entity_emitters :: (world: *World, dt: float) {
for e: world.entities {
for *inst: e.emitters {
if inst.definition == null {
inst.definition = get_emitter_def(inst.definition_name);
if inst.definition == null continue;
}
inst.position = entity_point(e, inst.offset);
tick_emitter_instance(inst, dt);
}
}
}
// Once per frame, from the game and from the editor.
draw_entities :: (view: View_Id, world: *World) {
for e: world.entities entity_dispatch_draw(e, view);
}
entity_draw_trile :: (e: *Entity, view: View_Id, trile: string, offset: Vector3, orientation: u8 = 0) {
mesh, ok := get_trile_mesh(trile);
if !ok return;
draw_trile(view, mesh, entity_trile_point(e, offset), compose_orientations(e.orientation, orientation));
}
entity_draw_billboard :: (e: *Entity, view: View_Id, animation: string, offset: Vector3, frame: s32 = 0) {
anim := get_animation_from_string(animation);
if anim == null return;
draw_animation_frame(view, anim, frame, entity_point(e, offset));
}
draw_entity_parts :: (e: *Entity, view: View_Id) {
for part: get_entity_parts(e.type) {
if part.kind == {
case .TRILE; entity_draw_trile(e, view, part.trile, part.offset, part.orientation);
case .BILLBOARD; entity_draw_billboard(e, view, part.animation, part.offset);
case .EMITTER; // tick_entity_emitters handles these
}
}
}
// Fields are saved as name/value string pairs rather than typed JSON, so adding,
// removing or renaming one never breaks loading an older world.
Entity_Field :: struct {
name : string;
value : string;
}
entity_fields_to_strings :: (e: *Entity) -> []Entity_Field {
result : [..]Entity_Field;
result.allocator = temp;
for member: entity_saved_members(e.type) {
ptr := (cast(*u8) e) + member.offset_in_bytes;
value : string;
if member.type == type_info(float) value = tprint("%", (cast(*float) ptr).*);
else if member.type == type_info(s32) value = tprint("%", (cast(*s32) ptr).*);
else if member.type == type_info(bool) value = tprint("%", (cast(*bool) ptr).*);
else if member.type == type_info(string) value = (cast(*string) ptr).*;
else if member.type == type_info(Vector3) {
v := (cast(*Vector3) ptr).*;
value = tprint("% % %", v.x, v.y, v.z);
} else continue;
array_add(*result, .{member.name, value});
}
return result;
}
entity_apply_field :: (e: *Entity, name: string, value: string) {
for member: entity_saved_members(e.type) {
if member.name != name continue;
ptr := (cast(*u8) e) + member.offset_in_bytes;
if member.type == type_info(float) {
v, ok := string_to_float(value);
if ok { (cast(*float) ptr).* = v; }
} else if member.type == type_info(s32) {
v, ok := string_to_int(value);
if ok { (cast(*s32) ptr).* = cast(s32) v; }
} else if member.type == type_info(bool) {
(cast(*bool) ptr).* = (value == "true");
} else if member.type == type_info(string) {
(cast(*string) ptr).* = copy_string(value);
} else if member.type == type_info(Vector3) {
parts := split(value, " ",, temp);
if parts.count != 3 return;
v : Vector3;
x, okx := string_to_float(parts[0]);
y, oky := string_to_float(parts[1]);
z, okz := string_to_float(parts[2]);
if okx && oky && okz { (cast(*Vector3) ptr).* = .{x, y, z}; }
}
return;
}
log_warn("Entity type % has no field '%', skipping", entity_type_name(e.type), name);
}
#scope_file
#import "String";
// The members an entity type declares itself: constants and the Entity base are
// neither editable nor saved.
entity_saved_members :: (type: Type) -> []Type_Info_Struct_Member {
result : [..]Type_Info_Struct_Member;
result.allocator = temp;
for (cast(*Type_Info_Struct) type).members {
if it.flags & (.CONSTANT | .USING) continue;
if array_find(it.notes, "NoSave") continue;
array_add(*result, it);
}
return result;
}
#if FLAG_TEST_ENGINE {
// The two placement helpers use different pivots, and getting them the wrong
// way round only shows up as parts drifting off a rotated entity, so both are
// pinned to the invariant they exist for.
test_entity_part_placement :: () {
s := begin_suite("entity part placement");
e : Entity;
e.position = .{10, 3, -5};
// A trile part on the entity's own cell has to stay on that cell however
// the entity is turned, or rotating would walk it off the block it was
// placed on.
on_its_block := true;
for ori: 0..ORIENTATION_COUNT-1 {
e.orientation = cast(u8) ori;
if entity_trile_point(*e, .{0, 0, 0}) != e.position on_its_block = false;
}
check(*s, "a trile part on the entity's own cell never moves", on_its_block);
// Point parts turn about the middle of that same cell, so a part sitting
// at the middle is the fixed point of all 24 rotations.
at_the_middle := true;
for ori: 0..ORIENTATION_COUNT-1 {
e.orientation = cast(u8) ori;
if entity_point(*e, ENTITY_PIVOT) != e.position + ENTITY_PIVOT at_the_middle = false;
}
check(*s, "a point part at the middle never moves", at_the_middle);
e.orientation = 0;
check(*s, "unturned point parts sit at their authored offset",
entity_point(*e, .{1, 2, 3}) == e.position + Vector3.{1, 2, 3});
check(*s, "unturned trile parts sit at their authored offset",
entity_trile_point(*e, .{1, 2, 3}) == e.position + Vector3.{1, 2, 3});
// Orientation 1 is the quarter twist about Y, which sends X to Z.
e.orientation = 1;
check(*s, "a turned entity carries its parts around with it",
entity_trile_point(*e, .{1, 0, 0}) == e.position + Vector3.{0, 0, 1});
end_suite(s);
}
// Every declared type must reach the table, or it would be unspawnable and
// unsaveable with no diagnostic anywhere.
test_entity_type_table :: () {
s := begin_suite("entity type table");
check(*s, "the table covers every declared type", ENTITY_TYPE_TABLE.count == ENTITY_TYPES.count);
found_all := true;
for type: ENTITY_TYPES if get_entity_type_info(type) == null found_all = false;
check(*s, "every declared type is findable by type", found_all);
names_match := true;
for info: ENTITY_TYPE_TABLE if info.name != entity_type_name(info.type) names_match = false;
check(*s, "table names match the type names saves use", names_match);
// Entity itself is never a declared type, so it stands in for one that
// this build does not have.
check(*s, "an unknown type has no info", get_entity_type_info(Entity) == null);
check(*s, "an unknown type has no parts", get_entity_parts(Entity).count == 0);
end_suite(s);
}
// Stallable because the type table is: ENTITY_TYPES only exists once the
// metaprogram has seen every @Entity in the program.
#run,stallable {
test_entity_part_placement();
test_entity_type_table();
}
}
gen_entity_type_table :: () -> string {
builder : String_Builder;
append(*builder, "ENTITY_TYPE_TABLE :: Entity_Type_Info.[\n");
for ENTITY_TYPES {
ti := cast(*Type_Info_Struct) it;
marker := "\"\"";
for member: ti.members {
if member.name == "EDITOR_MARKER" && (member.flags & .CONSTANT) {
marker = tprint("%.EDITOR_MARKER", ti.name);
break;
}
}
print_to_builder(*builder,
" .{name = \"%\", type = %, parts = %.PARTS, editor_marker = %, create = () -> *Entity { return New(%); }},\n",
ti.name, ti.name, ti.name, marker, ti.name);
}
append(*builder, "];\n");
return builder_to_string(*builder);
}

View File

@ -1,88 +0,0 @@
// Our foliage system. Cells on a 2D grid.
Foliage_Type :: enum u8 {
GRASS; // For stuff that you want to just paint and gets rendered a lot. Grass or hay.
TREE; // For when you want single placements of something. Trees or bushes.
}
Foliage_Frame_Mode :: enum u8 {
VARIANT; // The seed picks one frame and it stays there.
LOOP; // Plays the animation, with the seed offsetting the start.
}
Foliage_Def :: struct {
name : string;
type : Foliage_Type;
// What we render
animation_name : string;
frame_mode : Foliage_Frame_Mode;
density : float = 0.5; // Only does anything when foliage type is grass.
scale_min : float = 1;
scale_max : float = 1;
jitter : float = 0.4;
sway_intensity : float;
sway_speed : float = 1;
}
FOLIAGE_NONE :: 0;
// Chunks in the world contain a 2D grid of these.
Foliage_Cell :: struct {
id : u8; // FOLIAGE_NONE when empty.
y : u8; // Local y inside the chunk, so we don't draw grass on top of a bridge.
}
g_foliage_defs : [..]Foliage_Def;
get_foliage_def :: (name: string) -> *Foliage_Def {
for *def: g_foliage_defs {
if def.name == name then return def;
}
return null;
}
register_foliage_def :: (def: Foliage_Def) {
for *existing: g_foliage_defs {
if existing.name == def.name {
existing.* = def;
return;
}
}
array_add(*g_foliage_defs, def);
}
get_foliage_def_names :: () -> []string {
names : [..]string;
names.allocator = temp;
for def: g_foliage_defs {
array_add(*names, def.name);
}
return names;
}
load_foliage_defs_from_json :: (s: string) {
success, defs := Jaison.json_parse_string(s, [..]Foliage_Def,, temp);
if !success {
log_error("Failed to parse foliage definitions");
return;
}
array_reset(*g_foliage_defs);
for defs {
def := it;
def.name = sprint("%", it.name);
def.animation_name = sprint("%", it.animation_name);
array_add(*g_foliage_defs, def);
}
log_info("Loaded % foliage definitions from pack", g_foliage_defs.count);
}
save_foliage_defs :: () {
#if OS != .WASM {
file :: #import "File";
json := Jaison.json_write_string(g_foliage_defs, " ");
file.write_entire_file(tprint("%/game_core/foliage.json", GAME_RESOURCES_DIR), json);
log_info("Saved % foliage definitions", g_foliage_defs.count);
}
}

View File

@ -28,7 +28,6 @@ Editor_Action :: enum {
LEVEL_TOOL_LINE;
LEVEL_TOOL_INSPECTOR;
LEVEL_TOOL_VIEWER;
LEVEL_TOOL_ENTITY;
// Level editor — tabs
LEVEL_TAB_TOOLS;
LEVEL_TAB_INFO;
@ -39,9 +38,11 @@ Editor_Action :: enum {
STUDIO_TRILE;
STUDIO_LEVEL;
STUDIO_MATERIAL;
STUDIO_SWATCHES;
STUDIO_FOLIAGE;
// Trile editor — view modes
TRIXEL_VIEW_LIT;
TRIXEL_VIEW_NORMALS;
TRIXEL_VIEW_ALBEDO;
TRIXEL_VIEW_MIXED;
TRIXEL_VIEW_TILE;
// Trile editor — tiling controls
TRIXEL_TILING_AXIS;
@ -49,7 +50,6 @@ Editor_Action :: enum {
// Trile editor — undo/redo
TRIXEL_UNDO;
TRIXEL_REDO;
TRIXEL_TOOL_PICK;
}
#scope_file;
@ -64,15 +64,8 @@ Key_Combo :: struct {
shift : bool;
}
MAX_BINDINGS_PER_ACTION :: 4;
Action_Bindings :: struct {
combos : [MAX_BINDINGS_PER_ACTION]Key_Combo;
count : s32;
}
// 5000 different actions ought to be enough for anything right?
keys : [5000]Action_Bindings;
keys : [5000]Key_Combo;
check_action :: (action: $T, status: Key_Current_State) -> bool {
offset := 0;
@ -81,16 +74,12 @@ check_action :: (action: $T, status: Key_Current_State) -> bool {
}
if console_open_ignore_input then return false;
if ui_keyboard_captured then return false;
bindings := *keys[cast(s32)action + offset];
combo := keys[cast(s32)action + offset];
ctrl_down := cast(bool)(input_button_states[Key_Code.CTRL] & .DOWN);
shift_down := cast(bool)(input_button_states[Key_Code.SHIFT] & .DOWN);
for 0..bindings.count-1 {
combo := bindings.combos[it];
if combo.ctrl != ctrl_down then continue;
if combo.shift != shift_down then continue;
if input_button_states[combo.key] & status then return true;
}
return false;
if combo.ctrl != ctrl_down then return false;
if combo.shift != shift_down then return false;
return cast(bool)(input_button_states[combo.key] & status);
}
@ -102,17 +91,7 @@ set_binding :: (action: $T, combo: Key_Combo) {
offset = EDITOR_ACTION_COUNT;
}
bindings := *keys[cast(s32)action + offset];
for 0..bindings.count-1 {
c := bindings.combos[it];
if c.key == combo.key && c.ctrl == combo.ctrl && c.shift == combo.shift return;
}
if bindings.count >= MAX_BINDINGS_PER_ACTION {
log_warn("Action % already has % bindings, ignoring %", action, MAX_BINDINGS_PER_ACTION, combo);
return;
}
bindings.combos[bindings.count] = combo;
bindings.count += 1;
keys[cast(s32)action + offset] = combo;
}
is_action_down :: (action: $T) -> bool {
@ -123,10 +102,6 @@ is_action_start :: (action: $T) -> bool {
return check_action(action, .START);
}
is_action_stop :: (action: $T) -> bool {
return check_action(action, .END);
}
init_keybinds :: () {
set_default_bindings();
}
@ -136,7 +111,7 @@ get_action_combo :: (action: $T) -> Key_Combo {
#if T != Editor_Action {
offset = EDITOR_ACTION_COUNT;
}
return keys[cast(s32)action + offset].combos[0];
return keys[cast(s32)action + offset];
}
keybind_button :: (r: GR.Rect, label: string, action: Editor_Action, theme: *GR.Button_Theme, loc := #caller_location) -> bool {
@ -205,7 +180,6 @@ set_default_bindings :: () {
set(.LEVEL_TOOL_LINE, cast(Key_Code) #char "4");
set(.LEVEL_TOOL_INSPECTOR, cast(Key_Code) #char "5");
set(.LEVEL_TOOL_VIEWER, cast(Key_Code) #char "6");
set(.LEVEL_TOOL_ENTITY, cast(Key_Code) #char "7");
set(.LEVEL_TAB_TOOLS, cast(Key_Code) #char "T");
set(.LEVEL_TAB_INFO, cast(Key_Code) #char "I");
set(.LEVEL_TAB_TACOMA, cast(Key_Code) #char "X");
@ -213,12 +187,13 @@ set_default_bindings :: () {
set(.STUDIO_TRILE, cast(Key_Code) #char "1", ctrl = true);
set(.STUDIO_LEVEL, cast(Key_Code) #char "2", ctrl = true);
set(.STUDIO_MATERIAL, cast(Key_Code) #char "3", ctrl = true);
set(.STUDIO_SWATCHES, cast(Key_Code) #char "4", ctrl = true);
set(.STUDIO_FOLIAGE, cast(Key_Code) #char "5", ctrl = true);
set(.TRIXEL_VIEW_LIT, cast(Key_Code) #char "5");
set(.TRIXEL_VIEW_NORMALS, cast(Key_Code) #char "6");
set(.TRIXEL_VIEW_ALBEDO, cast(Key_Code) #char "7");
set(.TRIXEL_VIEW_MIXED, cast(Key_Code) #char "8");
set(.TRIXEL_VIEW_TILE, cast(Key_Code) #char "9");
set(.TRIXEL_TILING_AXIS, cast(Key_Code) #char "Q");
set(.TRIXEL_TILING_ROTATE, cast(Key_Code) #char "R");
set(.TRIXEL_UNDO, cast(Key_Code) #char "Z", ctrl = true);
set(.TRIXEL_REDO, cast(Key_Code) #char "Y", ctrl = true);
set(.TRIXEL_TOOL_PICK, cast(Key_Code) #char "K");
}

View File

@ -1,4 +1,4 @@
#if FLAG_USE_TEST_GAME {
#if FLAG_TEST_EXE_ENGINE {
PACK_DIR :: "./test_packs";
GAME_RESOURCES_DIR :: "./test_game/resources";
} else {

View File

@ -18,10 +18,8 @@ _emit :: (level: Log_Level, message: string) {
else "[INFO] ";
// We need to protect the logger from custom allocators since we don't
// want those meddling with our log row allocation! Before the engine has
// set one up — at compile time, in a #run — there is nothing to protect
// against and nothing to allocate from, so the current one has to do.
push_allocator(ifx default_allocator.proc then default_allocator else context.allocator);
// want those meddling with our log row allocation!
push_allocator(default_allocator);
line := copy_string(tprint("%1%2", prefix, message));
print("%\n", line);
console_add_output_line(line);

View File

@ -15,17 +15,6 @@ String :: #import "String";
Jaison :: #import "Jaison";
stbi :: #import "stb_image";
FLAG_ANY_TEST :: FLAG_TEST_ENGINE || FLAG_TEST_EXE_ENGINE || FLAG_TEST_GAME || FLAG_TEST_EXE_GAME;
FLAG_ANY_EXE_TEST :: FLAG_TEST_EXE_ENGINE || FLAG_TEST_EXE_GAME;
// The engine's own tests build against test_game/, so they still compile on a
// checkout that does not have the game/ directory. Game tests build against the
// real game, which is the whole point of them.
FLAG_USE_TEST_GAME :: FLAG_TEST_ENGINE || FLAG_TEST_EXE_ENGINE;
// The framework (suites, checks, the exe-test command list) is available under
// every test flag; the engine's own test cases are not.
#if FLAG_ANY_TEST { #load "tests/framework.jai"; }
#if (FLAG_TEST_ENGINE || FLAG_TEST_EXE_ENGINE) { #load "tests/index.jai"; }
#load "logging.jai";
@ -35,6 +24,7 @@ FLAG_USE_TEST_GAME :: FLAG_TEST_ENGINE || FLAG_TEST_EXE_ENGINE;
#load "rendering/rendering.jai";
#load "input/hotkeys.jai";
#load "ui/ui.jai";
#load "editor/rdm_disk.jai";
#if !FLAG_RELEASE_BUILD { #load "editor/editor.jai"; }
#load "time.jai";
#load "events.jai";
@ -42,20 +32,18 @@ FLAG_USE_TEST_GAME :: FLAG_TEST_ENGINE || FLAG_TEST_EXE_ENGINE;
#load "ray.jai";
#load "profiling.jai";
#load "particles/particles.jai";
#load "foliage.jai";
#load "orientation.jai";
#load "entities.jai";
#load "world.jai";
#load "utils.jai";
#load "audio/audio.jai";
#load "assets/asset_manager.jai";
#load "settings_menu.jai";
#load "loading_screen.jai";
#load "ui/demo.jai";
#if FLAG_USE_TEST_GAME {
#load "../test_game/game.jai";
} else {
#if !FLAG_TEST_EXE_ENGINE {
#load "../game/game.jai";
} else {
#load "../test_game/game.jai";
}
last_frame_time : float64; // timestamp of the last frame
@ -67,7 +55,11 @@ V_MAJOR :: 0;
V_MINOR :: 7;
state: struct {
pass_action_clear : sg_pass_action;
pass_action_clear_gbuf : sg_pass_action;
pass_action : sg_pass_action;
dpi_scale : float;
fons : *FONScontext;
font_default : Ui_Font;
};
@ -97,7 +89,15 @@ round_pow2 :: (v: float) -> s32 {
debug_font : *Font;
init :: () {
scene_init();
sg_setup(*(sg_desc.{
environment = cast,force(sg_environment) sglue_environment(),
logger = .{ func = slog_func },
attachments_pool_size = 64,
buffer_pool_size = 1024,
}));
sgl_setup(*(sgl_desc_t.{
logger = .{ func = slog_func },
}));
sfetch_setup(*(sfetch_desc_t.{
max_requests = 128,
num_channels = 2,
@ -108,16 +108,25 @@ init :: () {
#if OS != .WASM { Thread.init(*g_mixer.mutex); }
audio_init_thread_context();
saudio_setup(*(saudio_desc.{
logger = .{ func = slog_func },
stream_cb = sokol_audio_callback,
num_channels = 2,
}));
g_audio_sample_rate = saudio_sample_rate();
stm_setup();
state.dpi_scale = sapp_dpi_scale();
atlas_dim := round_pow2(1024.0 * state.dpi_scale);
text_init(atlas_dim);
fons_context := sfons_create(*(sfons_desc_t.{
width = atlas_dim,
height = atlas_dim,
}));
state.fons = fons_context;
state.font_default.fons_font = FONS_INVALID;
create_pipelines();
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;
@ -130,22 +139,19 @@ init :: () {
#if FLAG_TEST_EXE_ENGINE {
engine_exe_tests_add();
}
#if FLAG_TEST_EXE_GAME {
// Fixed-name hook, same convention as game_init / game_tick.
game_exe_tests_add();
}
}
init_after_core :: () {
init_plane_textures();
init_brdf_lut();
add_font_from_pack("boot", "DroidSerif-Regular.ttf");
ui_init_font_fields(*state.font_default);
init_ui();
ltriles();
tt := get_trile_table_ptr();
if tt.count == 0 {
test : Trile;
trile_fill(*test, first_used_swatch());
set_trile("test", test);
set_trile("test", Trile.{});
} else {
name : string;
for v : tt {
@ -159,6 +165,8 @@ init_after_core :: () {
init_keybinds();
init_settings_menu();
}
init_rendering();
load_post_process_from_pack();
audio_init();
@ -169,9 +177,10 @@ init_after_core :: () {
// We want to do this last.
game_init();
lock_cursor_if_should();
// #if UNCAPPED_FRAMES then sapp_glx_swapinterval(0);
}
is_in_reflection_pass : bool = false;
delta_time_accumulator : float64 = 0;
@ -193,6 +202,7 @@ frame :: () {
if show_loading_screen() {
input_per_frame_event_and_flag_update();
if init_after_core_done {
fonsClearState(state.fons);
draw_loading_screen();
tick_ui();
ui_pass();
@ -211,10 +221,11 @@ frame :: () {
g_mixer.paused = false;
}
fonsClearState(state.fons);
add_frame_profiling_point("After loading logic");
#if FLAG_ANY_EXE_TEST {
#if FLAG_TEST_EXE_ENGINE {
run_exe_tests();
}
@ -231,28 +242,21 @@ frame :: () {
if delta_time_accumulator > (4.0/60.0) then delta_time_accumulator = 4.0/60.0;
}
if should_tick_game && !settings_menu_blocks_game() {
while delta_time_accumulator > (1.0/480.0) {
game_tick(1.0/480.0);
tick_particles(1.0/480.0);
delta_time_accumulator -= (1.0/480.0);
}
}
fonsClearState(state.fons);
for event: Input.events_this_frame {
GR.getrect_handle_event(event);
}
tick_ui();
if should_tick_game && !settings_menu_blocks_game () {
firstTick := true;
while delta_time_accumulator > (1.0/480.0) {
game_tick(1.0/480.0);
if firstTick {
input_per_frame_event_and_flag_update();
firstTick = false;
}
tick_particles(1.0/480.0);
delta_time_accumulator -= (1.0/480.0);
}
} else {
input_per_frame_event_and_flag_update();
}
add_frame_profiling_point("After game ticks");
tick_ui();
add_frame_profiling_point("After UI tick");
if should_tick_game then game_draw();
@ -268,6 +272,7 @@ frame :: () {
draw_editor();
}
add_particle_render_tasks();
add_frame_profiling_point("After editor draw");
render();
add_frame_profiling_point("After rendering");
@ -280,12 +285,13 @@ frame :: () {
#if OS != .WASM { profiler_update(); }
reset_temporary_storage();
frame_end_time := get_time();
input_per_frame_event_and_flag_update();
}
cleanup :: () {
log_info("Cleanup called....");
audio_cleanup();
scene_shutdown();
sg_shutdown();
}
get_window_size :: () -> (s32, s32) {

View File

@ -1,98 +0,0 @@
/*
Entity types register themselves with an @Entity note on the struct, the same
way console commands register with @Command. The generators in entities.jai
all read one list, ENTITY_TYPES, which is built here and injected once the
rest of the program has been typechecked — that is why those generators are
written as #run,stallable.
*/
#import "Sort";
entity_type_names : [..]string;
entity_types_emitted := false;
add_entity_types :: (message: *Message, w: *Workspace) {
if message.kind == .PHASE {
phase := cast(*Message_Phase) message;
if phase.phase == .TYPECHECKED_ALL_WE_CAN && !entity_types_emitted {
entity_types_emitted = true;
emit_entity_types(w);
}
return;
}
if message.kind != .TYPECHECKED then return;
code := cast(*Message_Typechecked) message;
for code.declarations {
decl := it.expression;
if !decl.expression || decl.expression.kind != .STRUCT then continue;
if !is_main_program(decl) then continue;
noted := false;
for note: decl.notes if note.text == "Entity" noted = true;
embeds := struct_embeds_entity(cast(*Code_Struct) decl.expression);
// Without this the note is as easy to forget as the hand-written list it
// replaced, and forgetting it is silent: the type compiles fine and is
// simply invisible to spawning, the editor and saving.
if embeds && !noted {
report(decl, sprint("Struct % embeds Entity but is not marked @Entity, so nothing can spawn, edit or save it.", decl.name));
continue;
}
if !noted then continue;
if !embeds {
report(decl, sprint("Struct % is marked @Entity but has no '#as using base : Entity;'.", decl.name));
continue;
}
if decl.flags & .SCOPE_FILE {
report(decl, sprint("Entity type % is file scoped, that is not okay. The generated type table has to name it.", decl.name));
continue;
}
array_add(*entity_type_names, sprint("%", decl.name));
}
}
#scope_file
// Emitted even when the game declares no entity types at all: the generators in
// entities.jai are stalled waiting for this name and would never come unstuck.
emit_entity_types :: (w: *Workspace) {
// Sorted by name so the generated table — and with it the order of the
// editor's spawn list — does not shuffle between builds just because
// typechecking happened to finish in a different order.
quick_sort(entity_type_names, compare_strings);
builder : String_Builder;
append(*builder, "ENTITY_TYPES :: Type.[");
for entity_type_names {
if it_index > 0 then append(*builder, ", ");
append(*builder, it);
}
append(*builder, "];\n");
add_build_string(builder_to_string(*builder), w.*);
}
struct_embeds_entity :: (s: *Code_Struct) -> bool {
if !s.defined_type then return false;
for s.defined_type.members {
if !(it.flags & .USING) then continue;
if it.type.type != .STRUCT then continue;
if (cast(*Type_Info_Struct) it.type).name == "Entity" then return true;
}
return false;
}
// Game and engine code only. Module code never declares entity types, and
// build strings have no enclosing load to ask about.
is_main_program :: (decl: *Code_Declaration) -> bool {
if !decl.enclosing_load then return false;
if !decl.enclosing_load.enclosing_import then return false;
return decl.enclosing_load.enclosing_import.module_type == .MAIN_PROGRAM;
}
report :: (decl: *Code_Declaration, error: string) {
compiler_report(error, make_location(cast(*Code_Node) decl), .ERROR);
}

View File

@ -12,8 +12,9 @@ lint_checks :: (message: *Message) -> bool {
if message.kind != .TYPECHECKED then return true;
code := cast(*Message_Typechecked) message;
lint_ok := lint_sokol_graphics(code);
lint_ok := true;
for code.declarations {
import := it.expression.enclosing_load.enclosing_import;
if import.module_type != .MAIN_PROGRAM then return true;
@ -55,39 +56,3 @@ lint_procedure_name :: (name: string, node: Code_Node) -> bool {
return true;
}
}
lint_sokol_graphics :: (code: *Message_Typechecked) -> bool {
SOKOL_GRAPHICS_MODULES :: string.["gfx", "glue", "gl", "shape", "debugtext", "fontstash"];
is_sokol_graphics :: (path: string) -> bool {
for SOKOL_GRAPHICS_MODULES {
if contains(path, tprint("/sokol-jai/sokol/%/", it)) then return true;
}
return false;
}
ok := true;
for code.all {
load := it.expression.enclosing_load;
if !load || !load.enclosing_import || load.enclosing_import.module_type != .MAIN_PROGRAM then continue;
if contains(load.fully_pathed_filename, "/renderer_sokol/") then continue;
in_scene := ends_with(load.fully_pathed_filename, "/rendering/scene.jai");
for sub: it.subexpressions {
if sub.kind != .IDENT then continue;
decl := (cast(*Code_Ident) sub).resolved_declaration;
if !decl || !decl.enclosing_load then continue;
decl_path := decl.enclosing_load.fully_pathed_filename;
if is_sokol_graphics(decl_path) {
compiler_report(sprint("Linter: % is sokol graphics; only src/renderer_sokol/ may use it\n", decl.name), make_location(sub), .ERROR);
} else if contains(decl_path, "/renderer_sokol/") && !in_scene {
compiler_report(sprint("Linter: % is part of the renderer; only src/rendering/scene.jai may use it\n", decl.name), make_location(sub), .ERROR);
} else {
continue;
}
lint_errors_found += 1;
ok = false;
}
}
return ok;
}

View File

@ -1,10 +1,9 @@
#load "ascii.jai";
#load "../resource_mirror.jai";
#load "pack.jai";
#load "lint.jai";
#load "hacks.jai";
#load "console_commands.jai";
#load "entity_types.jai";
#load "shaderload.jai";
endframe_modified := false;
pointerimage_modified := false;
@ -15,6 +14,4 @@ custom_message_handler :: (message: *Message, w: *Workspace) {
remove_getrect_custom_cursor(message, w);
add_console_commands(message, w);
add_entity_types(message, w);
}

View File

@ -3,9 +3,11 @@
should_ignore_file :: (name: string) -> bool {
if name.count > 0 && name[0] == #char "." return true;
// Aseprite sources are inputs (see resource_mirror.jai), not shippable
// assets; only the .sheet.png/.sheet.json they export go into the pack.
if is_aseprite_source(name) return true;
ok, left, right := split_from_right(name, #char ".");
if right == "aseprite" {
print("Ignoring % as aseprite file...\n", name);
return true;
}
return false;
}
@ -51,10 +53,11 @@ create_pack :: (include_test_resources: bool = false, pack_dir: string = "./pack
#import "Simple_Package";
#import "File";
// Packs come from the processed copy of the resource trees, never the trees
// themselves. See resource_mirror.jai.
for prepare_resource_mirror(include_test_resources) {
util.visit_files(it, true, true, file_visit_handler);
util.visit_files("./resources", true, true, file_visit_handler);
if include_test_resources {
util.visit_files("./test_game/resources", true, true, file_visit_handler);
} else {
util.visit_files("./game/resources", true, true, file_visit_handler);
}
make_directory_if_it_does_not_exist(pack_dir);

16
src/meta/shaderload.jai Normal file
View File

@ -0,0 +1,16 @@
#scope_file
get_shader_paths :: () -> []string {
util :: #import "File_Utilities";
return util.file_list("./src/shaders/jai");
}
#scope_export
add_shaders_to_workspace :: (w: Workspace) {
file :: #import "File";
paths := get_shader_paths();
for paths {
add_build_file(file.get_absolute_path(it), w);
}
}

View File

@ -1,286 +0,0 @@
// The 24 axis-aligned cube rotations, encoded as 'face * 4 + twist'. These
// matrices are the reference: the renderer has to turn an orientation into
// exactly this rotation, so that placement on the CPU lines up with what is
// drawn. They are row-major operators (v' = M * v).
ORIENTATION_COUNT :: 24;
Orientation_Matrix :: struct {
m : [3][3]s8;
}
orientation_matrix :: (ori: u8) -> Orientation_Matrix {
return ORIENTATION_MATRICES[ori % ORIENTATION_COUNT];
}
rotate_by_orientation :: (ori: u8, v: Vector3) -> Vector3 {
m := orientation_matrix(ori).m;
return .{
cast(float)m[0][0] * v.x + cast(float)m[0][1] * v.y + cast(float)m[0][2] * v.z,
cast(float)m[1][0] * v.x + cast(float)m[1][1] * v.y + cast(float)m[1][2] * v.z,
cast(float)m[2][0] * v.x + cast(float)m[2][1] * v.y + cast(float)m[2][2] * v.z,
};
}
// The orientation equal to applying 'inner' first and then 'outer'. Used to fold
// an entity's rotation into the orientation its parts were authored with.
compose_orientations :: (outer: u8, inner: u8) -> u8 {
return ORIENTATION_COMPOSE[outer % ORIENTATION_COUNT][inner % ORIENTATION_COUNT];
}
// The quarter turn that carries 'u' onto 'v' and leaves 'n' alone, where u, v, n
// are a set of perpendicular unit axes. Taking the basis from the caller keeps
// the result consistent with whatever handedness the caller draws with, instead
// of relying on a sign convention agreed at a distance.
orientation_quarter_turn :: (u: Vector3, v: Vector3, n: Vector3) -> u8 {
// M = v*uT - u*vT + n*nT sends u -> v, v -> -u, n -> n.
m : Orientation_Matrix;
ui := to_axis_ints(u);
vi := to_axis_ints(v);
ni := to_axis_ints(n);
for i: 0..2 {
for j: 0..2 {
m.m[i][j] = vi[i]*ui[j] - ui[i]*vi[j] + ni[i]*ni[j];
}
}
ori, ok := orientation_from_matrix(m);
if !ok {
log_error("Rotate gizmo basis is not axis-aligned; ignoring the turn.");
return 0;
}
return ori;
}
orientation_from_matrix :: (m: Orientation_Matrix) -> (ori: u8, ok: bool) {
for i: 0..ORIENTATION_COUNT-1 {
if mat3_equal(m, ORIENTATION_MATRICES[i]) return cast(u8) i, true;
}
return 0, false;
}
#scope_file
ORIENTATION_MATRICES :: #run generate_orientation_matrices();
ORIENTATION_COMPOSE :: #run generate_orientation_compose_table();
// Axis vectors only ever hold 0 or +-1 here, but they arrive as floats.
to_axis_ints :: (v: Vector3) -> [3]s8 {
round_axis :: (f: float) -> s8 {
if f > 0.5 then return 1;
if f < -0.5 then return -1;
return 0;
}
return .[round_axis(v.x), round_axis(v.y), round_axis(v.z)];
}
mat3_of :: (a: s8, b: s8, c: s8, d: s8, e: s8, f: s8, g: s8, h: s8, i: s8) -> Orientation_Matrix {
r : Orientation_Matrix;
r.m[0][0] = a; r.m[0][1] = b; r.m[0][2] = c;
r.m[1][0] = d; r.m[1][1] = e; r.m[1][2] = f;
r.m[2][0] = g; r.m[2][1] = h; r.m[2][2] = i;
return r;
}
// (c, s) are cos and sin of the angle, always in {-1, 0, 1} here.
rot_x_effective :: (s: s8, c: s8) -> Orientation_Matrix { return mat3_of(1, 0, 0, 0, c, s, 0, -s, c); }
rot_y_effective :: (s: s8, c: s8) -> Orientation_Matrix { return mat3_of(c, 0, -s, 0, 1, 0, s, 0, c); }
rot_z_effective :: (s: s8, c: s8) -> Orientation_Matrix { return mat3_of(c, s, 0, -s, c, 0, 0, 0, 1); }
mat3_mul :: (a: Orientation_Matrix, b: Orientation_Matrix) -> Orientation_Matrix {
r : Orientation_Matrix;
for i: 0..2 {
for j: 0..2 {
sum : s8 = 0;
for k: 0..2 sum += a.m[i][k] * b.m[k][j];
r.m[i][j] = sum;
}
}
return r;
}
mat3_equal :: (a: Orientation_Matrix, b: Orientation_Matrix) -> bool {
for i: 0..2 for j: 0..2 if a.m[i][j] != b.m[i][j] return false;
return true;
}
// Mirrors get_orientation_matrix: a face rotation followed by a twist about Y.
generate_orientation_matrices :: () -> [ORIENTATION_COUNT]Orientation_Matrix {
identity :: #run mat3_of(1, 0, 0, 0, 1, 0, 0, 0, 1);
// sin/cos of face and twist angles, written out to keep this exact.
faces : [6]Orientation_Matrix;
faces[0] = identity;
faces[1] = rot_x_effective( 0, -1); // PI
faces[2] = rot_z_effective(-1, 0); // -PI/2
faces[3] = rot_z_effective( 1, 0); // PI/2
faces[4] = rot_x_effective( 1, 0); // PI/2
faces[5] = rot_x_effective(-1, 0); // -PI/2
twists : [4]Orientation_Matrix;
twists[0] = identity;
twists[1] = rot_y_effective( 1, 0);
twists[2] = rot_y_effective( 0, -1);
twists[3] = rot_y_effective(-1, 0);
result : [ORIENTATION_COUNT]Orientation_Matrix;
for face: 0..5 {
for twist: 0..3 {
result[face * 4 + twist] = mat3_mul(faces[face], twists[twist]);
}
}
return result;
}
generate_orientation_compose_table :: () -> [ORIENTATION_COUNT][ORIENTATION_COUNT]u8 {
mats := generate_orientation_matrices();
table : [ORIENTATION_COUNT][ORIENTATION_COUNT]u8;
for a: 0..ORIENTATION_COUNT-1 {
for b: 0..ORIENTATION_COUNT-1 {
product := mat3_mul(mats[a], mats[b]);
found := false;
for c: 0..ORIENTATION_COUNT-1 {
if mat3_equal(product, mats[c]) {
table[a][b] = cast(u8) c;
found = true;
break;
}
}
assert(found, "Cube rotation % * % is not one of the 24 orientations", a, b);
}
}
return table;
}
#if FLAG_TEST_ENGINE {
v3_eq :: (a: Vector3, b: Vector3) -> bool {
return abs(a.x - b.x) < 0.001 && abs(a.y - b.y) < 0.001 && abs(a.z - b.z) < 0.001;
}
test_orientation_set :: () {
s := begin_suite("cube orientations");
distinct := true;
for i: 0..ORIENTATION_COUNT-1 {
for j: i+1..ORIENTATION_COUNT-1 {
if mat3_equal(ORIENTATION_MATRICES[i], ORIENTATION_MATRICES[j]) distinct = false;
}
}
check(*s, "all 24 orientations are distinct", distinct);
// Rotations preserve handedness, so every determinant must be +1. A -1
// would mean the set had picked up a reflection.
proper := true;
for i: 0..ORIENTATION_COUNT-1 {
m := ORIENTATION_MATRICES[i].m;
det := cast(int)m[0][0] * (cast(int)m[1][1]*m[2][2] - cast(int)m[1][2]*m[2][1])
- cast(int)m[0][1] * (cast(int)m[1][0]*m[2][2] - cast(int)m[1][2]*m[2][0])
+ cast(int)m[0][2] * (cast(int)m[1][0]*m[2][1] - cast(int)m[1][1]*m[2][0]);
if det != 1 proper = false;
}
check(*s, "every orientation is a proper rotation", proper);
check(*s, "orientation 0 is the identity",
v3_eq(rotate_by_orientation(0, .{1, 2, 3}), .{1, 2, 3}));
// Orientations 1..3 are the twists: rotations about Y, so Y is fixed.
twists_about_y := true;
for ori: 1..3 {
if !v3_eq(rotate_by_orientation(cast(u8)ori, .{0, 1, 0}), .{0, 1, 0}) twists_about_y = false;
}
check(*s, "the twist orientations turn about Y", twists_about_y);
// Rotations are rigid: lengths survive.
lengths_kept := true;
for ori: 0..ORIENTATION_COUNT-1 {
r := rotate_by_orientation(cast(u8)ori, .{1, 2, 3});
if abs(length(r) - length(Vector3.{1, 2, 3})) > 0.001 lengths_kept = false;
}
check(*s, "rotating preserves length", lengths_kept);
// Pinned against the shader's decode. If these drift, entity part offsets
// will rotate the opposite way from the trile geometry they belong to.
// Orientation 1 is face 0 twist 1, the shader's rot_y(PI/2).
check(*s, "orientation 1 (twist 90) sends X to Z",
v3_eq(rotate_by_orientation(1, .{1, 0, 0}), .{0, 0, 1}));
// Orientation 4 is face 1 twist 0, the shader's rot_x(PI).
check(*s, "orientation 4 (face 1) flips Y and Z",
v3_eq(rotate_by_orientation(4, .{0, 1, 1}), .{0, -1, -1}));
// Orientation 16 is face 4 twist 0, the shader's rot_x(PI/2).
check(*s, "orientation 16 (face 4) sends Y to -Z",
v3_eq(rotate_by_orientation(16, .{0, 1, 0}), .{0, 0, -1}));
end_suite(s);
}
test_orientation_compose :: () {
s := begin_suite("orientation composition");
identity_ok := true;
for ori: 0..ORIENTATION_COUNT-1 {
if compose_orientations(0, cast(u8)ori) != cast(u8)ori identity_ok = false;
if compose_orientations(cast(u8)ori, 0) != cast(u8)ori identity_ok = false;
}
check(*s, "composing with orientation 0 changes nothing", identity_ok);
// Composing a fixed orientation with each of the 24 must hit each exactly
// once, or the table is not a group and some rotations are unreachable.
permutation := true;
for a: 0..ORIENTATION_COUNT-1 {
seen : [ORIENTATION_COUNT]bool;
for b: 0..ORIENTATION_COUNT-1 {
r := compose_orientations(cast(u8)a, cast(u8)b);
if seen[r] permutation = false;
seen[r] = true;
}
}
check(*s, "every row of the compose table is a permutation", permutation);
// compose(outer, inner) must mean "inner first, then outer".
order_ok := true;
v := Vector3.{1, 2, 3};
for a: 0..ORIENTATION_COUNT-1 {
for b: 0..ORIENTATION_COUNT-1 {
combined := rotate_by_orientation(compose_orientations(cast(u8)a, cast(u8)b), v);
stepwise := rotate_by_orientation(cast(u8)a, rotate_by_orientation(cast(u8)b, v));
if !v3_eq(combined, stepwise) order_ok = false;
}
}
check(*s, "composing matches applying inner then outer", order_ok);
end_suite(s);
}
test_orientation_quarter_turn :: () {
s := begin_suite("orientation quarter turns");
X :: Vector3.{1, 0, 0};
Y :: Vector3.{0, 1, 0};
Z :: Vector3.{0, 0, 1};
q := orientation_quarter_turn(X, Z, Y); // turn X toward Z about Y
check(*s, "a quarter turn carries u onto v", v3_eq(rotate_by_orientation(q, X), Z));
check(*s, "a quarter turn carries v onto -u", v3_eq(rotate_by_orientation(q, Z), .{-1, 0, 0}));
check(*s, "a quarter turn leaves its axis alone", v3_eq(rotate_by_orientation(q, Y), Y));
four := compose_orientations(q, compose_orientations(q, compose_orientations(q, q)));
check(*s, "four quarter turns return to the start", four == 0);
// Dragging the ring back the other way must undo the turn exactly.
back := orientation_quarter_turn(Z, X, Y);
check(*s, "the reverse turn cancels the forward one", compose_orientations(q, back) == 0);
all_valid := true;
for pair: Vector3.[X, Y, Z] {
u := pair;
v := ifx v3_eq(u, X) then Y else ifx v3_eq(u, Y) then Z else X;
n := cross(u, v);
t := orientation_quarter_turn(u, v, n);
if !v3_eq(rotate_by_orientation(t, u), v) all_valid = false;
}
check(*s, "quarter turns work on every axis pair", all_valid);
end_suite(s);
}
#run {
test_orientation_set();
test_orientation_compose();
test_orientation_quarter_turn();
}
}

View File

@ -1,7 +1,5 @@
#if !FLAG_RELEASE_BUILD && OS != .WASM {
#load "resource_mirror.jai";
#import "String";
Pack_Writer :: #import "Simple_Package";
File_Util :: #import "File_Utilities";
@ -38,11 +36,8 @@ _hotreload_visitor :: (info: *File_Util.File_Visit_Info, packs: *Table(string, P
recreate_packs_on_disk :: () {
packs: Table(string, Pack_Writer.Create_Package);
// Same mirror-and-process pipeline the build uses, so a hot reload picks up
// edited .aseprite files too, not just plain assets.
for prepare_resource_mirror(FLAG_USE_TEST_GAME) {
File_Util.visit_files(it, true, *packs, _hotreload_visitor);
}
File_Util.visit_files("./resources", true, *packs, _hotreload_visitor);
File_Util.visit_files(GAME_RESOURCES_DIR, true, *packs, _hotreload_visitor);
for pack, key: packs {
Pack_Writer.write(*pack, tprint("%/%.pack", PACK_DIR, key));
log_info("Hot-reload: wrote pack '%'", key);

View File

@ -19,7 +19,6 @@ Particle_Emitter_Config :: struct {
color_start : Vector4 = .{1, 1, 1, 1};
color_end : Vector4 = .{1, 1, 1, 0};
gravity : float = 2.0;
glow : float; // 0..1, like swatch emittance
blend_mode : Particle_Blend_Mode = .ADDITIVE;
}
@ -42,25 +41,61 @@ Particle :: struct {
definition : *Particle_Emitter_Config;
}
MAX_PARTICLES :: 16384;
BUCKET_MAX_PARTICLES :: 2048;
PARTICLE_BUCKETS :: 16;
MAX_PARTICLES :: PARTICLE_BUCKETS * BUCKET_MAX_PARTICLES;
Particle_Bucket :: struct {
in_use : bool;
name : string;
particles : [BUCKET_MAX_PARTICLES] Particle;
}
g_particle_buckets : [PARTICLE_BUCKETS] Particle_Bucket;
Particle_Render_Buffer :: struct {
total_count : s32;
pos_size : [MAX_PARTICLES] Vector4;
uv_rects : [MAX_PARTICLES] Vector4;
colors : [MAX_PARTICLES] Vector4;
}
g_particle_render_buffer : Particle_Render_Buffer;
g_particles : [MAX_PARTICLES] Particle;
g_emitter_defs : [..] Particle_Emitter_Config;
clear_particles :: () {
for *p: g_particles p.alive = false;
for *bucket: g_particle_buckets {
bucket.in_use = false;
bucket.name = "";
for *p: bucket.particles p.alive = false;
}
}
count_alive_particles :: () -> s32 {
count : s32 = 0;
for *bucket: g_particle_buckets {
if !bucket.in_use then continue;
for p: bucket.particles {
if p.alive then count += 1;
}
}
return count;
}
tick_particles_physics :: (dt: float) {
for *p: g_particles {
if !p.alive then continue;
p.age += dt;
if p.age >= p.lifetime {
p.alive = false;
continue;
for *bucket: g_particle_buckets {
for *p: bucket.particles {
if !p.alive then continue;
p.age += dt;
if p.age >= p.lifetime {
p.alive = false;
continue;
}
p.velocity.y -= p.gravity * dt;
p.position += p.velocity * dt;
}
p.velocity.y -= p.gravity * dt;
p.position += p.velocity * dt;
}
}
@ -98,78 +133,68 @@ tick_emitter_instance :: (inst: *Particle_Emitter_Instance, dt: float) {
}
}
// Live particles are global, so the game and each editor view draw them into
// their own view.
draw_live_particles :: (view: View_Id) {
Batch :: struct {
anim_name : string;
blend_mode : Particle_Blend_Mode;
anim : *Animation;
particles : [..]Particle_Instance;
}
add_particle_render_tasks :: () {
buf := *g_particle_render_buffer;
total_count : s32 = 0;
batches : [..] Batch;
batches.allocator = temp;
for *bucket: g_particle_buckets {
if !bucket.in_use then continue;
has_water, water_y := particle_water_height();
for *p: g_particles {
if !p.alive then continue;
if p.definition == null then continue;
if has_water && p.position.y < water_y then continue;
def := p.definition;
def := get_emitter_def(bucket.name);
if def == null then continue;
if def.animation_name.count == 0 then continue;
batch : *Batch = null;
for *b: batches {
if b.anim_name == def.animation_name && b.blend_mode == def.blend_mode {
batch = b;
break;
}
}
anim := get_animation_from_string(def.animation_name);
if anim == null then continue;
if anim.frames.count == 0 then continue;
if batch == null {
anim := get_animation_from_string(def.animation_name);
if anim == null then continue;
if anim.frames.count == 0 then continue;
batch = array_add(*batches);
batch.anim_name = def.animation_name;
batch.blend_mode = def.blend_mode;
batch.anim = anim;
batch.particles.allocator = temp;
}
instance_offset := total_count;
bucket_count : s32 = 0;
t := p.age / p.lifetime;
anim := batch.anim;
frame := clamp(cast(s32)(t * cast(float)anim.frames.count), 0, cast(s32)(anim.frames.count - 1));
f := anim.frames[frame];
array_add(*batch.particles, .{
position = p.position,
size = def.size_start + (def.size_end - def.size_start) * t,
uv_rect = .{
for *p: bucket.particles {
if !p.alive then continue;
if total_count + bucket_count >= MAX_PARTICLES then break;
t := p.age / p.lifetime;
size := def.size_start + (def.size_end - def.size_start) * t;
col := lerp_color(def.color_start, def.color_end, t);
idx := instance_offset + bucket_count;
buf.pos_size[idx] = .{p.position.x, p.position.y, p.position.z, size};
frame_count := anim.frames.count;
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];
buf.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,
},
color = lerp_color(def.color_start, def.color_end, t),
glow = def.glow,
});
};
buf.colors[idx] = col;
bucket_count += 1;
}
if bucket_count > 0 {
draw_task : Rendering_Task_Particles;
draw_task.count = bucket_count;
draw_task.instance_offset = instance_offset;
draw_task.blend_mode = def.blend_mode;
draw_task.sheet = anim.sheet;
add_rendering_task(draw_task);
total_count += bucket_count;
}
}
for batches {
blend := ifx it.blend_mode == .ADDITIVE then Blend_Mode.ADDITIVE else .ALPHA;
draw_particles(view, it.anim.sheet, blend, it.particles);
}
}
buf.total_count = total_count;
particle_water_height :: () -> (has_water: bool, height: float) {
#if !FLAG_RELEASE_BUILD {
if in_editor_view && !editor_shows_water() return false, 0;
if total_count > 0 {
upload_task : Rendering_Task_Particles_Buffer;
upload_task.total_count = total_count;
add_rendering_task(upload_task);
}
curworld := get_current_world();
if !curworld.valid || curworld.world.conf.indoor return false, 0;
return true, effective_plane_height(*curworld.world.conf);
}
get_emitter_def :: (name: string) -> *Particle_Emitter_Config {
@ -216,8 +241,32 @@ save_particle_defs :: () {
}
}
#scope_file
Random :: #import "Random";
rng :: inline (lo: float, hi: float) -> float {
return lo + Random.random_get_zero_to_one() * (hi - lo);
}
find_or_claim_bucket :: (name: string) -> *Particle_Bucket {
free_slot : *Particle_Bucket = null;
for *bucket: g_particle_buckets {
if bucket.in_use && bucket.name == name then return bucket;
if !bucket.in_use && free_slot == null then free_slot = bucket;
}
if free_slot {
free_slot.in_use = true;
free_slot.name = name;
}
return free_slot;
}
spawn_one_particle :: (position: Vector3, def: *Particle_Emitter_Config) {
for *p: g_particles {
bucket := find_or_claim_bucket(def.name);
if bucket == null then return;
for *p: bucket.particles {
if p.alive then continue;
p.alive = true;
p.age = 0;
@ -238,14 +287,6 @@ spawn_one_particle :: (position: Vector3, def: *Particle_Emitter_Config) {
}
}
#scope_file
Random :: #import "Random";
rng :: inline (lo: float, hi: float) -> float {
return lo + Random.random_get_zero_to_one() * (hi - lo);
}
lerp_color :: (a: Vector4, b: Vector4, t: float) -> Vector4 {
return a * (1.0 - t) + b * t;
}

View File

@ -1,4 +1,9 @@
#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);

View File

@ -1,86 +1 @@
#load "colliders.jai";
#load "worldphysics.jai";
Physics_Body :: struct {
half_extents : Vector3;
velocity : Vector3;
position : Vector3;
// Status, don't write to these outside of the engine physics code:
on_ground : bool;
hit_x : bool;
hit_y : bool;
hit_z : bool;
// Config:
prevent_falling : bool = true;
max_step_height : float = 0.0;
gravity : float = 20;
downward_probe_dist : float = 0.01;
}
move_body :: (body : *Physics_Body, dt: float) {
// reset status
body.on_ground = false;
body.hit_x = false;
body.hit_y = false;
body.hit_z = false;
// apply velocity XZ
body.position.x += body.velocity.x * dt;
if body_overlaps_solid(body) {
body.hit_x = true;
if body.velocity.x < 0 {
hit_cell := floor(body.position.x - body.half_extents.x);
body.position.x = hit_cell + 1.0 + body.half_extents.x;
} else {
hit_cell := floor(body.position.x + body.half_extents.x);
body.position.x = hit_cell - body.half_extents.x;
}
body.velocity.x = 0;
}
body.position.z+= body.velocity.z * dt;
if body_overlaps_solid(body) {
body.hit_z = true;
if body.velocity.z < 0 {
hit_cell := floor(body.position.z - body.half_extents.z);
body.position.z = hit_cell + 1.0 + body.half_extents.z;
} else {
hit_cell := floor(body.position.z + body.half_extents.z);
body.position.z = hit_cell - body.half_extents.z;
}
body.velocity.z = 0;
}
// gravity
probe_body := body.*;
probe_body.position.y -= body.downward_probe_dist;
if body.velocity.y <= 0 && body_overlaps_solid(*probe_body) {
// Do this so we don't stutter on the ground
body.hit_y = true;
body.on_ground = true;
body.velocity.y = 0;
hit_cell := floor(probe_body.position.y - body.half_extents.y);
body.position.y = hit_cell + 1.0 + body.half_extents.y;
} else {
body.velocity.y -= body.gravity * dt;
}
// apply velocity Y
body.position.y += body.velocity.y * dt;
if body_overlaps_solid(body) {
body.hit_y = true;
if body.velocity.y < 0 {
body.on_ground = true;
hit_cell := floor(body.position.y - body.half_extents.y);
body.position.y = hit_cell + 1.0 + body.half_extents.y;
} else {
hit_cell := floor(body.position.y + body.half_extents.y);
body.position.y = hit_cell - body.half_extents.y;
}
body.velocity.y = 0;
}
}

View File

@ -1,44 +0,0 @@
is_cell_solid :: (x : s32, y : s32, z : s32) -> bool {
world := get_current_world();
if !world.valid then return false;
key := world_to_chunk_coord(x, y, z);
chunk := table_find_pointer(*world.world.chunks, key);
if !chunk then return false;
lx, ly, lz := world_to_local(x, y, z);
return chunk_get_occupancy_mask_at(chunk, lx, ly, lz) != .FREE;
}
is_trile_collision :: (fx: float, fy: float, fz: float) -> bool {
x := cast(s32)floor(fx);
y := cast(s32)floor(fy);
z := cast(s32)floor(fz);
return is_cell_solid(x, y, z);
}
// A little bit of leeway into collisions so we aren't jittering
SKIN :: 0.001;
body_overlaps_solid :: (body : *Physics_Body) -> bool {
max := body.position + body.half_extents;
min := body.position - body.half_extents;
min_x := cast(s32)floor(min.x + SKIN);
min_y := cast(s32)floor(min.y + SKIN);
min_z := cast(s32)floor(min.z + SKIN);
max_x := cast(s32)floor(max.x - SKIN);
max_y := cast(s32)floor(max.y - SKIN);
max_z := cast(s32)floor(max.z - SKIN);
for x : min_x..max_x {
for y : min_y..max_y {
for z : min_z..max_z {
if is_cell_solid(x,y,z) {
return true;
}
}
}
}
return false;
}

View File

@ -1,230 +0,0 @@
// Float, so brightness above 1 survives until post-processing.
VIEW_COLOR_FORMAT :: sg_pixel_format.RGBA16F;
VIEW_DEPTH_FORMAT :: sg_pixel_format.DEPTH;
VIEW_SCENE_FORMAT :: sg_pixel_format.R32F;
VIEW_GLOW_FORMAT :: sg_pixel_format.R16F;
SHADOW_TINT_FORMAT :: sg_pixel_format.RGBA16F; // glass colour, and the nearest glass depth in alpha
ALPHA_BLEND :: sg_blend_state.{
enabled = true,
src_factor_rgb = .SRC_ALPHA,
dst_factor_rgb = .ONE_MINUS_SRC_ALPHA,
};
MULTIPLY_BLEND :: sg_blend_state.{
enabled = true,
src_factor_rgb = .DST_COLOR,
dst_factor_rgb = .ZERO,
};
ADDITIVE_BLEND :: sg_blend_state.{
enabled = true,
src_factor_rgb = .SRC_ALPHA,
dst_factor_rgb = .ONE,
};
Pipeline_Target :: enum {
SCREEN;
VIEW_OPAQUE;
VIEW_TRANSPARENT;
SHADOW;
POST; // colour only, view format
}
samplers : struct {
nearest_clamp : sg_sampler;
linear_clamp : sg_sampler;
linear_repeat : sg_sampler;
shadow : sg_sampler;
}
samplers_init :: () {
samplers.nearest_clamp = sg_make_sampler(*(sg_sampler_desc.{ min_filter = .NEAREST, mag_filter = .NEAREST, wrap_u = .CLAMP_TO_EDGE, wrap_v = .CLAMP_TO_EDGE }));
samplers.linear_clamp = sg_make_sampler(*(sg_sampler_desc.{ min_filter = .LINEAR, mag_filter = .LINEAR, wrap_u = .CLAMP_TO_EDGE, wrap_v = .CLAMP_TO_EDGE }));
samplers.linear_repeat = sg_make_sampler(*(sg_sampler_desc.{ min_filter = .LINEAR, mag_filter = .LINEAR, wrap_u = .REPEAT, wrap_v = .REPEAT }));
samplers.shadow = sg_make_sampler(*(sg_sampler_desc.{ min_filter = .NEAREST, mag_filter = .NEAREST, wrap_u = .CLAMP_TO_EDGE, wrap_v = .CLAMP_TO_EDGE, compare = .LESS_EQUAL }));
}
// Vertex layout and anything else specific is filled in by the caller.
pipeline_desc :: (shader: sg_shader_desc, target: Pipeline_Target, blend := sg_blend_state.{}, depth_write := false) -> sg_pipeline_desc {
shader_desc := shader;
desc : sg_pipeline_desc;
desc.shader = sg_make_shader(*shader_desc);
desc.colors[0].blend = blend;
if target != .SCREEN {
desc.colors[0].pixel_format = VIEW_COLOR_FORMAT;
desc.depth.pixel_format = VIEW_DEPTH_FORMAT;
desc.depth.compare = .LESS_EQUAL;
desc.depth.write_enabled = depth_write;
desc.sample_count = 1;
}
// Glow is attachment 1 in both view passes, blended like colour.
if target == .VIEW_OPAQUE || target == .VIEW_TRANSPARENT {
desc.color_count = 2;
desc.colors[1].pixel_format = VIEW_GLOW_FORMAT;
desc.colors[1].blend = blend;
}
if target == .VIEW_OPAQUE {
desc.color_count = 3;
desc.colors[2].pixel_format = VIEW_SCENE_FORMAT;
}
// Shadow casters write depth only; glass turns the tint back on.
if target == .SHADOW {
desc.colors[0].pixel_format = SHADOW_TINT_FORMAT;
desc.colors[0].write_mask = .NONE;
}
if target == .POST desc.depth.pixel_format = .NONE;
return desc;
}
Pass_Kind :: enum {
MAIN;
REFLECTION;
SHADOW;
}
Pass :: struct {
scene : *Scene;
view_index : int;
view : View;
kind : Pass_Kind;
params : View_Params;
shadow : Shadow_Params;
frustum : [6]Vector4;
}
make_pass :: (scene: *Scene, view_index: int, kind: Pass_Kind) -> Pass {
pass := Pass.{ scene = scene, view_index = view_index, view = scene.views[view_index], kind = kind };
env := view_environment(pass.view);
height := env.reflection_height;
camera := pass.view.camera.position;
view_proj := scene.view_proj[view_index];
pass.params.clip = .[0, 0, 0, 1];
if kind == .REFLECTION {
camera.y = 2 * height - camera.y;
view_proj = scene.reflection_view_proj[view_index];
pass.params.clip = .[0, 1, 0, -height];
}
if kind == .SHADOW {
// Camera-facing billboards turn to the sun, so they cast their full silhouette.
camera = pass.view.camera.target + env.sun_direction * 1000;
view_proj = scene.shadow_view_proj[view_index];
}
pass.frustum = frustum_planes(view_proj);
_, inv_view_proj := inverse(view_proj);
pass.params.view_proj = view_proj.floats;
pass.params.inv_view_proj = inv_view_proj.floats;
pass.params.camera = .[camera.x, camera.y, camera.z, scene.time];
pass.params.sun_direction = .[env.sun_direction.x, env.sun_direction.y, env.sun_direction.z, 0];
pass.params.sun_color = .[env.sun_color.x, env.sun_color.y, env.sun_color.z, 0];
pass.params.shade_color = .[env.shade_color.x, env.shade_color.y, env.shade_color.z, 0];
// From shadow clip space to shadow map uv and stored depth, which differ per backend.
gl := sg_query_backend() == .GLCORE || sg_query_backend() == .GLES3;
flip := sg_query_features().origin_top_left;
to_uv := Matrix4.{
0.5, 0, 0, 0.5,
0, ifx flip then -0.5 else 0.5, 0, 0.5,
0, 0, ifx gl then 0.5 else 1, ifx gl then 0.5 else 0,
0, 0, 0, 1,
};
pass.shadow.shadow_uv = (scene.shadow_view_proj[view_index] * transpose(to_uv)).floats;
return pass;
}
frustum_planes :: (m: Matrix4) -> [6]Vector4 {
column :: (m: Matrix4, j: int) -> Vector4 { return .{m.coef[0][j], m.coef[1][j], m.coef[2][j], m.coef[3][j]}; }
x, y, z, w := column(m, 0), column(m, 1), column(m, 2), column(m, 3);
return .[w + x, w - x, w + y, w - y, w + z, w - z];
}
box_in_frustum :: (planes: [6]Vector4, lo: Vector3, hi: Vector3) -> bool {
for planes {
x := ifx it.x > 0 then hi.x else lo.x;
y := ifx it.y > 0 then hi.y else lo.y;
z := ifx it.z > 0 then hi.z else lo.z;
if it.x * x + it.y * y + it.z * z + it.w < 0 return false;
}
return true;
}
apply_uniforms :: (slot: s32, params: *$T) {
sg_apply_uniforms(slot, *(sg_range.{ params, size_of(T) }));
}
Slots :: struct (T: Type) {
items : [..]T;
free : [..]u32;
}
// Ids start at 1 so that 0 can mean none.
slot_add :: (slots: *Slots($T)) -> u32, *T {
if slots.free.count {
id := pop(*slots.free);
return id, *slots.items[id - 1];
}
item := array_add(*slots.items);
return xx slots.items.count, item;
}
slot_get :: (slots: *Slots($T), id: u32) -> *T {
if id == 0 || id > slots.items.count return null;
return *slots.items[id - 1];
}
slot_remove :: (slots: *Slots($T), id: u32) {
slots.items[id - 1] = .{};
array_add(*slots.free, id);
}
Stream_Buffer :: struct {
buffer : sg_buffer;
size : int;
}
stream_upload :: (stream: *Stream_Buffer, data: []$T) {
if data.count == 0 return;
size := data.count * size_of(T);
if size > stream.size {
if stream.size sg_destroy_buffer(stream.buffer);
stream.size = max(size, stream.size * 2);
stream.buffer = sg_make_buffer(*(sg_buffer_desc.{ size = xx stream.size, usage = .STREAM }));
}
sg_update_buffer(stream.buffer, *(sg_range.{ data.data, xx size }));
}
Keyed :: struct (Instance: Type) {
key : s64;
instance : Instance;
}
Batch :: struct {
view_index : s32;
key : s64;
first : s32;
count : s32;
}
// Sorts the view's draws by key and appends one batch per run of equal keys.
batch_instances :: (view_index: int, draws: []Keyed($I), batches: *[..]Batch, instances: *[..]I) {
quick_sort(draws, (a: Keyed(I), b: Keyed(I)) -> s64 { return a.key - b.key; });
for draws {
if it_index == 0 || draws[it_index - 1].key != it.key {
array_add(batches, .{view_index = xx view_index, key = it.key, first = xx instances.count});
}
array_add(instances, it.instance);
batches.*[batches.count - 1].count += 1;
}
}
bind_shadow :: (pass: *Pass, bind: *sg_bindings) {
bind.images[IMG_shadow_map] = view_shadow_image(pass.view_index);
bind.samplers[SMP_shadow_smp] = samplers.shadow;
bind.images[IMG_shadow_tint] = view_shadow_tint_image(pass.view_index);
bind.samplers[SMP_shadow_tint_smp] = samplers.nearest_clamp;
}
view_environment :: (view: View) -> Environment {
return ifx view.environment then view.environment.* else .{};
}

View File

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

View File

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

View File

@ -1,133 +0,0 @@
#import,dir "../../modules/sokol-jai/sokol/gfx"(DEBUG = !FLAG_RELEASE_BUILD);
#import,dir "../../modules/sokol-jai/sokol/glue"(DEBUG = !FLAG_RELEASE_BUILD);
#import "Sort";
#load "shaders/jai/shaders.jai";
#load "gpu.jai";
#load "ui.jai";
#load "triles.jai";
#load "probes.jai";
#load "views.jai";
#load "water.jai";
#load "sprites.jai";
#load "sky.jai";
#load "lines.jai";
renderer_init :: (orientations: [ORIENTATION_COUNT]Matrix3) {
sg_setup(*(sg_desc.{
environment = cast,force(sg_environment) sglue_environment(),
logger = .{ func = slog_func },
// Each view holds 10 attachments and 15 images; each trile mesh 2 buffers.
attachments_pool_size = 64,
image_pool_size = 512,
buffer_pool_size = 1024,
}));
white := u8.[255, 255, 255, 255];
renderer.white = make_image(1, 1, white);
samplers_init();
ui_init();
triles_init(orientations);
probes_init();
views_init();
water_init();
sprites_init();
sky_init();
lines_init();
}
renderer_shutdown :: () {
sg_shutdown();
}
renderer_texture_create :: (width: s32, height: s32, pixels: []u8) -> u32 {
id, texture := slot_add(*renderer.textures);
texture.width = width;
texture.height = height;
texture.image = make_image(width, height, pixels);
return id;
}
// Immutable images are recreated rather than updated, so a texture can be
// updated any number of times in a frame.
renderer_texture_update :: (id: u32, pixels: []u8) {
texture := slot_get(*renderer.textures, id);
sg_destroy_image(texture.image);
texture.image = make_image(texture.width, texture.height, pixels);
}
renderer_texture_destroy :: (id: u32) {
sg_destroy_image(slot_get(*renderer.textures, id).image);
slot_remove(*renderer.textures, id);
}
renderer_render :: (scene: *Scene, width: s32, height: s32) {
triles_upload(scene);
probes_upload();
sprites_upload(scene);
lines_upload(scene);
ui_upload(scene);
for view, view_index: scene.views {
shadow := make_pass(scene, view_index, .SHADOW);
view_begin_shadow_pass(view_index);
triles_draw(*shadow);
sprites_draw(*shadow, transparent = false);
triles_draw_glass_shadow(*shadow);
sg_end_pass();
for kind: Pass_Kind.[.REFLECTION, .MAIN] {
pass := make_pass(scene, view_index, kind);
view_begin_pass(view_index, width, height, kind, opaque = true);
sky_draw(*pass);
water_draw_floor(*pass);
triles_draw(*pass);
sprites_draw(*pass, transparent = false);
sg_end_pass();
view_begin_pass(view_index, width, height, kind, opaque = false);
water_draw(*pass);
sprites_draw(*pass, transparent = true);
lines_draw(*pass, width, height);
sg_end_pass();
if kind == .MAIN view_shafts(*pass);
}
view_bloom(view_index);
}
action : sg_pass_action;
action.colors[0] = .{ load_action = .CLEAR, clear_value = .{0, 0, 0, 1} };
sg_begin_pass(*(sg_pass.{ action = action, swapchain = cast,force(sg_swapchain) sglue_swapchain() }));
for scene.views view_composite(it_index, view_environment(it));
ui_draw(scene, width, height);
sg_end_pass();
sg_commit();
}
texture_image :: (id: u32) -> sg_image {
texture := slot_get(*renderer.textures, id);
if !texture || !texture.image.id return renderer.white;
return texture.image;
}
#scope_file
Renderer_Texture :: struct {
image : sg_image;
width : s32;
height : s32;
}
renderer : struct {
textures : Slots(Renderer_Texture);
white : sg_image;
}
make_image :: (width: s32, height: s32, pixels: []u8) -> sg_image {
desc := sg_image_desc.{ width = width, height = height, pixel_format = .RGBA8 };
desc.data.subimage[0][0] = .{ pixels.data, xx pixels.count };
return sg_make_image(*desc);
}

View File

@ -1,23 +0,0 @@
compile_sokol_shaders :: () -> bool {
dir :: "src/renderer_sokol/shaders";
input :: "src/renderer_sokol/shaders/shaders.glsl";
output :: "src/renderer_sokol/shaders/jai/shaders.jai";
shdc :: #ifx OS == .MACOS then "src/renderer_sokol/shaders/sokol-shdc-mac" else "src/renderer_sokol/shaders/sokol-shdc";
out_time, _, out_exists := file_modtime_and_size(output);
stale := !out_exists;
for file_list(dir) {
if !ends_with(it, ".glsl") continue;
src_time := file_modtime_and_size(it);
if compare_apollo_times(src_time, out_time) > 0 stale = true;
}
if !stale return true;
result, out, error := run_command(shdc, "-i", input, "-o", output,
"-l", "glsl430:glsl300es:metal_macos", "-f", "sokol_jai", capture_and_return_output = true);
if result.exit_code != 0 {
log_error("sokol-shdc failed:\n%%", out, error);
return false;
}
return true;
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,194 +0,0 @@
// Texture coordinates come from the pixel position, which lines up with
// render-target textures the same way on every backend.
@vs vs_fullscreen
@include_block fullscreen
void main() {
gl_Position = vec4(fullscreen_position(gl_VertexIndex), 0.0, 1.0);
}
@end
// The first level: colour times glow, averaged over each 2x2 block of view pixels.
@fs fs_bloom_glow
layout(binding=0) uniform texture2D bloom_source;
layout(binding=1) uniform texture2D bloom_glow;
layout(binding=0) uniform sampler bloom_smp;
out vec4 frag_color;
vec3 glowing(ivec2 p) {
return texelFetch(sampler2D(bloom_source, bloom_smp), p, 0).rgb * texelFetch(sampler2D(bloom_glow, bloom_smp), p, 0).r;
}
void main() {
ivec2 p = ivec2(gl_FragCoord.xy) * 2;
vec3 c = (glowing(p) + glowing(p + ivec2(1, 0)) + glowing(p + ivec2(0, 1)) + glowing(p + ivec2(1, 1))) * 0.25;
frag_color = vec4(c, 1.0);
}
@end
@fs fs_bloom_down
layout(binding=0) uniform texture2D bloom_source;
layout(binding=0) uniform sampler bloom_smp;
out vec4 frag_color;
vec3 tap(vec2 uv) { return texture(sampler2D(bloom_source, bloom_smp), uv).rgb; }
void main() {
vec2 texel = 1.0 / vec2(textureSize(sampler2D(bloom_source, bloom_smp), 0));
vec2 uv = gl_FragCoord.xy * 2.0 * texel;
vec3 c = (tap(uv + texel * vec2(-1, -1)) + tap(uv + texel * vec2(1, -1)) +
tap(uv + texel * vec2(-1, 1)) + tap(uv + texel * vec2(1, 1))) * 0.25;
frag_color = vec4(c, 1.0);
}
@end
@fs fs_bloom_up
layout(binding=0) uniform texture2D bloom_source;
layout(binding=0) uniform sampler bloom_smp;
out vec4 frag_color;
vec3 tap(vec2 uv) { return texture(sampler2D(bloom_source, bloom_smp), uv).rgb; }
// A 3x3 tent over the smaller level, added onto the larger one.
void main() {
vec2 texel = 1.0 / vec2(textureSize(sampler2D(bloom_source, bloom_smp), 0));
vec2 uv = gl_FragCoord.xy * 0.5 * texel;
vec3 c = tap(uv) * 4.0
+ (tap(uv + texel * vec2(-1, 0)) + tap(uv + texel * vec2(1, 0)) + tap(uv + texel * vec2(0, -1)) + tap(uv + texel * vec2(0, 1))) * 2.0
+ (tap(uv + texel * vec2(-1, -1)) + tap(uv + texel * vec2(1, -1)) + tap(uv + texel * vec2(-1, 1)) + tap(uv + texel * vec2(1, 1)));
frag_color = vec4(c / 16.0, 1.0);
}
@end
// Sunlight that came through glass, scattered toward the camera by the air between
// it and the first opaque surface. Half resolution.
@vs vs_shafts
@include_block view
@include_block fullscreen
out vec4 v_point;
out vec3 v_camera;
out vec3 v_sun;
out float v_time;
void main() {
vec2 p = fullscreen_position(gl_VertexIndex);
gl_Position = vec4(p, 0.0, 1.0);
v_point = inv_view_proj * vec4(p, 0.5, 1.0);
v_camera = camera.xyz;
v_sun = sun_color.rgb;
v_time = camera.w;
}
@end
@fs fs_shafts
@include_block shadow
layout(binding=1) uniform shaft_params {
float density; // how much light scatters per unit, and how fast it fades
float amount;
float max_distance;
float steps;
};
layout(binding=0) uniform texture2D shaft_scene;
layout(binding=0) uniform sampler shaft_scene_smp;
@image_sample_type shaft_scene unfilterable_float
@sampler_type shaft_scene_smp nonfiltering
in vec4 v_point;
in vec3 v_camera;
in vec3 v_sun;
in float v_time;
out vec4 frag_color;
const vec3 SHAFT_DRIFT = vec3(0.4, 0.15, 0.3); // how the air moves, units per second
const float SHAFT_SCALE = 0.8; // size of the dusty patches, inverse units
const float SHAFT_SHIMMER = 1.0; // how much the dust thins and thickens the light
const float SHAFT_TINT = 0.6; // how much of the glass colour the shafts keep
float hash(vec3 p) {
p = fract(p * 0.3183099 + 0.1);
p *= 17.0;
return fract(p.x * p.y * p.z * (p.x + p.y + p.z));
}
float value_noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(mix(mix(hash(i), hash(i + vec3(1, 0, 0)), f.x),
mix(hash(i + vec3(0, 1, 0)), hash(i + vec3(1, 1, 0)), f.x), f.y),
mix(mix(hash(i + vec3(0, 0, 1)), hash(i + vec3(1, 0, 1)), f.x),
mix(hash(i + vec3(0, 1, 1)), hash(i + vec3(1, 1, 1)), f.x), f.y), f.z);
}
// Two layers drifting at different speeds, so the dust churns instead of sliding.
float dust(vec3 p) {
vec3 q = p * SHAFT_SCALE;
float n = value_noise(q + SHAFT_DRIFT * v_time * SHAFT_SCALE) * 0.65
+ value_noise(q * 2.3 - SHAFT_DRIFT.zxy * v_time * SHAFT_SCALE * 1.7) * 0.35;
// Sharpened, so the patches read as clear gaps and thick wisps rather than a soft wobble.
n = smoothstep(0.25, 0.75, n);
return mix(1.0 - SHAFT_SHIMMER, 1.0 + SHAFT_SHIMMER, n);
}
void main() {
vec3 dir = normalize(v_point.xyz / v_point.w - v_camera);
float len = min(texelFetch(sampler2D(shaft_scene, shaft_scene_smp), ivec2(gl_FragCoord.xy) * 2, 0).r, max_distance);
float step = len / steps;
// Interleaved gradient noise spreads the step banding into a fine dither.
float jitter = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
vec3 light = vec3(0.0);
for (float i = 0.0; i < steps; i += 1.0) {
float t = (i + jitter) * step;
// Snapped to the trixel grid, so the shafts are blocky like the patches they make.
vec3 p = (floor((v_camera + dir * t) * 16.0) + 0.5) / 16.0;
vec4 s = shadow_uv * vec4(p, 1.0);
if (s.x < 0.0 || s.x > 1.0 || s.y < 0.0 || s.y > 1.0 || s.z > 1.0) continue;
vec4 tint = texture(sampler2D(shadow_tint, shadow_tint_smp), s.xy);
if (s.z < tint.a + 0.001) continue;
float visible = texture(sampler2DShadow(shadow_map, shadow_smp), vec3(s.xy, s.z - 0.0005));
vec3 color = mix(vec3(brightest(tint.rgb)), tint.rgb, SHAFT_TINT);
light += visible * color * exp(-density * t) * dust(p);
}
frag_color = vec4(light * v_sun * density * step * amount, 1.0);
}
@end
@fs fs_post
layout(binding=0) uniform post_params {
float exposure; // in stops
float contrast;
float saturation;
float bloom_amount;
};
layout(binding=0) uniform texture2D view_color;
layout(binding=1) uniform texture2D bloom;
layout(binding=2) uniform texture2D shafts;
layout(binding=0) uniform sampler view_smp;
layout(binding=1) uniform sampler bloom_view_smp;
out vec4 frag_color;
void main() {
vec2 uv = gl_FragCoord.xy / vec2(textureSize(sampler2D(view_color, view_smp), 0));
vec3 c = texture(sampler2D(view_color, view_smp), uv).rgb;
c += texture(sampler2D(bloom, bloom_view_smp), uv).rgb * bloom_amount;
// A small tent blur over the half resolution shafts hides their dither.
vec2 st = 1.0 / vec2(textureSize(sampler2D(shafts, bloom_view_smp), 0));
c += (texture(sampler2D(shafts, bloom_view_smp), uv).rgb * 2.0
+ texture(sampler2D(shafts, bloom_view_smp), uv + st * vec2( 1, 1)).rgb
+ texture(sampler2D(shafts, bloom_view_smp), uv + st * vec2(-1, 1)).rgb
+ texture(sampler2D(shafts, bloom_view_smp), uv + st * vec2( 1, -1)).rgb
+ texture(sampler2D(shafts, bloom_view_smp), uv + st * vec2(-1, -1)).rgb) / 6.0;
c *= exp2(exposure);
c = (c - 0.5) * contrast + 0.5;
c = mix(vec3(dot(c, vec3(0.2126, 0.7152, 0.0722))), c, saturation);
frag_color = vec4(clamp(c, 0.0, 1.0), 1.0);
}
@end
@program bloom_glow vs_fullscreen fs_bloom_glow
@program bloom_down vs_fullscreen fs_bloom_down
@program bloom_up vs_fullscreen fs_bloom_up
@program shafts vs_shafts fs_shafts
@program post vs_fullscreen fs_post

View File

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

View File

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

View File

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

View File

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

View File

@ -1,34 +0,0 @@
@vs vs_ui
in vec2 position;
in vec2 uv;
in vec4 color0;
layout(binding=0) uniform ui_vs_params {
vec2 screen_size;
};
out vec2 v_uv;
out vec4 v_color;
void main() {
vec2 ndc = position / screen_size * 2.0 - 1.0;
gl_Position = vec4(ndc.x, -ndc.y, 0.0, 1.0);
v_uv = uv;
v_color = color0;
}
@end
@fs fs_ui
in vec2 v_uv;
in vec4 v_color;
out vec4 frag_color;
layout(binding=0) uniform texture2D tex;
layout(binding=0) uniform sampler smp;
void main() {
frag_color = texture(sampler2D(tex, smp), v_uv) * v_color;
}
@end
@program ui vs_ui fs_ui

View File

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

View File

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

View File

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

View File

@ -1,330 +0,0 @@
triles_init :: (orientations: [ORIENTATION_COUNT]Matrix3) {
desc := pipeline_desc(trile_shader_desc(sg_query_backend()), .VIEW_OPAQUE, depth_write = true);
desc.layout.buffers[1].step_func = .PER_INSTANCE;
desc.layout.attrs[ATTR_trile_position] = .{ format = .FLOAT3 };
desc.layout.attrs[ATTR_trile_data_n] = .{ format = .UBYTE4N };
desc.layout.attrs[ATTR_trile_instance] = .{ format = .FLOAT4, buffer_index = 1 };
desc.layout.attrs[ATTR_trile_tint] = .{ format = .FLOAT, buffer_index = 1 };
desc.layout.attrs[ATTR_trile_cut] = .{ format = .FLOAT, buffer_index = 1 };
desc.index_type = .UINT16;
desc.cull_mode = .BACK;
desc.face_winding = .CCW;
triles.pipeline = sg_make_pipeline(*desc);
// Back faces only, so lit faces don't shadow themselves.
shadow_desc := pipeline_desc(trile_shadow_shader_desc(sg_query_backend()), .SHADOW, depth_write = true);
shadow_desc.layout = desc.layout;
shadow_desc.index_type = .UINT16;
shadow_desc.cull_mode = .FRONT;
shadow_desc.face_winding = .CCW;
triles.shadow_pipeline = sg_make_pipeline(*shadow_desc);
// Alpha keeps the nearest glass depth, so light shafts start at the window.
glass_blend := MULTIPLY_BLEND;
glass_blend.src_factor_alpha = .ONE;
glass_blend.dst_factor_alpha = .ONE;
glass_blend.op_alpha = .MIN;
glass_desc := pipeline_desc(trile_glass_shadow_shader_desc(sg_query_backend()), .SHADOW, glass_blend);
glass_desc.colors[0].write_mask = .RGBA;
glass_desc.layout = desc.layout;
glass_desc.index_type = .UINT16;
glass_desc.cull_mode = .FRONT;
glass_desc.face_winding = .CCW;
triles.glass_shadow_pipeline = sg_make_pipeline(*glass_desc);
for orientations {
for r: 0..2 for c: 0..2 triles.params.orientations[it_index * 3 + r][c] = it.coef[r][c];
}
triles.palette_dirty = true;
}
renderer_trile_mesh_create :: (vertices: []Trile_Vertex, indices: []u16) -> u32 {
id, mesh := slot_add(*triles.meshes);
make_mesh(mesh, vertices, indices);
return id;
}
renderer_trile_mesh_update :: (id: u32, vertices: []Trile_Vertex, indices: []u16) {
mesh := slot_get(*triles.meshes, id);
free_mesh(mesh);
make_mesh(mesh, vertices, indices);
}
renderer_trile_mesh_destroy :: (id: u32) {
free_mesh(slot_get(*triles.meshes, id));
slot_remove(*triles.meshes, id);
}
renderer_swatch_set :: (id: u8, color: [3]u8, reflectivity: u8, gloss: u8, emittance: u8, glass: bool) {
for 0..2 triles.palette[0][id][it] = color[it];
triles.palette[0][id][3] = xx (min(cast(int) emittance, 127) * 255 / 127); // emittance is 0..127
triles.palette[1][id][0] = xx ifx glass then 255 else 0;
triles.palette[1][id][1] = reflectivity;
triles.palette[1][id][2] = gloss;
triles.palette_dirty = true;
}
triles_upload :: (scene: *Scene) {
if triles.palette_dirty {
triles.palette_dirty = false;
if triles.palette_image.id sg_destroy_image(triles.palette_image);
desc := sg_image_desc.{ width = 256, height = 2, pixel_format = .RGBA8 };
desc.data.subimage[0][0] = .{ triles.palette.data, size_of(type_of(triles.palette)) };
triles.palette_image = sg_make_image(*desc);
}
if triles.world_version != scene.world_version world_rebuild(scene);
array_reset_keeping_memory(*triles.instances);
array_reset_keeping_memory(*triles.batches);
chunk_count := triles.world_chunks.count;
array_resize(*triles.cut_chunks, scene.views.count * chunk_count, initialize = false);
memset(triles.cut_chunks.data, 0, triles.cut_chunks.count);
draws : [..]Keyed(Instance);
draws.allocator = temp;
for view, view_index: scene.views {
array_reset_keeping_memory(*draws);
if view.draw_world {
cut_chunks := array_view(triles.cut_chunks, view_index * chunk_count, chunk_count);
for view_cut_chunks(.{xx view_index}) {
index := table_find_pointer(*triles.chunk_index, it);
if index cut_chunks[index.*] = true;
}
for batch: triles.world_batches {
if !batch.cuttable || !cut_chunks[batch.chunk] continue;
for batch.first..batch.first + batch.count - 1 {
instance := triles.world_instances[it];
instance.cut = trile_cut(.{xx view_index}, .{batch.mesh}, instance.position);
array_add(*draws, .{MAIN_ONLY_KEY | cast(s64) batch.mesh, instance});
}
}
}
for scene.triles {
if it.view.index != view_index continue;
preview : s64 = ifx it.tint != .NONE then MAIN_ONLY_KEY else 0;
cut := ifx preview then 0.0 else trile_cut(it.view, it.mesh, it.position);
array_add(*draws, .{preview | it.mesh.id, .{it.position, xx it.orientation, xx it.tint, cut}});
}
batch_instances(view_index, draws, *triles.batches, *triles.instances);
}
stream_upload(*triles.stream, triles.instances);
}
// After every opaque caster, so glass behind something solid doesn't tint.
triles_draw_glass_shadow :: (pass: *Pass) {
sg_apply_pipeline(triles.glass_shadow_pipeline);
apply_uniforms(UB_view_params, *pass.params);
triles.params.show_hidden = 1;
apply_uniforms(UB_trile_params, *triles.params);
for pass_draws(pass) {
mesh := slot_get(*triles.meshes, it.mesh);
if !mesh || mesh.index_count == 0 continue;
bind : sg_bindings;
bind.vertex_buffers[0] = mesh.vertices;
bind.index_buffer = mesh.indices;
bind.vertex_buffers[1] = it.buffer;
bind.vertex_buffer_offsets[1] = it.first * size_of(Instance);
bind.images[IMG_palette] = triles.palette_image;
bind.samplers[SMP_palette_smp] = samplers.nearest_clamp;
sg_apply_bindings(*bind);
sg_draw(0, mesh.index_count, it.count);
}
}
triles_draw :: (pass: *Pass) {
// Cut triles are only hidden from the camera itself, not from shadows or reflections.
triles.params.show_hidden = xx (pass.kind != .MAIN);
sg_apply_pipeline(ifx pass.kind == .SHADOW then triles.shadow_pipeline else triles.pipeline);
apply_uniforms(UB_view_params, *pass.params);
apply_uniforms(UB_trile_params, *triles.params);
if pass.kind != .SHADOW {
apply_uniforms(UB_shadow_params, *pass.shadow);
fs_params := Trile_Fs_Params.{
sun_dir = pass.params.sun_direction,
use_reflection = xx (pass.kind == .MAIN),
reflection_height = view_environment(pass.view).reflection_height,
};
probes_set_params(*fs_params);
apply_uniforms(UB_trile_fs_params, *fs_params);
}
for pass_draws(pass) {
mesh := slot_get(*triles.meshes, it.mesh);
if !mesh || mesh.index_count == 0 continue;
bind : sg_bindings;
bind.vertex_buffers[0] = mesh.vertices;
bind.index_buffer = mesh.indices;
bind.vertex_buffers[1] = it.buffer;
bind.vertex_buffer_offsets[1] = it.first * size_of(Instance);
bind.images[IMG_palette] = triles.palette_image;
bind.samplers[SMP_palette_smp] = samplers.nearest_clamp;
if pass.kind != .SHADOW {
bind_shadow(pass, *bind);
bind.images[IMG_trile_reflection] = ifx pass.kind == .MAIN then view_reflection_image(pass.view_index) else texture_image(0);
bind.images[IMG_trile_reflection_glow] = ifx pass.kind == .MAIN then view_reflection_glow_image(pass.view_index) else texture_image(0);
bind.samplers[SMP_trile_reflection_smp] = samplers.linear_clamp;
probes_bind(*bind);
}
sg_apply_bindings(*bind);
sg_draw(0, mesh.index_count, it.count);
}
}
#scope_file
Mesh :: struct {
vertices : sg_buffer;
indices : sg_buffer;
index_count : s32;
}
Instance :: struct {
position : Vector3;
orientation : float;
tint : float;
cut : float; // 0..1 dissolved away from the camera, still casts shadows
}
MAIN_ONLY_KEY :: 1 << 32;
World_Chunk :: struct {
key : Chunk_Key;
origin : Vector3;
}
World_Batch :: struct {
mesh : u32;
chunk : s32;
cuttable : bool;
first : s32;
count : s32;
}
Trile_Draw :: struct {
mesh : u32;
buffer : sg_buffer;
first : s32;
count : s32;
}
triles : struct {
pipeline : sg_pipeline;
shadow_pipeline : sg_pipeline;
glass_shadow_pipeline : sg_pipeline;
params : Trile_Params;
meshes : Slots(Mesh);
palette : [2][256][4]u8; // row 0: colour, emittance; row 1: glass, reflectivity, gloss
palette_image : sg_image;
palette_dirty : bool;
instances : [..]Instance;
batches : [..]Batch;
stream : Stream_Buffer;
world_version : u64;
world_chunks : [..]World_Chunk;
chunk_index : Table(Chunk_Key, s32, chunk_key_hash, chunk_key_compare);
world_batches : [..]World_Batch;
world_instances : [..]Instance;
world_buffer : sg_buffer;
cut_chunks : [..]bool;
}
world_rebuild :: (scene: *Scene) {
triles.world_version = scene.world_version;
array_reset_keeping_memory(*triles.world_chunks);
array_reset_keeping_memory(*triles.world_batches);
array_reset_keeping_memory(*triles.world_instances);
table_reset(*triles.chunk_index);
for scene.chunks array_add(*triles.world_chunks, .{it_index, it.origin});
quick_sort(triles.world_chunks, (a: World_Chunk, b: World_Chunk) -> s64 {
if a.key.x != b.key.x return a.key.x - b.key.x;
if a.key.z != b.key.z return a.key.z - b.key.z;
return a.key.y - b.key.y;
});
Piece :: struct {
key : s64;
chunk : s32;
group : *World_Group;
}
pieces : [..]Piece;
pieces.allocator = temp;
for chunk, index: triles.world_chunks {
table_set(*triles.chunk_index, chunk.key, xx index);
scene_chunk := table_find_pointer(*scene.chunks, chunk.key);
for * scene_chunk.groups array_add(*pieces, .{(cast(s64) it.mesh.id << 32) | index, xx index, it});
}
quick_sort(pieces, (a: Piece, b: Piece) -> s64 { return a.key - b.key; });
occluders := world_occluders();
for pieces {
chunk := triles.world_chunks[it.chunk];
batch := World_Batch.{mesh = it.group.mesh.id, chunk = it.chunk, cuttable = trile_mesh_cuttable(it.group.mesh), first = xx triles.world_instances.count};
for instance: it.group.instances {
if trile_buried(*occluders, chunk.key, instance.x, instance.y, instance.z) continue;
position := chunk.origin + Vector3.{xx instance.x, xx instance.y, xx instance.z};
array_add(*triles.world_instances, .{position, xx instance.orientation, 0, 0});
}
batch.count = xx (triles.world_instances.count - batch.first);
if batch.count array_add(*triles.world_batches, batch);
}
if triles.world_buffer.id sg_destroy_buffer(triles.world_buffer);
triles.world_buffer = .{};
if triles.world_instances.count {
size := triles.world_instances.count * size_of(Instance);
triles.world_buffer = sg_make_buffer(*(sg_buffer_desc.{ data = .{ triles.world_instances.data, xx size } }));
}
}
pass_draws :: (pass: *Pass) -> []Trile_Draw {
draws : [..]Trile_Draw;
draws.allocator = temp;
main := pass.kind == .MAIN;
if pass.view.draw_world && triles.world_batches.count {
chunk_count := triles.world_chunks.count;
visible := NewArray(chunk_count, bool,, temp);
for triles.world_chunks visible[it_index] = box_in_frustum(pass.frustum, it.origin, it.origin + Vector3.{CHUNK_SIZE, CHUNK_SIZE, CHUNK_SIZE});
cut_chunks := array_view(triles.cut_chunks, pass.view_index * chunk_count, chunk_count);
run : Trile_Draw;
for triles.world_batches {
if !visible[it.chunk] || (main && it.cuttable && cut_chunks[it.chunk]) continue;
if run.count && run.mesh == it.mesh && run.first + run.count == it.first {
run.count += it.count;
continue;
}
if run.count array_add(*draws, run);
run = .{it.mesh, triles.world_buffer, it.first, it.count};
}
if run.count array_add(*draws, run);
}
for triles.batches {
if it.view_index != pass.view_index || (!main && it.key & MAIN_ONLY_KEY) continue;
array_add(*draws, .{xx (it.key & ~MAIN_ONLY_KEY), triles.stream.buffer, it.first, it.count});
}
return draws;
}
make_mesh :: (mesh: *Mesh, vertices: []Trile_Vertex, indices: []u16) {
mesh.index_count = xx indices.count;
if indices.count {
mesh.vertices = sg_make_buffer(*(sg_buffer_desc.{ data = .{ vertices.data, xx (vertices.count * size_of(Trile_Vertex)) } }));
mesh.indices = sg_make_buffer(*(sg_buffer_desc.{ type = .INDEXBUFFER, data = .{ indices.data, xx (indices.count * size_of(u16)) } }));
}
}
free_mesh :: (mesh: *Mesh) {
if mesh.index_count {
sg_destroy_buffer(mesh.vertices);
sg_destroy_buffer(mesh.indices);
}
mesh.* = .{};
}

View File

@ -1,48 +0,0 @@
ui_init :: () {
desc := pipeline_desc(ui_shader_desc(sg_query_backend()), .SCREEN, blend = ALPHA_BLEND);
desc.layout.attrs[ATTR_ui_position] = .{ format = .FLOAT2 };
desc.layout.attrs[ATTR_ui_uv] = .{ format = .FLOAT2 };
desc.layout.attrs[ATTR_ui_color0] = .{ format = .FLOAT4 };
ui.pipeline = sg_make_pipeline(*desc);
}
ui_upload :: (scene: *Scene) {
array_reset_keeping_memory(*ui.vertices);
for scene.ui array_add(*ui.vertices, ..it.vertices);
stream_upload(*ui.stream, ui.vertices);
}
ui_draw :: (scene: *Scene, w: s32, h: s32) {
if ui.vertices.count == 0 return;
sg_apply_pipeline(ui.pipeline);
params := Ui_Vs_Params.{ screen_size = .[xx w, xx h] };
apply_uniforms(UB_ui_vs_params, *params);
bind : sg_bindings;
bind.vertex_buffers[0] = ui.stream.buffer;
bind.samplers[SMP_smp] = samplers.nearest_clamp;
first : s32;
for scene.ui {
if it.set_scissor {
s := it.scissor;
sg_apply_scissor_rect(s.x, s.y, max(s.w, 0), max(s.h, 0), true);
} else if it.clear_scissor {
sg_apply_scissor_rect(0, 0, w, h, true);
} else if it.vertices.count {
bind.images[IMG_tex] = texture_image(it.texture.id);
sg_apply_bindings(*bind);
sg_draw(first, xx it.vertices.count, 1);
first += xx it.vertices.count;
}
}
}
#scope_file
ui : struct {
pipeline : sg_pipeline;
stream : Stream_Buffer;
vertices : [..]Ui_Vertex;
}

Some files were not shown because too many files have changed in this diff Show More