Compare commits

...

134 Commits

Author SHA1 Message Date
ab3c032313 /apps/examples/circle-raster: acc variant 2025-01-25 02:28:40 +03:00
34a3de73c6 twn_utils.c: make profiling public 2025-01-25 00:53:44 +03:00
597168c282 /apps/examples/circle-raster: benchmarking and int32_t variant 2025-01-25 00:53:28 +03:00
37cd8cf2cf /apps/examples/circle-raster: simple code for grid-centered circle rasterization 2025-01-24 23:26:32 +03:00
cb5f207761 twn_util.c: fix profile worst case, update formatting 2025-01-24 23:06:34 +03:00
6e421543c4 cmake ctx.mouse_position viewport and resolution based 2025-01-24 21:52:11 +03:00
c97d9b2568 twn_draw.c: fix upper limit for camera fov warn 2025-01-24 16:28:06 +03:00
e281ba593c twn_vec.h: add vec2_length(), remove legacy code 2025-01-24 04:28:09 +03:00
a20be2c523 twn_draw.c: add warning for erroneous fov parameters 2025-01-24 04:27:45 +03:00
b20e7202fe twn_util.c: add handling of NULL in log_<type> 2025-01-23 22:15:09 +03:00
045d2764fa twn_util.c: add logging over base types 2025-01-23 22:13:01 +03:00
53917b05b7 twn_draw.c: draw_box() 2025-01-23 21:53:15 +03:00
0dc0a18019 fix color for line rendering 2025-01-23 20:05:29 +03:00
2df5616410 add line drawing 2025-01-23 04:29:59 +03:00
3f9906a918 remove fog start and fog end parameters 2025-01-20 19:30:17 +03:00
8a5d639f95 remove indirection in vertex builder 2025-01-17 22:48:35 +03:00
40aef0a1f9 scale in mixing to prevent excessive gain 2025-01-15 21:31:19 +03:00
2de536210b fix --release switch 2025-01-15 07:58:42 +03:00
449d4d3c32 move opengl library loading to a thread that starts as soon as possible and is awaited as late as we can allow 2025-01-15 07:54:45 +03:00
8233f31269 oopsie 2025-01-15 06:50:19 +03:00
45b8b21ec3 add todo 2025-01-15 06:08:55 +03:00
4659cf2aef profile atlas recreation 2025-01-15 06:04:02 +03:00
db530ca3a0 make audio device init delayed until first use 2025-01-15 05:43:56 +03:00
f0dfd5627a more startup profiling, removal of irrelevant calls and zeroing in textures_cache_init() 2025-01-15 05:39:18 +03:00
0da1e413aa only init the necessary with SDL, speeding up the startup 2025-01-15 05:00:45 +03:00
8c165974c7 twn_util.c: make profiler collect worst case, dispaly the stats with awareness of sample count 2025-01-15 04:55:18 +03:00
1ba33bdc26 collect profile of startup and pack resolution 2025-01-15 04:47:36 +03:00
760515c551 minor optimization of strncmp for literal comparison 2025-01-15 04:36:00 +03:00
9d0a2cab81 expose audio to twnlua 2025-01-15 04:15:08 +03:00
d5b42fa242 add a todo 2025-01-15 01:01:16 +03:00
851ab80292 remove ctx.update_multiplicity 2025-01-15 00:52:42 +03:00
688d71953a make inputs up-to-date for game tick 2025-01-15 00:43:46 +03:00
63abf3d374 disable vsync, make us rule over frames fully 2025-01-15 00:31:17 +03:00
80c77424e2 /apps/demos/scenery: more detailed terrain 2025-01-15 00:30:46 +03:00
82d4f21a4b twn_textures.c: minor optimization 2025-01-15 00:11:47 +03:00
3990f78a74 twn_textures.c: make missing texture single and reused 2025-01-15 00:10:30 +03:00
f0ad9b9a8a twn_textures.c: fix repeated bind to work over varying channel count 2025-01-14 23:47:59 +03:00
ea0af5159f only enable fog for 3d 2025-01-14 23:28:48 +03:00
5059802d09 big rendering overhaul (cleaning and api abstraction) 2025-01-14 23:20:54 +03:00
b7cb37c06a no TWNBUILDDIR 2025-01-14 04:47:53 +03:00
664f123a85 twn_input.c: zero ctx.mouse_movement on focus lost 2025-01-14 02:56:55 +03:00
2351d4114c twn_draw.c: add draw_quad() 2025-01-14 02:53:18 +03:00
86bf16b680 make mouse movement and capture depend on window focus 2025-01-14 02:07:54 +03:00
dbe6217e24 /apps/twnlua: add .gitignore 2025-01-14 01:37:16 +03:00
b037d7a0b9 /apps/twnlua: ctx uploading 2025-01-14 01:35:54 +03:00
e984e95fa8 /apps/twnlua: make dependent on twn_api.json for rebuilding 2025-01-14 00:30:48 +03:00
4ed3764c1d twn_input.c: remove input_mouse_captured(), add ctx.mouse_capture 2025-01-14 00:28:21 +03:00
6d19d2d819 /apps/twnlua: make no warnings 2025-01-14 00:06:55 +03:00
5bce3e5238 twn_textures.c: remove unused amask 2025-01-13 23:56:22 +03:00
6298394957 twn_audio.c: a lot of fixes, optional TWN_FEATURE_PUSH_AUDIO for converging game ticks and audio, proper .wav handling with resample 2025-01-13 23:52:55 +03:00
eefd53a630 twn_audio.c: .wav support and scratch channels 2025-01-13 19:56:20 +03:00
87ae1a7312 missing textures: fix double free 2025-01-13 18:09:06 +03:00
3052bb693a /apps/demos/scenery: skip title scene 2025-01-13 17:36:18 +03:00
5f6c8dd8e6 missing texture when loading fails 2025-01-13 17:35:50 +03:00
c694dfff82 use flatshading for space and skip setting irrelevant vertex color 2025-01-13 09:18:51 +03:00
b6ca9bedb4 /apps/twnlua: don't use module tables 2025-01-13 08:57:21 +03:00
8d67e44009 /apps/twnlua: use lua_numberx for slightly more optimized defaults 2025-01-12 03:51:02 +03:00
192907a0db use slot size of 128 for twnlua allocator 2025-01-12 03:21:05 +03:00
e8b02570a2 slot based allocator for lua, usage of lua_createtable 2025-01-12 02:44:41 +03:00
46e077ba63 make ctx.frame_number overflow to 0 2025-01-11 17:33:05 +03:00
41d0e24780 /apps/twnlua: ctx.udata preservation (not yet for reload case) 2025-01-11 16:22:41 +03:00
777a06a002 /apps/twnlua: expose ctx 2025-01-11 16:01:41 +03:00
313108092b don't use clamped float random_seed internally 2025-01-10 02:52:04 +03:00
83e2dc5468 make vec4 and matrix types internal 2025-01-10 02:40:52 +03:00
951d9c76c8 use floats for ctx.frame_number and ctx.random_seed 2025-01-10 02:20:21 +03:00
f3848d2d52 progress on twnlua bindgen 2025-01-09 21:47:08 +03:00
8c401eda75 api changes and progress on filling in twn_api.json 2025-01-07 14:14:21 +03:00
5c89c55b3e /apps/twnlua: support out of tree usage 2025-01-07 13:22:31 +03:00
5b05386bb0 changes to twn.toml specification of resolution, make it optional as well 2025-01-06 21:19:26 +03:00
b0549612a9 /apps/demos/scenery: lock movement direction to a plane 2025-01-06 15:38:40 +03:00
6463ac3dd7 /apps/demos/scenery: separate height_at(), position grass right 2025-01-06 15:34:12 +03:00
e914cad0dd infer texture mode for triangles 2025-01-05 23:43:30 +03:00
a9d9936cb7 /apps/demos/scenery: reduce fov 2025-01-05 23:37:08 +03:00
4dd028aeae support arbitrary count of billboards per batch (in regards to preallocated short index buffer) 2025-01-05 23:36:48 +03:00
d7a119a592 fix aspect ratio as well as billboard scaling 2025-01-05 21:08:54 +03:00
cb6c1df0be disallow ghostly billboards, reenable skybox in scenery demo 2025-01-05 19:59:23 +03:00
3bfa86066e billboards! 2025-01-05 19:46:05 +03:00
7c0bf39f12 CMakeLists.txt: fix no amalgam build 2025-01-03 22:04:16 +03:00
4f2b8ccd01 separate the rest of general drawing code 2025-01-03 21:59:00 +03:00
472a0657f3 twn_draw.c: remove gl headers 2025-01-03 21:10:15 +03:00
0f368e2700 move render_circle to twn_draw.c 2025-01-03 21:08:54 +03:00
33471b4c46 generalization of deferred commands and any_gl rendering where appropriate 2025-01-03 21:01:26 +03:00
edcb7fc39c make deferred draw primitives agnostic to backend 2025-01-03 19:59:37 +03:00
f9a8448782 make SpritePrimitive take less space (52 -> 48 bytes) 2025-01-03 19:48:00 +03:00
6d5732cc2b tweaks to tooling 2025-01-03 11:55:39 +03:00
62d738cbbe /docs/interop.md: update 2025-01-03 11:49:00 +03:00
f4a3298906 disallow pointer to pointer in api 2025-01-03 11:45:10 +03:00
8ec5a96333 /apps/demos/scenery: cleanup 2025-01-02 13:35:13 +03:00
4277852fc5 /apps/demos/scenery: add walking 2025-01-02 13:29:28 +03:00
dc2535358e make input coordinates respect the viewport 2024-12-24 10:24:50 +03:00
190eb1f107 twnlua: use stb_ds.h for enum conversions 2024-12-24 10:03:19 +03:00
e06c879869 twnlua: partial impl for return propagation, input now works 2024-12-23 22:02:17 +03:00
e7ed72dfc0 twnlua: bindgen.py capable of converting share/twn_api.h spec to lua bindings 2024-12-23 20:59:00 +03:00
c4c097f050 Revert "amalgam of batch and bash scripts for twn basetool"
This reverts commit cd9c65212d.
2024-11-06 17:20:38 +03:00
1d34c91106 add .editorconfig 2024-11-05 16:51:48 +03:00
0d81236331 set eol=lf in .gitattributes 2024-11-05 16:47:30 +03:00
f9bb6412b7 make build directory now prefixed with dot, as it what clangd expects, remove .clangd file 2024-11-05 01:56:50 +03:00
b18f6f1d87 CMakeLists.txt: also allow GNU linker to use thinlto cache 2024-11-04 16:26:56 +03:00
2f94e17852 twn_util_c.h: profile_list_stats() for average summary, without spam in console 2024-11-04 16:22:13 +03:00
26a2bf293f twn_util_c.h: internal profiling api 2024-11-04 16:04:02 +03:00
4b0d584b7e mark scripts executable again 2024-11-04 13:36:17 +03:00
19215d5795 thinlto with either GNUgold or LDD supported 2024-11-04 08:32:10 +03:00
1c97053675 stb_ds.h: fix STBDS_FREE used outside of implementation 2024-11-04 08:09:15 +03:00
ee7fc42fbc inclide math.h so that it works under windows 2024-11-04 07:13:29 +03:00
cd9c65212d amalgam of batch and bash scripts for twn basetool 2024-11-04 07:13:10 +03:00
ccaef34d61 update interop.md 2024-11-03 23:30:29 +03:00
833f7dbc53 update interop.md 2024-11-03 23:27:10 +03:00
a7feb7b61b update interop.md 2024-11-03 23:25:06 +03:00
4be27816c2 scenery: make render distance come from a define 2024-11-03 23:09:10 +03:00
d794ca862f remove junky UncoloredSpaceTriangle union, hide vertex generation from generic triangle implementation 2024-11-03 23:08:53 +03:00
26c75ffd7c optimize case of sequential shared radius circle drawing by reusing the geometry by just offsetting it 2024-11-03 22:33:18 +03:00
e4da4a8b7f add a TODO 2024-11-03 21:54:55 +03:00
963d549eed CMakeLists.txt: only put -s for gcc 2024-10-29 12:32:21 +03:00
9121da0675 yet another api rework, removal of integer types in public api, optionals at the end, some cleaning 2024-10-29 12:25:24 +03:00
6464d14b3e twn_input.h: remove unbinding 2024-10-28 13:04:49 +03:00
eff9fe6918 twn_util.h: make return structs comply to type naming 2024-10-28 12:39:42 +03:00
d11143ac86 twn_draw.h: new camera api 2024-10-28 12:34:48 +03:00
1d35a3859b /docs/interop.md: valid relative hyperlinks 2024-10-22 20:42:07 +03:00
9da26638c8 rework input to be in line with rendering semantics 2024-10-22 20:32:17 +03:00
a22bcfd97e rework timers, update overlap/intersect and other procedures, some other things i dont remember 2024-10-22 14:45:30 +03:00
a527036436 /docs/interop.md 2024-10-22 13:53:10 +03:00
b390e9db23 audio_set() -> audio_set_parameter(), with string based convention 2024-10-22 13:52:24 +03:00
5a08c01208 fix warnings 2024-10-22 11:06:02 +03:00
eff2d9c5e1 direct header includes, remove redundant ones 2024-10-22 10:39:40 +03:00
8aecc2bd06 fix rects render 2024-10-22 10:05:53 +03:00
1296d41ad7 deferred fog, fix of first frame on double buffered option 2024-10-22 09:47:47 +03:00
48f63fc9df deferred skybox, fixes to use of ARB_depth_clamp extension usage, have TextureKey explicitly in QuadBatch 2024-10-22 09:30:14 +03:00
c49789f1f4 make deferred space triangles work 2024-10-19 20:02:39 +03:00
a7b09b9f39 fix circle rendering over new impl 2024-10-19 19:16:18 +03:00
399b199266 move --gc-sections to shared libraries only 2024-10-18 20:41:34 +03:00
73b6ab047d make /bin/build.sh use clang if it can find it by default 2024-10-18 20:33:46 +03:00
024f17de91 make cmake output less verbose 2024-10-18 20:33:25 +03:00
92de2c00c0 make use of thinlto for release builds 2024-10-18 20:33:03 +03:00
b683594013 proper size of build_vertex_buffer for circle indices 2024-10-18 18:45:17 +03:00
90 changed files with 3742 additions and 2155 deletions

View File

@ -1,2 +0,0 @@
CompileFlags:
CompilationDatabase: "./.build/"

15
.editorconfig Normal file
View File

@ -0,0 +1,15 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
[*.{c,h,py}]
indent_style = space
indent_size = 4
[CMakeListst.txt]
indent_style = space
indent_size = 8

2
.gitattributes vendored
View File

@ -2,3 +2,5 @@
*.ogg filter=lfs diff=lfs merge=lfs -text
*.xm filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
text=auto eol=lf

4
.gitignore vendored
View File

@ -22,8 +22,8 @@
.vscode/
.idea/
.cache/
.build/
.build-web/
build/
build-web/
build/
out/

View File

@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.21)
project(townengine LANGUAGES C)
set(CMAKE_MESSAGE_LOG_LEVEL "WARNING")
set(CMAKE_INSTALL_MESSAGE NEVER)
# SDL dependencies
# for whatever reason Emscripten has SDL2 config, but not actual SDL2 port by default
if(NOT EMSCRIPTEN)
@ -24,6 +27,7 @@ set(TWN_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "")
# feature configuration, set them with -DFEATURE=ON/OFF in cli
option(TWN_FEATURE_DYNLIB_GAME "Enable dynamic library loading support" ON)
option(TWN_FEATURE_PUSH_AUDIO "Enable frame based audio push for easy realtime audio" ON)
option(TWN_USE_AMALGAM "Enable use of twn_amalgam.c as a single compilation unit" ON)
# todo: figure out how to compile for dynamic linking instead
@ -81,8 +85,7 @@ endif()
if(TWN_RENDERING_API MATCHES OPENGL_15)
set(SYSTEM_SOURCE_FILES ${SYSTEM_SOURCE_FILES}
src/rendering/twn_gl_any_rendering.c
src/rendering/twn_gl_15_rendering.c
src/rendering/twn_gl_15_gpu_texture.c)
src/rendering/twn_gl_15_rendering.c)
endif()
set(TWN_THIRD_PARTY_SOURCE_FILES
@ -99,17 +102,18 @@ set(TWN_NONOPT_SOURCE_FILES
src/twn_audio.c include/twn_audio.h
src/twn_util.c include/twn_util.h
src/twn_input.c include/twn_input.h
src/twn_camera.c include/twn_camera.h
src/twn_camera.c src/twn_camera_c.h
src/twn_textures.c src/twn_textures_c.h
src/rendering/twn_draw.c src/rendering/twn_draw_c.h
src/rendering/twn_quads.c
src/rendering/twn_sprites.c
src/rendering/twn_rects.c
src/rendering/twn_text.c
src/rendering/twn_triangles.c
src/rendering/twn_billboards.c
src/rendering/twn_circles.c
src/rendering/twn_skybox.c
src/rendering/twn_fog.c)
src/rendering/twn_skybox.c)
set(TWN_SOURCE_FILES
$<IF:$<BOOL:${TWN_USE_AMALGAM}>,src/twn_amalgam.c src/twn_stb.c,${TWN_NONOPT_SOURCE_FILES}>
@ -137,6 +141,8 @@ set_target_properties(${TWN_TARGET} PROPERTIES
C_STANDARD_REQUIRED ON
C_EXTENSIONS ON) # extensions are required by stb_ds.h
target_compile_definitions(${TWN_TARGET} PRIVATE $<$<BOOL:${TWN_FEATURE_PUSH_AUDIO}>:TWN_FEATURE_PUSH_AUDIO>)
# precompile commonly used not-so-small headers
target_precompile_headers(${TWN_TARGET} PRIVATE
$<$<NOT:$<BOOL:${EMSCRIPTEN}>>:third-party/glad/include/glad/glad.h>
@ -156,14 +162,13 @@ function(give_options_without_warnings target)
set(BUILD_FLAGS_RELEASE
-O3
-flto=auto
-flto=$<IF:$<STREQUAL:${CMAKE_C_COMPILER_ID},Clang>,thin,auto>
-mavx -mavx2
-Wl,--gc-sections
-fdata-sections
-ffunction-sections
-funroll-loops
-fomit-frame-pointer
-s)
$<$<STREQUAL:${CMAKE_C_COMPILER_ID},Gnu>:-s>)
set(BUILD_FLAGS_DEBUG
-O0
@ -173,6 +178,19 @@ function(give_options_without_warnings target)
$<$<BOOL:${TWN_SANITIZE}>:-fstack-protector-all -fsanitize=undefined -fsanitize=address>
$<$<BOOL:${EMSCRIPTEN}>:-gsource-map>)
if (CMAKE_C_COMPILER_LINKER_ID MATCHES GNU OR CMAKE_C_COMPILER_LINKER_ID MATCHES GNUgold)
set(THINLTO_USAGE "-plugin-opt,")
endif()
if (CMAKE_C_COMPILER_LINKER_ID MATCHES LLD)
set(THINLTO_USAGE "--thinlto-")
endif()
if (THINLTO_USAGE)
set(BUILD_SHARED_LIBRARY_FLAGS_RELEASE
$<$<STREQUAL:${CMAKE_C_COMPILER_ID},Clang>:-Wl,${THINLTO_USAGE}cache-dir=${CMAKE_CURRENT_BINARY_DIR}/linker-cache/>
$<$<STREQUAL:${CMAKE_C_COMPILER_ID},Clang>:-Wl,${THINLTO_USAGE}cache-policy=prune_after=30m>)
endif()
target_compile_options(${target} PUBLIC
${BUILD_FLAGS}
$<$<CONFIG:Release>:${BUILD_FLAGS_RELEASE}>
@ -186,6 +204,15 @@ function(give_options_without_warnings target)
-Bsymbolic-functions
$<$<BOOL:${LINUX}>:-Wl,--hash-style=gnu>)
get_target_property(target_type ${target} TYPE)
if (target_type MATCHES SHARED_LIBRARY)
target_compile_options(${target} PUBLIC
$<$<CONFIG:Release>:${BUILD_SHARED_LIBRARY_FLAGS_RELEASE}>)
target_link_options(${target} PUBLIC
$<$<CONFIG:Release>:${BUILD_SHARED_LIBRARY_FLAGS_RELEASE}>)
endif()
target_compile_definitions(${target} PRIVATE
$<$<BOOL:${TWN_FEATURE_DYNLIB_GAME}>:TWN_FEATURE_DYNLIB_GAME>
$<$<BOOL:${LINUX}>:_GNU_SOURCE>)
@ -316,8 +343,3 @@ include_deps(${TWN_TARGET})
link_deps(${TWN_TARGET})
target_link_libraries(${TWN_TARGET} PUBLIC twn_third_parties)
target_include_directories(${TWN_TARGET} PRIVATE ${TWN_ROOT_DIR}/src)
# move compie_commands.json into root directory so that it plays nicer with code editors without any configuration
add_custom_target(copy-compile-commands ALL
${CMAKE_COMMAND} -E copy_if_different ${CMAKE_BINARY_DIR}/compile_commands.json
${TWN_ROOT_DIR})

View File

@ -6,7 +6,7 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
add_subdirectory($ENV{TWNROOT} $ENV{TWNBUILDDIR})
add_subdirectory($ENV{TWNROOT} $ENV{TWNROOT}/build)
set(SOURCE_FILES
game.c

View File

@ -5,7 +5,6 @@ app_id = "bunnymark"
dev_id = "morshy"
[game]
base_render_width = 640
base_render_height = 480
resolution = [ 640, 480 ]
[engine]

View File

@ -20,13 +20,13 @@ static void handle_input(void)
if (ctx.mouse_position.y <= 60)
return;
if (input_is_action_pressed("add_a_bit"))
if (input_action_pressed("add_a_bit"))
{ // Left click
for (int i = 0; i < LEFT_CLICK_ADD; i++)
{
if (state->bunniesCount < MAX_BUNNIES)
{
state->bunnies[state->bunniesCount].position = input_get_action_position("add_a_bit");
state->bunnies[state->bunniesCount].position = input_action_position("add_a_bit");
state->bunnies[state->bunniesCount].speed.x = (float)(rand() % 500 - 250) / 60.0f;
state->bunnies[state->bunniesCount].speed.y = (float)(rand() % 500 - 250) / 60.0f;
state->bunnies[state->bunniesCount].color =
@ -38,13 +38,13 @@ static void handle_input(void)
}
}
if (input_is_action_pressed("add_a_lot"))
if (input_action_pressed("add_a_lot"))
{ // Right click
for (int i = 0; i < RIGHT_CLICK_ADD; i++)
{
if (state->bunniesCount < MAX_BUNNIES)
{
state->bunnies[state->bunniesCount].position = input_get_action_position("add_a_lot");
state->bunnies[state->bunniesCount].position = input_action_position("add_a_lot");
state->bunnies[state->bunniesCount].speed.x = (float)(rand() % 500 - 250) / 60.0f;
state->bunnies[state->bunniesCount].speed.y = (float)(rand() % 500 - 250) / 60.0f;
state->bunnies[state->bunniesCount].color =
@ -67,14 +67,11 @@ void game_tick(void)
// Allocating State struct to store data there
if (!ctx.udata)
ctx.udata = ccalloc(1, sizeof(State));
input_add_action("add_a_bit");
input_bind_action_control("add_a_bit", CONTROL_LEFT_MOUSE);
input_add_action("add_a_lot");
input_bind_action_control("add_a_lot", CONTROL_RIGHT_MOUSE);
}
input_action("add_a_bit", CONTROL_LEFT_MOUSE);
input_action("add_a_lot", CONTROL_RIGHT_MOUSE);
State *state = ctx.udata;
for (int i = 0; i < state->bunniesCount; i++)
@ -97,7 +94,7 @@ void game_tick(void)
for (int i = 0; i < state->bunniesCount; i++)
{ // Draw each bunny based on their position and color, also scale accordingly
m_sprite(m_set(path, "wabbit_alpha.png"),
m_sprite(m_set(texture, "wabbit_alpha.png"),
m_set(rect, ((Rect){.x = state->bunnies[i].position.x,
.y = state->bunnies[i].position.y,
.w = BUNNY_W * SPRITE_SCALE,

View File

@ -6,7 +6,7 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
add_subdirectory($ENV{TWNROOT} $ENV{TWNBUILDDIR})
add_subdirectory($ENV{TWNROOT} $ENV{TWNROOT}/build)
set(SOURCE_FILES
game.c

View File

@ -5,7 +5,6 @@ app_id = "platformer-demo"
dev_id = "townengine-team"
[game]
base_render_width = 640
base_render_height = 360
resolution = [ 640, 360 ]
[engine]

View File

@ -18,45 +18,18 @@ void game_tick(void) {
state->ctx = &ctx;
state->scene = title_scene(state);
}
input_add_action("debug_toggle");
input_bind_action_control("debug_toggle", CONTROL_BACKSPACE);
input_add_action("debug_dump_atlases");
input_bind_action_control("debug_dump_atlases", CONTROL_HOME);
input_add_action("player_left");
input_bind_action_control("player_left", CONTROL_A);
input_add_action("player_right");
input_bind_action_control("player_right", CONTROL_D);
input_add_action("player_forward");
input_bind_action_control("player_forward", CONTROL_W);
input_add_action("player_backward");
input_bind_action_control("player_backward", CONTROL_S);
input_add_action("player_jump");
input_bind_action_control("player_jump", CONTROL_SPACE);
input_add_action("player_run");
input_bind_action_control("player_run", CONTROL_LSHIFT);
input_add_action("ui_accept");
input_bind_action_control("ui_accept", CONTROL_RETURN);
input_add_action("mouse_capture_toggle");
input_bind_action_control("mouse_capture_toggle", CONTROL_ESCAPE);
}
State *state = ctx.udata;
if (input_is_action_just_pressed("debug_toggle")) {
input_action("debug_toggle", CONTROL_BACKSPACE);
input_action("debug_dump_atlases", CONTROL_HOME);
if (input_action_just_pressed("debug_toggle")) {
ctx.debug = !ctx.debug;
}
if (input_is_action_just_pressed("debug_dump_atlases")) {
if (input_action_just_pressed("debug_dump_atlases")) {
textures_dump_atlases();
}

View File

@ -11,28 +11,28 @@
static void update_timers(Player *player) {
tick_timer(&player->jump_air_timer);
tick_timer(&player->jump_coyote_timer);
tick_timer(&player->jump_buffer_timer);
player->jump_air_timer = timer_tick_frames(player->jump_air_timer);
player->jump_coyote_timer = timer_tick_frames(player->jump_coyote_timer);
player->jump_buffer_timer = timer_tick_frames(player->jump_buffer_timer);
}
static void input_move(Player *player) {
/* apply horizontal damping when the player stops moving */
/* in other words, make it decelerate to a standstill */
if (!input_is_action_pressed("player_left") &&
!input_is_action_pressed("player_right"))
if (!input_action_pressed("player_left") &&
!input_action_pressed("player_right"))
{
player->dx *= player->horizontal_damping;
}
int input_dir = 0;
if (input_is_action_pressed("player_left"))
if (input_action_pressed("player_left"))
input_dir = -1;
if (input_is_action_pressed("player_right"))
if (input_action_pressed("player_right"))
input_dir = 1;
if (input_is_action_pressed("player_left") &&
input_is_action_pressed("player_right"))
if (input_action_pressed("player_left") &&
input_action_pressed("player_right"))
input_dir = 0;
player->dx += (float)input_dir * player->run_horizontal_speed;
@ -56,7 +56,7 @@ static void jump(Player *player) {
static void input_jump(Player *player) {
player->current_gravity_multiplier = player->jump_default_multiplier;
if (input_is_action_just_pressed("player_jump")) {
if (input_action_just_pressed("player_jump")) {
player->jump_air_timer = 0;
player->jump_buffer_timer = player->jump_buffer_ticks;
@ -65,7 +65,7 @@ static void input_jump(Player *player) {
}
}
if (input_is_action_pressed("player_jump")) {
if (input_action_pressed("player_jump")) {
if (player->action != PLAYER_ACTION_GROUND && player->jump_air_timer > 0) {
player->current_gravity_multiplier = player->jump_boosted_multiplier;
player->dy += player->jump_force_increase;
@ -147,7 +147,7 @@ static bool corner_correct(Player *player, Rect collision) {
static void calc_collisions_x(Player *player) {
Rect collision;
bool is_colliding = world_find_intersect_frect(player->world, player->collider_x, &collision);
bool is_colliding = world_find_rect_intersects(player->world, player->collider_x, &collision);
if (!is_colliding) return;
float player_center_x = player->collider_x.x + (player->collider_x.w / 2);
@ -164,7 +164,7 @@ static void calc_collisions_x(Player *player) {
static void calc_collisions_y(Player *player) {
Rect collision;
bool is_colliding = world_find_intersect_frect(player->world, player->collider_y, &collision);
bool is_colliding = world_find_rect_intersects(player->world, player->collider_y, &collision);
if (!is_colliding) return;
float player_center_y = player->collider_y.y + (player->collider_y.h / 2);
@ -255,6 +255,10 @@ static void drawdef(Player *player) {
draw_circle((Vec2) { 256, 128 },
24,
(Color) { 255, 0, 0, 255 });
draw_circle((Vec2) { 304, 128 },
24,
(Color) { 255, 0, 0, 255 });
}

View File

@ -11,28 +11,15 @@
static void ingame_tick(State *state) {
SceneIngame *scn = (SceneIngame *)state->scene;
input_action("player_left", CONTROL_A);
input_action("player_right", CONTROL_D);
input_action("player_forward", CONTROL_W);
input_action("player_backward", CONTROL_S);
input_action("player_jump", CONTROL_SPACE);
input_action("player_run", CONTROL_LSHIFT);
world_drawdef(scn->world);
player_calc(scn->player);
const Vec3 right = m_vec_norm(m_vec_cross(scn->cam.target, scn->cam.up));
const float speed = 0.04f; /* TODO: put this in a better place */
if (input_is_action_pressed("player_left"))
scn->cam.pos = vec3_sub(scn->cam.pos, m_vec_scale(right, speed));
if (input_is_action_pressed("player_right"))
scn->cam.pos = vec3_add(scn->cam.pos, m_vec_scale(right, speed));
if (input_is_action_pressed("player_forward"))
scn->cam.pos = vec3_add(scn->cam.pos, m_vec_scale(scn->cam.target, speed));
if (input_is_action_pressed("player_backward"))
scn->cam.pos = vec3_sub(scn->cam.pos, m_vec_scale(scn->cam.target, speed));
if (input_is_action_pressed("player_jump"))
scn->cam.pos.y += speed;
if (input_is_action_pressed("player_run"))
scn->cam.pos.y -= speed;
}
@ -54,7 +41,5 @@ Scene *ingame_scene(State *state) {
new_scene->world = world_create();
new_scene->player = player_create(new_scene->world);
new_scene->cam = (Camera){ .pos = { 32, 0, 1 }, .up = { 0, 1, 0 }, .fov = (float)M_PI_2 };
return (Scene *)new_scene;
}

View File

@ -15,8 +15,6 @@ typedef struct SceneIngame {
World *world;
Player *player;
Camera cam;
/* TODO: put this in a better place */
float yaw;
float pitch;

View File

@ -14,7 +14,9 @@ static void title_tick(State *state) {
SceneTitle *scn = (SceneTitle *)state->scene;
(void)scn;
if (input_is_action_just_pressed("ui_accept")) {
input_action("ui_accept", CONTROL_RETURN);
if (input_action_just_pressed("ui_accept")) {
switch_to(state, ingame_scene);
return;
}
@ -23,13 +25,13 @@ static void title_tick(State *state) {
((float)ctx.resolution.x / 2) - ((float)320 / 2), 64, 320, 128 }));
/* draw the tick count as an example of dynamic text */
size_t text_str_len = snprintf(NULL, 0, "%lu", state->ctx->frame_number) + 1;
size_t text_str_len = snprintf(NULL, 0, "%llu", (unsigned long long)state->ctx->frame_number) + 1;
char *text_str = cmalloc(text_str_len);
snprintf(text_str, text_str_len, "%lu", state->ctx->frame_number);
snprintf(text_str, text_str_len, "%llu", (unsigned long long)state->ctx->frame_number);
const char *font = "fonts/kenney-pixel.ttf";
int text_h = 32;
int text_w = draw_text_width(text_str, text_h, font);
float text_h = 32;
float text_w = draw_text_width(text_str, text_h, font);
draw_rectangle(
(Rect) {

View File

@ -12,11 +12,11 @@ static void update_tiles(struct World *world) {
for (size_t row = 0; row < world->tilemap_height; ++row) {
for (size_t col = 0; col < world->tilemap_width; ++col) {
world->tiles[(row * world->tilemap_width) + col] = (struct Tile) {
.rect = (Recti) {
.x = (int)col * world->tile_size,
.y = (int)row * world->tile_size,
.w = world->tile_size,
.h = world->tile_size,
.rect = (Rect) {
.x = (float)(col * world->tile_size),
.y = (float)(row * world->tile_size),
.w = (float)world->tile_size,
.h = (float)world->tile_size,
},
.type = world->tilemap[row][col],
};
@ -25,10 +25,10 @@ static void update_tiles(struct World *world) {
}
static Vec2i to_grid_location(struct World *world, float x, float y) {
return (Vec2i) {
.x = (int)floor(x / (float)world->tile_size),
.y = (int)floor(y / (float)world->tile_size),
static Vec2 to_grid_location(struct World *world, float x, float y) {
return (Vec2) {
.x = floor(x / (float)world->tile_size),
.y = floor(y / (float)world->tile_size),
};
}
@ -39,8 +39,7 @@ static void drawdef_debug(struct World *world) {
for (size_t i = 0; i < world->tilemap_height * world->tilemap_width; ++i) {
if (world->tiles[i].type == TILE_TYPE_VOID) continue;
draw_rectangle(to_frect(world->tiles[i].rect),
(Color) { 255, 0, 255, 128 });
draw_rectangle(world->tiles[i].rect, (Color) { 255, 0, 255, 128 });
}
}
@ -106,14 +105,14 @@ void world_drawdef(struct World *world) {
if (world->tiles[i].type == TILE_TYPE_VOID)
continue;
m_sprite("/assets/white.png", to_frect(world->tiles[i].rect));
m_sprite("/assets/white.png", world->tiles[i].rect);
}
drawdef_debug(world);
}
bool world_find_intersect_frect(struct World *world, Rect rect, Rect *intersection) {
bool world_find_rect_intersects(struct World *world, Rect rect, Rect *intersection) {
bool is_intersecting = false;
const size_t tile_count = world->tilemap_height * world->tilemap_width;
@ -121,44 +120,12 @@ bool world_find_intersect_frect(struct World *world, Rect rect, Rect *intersecti
if (world->tiles[i].type == TILE_TYPE_VOID)
continue;
Rect tile_frect = {
.x = (float)(world->tiles[i].rect.x),
.y = (float)(world->tiles[i].rect.y),
.w = (float)(world->tiles[i].rect.w),
.h = (float)(world->tiles[i].rect.h),
};
Rect const tile_frect = world->tiles[i].rect;
if (intersection == NULL) {
Rect temp;
is_intersecting = overlap_frect(&rect, &tile_frect, &temp);
} else {
is_intersecting = overlap_frect(&rect, &tile_frect, intersection);
}
is_intersecting = rect_intersects(rect, tile_frect);
if (is_intersecting)
break;
}
return is_intersecting;
}
bool world_find_intersect_rect(struct World *world, Recti rect, Recti *intersection) {
bool is_intersecting = false;
const size_t tile_count = world->tilemap_height * world->tilemap_width;
for (size_t i = 0; i < tile_count; ++i) {
if (world->tiles[i].type == TILE_TYPE_VOID)
continue;
Recti *tile_rect = &world->tiles[i].rect;
if (intersection == NULL) {
Recti temp;
is_intersecting = overlap_rect(&rect, tile_rect, &temp);
} else {
is_intersecting = overlap_rect(&rect, tile_rect, intersection);
}
if (intersection)
*intersection = rect_overlap(rect, tile_frect);
if (is_intersecting)
break;
@ -169,20 +136,20 @@ bool world_find_intersect_rect(struct World *world, Recti rect, Recti *intersect
bool world_is_tile_at(struct World *world, float x, float y) {
Vec2i position_in_grid = to_grid_location(world, x, y);
return world->tilemap[position_in_grid.y][position_in_grid.x] != TILE_TYPE_VOID;
Vec2 position_in_grid = to_grid_location(world, x, y);
return world->tilemap[(int32_t)position_in_grid.y][(int32_t)position_in_grid.x] != TILE_TYPE_VOID;
}
void world_place_tile(struct World *world, float x, float y) {
Vec2i position_in_grid = to_grid_location(world, x, y);
world->tilemap[position_in_grid.y][position_in_grid.x] = TILE_TYPE_SOLID;
Vec2 position_in_grid = to_grid_location(world, x, y);
world->tilemap[(int32_t)position_in_grid.y][(int32_t)position_in_grid.x] = TILE_TYPE_SOLID;
update_tiles(world);
}
void world_remove_tile(struct World *world, float x, float y) {
Vec2i position_in_grid = to_grid_location(world, x, y);
world->tilemap[position_in_grid.y][position_in_grid.x] = TILE_TYPE_VOID;
Vec2 position_in_grid = to_grid_location(world, x, y);
world->tilemap[(int32_t)position_in_grid.y][(int32_t)position_in_grid.x] = TILE_TYPE_VOID;
update_tiles(world);
}

View File

@ -14,7 +14,7 @@ typedef enum TileType {
typedef struct Tile {
Recti rect;
Rect rect;
TileType type;
} Tile;
@ -34,8 +34,7 @@ typedef struct World {
World *world_create(void);
void world_destroy(World *world);
void world_drawdef(World *world);
bool world_find_intersect_frect(World *world, Rect rect, Rect *intersection);
bool world_find_intersect_rect(World *world, Recti rect, Recti *intersection);
bool world_find_rect_intersects(World *world, Rect rect, Rect *intersection);
bool world_is_tile_at(World *world, float x, float y);
void world_place_tile(World *world, float x, float y);
void world_remove_tile(World *world, float x, float y);

View File

@ -6,7 +6,7 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
add_subdirectory($ENV{TWNROOT} $ENV{TWNBUILDDIR})
add_subdirectory($ENV{TWNROOT} $ENV{TWNROOT}/build)
set(SOURCE_FILES
game.c

View File

@ -1,11 +1,10 @@
[about]
title = "Serene Scenery"
developer = "Townengine Team"
app_id = "platformer-demo"
app_id = "scenery-demo"
dev_id = "townengine-team"
[game]
base_render_width = 640
base_render_height = 360
resolution = [ 640, 360 ]
[engine]

View File

@ -1,6 +1,7 @@
#include "state.h"
#include "scenes/scene.h"
#include "scenes/title.h"
#include "scenes/ingame.h"
#include "twn_game_api.h"
@ -17,47 +18,20 @@ void game_tick(void) {
State *state = ctx.udata;
state->ctx = &ctx;
state->scene = title_scene(state);
state->scene = ingame_scene(state);
}
input_add_action("debug_toggle");
input_bind_action_control("debug_toggle", CONTROL_BACKSPACE);
input_add_action("debug_dump_atlases");
input_bind_action_control("debug_dump_atlases", CONTROL_HOME);
input_add_action("player_left");
input_bind_action_control("player_left", CONTROL_A);
input_add_action("player_right");
input_bind_action_control("player_right", CONTROL_D);
input_add_action("player_forward");
input_bind_action_control("player_forward", CONTROL_W);
input_add_action("player_backward");
input_bind_action_control("player_backward", CONTROL_S);
input_add_action("player_jump");
input_bind_action_control("player_jump", CONTROL_SPACE);
input_add_action("player_run");
input_bind_action_control("player_run", CONTROL_LSHIFT);
input_add_action("ui_accept");
input_bind_action_control("ui_accept", CONTROL_RETURN);
input_add_action("mouse_capture_toggle");
input_bind_action_control("mouse_capture_toggle", CONTROL_ESCAPE);
}
State *state = ctx.udata;
if (input_is_action_just_pressed("debug_toggle")) {
input_action("debug_toggle", CONTROL_BACKSPACE);
input_action("debug_dump_atlases", CONTROL_HOME);
if (input_action_just_pressed("debug_toggle")) {
ctx.debug = !ctx.debug;
}
if (input_is_action_just_pressed("debug_dump_atlases")) {
if (input_action_just_pressed("debug_dump_atlases")) {
textures_dump_atlases();
}

View File

@ -12,63 +12,147 @@
#include <stdlib.h>
static void ingame_tick(State *state) {
#define TERRAIN_FREQUENCY 0.15f
#define TERRAIN_DISTANCE 64
#define HALF_TERRAIN_DISTANCE ((float)TERRAIN_DISTANCE / 2)
#define PLAYER_HEIGHT 0.6f
static float heightmap[TERRAIN_DISTANCE][TERRAIN_DISTANCE];
static void process_fly_mode(State *state) {
SceneIngame *scn = (SceneIngame *)state->scene;
if (input_is_mouse_captured()) {
const float sensitivity = 0.6f; /* TODO: put this in a better place */
scn->yaw += (float)ctx.mouse_movement.x * sensitivity;
scn->pitch -= (float)ctx.mouse_movement.y * sensitivity;
scn->pitch = clampf(scn->pitch, -89.0f, 89.0f);
DrawCameraFromPrincipalAxesResult dir_and_up =
draw_camera_from_principal_axes(scn->pos, (float)M_PI_2 * 0.8f, scn->roll, scn->pitch, scn->yaw);
const float yaw_rad = scn->yaw * (float)DEG2RAD;
const float pitch_rad = scn->pitch * (float)DEG2RAD;
scn->cam.target = m_vec_norm(((Vec3){
cosf(yaw_rad) * cosf(pitch_rad),
sinf(pitch_rad),
sinf(yaw_rad) * cosf(pitch_rad)
}));
}
const Vec3 right = m_vec_norm(m_vec_cross(scn->cam.target, scn->cam.up));
const Vec3 right = m_vec_norm(m_vec_cross(dir_and_up.direction, dir_and_up.up));
const float speed = 0.04f; /* TODO: put this in a better place */
if (input_is_action_pressed("player_left"))
scn->cam.pos = vec3_sub(scn->cam.pos, m_vec_scale(right, speed));
if (input_action_pressed("player_left"))
scn->pos = vec3_sub(scn->pos, m_vec_scale(right, speed));
if (input_is_action_pressed("player_right"))
scn->cam.pos = vec3_add(scn->cam.pos, m_vec_scale(right, speed));
if (input_action_pressed("player_right"))
scn->pos = vec3_add(scn->pos, m_vec_scale(right, speed));
if (input_is_action_pressed("player_forward"))
scn->cam.pos = vec3_add(scn->cam.pos, m_vec_scale(scn->cam.target, speed));
if (input_action_pressed("player_forward"))
scn->pos = vec3_add(scn->pos, m_vec_scale(dir_and_up.direction, speed));
if (input_is_action_pressed("player_backward"))
scn->cam.pos = vec3_sub(scn->cam.pos, m_vec_scale(scn->cam.target, speed));
if (input_action_pressed("player_backward"))
scn->pos = vec3_sub(scn->pos, m_vec_scale(dir_and_up.direction, speed));
if (input_is_action_pressed("player_jump"))
scn->cam.pos.y += speed;
if (input_action_pressed("player_jump"))
scn->pos.y += speed;
if (input_is_action_pressed("player_run"))
scn->cam.pos.y -= speed;
if (input_action_pressed("player_run"))
scn->pos.y -= speed;
}
/* toggle mouse capture with end key */
if (input_is_action_just_pressed("mouse_capture_toggle")) {
input_set_mouse_captured(!input_is_mouse_captured());
static float height_at(SceneIngame *scn, Vec2 position) {
float height0, height1, height2, weight0, weight1, weight2;
int const x = (int)(HALF_TERRAIN_DISTANCE + (position.x - scn->pos.x));
int const y = (int)(HALF_TERRAIN_DISTANCE + (position.y - scn->pos.z));
height0 = heightmap[x][y];
height1 = heightmap[x + 1][y + 1];
Vec2 incell = { position.x - floorf(position.x), position.y - floorf(position.y) };
/* who needs barycentric coordinates, am i right? */
weight0 = 1 / sqrtf(powf(incell.x, 2) + powf(incell.y, 2));
weight1 = 1 / sqrtf(powf(1 - incell.x, 2) + powf(1 - incell.y, 2));
/* find which triangle we're directly under */
/* for this manhattan distance is sufficient */
if (incell.x + (1 - incell.y) < (1 - incell.x) + incell.y) {
height2 = heightmap[x][y + 1];
weight2 = 1 / sqrtf(powf(incell.x, 2) + powf(1 - incell.y, 2));
} else {
height2 = heightmap[x + 1][y];
weight2 = 1 / sqrtf(powf(1 - incell.x, 2) + powf(incell.y, 2));
}
draw_camera(&scn->cam);
return (height0 * weight0 + height1 * weight1 + height2 * weight2) / (weight0 + weight1 + weight2);
}
#define TERRAIN_FREQUENCY 0.1f
for (int ly = 64; ly--;) {
for (int lx = 64; lx--;) {
float x = SDL_truncf(scn->cam.pos.x + 32 - (float)lx);
float y = SDL_truncf(scn->cam.pos.z + 32 - (float)ly);
static void process_ground_mode(State *state) {
SceneIngame *scn = (SceneIngame *)state->scene;
float d0 = stb_perlin_noise3((float)x * TERRAIN_FREQUENCY, (float)y * TERRAIN_FREQUENCY, 0, 0, 0, 0) * 3 - 6;
float d1 = stb_perlin_noise3((float)(x + 1) * TERRAIN_FREQUENCY, (float)y * TERRAIN_FREQUENCY, 0, 0, 0, 0) * 3 - 6;
float d2 = stb_perlin_noise3((float)(x + 1) * TERRAIN_FREQUENCY, (float)(y - 1) * TERRAIN_FREQUENCY, 0, 0, 0, 0) * 3 - 6;
float d3 = stb_perlin_noise3((float)x * TERRAIN_FREQUENCY, (float)(y - 1) * TERRAIN_FREQUENCY, 0, 0, 0, 0) * 3 - 6;
DrawCameraFromPrincipalAxesResult dir_and_up =
draw_camera_from_principal_axes(scn->pos, (float)M_PI_2 * 0.8f, scn->roll, scn->pitch, scn->yaw);
dir_and_up.direction.y = 0;
dir_and_up.direction = vec3_norm(dir_and_up.direction);
const Vec3 right = m_vec_norm(m_vec_cross(dir_and_up.direction, dir_and_up.up));
const float speed = 0.18f; /* TODO: put this in a better place */
Vec3 target = scn->pos;
/* gravity */
{
float const height = height_at(scn, (Vec2){scn->pos.x, scn->pos.z});
if (target.y > height + PLAYER_HEIGHT)
target.y = target.y - 0.4f;
if (target.y < height + PLAYER_HEIGHT)
target.y = height + PLAYER_HEIGHT;
}
/* movement */
{
Vec3 direction = {0, 0, 0};
if (input_action_pressed("player_left"))
direction = m_vec_sub(direction, m_vec_scale(right, speed));
if (input_action_pressed("player_right"))
direction = m_vec_add(direction, m_vec_scale(right, speed));
if (input_action_pressed("player_forward"))
direction = m_vec_add(direction, m_vec_scale(dir_and_up.direction, speed));
if (input_action_pressed("player_backward"))
direction = m_vec_sub(direction, m_vec_scale(dir_and_up.direction, speed));
target = m_vec_add(target, direction);
}
/* interpolate */
scn->pos.x = (target.x - scn->pos.x) * (0.13f / 0.9f) + scn->pos.x;
scn->pos.y = (target.y - scn->pos.y) * (0.13f / 0.9f) + scn->pos.y;
scn->pos.z = (target.z - scn->pos.z) * (0.13f / 0.9f) + scn->pos.z;
}
static void generate_terrain(SceneIngame *scn) {
for (int ly = 0; ly < TERRAIN_DISTANCE; ly++) {
for (int lx = 0; lx < TERRAIN_DISTANCE; lx++) {
float x = floorf(scn->pos.x - HALF_TERRAIN_DISTANCE + (float)lx);
float y = floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly);
float height = stb_perlin_noise3((float)x * TERRAIN_FREQUENCY, (float)y * TERRAIN_FREQUENCY, 0, 0, 0, 0) * 3 - 1;
height += stb_perlin_noise3((float)x * TERRAIN_FREQUENCY / 10, (float)y * TERRAIN_FREQUENCY / 10, 0, 0, 0, 0) * 10 - 1;
heightmap[lx][ly] = height;
}
}
}
static void draw_terrain(SceneIngame *scn) {
for (int ly = TERRAIN_DISTANCE - 1; ly > 0; ly--) {
for (int lx = 0; lx < TERRAIN_DISTANCE - 1; lx++) {
int32_t x = (int32_t)(floorf(scn->pos.x - HALF_TERRAIN_DISTANCE + (float)lx));
int32_t y = (int32_t)(floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly));
float d0 = heightmap[lx][ly];
float d1 = heightmap[lx + 1][ly];
float d2 = heightmap[lx + 1][ly - 1];
float d3 = heightmap[lx][ly - 1];
draw_triangle("/assets/grass.png",
(Vec3){ (float)x, d0, (float)y },
@ -76,7 +160,10 @@ static void ingame_tick(State *state) {
(Vec3){ (float)x, d3, (float)y - 1 },
(Vec2){ 128, 128 },
(Vec2){ 128, 0 },
(Vec2){ 0, 128 });
(Vec2){ 0, 128 },
(Color){255, 255, 255, 255},
(Color){255, 255, 255, 255},
(Color){255, 255, 255, 255});
draw_triangle("/assets/grass.png",
(Vec3){ (float)x + 1, d1, (float)y },
@ -84,12 +171,61 @@ static void ingame_tick(State *state) {
(Vec3){ (float)x, d3, (float)y - 1 },
(Vec2){ 128, 0 },
(Vec2){ 0, 0 },
(Vec2){ 0, 128 });
(Vec2){ 0, 128 },
(Color){255, 255, 255, 255},
(Color){255, 255, 255, 255},
(Color){255, 255, 255, 255});
draw_billboard("/assets/grasses/10.png",
(Vec3){ (float)x, d0 + 0.15f, (float)y },
(Vec2){0.3f, 0.3f},
(Color){255, 255, 255, 255}, true);
}
}
}
static void ingame_tick(State *state) {
SceneIngame *scn = (SceneIngame *)state->scene;
input_action("player_left", CONTROL_A);
input_action("player_right", CONTROL_D);
input_action("player_forward", CONTROL_W);
input_action("player_backward", CONTROL_S);
input_action("player_jump", CONTROL_SPACE);
input_action("player_run", CONTROL_LSHIFT);
input_action("mouse_capture_toggle", CONTROL_ESCAPE);
input_action("toggle_camera_mode", CONTROL_C);
if (scn->mouse_captured) {
const float sensitivity = 0.4f * (float)DEG2RAD; /* TODO: put this in a better place */
scn->yaw += (float)ctx.mouse_movement.x * sensitivity;
scn->pitch -= (float)ctx.mouse_movement.y * sensitivity;
scn->pitch = clampf(scn->pitch, (float)-M_PI * 0.49f, (float)M_PI * 0.49f);
}
if (input_action_just_pressed("toggle_camera_mode"))
scn->flying_camera = !scn->flying_camera;
if (scn->flying_camera) {
process_fly_mode(state);
} else {
process_ground_mode(state);
}
/* toggle mouse capture with end key */
if (input_action_just_pressed("mouse_capture_toggle"))
scn->mouse_captured = !scn->mouse_captured;
ctx.mouse_capture = scn->mouse_captured;
generate_terrain(scn);
draw_terrain(scn);
draw_skybox("/assets/miramar/miramar_*.tga");
draw_fog(0.9f, 1.0f, 0.05f, (Color){ 140, 147, 160, 255 });
ctx.fog_color = (Color){ 140, 147, 160, 255 };
ctx.fog_density = 0.03f;
}
@ -105,13 +241,13 @@ Scene *ingame_scene(State *state) {
new_scene->base.tick = ingame_tick;
new_scene->base.end = ingame_end;
new_scene->cam = (Camera){ .pos = { 32, 0, 1 }, .up = { 0, 1, 0 }, .fov = (float)M_PI_2 };
new_scene->mouse_captured = true;
m_audio(m_set(path, "music/mod65.xm"),
m_opt(channel, "soundtrack"),
m_opt(repeat, true));
input_set_mouse_captured(true);
new_scene->pos = (Vec3){ 0.1f, 0.0, 0.1f };
return (Scene *)new_scene;
}

View File

@ -6,16 +6,19 @@
#include "../state.h"
#include "scene.h"
#include <stdbool.h>
typedef struct SceneIngame {
Scene base;
Camera cam;
/* TODO: put this in a better place */
Vec3 pos;
float yaw;
float pitch;
float roll;
bool mouse_captured;
bool flying_camera;
} SceneIngame;

View File

@ -11,7 +11,9 @@ static void title_tick(State *state) {
SceneTitle *scn = (SceneTitle *)state->scene;
(void)scn;
if (input_is_action_just_pressed("ui_accept")) {
input_action("ui_accept", CONTROL_RETURN);
if (input_action_just_pressed("ui_accept")) {
switch_to(state, ingame_scene);
return;
}
@ -20,20 +22,20 @@ static void title_tick(State *state) {
((float)ctx.resolution.x / 2) - ((float)320 / 2), 64, 320, 128 }));
/* draw the tick count as an example of dynamic text */
size_t text_str_len = snprintf(NULL, 0, "%llu", state->ctx->frame_number) + 1;
size_t text_str_len = snprintf(NULL, 0, "%llu", (unsigned long long)state->ctx->frame_number) + 1;
char *text_str = cmalloc(text_str_len);
snprintf(text_str, text_str_len, "%llu", state->ctx->frame_number);
snprintf(text_str, text_str_len, "%llu", (unsigned long long)state->ctx->frame_number);
const char *font = "/fonts/kenney-pixel.ttf";
int text_h = 32;
int text_w = draw_text_width(text_str, text_h, font);
float text_h = 32;
float text_w = draw_text_width(text_str, text_h, font);
draw_rectangle(
(Rect) {
.x = 0,
.y = 0,
.w = (float)text_w,
.h = (float)text_h,
.w = text_w,
.h = text_h,
},
(Color) { 0, 0, 0, 255 }
);

View File

@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.21)
project(circle-raster LANGUAGES C)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
add_subdirectory($ENV{TWNROOT} $ENV{TWNROOT}/build)
set(SOURCE_FILES
game.c
state.h
)
use_townengine(${PROJECT_NAME} "${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -0,0 +1,26 @@
# This file contains everything about the engine and your game that can be
# configured before it runs.
#
# Optional settings are commented out, with their default values shown.
# Invalid values in these settings will be ignored.
# Data about your game as an application
[about]
title = "Template"
developer = "You"
app_id = "template"
dev_id = "you"
# Game runtime details
[game]
resolution = [ 640, 480 ]
#debug = true
# Engine tweaks. You probably don't need to change these
[engine]
#ticks_per_second = 60 # minimum of 8
#keybind_slots = 3 # minimum of 1
#texture_atlas_size = 2048 # minimum of 32
#font_texture_size = 2048 # minimum of 1024
#font_oversampling = 4 # minimum of 0
#font_filtering = "linear" # possible values: "nearest", "linear"

View File

@ -0,0 +1,126 @@
#include "twn_game_api.h"
#include "state.h"
#include <malloc.h>
#include <math.h>
/* Emits `x` and `y` for every intersecting cell */
/* We snap position to the nearest corner, which means there's no aliasing */
/* It works great for integer radii */
#define m_iter_circle_pixels(p_center_x, p_center_y, p_radius) \
for (float y = (p_center_y + ceilf(p_radius)) - 1; y > (p_center_y - ceilf(p_radius)) - 1; --y) \
for (float x = p_center_x - ceilf(sqrtf(p_radius * p_radius - (y - p_center_y + (y <= p_center_y)) * (y - p_center_y + (y <= p_center_y)))); x < p_center_x + ceilf(sqrtf(p_radius * p_radius - (y - p_center_y + (y <= p_center_y)) * (y - p_center_y + (y <= p_center_y)))); ++x)
static int32_t ceil_sqrt(int32_t const n) {
int32_t res = 1;
#pragma clang loop unroll_count(8)
while(res * res < n)
res++;
return res;
}
static void benchmark(struct state *state) {
volatile float x, y;
profile_start("float");
for (int i = 0; i < 1000; ++i) {
float const rs = state->r * state->r;
float const cr = ceilf(state->r);
for (float iy = -cr; iy <= cr - 1; ++iy) {
float const dx = ceilf(sqrtf(rs - (iy + (iy <= 0)) * (iy + (iy <= 0))));
for (float ix = -dx; ix < dx; ++ix) {
x = ix;
y = iy;
}
}
}
profile_end("float");
profile_start("int32_t");
for (int i = 0; i < 1000; ++i) {
int32_t const rsi = (int32_t)state->r * (int32_t)state->r;
for (int32_t iy = -(int32_t)state->r; iy <= (int32_t)state->r - 1; ++iy) {
int32_t const dx = ceil_sqrt(rsi - (iy + (iy <= 0)) * (iy + (iy <= 0)));
for (int32_t ix = -dx; ix < dx; ++ix) {
x = (float)ix;
y = (float)iy;
}
}
}
profile_end("int32_t");
profile_start("int32_t acc");
for (int i = 0; i < 1000; ++i) {
int32_t const rsi = (int32_t)state->r * (int32_t)state->r;
int32_t acc = 1;
for (int32_t iy = (int32_t)state->r - 1; iy >= 0; --iy) {
while (acc * acc < rsi - iy * iy) acc++;
for (int32_t ix = -acc; ix < acc; ++ix) {
/* lower portion */
x = (float)ix;
y = (float)iy;
/* upper portion */
x = (float)ix;
y = (float)-iy - 1;
}
}
}
profile_end("int32_t acc");
(void)x; (void)y;
}
void game_tick(void) {
if (ctx.initialization_needed) {
if (!ctx.udata) {
ctx.udata = ccalloc(1, sizeof (struct state));
struct state *state = ctx.udata;
state->r = 10;
}
}
struct state *state = ctx.udata;
Vec2 const mouse_snap = {floorf(ctx.mouse_position.x / 8) * 8, floorf(ctx.mouse_position.y / 8) * 8};
input_action("up", CONTROL_LEFT_MOUSE);
input_action("down", CONTROL_RIGHT_MOUSE);
if (input_action_just_pressed("up"))
state->r += 1;
if (input_action_just_pressed("down"))
state->r -= 1;
int32_t const rsi = (int32_t)state->r * (int32_t)state->r;
int32_t acc = 1;
for (int32_t iy = (int32_t)state->r - 1; iy >= 0; --iy) {
while (acc * acc < rsi - iy * iy) acc++;
for (int32_t ix = -acc; ix < acc; ++ix) {
/* lower portion */
draw_box((Rect){mouse_snap.x + (float)ix * 8, mouse_snap.y + (float)iy * 8, 8, 8}, 1, (Color){125, 125, 0, 255});
/* upper portion */
draw_box((Rect){mouse_snap.x + (float)ix * 8, mouse_snap.y + (float)(-iy - 1) * 8, 8, 8}, 1, (Color){125, 125, 0, 255});
}
}
draw_circle(mouse_snap, state->r * 8, (Color){125, 125, 125, 125});
benchmark(state);
}
void game_end(void) {
/* do your deinitialization here */
struct state *state = ctx.udata;
free(state);
}

View File

@ -0,0 +1,11 @@
#ifndef STATE_H
#define STATE_H
#include "twn_game_api.h"
struct state {
float r;
};
#endif

View File

@ -6,7 +6,7 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
add_subdirectory($ENV{TWNROOT} $ENV{TWNBUILDDIR})
add_subdirectory($ENV{TWNROOT} $ENV{TWNROOT}/build)
set(SOURCE_FILES
game.c

View File

@ -13,8 +13,7 @@ dev_id = "you"
# Game runtime details
[game]
base_render_width = 640
base_render_height = 480
resolution = [ 640, 480 ]
#debug = true
# Engine tweaks. You probably don't need to change these

1
apps/twnlua/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
luabind.c

View File

@ -6,12 +6,21 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
add_subdirectory($ENV{TWNROOT} $ENV{TWNBUILDDIR})
add_subdirectory($ENV{TWNROOT} $ENV{TWNROOT}/build)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/luabind.c
COMMAND ${PYTHON3} ${CMAKE_CURRENT_SOURCE_DIR}/bindgen.py $ENV{TWNROOT}/share/twn_api.json > ${CMAKE_CURRENT_SOURCE_DIR}/luabind.c
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/bindgen.py $ENV{TWNROOT}/share/twn_api.json
)
set(SOURCE_FILES
game.c
state.h
${CMAKE_CURRENT_SOURCE_DIR}/luabind.c
lua/src/lapi.c
lua/src/lapi.h
lua/src/lauxlib.c
@ -74,4 +83,4 @@ set(SOURCE_FILES
)
use_townengine(${PROJECT_NAME} "${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR})
use_townengine(${PROJECT_NAME} "${SOURCE_FILES}" ${CMAKE_SOURCE_DIR})

211
apps/twnlua/bindgen.py Executable file
View File

@ -0,0 +1,211 @@
#!/bin/env python3
import sys, json
with open(sys.argv[1], 'r') if sys.argv[1] != "-" else sys.stdin as f:
api_source = f.read()
api = json.loads(api_source)
def default(parameter):
basetype = parameter["type"].rsplit(' *', 1)[0]
if parameter["type"] == "float":
return parameter["default"]
elif parameter["type"] == "bool":
return "true" if parameter["default"] else "false"
elif parameter["type"] == "char *":
if parameter["default"] == {}:
return "NULL"
else: return '"' + parameter["default"] + '"'
elif basetype in api["types"]:
if parameter["type"].endswith(" *"):
if parameter["default"] == {}:
return "NULL"
else:
return "&(%s){\n%s\n }" % \
(parameter["type"], ", \n".join(" .%s = %s" % (n, v) for n, v in parameter["default"].items()))
else:
return "(%s){\n%s\n }" % \
(parameter["type"], ", \n".join(" .%s = %s" % (n, v) for n, v in parameter["default"].items()))
raise BaseException("Unhandled default value of type '%s'" % parameter["type"])
def to_table(typedesc, variable, indent = 0):
binding = ' ' * indent + "lua_createtable(L, 0, %i);\n" % len(typedesc["fields"])
for field in typedesc["fields"]:
if field["type"] == "float" or field["type"] == "uint8_t":
binding += ' ' * indent + "lua_pushnumber(L, (double)(%s));\n" % (variable + ".%s" % field["name"])
elif field["type"] == "bool":
binding += ' ' * indent + "lua_pushboolean(L, (%s));\n" % (variable + ".%s" % field["name"])
elif field["type"] in api["types"]:
binding += to_table(api["types"][field["type"]], variable + ".%s" % field["name"], indent + 4)
else:
raise BaseException("Unhandled return field type '%s'" % (field["type"]))
binding += ' ' * indent + "lua_setfield(L, -2, \"%s\");\n" % field["name"]
return binding
def from_table(typedesc, variable, indent = 0):
binding = ""
for field in typedesc["fields"]:
binding += ' ' * indent + "lua_getfield(L, -1, \"%s\");\n" % field["name"]
if field["type"] == "float" or field["type"] == "uint8_t":
binding += ' ' * indent + "%s = (%s)lua_tonumber(L, -1);\n" % (variable + ".%s" % field["name"], field["type"])
elif field["type"] == "bool":
binding += ' ' * indent + "%s = lua_toboolean(L, -1);\n" % (variable + ".%s" % field["name"])
elif field["type"] in api["types"]:
binding += from_table(api["types"][field["type"]], variable + ".%s" % field["name"], indent + 4)
else:
raise BaseException("Unhandled return field type '%s'" % (field["type"]))
binding += ' ' * indent + "lua_pop(L, 1);\n"
return binding
print('#include "twn_game_api.h"\n')
# TODO: reuse implementation from the engine, this also breaks with statically compiled build
print('#define STB_DS_IMPLEMENTATION')
print('#include <stb_ds.h>')
print('#include <lua.h>')
print('#include <lualib.h>')
print('#include <lauxlib.h>\n')
bindings, used_converters = [], {}
for procedure, procedure_desc in api["procedures"].items():
binding = "static int binding_%s(lua_State *L) {\n" % procedure
binding += " luaL_checktype(L, 1, LUA_TTABLE);\n"
if "params" in procedure_desc:
for parameter in procedure_desc["params"]:
basetype = parameter["type"].rsplit(' *', 1)[0]
if parameter["type"].endswith("*") and not parameter["type"] == "char *":
binding += " %s %s_value;\n" % (basetype, parameter["name"])
binding += " %s %s;\n" % (parameter["type"] if not parameter["type"].endswith("*") else 'const ' + parameter["type"], parameter["name"])
binding += " lua_getfield(L, 1, \"%s\");\n" % parameter["name"]
if "default" in parameter and parameter["type"] != "float":
binding += " if (lua_isnoneornil(L, -1))\n"
binding += " %s = %s;\n" % (parameter["name"], default(parameter))
binding += " else\n "
if parameter["type"] == "float":
if "default" in parameter:
binding += " int is_%s_num;\n" % parameter["name"]
binding += " %s = (float)lua_tonumberx(L, -1, &is_%s_num);\n" % (parameter["name"], parameter["name"]);
binding += " if (!is_%s_num) %s = %s;\n" % (parameter["name"], parameter["name"], default(parameter))
else:
binding += " %s = (float)lua_tonumber(L, -1);\n" % (parameter["name"]);
elif parameter["type"] == "bool":
binding += " %s = lua_toboolean(L, -1);\n" % (parameter["name"]);
elif parameter["type"] == "char *":
binding += " %s = lua_tostring(L, -1);\n" % (parameter["name"]);
elif basetype in api["types"]:
used_converters[basetype] = api["types"][basetype]
if parameter["type"].endswith(" *"):
binding += " { %s_value = to_%s(L); %s = &%s_value; }\n" % (parameter["name"], basetype.lower(), parameter["name"], parameter["name"]);
else:
binding += " %s = to_%s(L);\n" % (parameter["name"], basetype.lower());
else:
raise BaseException("Unhandled parameter type '%s'" % (parameter["type"]))
if "return" in procedure_desc:
if procedure_desc["return"] == "bool":
binding += " lua_pushboolean(L, (int)(%s(%s)));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
elif procedure_desc["return"] == "float":
binding += " lua_pushnumber(L, (double)(%s(%s)));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
elif procedure_desc["return"] == "char *":
binding += " lua_pushstring(L, %s(%s));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
elif type(procedure_desc["return"]) is dict or procedure_desc["return"] in api["types"]:
# TODO: handle enums
type_desc = procedure_desc["return"] if type(procedure_desc["return"]) is dict else api["types"][procedure_desc["return"]]
binding += " %s result = %s(%s);\n" % (type_desc["c_type"], procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
binding += to_table(type_desc, "result", 4)
else:
raise BaseException("Unhandled return type '%s'" % (procedure_desc["return"]))
binding += " return 1;\n}\n"
else:
binding += " %s(%s);\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
binding += " return 0;\n}\n"
bindings += [binding]
storages, converters, initializers, deinitializers = [], [], [], []
for typename, typedesc in used_converters.items():
if "no_convert" in typedesc and typedesc["no_convert"]:
continue
converter = "static %s to_%s(lua_State *L) {\n" % (typename, typename.lower())
converter += " %s %s;\n" % (typename, typename.lower());
if "fields" in typedesc:
for field in typedesc["fields"]:
converter += " lua_getfield(L, -1, \"%s\");\n" % (field["name"]);
if field["type"] == "float":
converter += " %s.%s = (float)lua_tonumber(L, -1);\n" % (typename.lower(), field["name"]);
elif field["type"] == "uint8_t":
converter += " %s.%s = (uint8_t)lua_tointeger(L, -1);\n" % (typename.lower(), field["name"]);
else:
raise BaseException("Unhandled converter field type '%s'" % (field["type"]))
converter += " lua_pop(L, 1);\n";
# TODO: wild idea: use compile time built hash table
elif "enums" in typedesc:
storages += ["struct %sHashItem { char *key; %s value; };\nstatic struct %sHashItem *%s_map = NULL;\n" % (typename, typename, typename, typename.lower())]
# TODO: use arena
for enum in typedesc["enums"]:
initializer = " shput(%s_map, \"%s\", %s);" % (typename.lower(), enum, typedesc["enums"][enum])
initializers += [initializer]
deinitializers += [" shfree(%s_map);" % typename.lower()]
converter += " char const *value = lua_tostring(L, -1);\n";
converter += " %s = shget(%s_map, value);\n" % (typename.lower(), typename.lower())
converter += " lua_pop(L, 1);\n";
converter += " return %s;\n}\n" % (typename.lower())
converters += [converter]
print('\n'.join(storages))
print("extern void bindgen_init(void);\n")
print("void bindgen_init(void) {\n" + '\n'.join(initializers) + "\n}\n")
print('\n'.join(converters))
print('\n'.join(bindings))
loader = "extern void bindgen_load_%s(lua_State *L);\n" % api["name"]
loader += "void bindgen_load_%s(lua_State *L) {\n" % api["name"]
loader += " bindgen_init();\n"
for procedure, procedure_desc in api["procedures"].items():
loader += " lua_pushcfunction(L, binding_%s);\n" % procedure
loader += " lua_setglobal(L, \"%s\");\n" % procedure
loader += "}\n"
print(loader)
unloader = "extern void bindgen_unload_%s(lua_State *L);\n" % api["name"]
unloader += "void bindgen_unload_%s(lua_State *L) {\n (void)L;\n" % api["name"]
unloader += '\n'.join(deinitializers)
unloader += "\n}\n"
print(unloader)
# exceptions for the base townengine api
# TODO: is there a way to generalize it? or rather, is there any need to do so?
if api["name"] == "twn":
contexter = "extern void bindgen_build_context(lua_State *L);\n"
contexter += "void bindgen_build_context(lua_State *L) {\n"
contexter += to_table(api["types"]["Context"], "ctx", 4)
contexter += "}\n\n"
contexter += "extern void bindgen_upload_context(lua_State *L);\n"
contexter += "void bindgen_upload_context(lua_State *L) {\n"
contexter += from_table(api["types"]["Context"], "ctx", 4)
contexter += "}"
print(contexter)

View File

@ -4,13 +4,18 @@ offset = { x = 0, y = 0 }
angle = 0
function game_tick()
rectangle {
input_action {
name = "press",
control = "A"
}
draw_rectangle {
rect = { x = 0, y = 0, w = 640, h = 360 },
color = { r = 127, g = 0, b = 127, a = 255 },
}
sprite {
path = "/assets/title.png",
draw_sprite {
texture = "/assets/title.png",
rect = {
x = 320 - (320 / 2),
y = 180 - (128 / 2),
@ -19,11 +24,13 @@ function game_tick()
},
}
text {
string = "IT KEEPS HAPPENING",
position = offset,
font = "/fonts/kenney-pixel.ttf",
}
if input_action_pressed { name = "press" } then
draw_text {
string = "it never happened",
position = offset,
font = "/fonts/kenney-pixel.ttf",
}
end
offset.x = ORIGIN.x + (math.cos(angle) * RADIUS)
offset.y = ORIGIN.y + (math.sin(angle) * RADIUS)

View File

@ -5,7 +5,6 @@ app_id = "twnlua"
dev_id = "somebody"
[game]
base_render_width = 640
base_render_height = 360
resolution = [ 640, 360 ]
[engine]

View File

@ -10,6 +10,13 @@
#include <malloc.h>
/* generated by bindgen.py */
void bindgen_load_twn(lua_State *L);
void bindgen_unload_twn(lua_State *L);
void bindgen_build_context(lua_State *L);
void bindgen_upload_context(lua_State *L);
/* require will go through physicsfs exclusively so that scripts can be in the data dir */
static int physfs_loader(lua_State *L) {
const char *name = luaL_checkstring(L, 1);
@ -37,198 +44,46 @@ static int physfs_loader(lua_State *L) {
}
static Rect table_to_rect(lua_State *L, int idx, const char *name) {
/* types are checked here to help prevent unexpected results */
Rect rect;
int is_num;
/* WARN! experimental and will probably be removed */
/* it is an attempt to ease memory usage problems posed by using lua, partially successful */
static void *custom_alloc(void *ud, void *ptr, size_t osize, size_t nsize) {
(void)ud;
lua_getfield(L, idx, "x");
rect.x = (float)lua_tonumberx(L, -1, &is_num);
if (!is_num)
luaL_error(L, "bad field 'x' in '%s' (number expected, got %s)", name, luaL_typename(L, -1));
lua_pop(L, 1);
/* small allocations are placed in slots, as there's a big chance they will not need to be resized */
static char slots[1024][128];
static int16_t free_slots[1024] = { [0] = -1 };
static size_t free_slot_count = 1024;
lua_getfield(L, idx, "y");
rect.y = (float)lua_tonumberx(L, -1, &is_num);
if (!is_num)
luaL_error(L, "bad field 'y' in '%s' (number expected, got %s)", name, luaL_typename(L, -1));
lua_pop(L, 1);
if (free_slots[0] == -1)
for (int i = 0; i < 1024; i++)
free_slots[i] = (int16_t)i;
lua_getfield(L, idx, "w");
rect.w = (float)lua_tonumberx(L, -1, &is_num);
if (!is_num)
luaL_error(L, "bad field 'w' in '%s' (number expected, got %s)", name, luaL_typename(L, -1));
lua_pop(L, 1);
if (nsize == 0) {
if (ptr && (char *)ptr >= &slots[0][0] && (char *)ptr <= &slots[1024-1][128-1])
free_slots[free_slot_count++] = (int16_t)(((uintptr_t)ptr - (uintptr_t)slots) / 128);
else if (osize)
SDL_free(ptr);
return NULL;
} else {
if (!ptr && nsize <= 128 && free_slot_count > 0) {
/* use a slot */
return slots[free_slots[--free_slot_count]];
}
lua_getfield(L, idx, "h");
rect.h = (float)lua_tonumberx(L, -1, &is_num);
if (!is_num)
luaL_error(L, "bad field 'h' in '%s' (number expected, got %s)", name, luaL_typename(L, -1));
lua_pop(L, 1);
if ((char *)ptr >= &slots[0][0] && (char *)ptr <= &slots[1024-1][128-1]) {
/* still fits */
if (nsize <= 128)
return ptr;
return rect;
}
/* move from slot to dynamic memory */
void *mem = SDL_malloc(nsize);
SDL_memcpy(mem, ptr, osize);
free_slots[free_slot_count++] = (int16_t)(((uintptr_t)ptr - (uintptr_t)slots) / 128);
return mem;
}
static Color table_to_color(lua_State *L, int idx) {
Color color;
lua_getfield(L, idx, "r");
color.r = (uint8_t)lua_tointeger(L, -1);
lua_pop(L, 1);
lua_getfield(L, idx, "g");
color.g = (uint8_t)lua_tointeger(L, -1);
lua_pop(L, 1);
lua_getfield(L, idx, "b");
color.b = (uint8_t)lua_tointeger(L, -1);
lua_pop(L, 1);
lua_getfield(L, idx, "a");
color.a = (uint8_t)lua_tointeger(L, -1);
lua_pop(L, 1);
return color;
}
/* sprite(data [table]) */
/* data should contain the following fields: */
/*
* path [string]
* rect [table]
* texture_region [table], optional
* color [table], optional
* rotation [number], optional
* flip_x [boolean], optional
* flip_y [boolean], optional
* stretch [boolean], optional
*/
static int b_sprite(lua_State *L) {
luaL_checktype(L, 1, LUA_TTABLE);
DrawSpriteArgs args = { 0 };
lua_getfield(L, 1, "path");
const char *field_path = lua_tostring(L, -1);
if (field_path == NULL)
luaL_error(L, "bad field 'path' in 'data' (string expected, got %s)", luaL_typename(L, -1));
args.path = field_path;
lua_getfield(L, 1, "rect");
if (!lua_istable(L, -1))
luaL_error(L, "bad field 'rect' in 'data' (table expected, got %s)", luaL_typename(L, -1));
args.rect = table_to_rect(L, -1, "data.rect");
lua_getfield(L, 1, "texture_region");
if (lua_istable(L, -1)) {
args.texture_region_opt = table_to_rect(L, -1, "data.texture_region");
args.texture_region_opt_set = true;
return SDL_realloc(ptr, nsize);
}
lua_getfield(L, 1, "color");
if (lua_istable(L, -1)) {
args.color_opt = table_to_color(L, -1);
args.color_opt_set = true;
}
lua_getfield(L, 1, "rotation");
if (lua_isnumber(L, -1)) {
args.rotation_opt = (float)lua_tonumber(L, -1);
args.rotation_opt_set = true;
}
lua_getfield(L, 1, "flip_x");
if (lua_isboolean(L, -1)) {
args.flip_x_opt = lua_toboolean(L, -1);
args.flip_x_opt_set = true;
}
lua_getfield(L, 1, "flip_y");
if (lua_isboolean(L, -1)) {
args.flip_y_opt = lua_toboolean(L, -1);
args.flip_y_opt_set = true;
}
lua_getfield(L, 1, "stretch");
if (lua_isboolean(L, -1)) {
args.stretch_opt = lua_toboolean(L, -1);
args.stretch_opt_set = true;
}
draw_sprite_args(args);
return 0;
}
/* rectangle(data [table]) */
/* data should contain the following fields: */
/*
* rect [table]
* color [table], optional, defaults to white
*/
static int b_rectangle(lua_State *L) {
luaL_checktype(L, 1, LUA_TTABLE);
lua_getfield(L, 1, "rect");
if (!lua_istable(L, -1))
luaL_error(L, "bad field 'rect' in 'data' (table expected, got %s)", luaL_typename(L, -1));
Rect rect = table_to_rect(L, -1, "data.rect");
Color color = { 255, 255, 255, 255 };
lua_getfield(L, 1, "color");
if (lua_istable(L, -1))
color = table_to_color(L, -1);
draw_rectangle(rect, color);
return 0;
}
/* text(data [table]) */
/* data should contain the following fields: */
/*
* string [string]
* position [table]
* height_px [number], optional, defaults to 22
* color [table], optional, defaults to black
* font [string]
*/
static int b_text(lua_State *L) {
luaL_checktype(L, 1, LUA_TTABLE);
lua_getfield(L, 1, "string");
const char *string = lua_tostring(L, -1);
if (string == NULL)
luaL_error(L, "bad field 'string' in 'data' (string expected, got %s)", luaL_typename(L, -1));
lua_getfield(L, 1, "position");
if (!lua_istable(L, -1))
luaL_error(L, "bad field 'position' in 'data' (table expected, got %s)", luaL_typename(L, -1));
lua_getfield(L, -1, "x");
float x = (float)lua_tonumber(L, -1);
lua_getfield(L, -2, "y");
float y = (float)lua_tonumber(L, -1);
lua_getfield(L, 1, "height_px");
int is_num;
int height_px = (int)lua_tointegerx(L, -1, &is_num);
if (!is_num)
height_px = 22;
lua_getfield(L, 1, "color");
Color color = { 0, 0, 0, 255 };
if (lua_istable(L, -1))
color = table_to_color(L, -1);
lua_getfield(L, 1, "font");
const char *font = lua_tostring(L, -1);
if (font == NULL)
luaL_error(L, "bad field 'font' in 'data' (string expected, got %s)", luaL_typename(L, -1));
draw_text(string, (Vec2) { x, y }, height_px, color, font);
return 0;
}
@ -246,14 +101,15 @@ void game_tick(void) {
}
state->L = luaL_newstate();
/* fakey version of luaL_openlibs() that excludes file i/o */
lua_setallocf(state->L, custom_alloc, NULL);
/* fakey version of luaL_openlibs() that excludes file i/o and os stuff */
{
static const luaL_Reg loaded_libs[] = {
{ LUA_GNAME, luaopen_base },
{ LUA_LOADLIBNAME, luaopen_package },
{ LUA_COLIBNAME, luaopen_coroutine },
{ LUA_TABLIBNAME, luaopen_table },
{ LUA_OSLIBNAME, luaopen_os },
{ LUA_STRLIBNAME, luaopen_string },
{ LUA_MATHLIBNAME, luaopen_math },
{ LUA_UTF8LIBNAME, luaopen_utf8 },
@ -269,7 +125,7 @@ void game_tick(void) {
/* package.searchers = { physfs_loader } */
lua_getglobal(state->L, "package");
lua_newtable(state->L);
lua_createtable(state->L, 0, 1);
lua_setfield(state->L, -2, "searchers");
lua_getfield(state->L, -1, "searchers");
@ -280,9 +136,11 @@ void game_tick(void) {
lua_pop(state->L, 2);
/* binding */
lua_register(state->L, "sprite", b_sprite);
lua_register(state->L, "rectangle", b_rectangle);
lua_register(state->L, "text", b_text);
// lua_register(state->L, "sprite", b_sprite);
// lua_register(state->L, "rectangle", b_rectangle);
// lua_register(state->L, "text", b_text);
bindgen_load_twn(state->L);
/* now finally get to running the code */
unsigned char *game_buf = NULL;
@ -299,16 +157,29 @@ void game_tick(void) {
State *state = ctx.udata;
bindgen_build_context(state->L);
lua_getglobal(state->L, "ctx");
if (!lua_isnoneornil(state->L, -1)) {
lua_getfield(state->L, -1, "udata");
lua_setfield(state->L, -3, "udata");
}
lua_pop(state->L, 1);
lua_setglobal(state->L, "ctx");
lua_getglobal(state->L, "game_tick");
if (lua_pcall(state->L, 0, 0, 0) != LUA_OK) {
log_critical("%s", lua_tostring(state->L, -1));
lua_pop(state->L, 1);
}
lua_getglobal(state->L, "ctx");
bindgen_upload_context(state->L);
}
void game_end(void) {
State *state = ctx.udata;
bindgen_unload_twn(state->L);
lua_close(state->L);
free(state);
}

View File

@ -1,8 +1,6 @@
#ifndef STATE_H
#define STATE_H
#include "twn_game_api.h"
#include <lua.h>

View File

@ -1,12 +1,19 @@
#!/bin/env sh
set +e
# check whether ninja is around (you better start running)
if [ -x "$(command -v ninja)" ]; then
generator="-G Ninja"
fi
if [ "$1" = "web" ]; then
emcmake cmake $generator -B .build-web "${@:2}" && cmake --build .build-web --parallel
else
cmake $generator -B .build "$@" && cmake --build .build --parallel
# check whether clang is around (it's just better)
if [ -x "$(command -v clang)" ]; then
cc="-DCMAKE_C_COMPILER=clang"
fi
if [ "$1" = "web" ]; then
emcmake cmake $generator $cc -B build-web "${@:2}" && cmake --build build-web --parallel
else
cmake $generator $cc -B build "$@" && cmake --build build --parallel
fi

View File

@ -1,4 +1,6 @@
#!/bin/env sh
# single header api generator with clang
set +e
clang -I./ -P -E -nostdinc -nobuiltininc -DTWN_NOT_C $TWNROOT/include/twn_game_api.h 2> /dev/null | clang-format

View File

@ -6,7 +6,6 @@ set +e
exe="$(basename $PWD)"
toolpath="$(dirname -- "${BASH_SOURCE[0]}")"
export TWNROOT=$(realpath "$toolpath"/../)
export TWNBUILDDIR=$(realpath "$toolpath"/../.build)
case "$1" in
build ) "$toolpath"/build.sh "${@:2}"

BIN
common-data/assets/grasses/10.png (Stored with Git LFS) Normal file

Binary file not shown.

16
docs/interop.md Normal file
View File

@ -0,0 +1,16 @@
# interoperability
api needs to facilitate easy interoperability with other languages and tools,
for that certain considerations are taken:
* number of public api calls is kept at the minimum
* procedure parameters can only use basic types, no aggregates, with exception of Vec/Matrix types and alike,
with no new additions, ever (see [/include/twn_types.h](../include/twn_types.h))
* optionals can be expressed via pointer passage of value primitives, assumed immutable, with the NULL expressing default value
* no opaque types, only keys if needed
* pointers to pointers aren't allowed
* when mutation on input is done, - it shouldn't be achieved by a mutable pointer, but the return value
* return value could be a simple aggregate that is translatable to a dictionary of primitives
* module prefix is used for namespacing, actual symbols come after the prefix (`<module>_<symbol>`)
* symbols should not contain numerics at the start nor after the namespace prefix
* 32 bit floating point is the only numeric type
* [/include/twn_api.json](../share/twn_api.json) file is hand-kept with a schema to aid automatic binding generation and tooling

View File

@ -1,10 +0,0 @@
api needs to facilitate easy interoperability with other languages and tools,
for that certain steps are taken:
* number of public api calls is kept at the minimum
* procedure signatures can only use basic types, no aggregates, with exception of Vec/Matrix types and alike,
with no expectation on new additions (see /include/twn_types.h)
* optionals can be expressed via pointer passage of value primitives, with NULL expressive default
* /include/twn_game_api.json file is hand-kept with a schema to aid automatic generation and other tooling
one of main inspirations for that is opengl model

1
hooks
View File

@ -6,3 +6,4 @@ set +e
# TODO: prevent double hooking
export PATH=$PATH:$(realpath $(dirname -- "${BASH_SOURCE[0]}")/bin/)
export TWNROOT=$(realpath $(dirname -- "${BASH_SOURCE[0]}"))

View File

@ -9,19 +9,14 @@
/* plays audio file at specified channel or at scratch channel if NULL is passed, without ability to refer to it later */
/* path path must contain valid file extension to infer which file format it is */
/* supported formats: .ogg, .xm */
TWN_API void audio_play(const char *path,
TWN_API void audio_play(const char *audio,
const char *channel, /* optional */
bool repeat, /* default: false */
float volume, /* default: 1.0f, range: 0.0f to 1.0f */
float panning); /* default: 0.0f, range: -1.0 to 1.0f */
typedef enum {
AUDIO_PARAM_REPEAT,
AUDIO_PARAM_VOLUME,
AUDIO_PARAM_PANNING,
} AudioParam;
TWN_API void audio_set(const char *channel, AudioParam param, float value);
/* possible parameter options: "volume", "panning", "repeat" */
TWN_API void audio_parameter(const char *channel, const char *parameter, float value);
/* TODO */
// TWN_API bool audio_ended(const char *channel);

View File

@ -16,21 +16,25 @@ typedef struct Context {
void *udata;
/* which frame is it, starting from 0 at startup */
uint64_t frame_number;
float frame_number;
/* real time spent on one frame (in seconds) */
/* townengine is fixed step based, so you don't have */
/* townengine is fixed step based, so you don't have to use delta */
/* TODO: actually set it */
float frame_duration;
/* it is disabled by having fog_density approximately equal to zero */
float fog_density;
Color fog_color;
/* resolution is set from config and dictates both logical and drawing space, as they're related */
/* even if scaling is done, game logic should never change over that */
Vec2i resolution;
Vec2i mouse_position;
Vec2i mouse_movement;
Vec2 resolution;
Vec2 mouse_position;
Vec2 mouse_movement;
/* is set on startup, should be used as source of randomness */
uint64_t random_seed;
float random_seed;
/* whether debugging logic should be enabled in user code */
bool debug;
@ -38,6 +42,7 @@ typedef struct Context {
/* is set to true when state is invalidated and needs to be rebuilt */
/* watch for it and handle properly! */
bool initialization_needed;
bool mouse_capture;
} Context;
/* when included after twn_engine_context there's an 'ctx' defined already */

View File

@ -3,20 +3,19 @@
#include "twn_types.h"
#include "twn_option.h"
#include "twn_camera.h"
#include "twn_engine_api.h"
#include <stdbool.h>
/* pushes a sprite onto the sprite render queue */
TWN_API void draw_sprite(char const *path,
TWN_API void draw_sprite(char const *texture,
Rect rect,
Rect const *texture_region, /* optional, default: NULL */
Color color, /* optional, default: all 255 */
float rotation, /* optional, default: 0 */
bool flip_x, /* optional, default: false */
bool flip_y, /* optional, default: false */
bool stretch); /* optional, default: false */
bool stretch); /* optional, default: true */
/* pushes a filled rectangle onto the rectangle render queue */
TWN_API void draw_rectangle(Rect rect, Color color);
@ -28,67 +27,81 @@ TWN_API void draw_circle(Vec2 position, float radius, Color color);
/* TODO: have font optional, with something minimal coming embedded */
TWN_API void draw_text(char const *string,
Vec2 position,
int height_px, /* optional, default: 22 */
Color color, /* optional, default: all 0 */
char const *font);
float height, /* optional, default: 22 */
Color color, /* optional, default: all 0 */
char const *font); /* optional, default: NULL */
TWN_API int draw_text_width(char const *string,
int height_px, /* TODO: make optional */
char const *font);
TWN_API float draw_text_width(char const *string,
float height, /* optional, default: 22 */
char const *font); /* optional, default: NULL */
TWN_API void draw_9slice(char const *texture_path,
int texture_w,
int texture_h,
int border_thickness,
Rect rect,
Color color); /* TODO: make optional */
TWN_API void draw_nine_slice(char const *texture,
Vec2 corners,
Rect rect,
float border_thickness, /* optional, default: 0 */
Color color); /* optional, default: all 255 */
TWN_API void draw_line(Vec2 start,
Vec2 finish,
float thickness, /* optional, default: 1 */
Color color); /* optional, default: all 255 */
TWN_API void draw_box(Rect rect,
float thickness, /* optional, default: 1 */
Color color); /* optional, default: all 255 */
/* pushes a textured 3d triangle onto the render queue */
/* vertices are in absolute coordinates, relative to world origin */
/* texture coordinates are in pixels */
TWN_API void draw_triangle(char const *path,
TWN_API void draw_triangle(char const *texture,
Vec3 v0,
Vec3 v1,
Vec3 v2,
Vec2 uv0,
Vec2 uv1,
Vec2 uv2);
Vec2 uv2,
Color c0, /* optional, default: all 255 */
Color c1, /* optional, default: all 255 */
Color c2); /* optional, default: all 255 */
// TODO: decide whether it's needed to begin with?
// intended usage for it is baked lighting, i would think.
/* pushes a colored textured 3d triangle onto the render queue */
// void unfurl_colored_triangle(const char *path,
// Vec3 v0,
// Vec3 v1,
// Vec3 v2,
// Vec2sh uv0,
// Vec2sh uv1,
// Vec2sh uv2,
// Color c0,
// Color c1,
// Color c2);
TWN_API void draw_quad(char const *texture,
Vec3 v0, /* upper-left */
Vec3 v1, /* bottom-left */
Vec3 v2, /* bottom-right */
Vec3 v3, /* upper-right */
Rect texture_region,
Color color); /* optional, default: all 255 */
// TODO:
// http://www.lighthouse3d.com/opengl/billboarding/index.php?billCheat2
// void unfurl_billboard(const char *path,
// Vec2 position,
// Vec2 scaling,
// Rect uvs);
TWN_API void draw_billboard(const char *texture,
Vec3 position,
Vec2 size,
Color color, /* optional, default: all 255 */
bool cylindrical); /* optional, default: false */
/* pushes a camera state to be used for all future unfurl_* commands */
TWN_API void draw_camera(const Camera *camera);
/* sets a perspective 3d camera to be used for all 3d commands */
TWN_API void draw_camera(Vec3 position, float fov, Vec3 up, Vec3 direction);
/* same as draw_camera(), but with first person controller in mind */
/* direction and up vectors are inferred from roll, pitch and yaw parameters (in radians) */
/* return value is direction and up vectors, so that you can use them in logic (such as controllers) */
typedef struct DrawCameraFromPrincipalAxesResult {
Vec3 direction;
Vec3 up;
} DrawCameraFromPrincipalAxesResult;
TWN_API DrawCameraFromPrincipalAxesResult draw_camera_from_principal_axes(Vec3 position,
float fov,
float roll,
float pitch,
float yaw);
/* expects '*' masks that will be expanded to 6 names: 'up', 'down', 'east', 'west', 'north' and 'south' */
TWN_API void draw_skybox(const char *paths);
TWN_API void draw_fog(float start, float end, float density, Color color);
TWN_API void draw_skybox(const char *textures);
#ifndef TWN_NOT_C
typedef struct DrawSpriteArgs {
char const *path;
char const *texture;
Rect rect;
m_option_list(

View File

@ -1,7 +1,9 @@
#ifndef TWN_ENGINE_API_H
#define TWN_ENGINE_API_H
#if defined(__WIN32)
#if defined(TWN_NOT_C)
#define TWN_API
#elif defined(__WIN32)
#define TWN_API __declspec(dllexport)
#else
#define TWN_API __attribute__((visibility("default")))

View File

@ -3,11 +3,11 @@
#define TWN_GAME_API_H
#include "twn_input.h"
#include "twn_context.h"
#include "twn_draw.h"
#include "twn_audio.h"
#include "twn_engine_api.h"
#include "twn_util.h"
#include "twn_context.h"
#ifndef TWN_NOT_C

View File

@ -10,19 +10,10 @@
#include <stddef.h>
TWN_API void input_bind_action_control(const char *action_name, Control control);
TWN_API void input_unbind_action_control(const char *action_name, Control control);
TWN_API void input_add_action(const char *action_name);
TWN_API void input_delete_action(const char *action_name);
TWN_API bool input_is_action_pressed(const char *action_name);
TWN_API bool input_is_action_just_pressed(const char *action_name);
TWN_API bool input_is_action_just_released(const char *action_name);
TWN_API Vec2 input_get_action_position(const char *action_name);
TWN_API void input_set_mouse_captured(bool enabled);
TWN_API bool input_is_mouse_captured(void);
TWN_API void input_action(const char *name, Control control);
TWN_API bool input_action_pressed(const char *name);
TWN_API bool input_action_just_pressed(const char *name);
TWN_API bool input_action_just_released(const char *name);
TWN_API Vec2 input_action_position(const char *name);
#endif

View File

@ -9,6 +9,8 @@ typedef enum TextureMode {
TEXTURE_MODE_OPAQUE, /* all pixels are solid */
TEXTURE_MODE_SEETHROUGH, /* some pixels are alpha zero */
TEXTURE_MODE_GHOSTLY, /* arbitrary alpha values */
TEXTURE_MODE_COUNT,
TEXTURE_MODE_UNKNOWN = -1, /* a sentinel */
} TextureMode;
#endif

View File

@ -6,13 +6,6 @@
/* plain data aggregates that are accepted between public procedure boundaries */
/* a point in some space (integer) */
typedef struct Vec2i {
int32_t x;
int32_t y;
} Vec2i;
/* a point in some space (floating point) */
typedef struct Vec2 {
float x;
@ -29,16 +22,6 @@ typedef struct Vec3 {
} Vec3;
/* a point in some three dimension space (floating point) */
/* y goes up, x goes to the right */
typedef struct Vec4 {
float x;
float y;
float z;
float w;
} Vec4;
/* 32-bit color data */
typedef struct Color {
uint8_t r;
@ -48,15 +31,6 @@ typedef struct Color {
} Color;
/* a rectangle with the origin at the upper left (integer) */
typedef struct Recti {
int32_t x;
int32_t y;
int32_t w;
int32_t h;
} Recti;
/* a rectangle with the origin at the upper left (floating point) */
typedef struct Rect {
float x;
@ -66,9 +40,4 @@ typedef struct Rect {
} Rect;
typedef struct Matrix4 {
Vec4 row[4];
} Matrix4;
#endif

View File

@ -24,74 +24,68 @@
TWN_API void *crealloc(void *ptr, size_t size);
TWN_API void *ccalloc(size_t num, size_t size);
TWN_API void log_info(const char *restrict format, ...);
TWN_API void log_critical(const char *restrict format, ...);
TWN_API void log_warn(const char *restrict format, ...);
/* saves all texture atlases as BMP files in the write directory */
TWN_API void textures_dump_atlases(void);
/* returns true if str ends with suffix */
TWN_API bool strends(const char *str, const char *suffix);
/* TODO: this is why generics were invented. sorry, i'm tired today */
TWN_API double clamp(double d, double min, double max);
TWN_API float clampf(float f, float min, float max);
TWN_API int clampi(int i, int min, int max);
/* sets buf_out to a pointer to a byte buffer which must be freed. */
/* returns the size of this buffer. */
TWN_API int64_t file_to_bytes(const char *path, unsigned char **buf_out);
/* returns a pointer to a string which must be freed */
TWN_API char *file_to_str(const char *path);
/* returns true if the file exists in the filesystem */
TWN_API bool file_exists(const char *path);
#endif /* TWN_NOT_C */
TWN_API void log_info(const char *restrict format, ...);
TWN_API void log_critical(const char *restrict format, ...);
TWN_API void log_warn(const char *restrict format, ...);
/* TODO: this is why generics were invented. sorry, i'm tired today */
TWN_API double clamp(double d, double min, double max);
TWN_API float clampf(float f, float min, float max);
TWN_API int clampi(int i, int min, int max);
/* sets buf_out to a pointer to a byte buffer which must be freed. */
/* returns the size of this buffer. */
TWN_API int64_t file_to_bytes(const char *path, unsigned char **buf_out);
/* returns a pointer to a string which must be freed */
TWN_API char *file_to_str(const char *path);
/* returns true if the file exists in the filesystem */
TWN_API bool file_exists(const char *path);
/* saves all texture atlases as BMP files in the write directory */
TWN_API void textures_dump_atlases(void);
/* returns true if str ends with suffix */
TWN_API TWN_API bool strends(const char *str, const char *suffix);
/* */
/* GAME LOGIC UTILITIES */
/* */
/* calculates the overlap of two rectangles and places it in result. */
/* result may be NULL. if this is the case, it will simply be ignored. */
/* returns true if the rectangles are indeed intersecting. */
TWN_API bool overlap_rect(const Recti *a, const Recti *b, Recti *result);
TWN_API bool overlap_frect(const Rect *a, const Rect *b, Rect *result);
/* calculates the overlap of two rectangles */
TWN_API Rect rect_overlap(Rect a, Rect b);
/* returns true if two rectangles are intersecting */
TWN_API bool intersect_rect(const Recti *a, const Recti *b);
TWN_API bool intersect_frect(const Rect *a, const Rect *b);
TWN_API bool rect_intersects(Rect a, Rect b);
TWN_API Vec2 rect_center(Rect rect);
/* TODO: generics and specials (see m_vec2_from() for an example)*/
TWN_API Rect to_frect(Recti rect);
TWN_API Vec2 frect_center(Rect rect);
/* decrements an lvalue (which should be an int), stopping at 0 */
/* decrements an integer value, stopping at 0 */
/* meant for tick-based timers in game logic */
/*
* example:
* tick_timer(&player->jump_air_timer);
*/
TWN_API void tick_timer(int *value);
TWN_API int32_t timer_tick_frames(int32_t frames_left);
/* decrements a floating point second-based timer, stopping at 0.0 */
/* decrements a floating point second-based timer, stopping at 0.0f */
/* meant for poll based real time logic in game logic */
/* note that it should be decremented only on the next tick after its creation */
TWN_API void tick_ftimer(float *value);
TWN_API float timer_tick_seconds(float seconds_left);
/* same as `tick_ftimer` but instead of clamping it repeats */
/* returns true if value was cycled */
TWN_API bool repeat_ftimer(float *value, float at);
typedef struct TimerElapseFramesResult {
bool elapsed; int32_t frames_left;
} TimerElapseFramesResult;
TWN_API TimerElapseFramesResult timer_elapse_frames(int32_t frames_left, int32_t interval);
typedef struct TimerElapseSecondsResult {
bool elapsed; float seconds_left;
} TimerElapseSecondsResult;
TWN_API TimerElapseSecondsResult timer_elapse_seconds(float seconds_left, float interval);
TWN_API void log_vec2(Vec2 vector, char const *message);
TWN_API void log_vec3(Vec3 vector, char const *message);
TWN_API void log_rect(Rect rect, char const *message);
TWN_API void profile_start(char profile[const static 1]);
TWN_API void profile_end(char profile[const static 1]);
TWN_API void profile_list_stats(void);
#endif

View File

@ -9,13 +9,28 @@
#include <math.h>
/* aren't macros to prevent double evaluation with side effects */
/* maybe could be inlined? i hope LTO will resolve this */
static inline Vec2 vec2_from_vec2i(Vec2i vec) {
return (Vec2) {
.x = (float)vec.x,
.y = (float)vec.y,
};
static inline Vec2 vec2_add(Vec2 a, Vec2 b) {
return (Vec2) { a.x + b.x, a.y + b.y };
}
static inline Vec2 vec2_sub(Vec2 a, Vec2 b) {
return (Vec2) { a.x - b.x, a.y - b.y };
}
static inline Vec2 vec2_div(Vec2 a, Vec2 b) {
return (Vec2) { a.x / b.x, a.y / b.y };
}
static inline Vec2 vec2_mul(Vec2 a, Vec2 b) {
return (Vec2) { a.x * b.x, a.y * b.y };
}
static inline Vec2 vec2_scale(Vec2 a, float s) {
return (Vec2) { a.x * s, a.y * s };
}
static inline float vec2_length(Vec2 a) {
return sqrtf(a.x * a.x + a.y * a.y);
}
static inline Vec3 vec3_add(Vec3 a, Vec3 b) {
@ -26,12 +41,12 @@ static inline Vec3 vec3_sub(Vec3 a, Vec3 b) {
return (Vec3) { a.x - b.x, a.y - b.y, a.z - b.z };
}
static inline Vec2 vec2_div(Vec2 a, Vec2 b) {
return (Vec2) { a.x / b.x, a.y / b.y };
static inline Vec3 vec3_div(Vec3 a, Vec3 b) {
return (Vec3) { a.x / b.x, a.y / b.y, a.z / b.z };
}
static inline Vec2 vec2_scale(Vec2 a, float s) {
return (Vec2) { a.x * s, a.y * s };
static inline Vec3 vec3_mul(Vec3 a, Vec3 b) {
return (Vec3) { a.x * b.x, a.y * b.y, a.z * b.z };
}
static inline Vec3 vec3_scale(Vec3 a, float s) {
@ -42,6 +57,10 @@ static inline float vec3_dot(Vec3 a, Vec3 b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
static inline float vec3_length(Vec3 a) {
return sqrtf(a.x * a.x + a.y * a.y + a.z * a.z);
}
static inline Vec3 vec3_cross(Vec3 a, Vec3 b) {
return (Vec3) {
a.y * b.z - a.z * b.y,
@ -83,16 +102,26 @@ static inline Vec3 vec3_rotate(Vec3 v, float angle, Vec3 axis) {
return v;
}
#define m_vec2_from(p_any_vec2) (_Generic((p_any_vec2), \
Vec2i: vec2_from_vec2i, \
)(p_any_vec2))
/* TODO: remove. */
#define m_vec_add(p_any_vec0, p_any_vec1) (_Generic((p_any_vec0), \
Vec2: vec2_add, \
Vec3: vec3_add \
)(p_any_vec0, p_any_vec1))
#define m_vec_sub(p_any_vec0, p_any_vec1) (_Generic((p_any_vec0), \
Vec2: vec2_sub, \
Vec3: vec3_sub \
)(p_any_vec0, p_any_vec1))
#define m_vec_div(p_any_vec0, p_any_vec1) (_Generic((p_any_vec0), \
Vec2: vec2_div \
Vec2: vec2_div, \
Vec3: vec3_div \
)(p_any_vec0, p_any_vec1))
#define m_vec_mul(p_any_vec0, p_any_vec1) (_Generic((p_any_vec0), \
Vec2: vec2_mul, \
Vec3: vec3_mul \
)(p_any_vec0, p_any_vec1))
#define m_vec_scale(p_any_vec, p_any_scalar) (_Generic((p_any_vec), \

533
share/twn_api.json Normal file
View File

@ -0,0 +1,533 @@
{
"name": "twn",
"procedures": {
"input_action": {
"module": "input",
"symbol": "action",
"header": "twn_input.h",
"params": [
{ "name": "name", "type": "char *" },
{ "name": "control", "type": "Control" }
]
},
"input_action_pressed": {
"module": "input",
"symbol": "action_pressed",
"header": "twn_input.h",
"params": [
{ "name": "name", "type": "char *" }
],
"return": "bool"
},
"input_action_just_pressed": {
"module": "input",
"symbol": "action_just_pressed",
"header": "twn_input.h",
"params": [
{ "name": "name", "type": "char *" }
],
"return": "bool"
},
"input_action_just_released": {
"module": "input",
"symbol": "action_just_released",
"header": "twn_input.h",
"params": [
{ "name": "name", "type": "char *" }
],
"return": "bool"
},
"input_action_position": {
"module": "input",
"symbol": "get_action_position",
"header": "twn_input.h",
"params": [
{ "name": "name", "type": "char *" }
],
"return": "Vec2"
},
"draw_sprite": {
"module": "draw",
"symbol": "sprite",
"header": "twn_draw.h",
"params": [
{ "name": "texture", "type": "char *" },
{ "name": "rect", "type": "Rect" },
{ "name": "texture_region", "type": "Rect *", "default": {} },
{ "name": "color", "type": "Color", "default": { "r": 255, "g": 255, "b": 255, "a": 255 } },
{ "name": "rotation", "type": "float", "default": 0.0 },
{ "name": "flip_x", "type": "bool", "default": false },
{ "name": "flip_y", "type": "bool", "default": false },
{ "name": "stretch", "type": "bool", "default": true }
]
},
"draw_rectangle": {
"module": "draw",
"symbol": "rectangle",
"header": "twn_draw.h",
"params": [
{ "name": "rect", "type": "Rect" },
{ "name": "color", "type": "Color", "default": { "r": 255, "g": 255, "b": 255, "a": 255 } }
]
},
"draw_circle": {
"module": "draw",
"symbol": "circle",
"header": "twn_draw.h",
"params": [
{ "name": "position", "type": "Vec2" },
{ "name": "radius", "type": "float" },
{ "name": "color", "type": "Color", "default": { "r": 255, "g": 255, "b": 255, "a": 255 } }
]
},
"draw_text": {
"module": "draw",
"symbol": "text",
"header": "twn_draw.h",
"params": [
{ "name": "string", "type": "char *" },
{ "name": "position", "type": "Vec2" },
{ "name": "height", "type": "float", "default": 22 },
{ "name": "color", "type": "Color", "default": { "r": 255, "g": 255, "b": 255, "a": 255 } },
{ "name": "font", "type": "char *", "default": {} }
]
},
"draw_text_width": {
"module": "draw",
"symbol": "text_width",
"header": "twn_draw.h",
"params": [
{ "name": "string", "type": "char *" },
{ "name": "height", "type": "float", "default": 22 },
{ "name": "font", "type": "char *", "default": {} }
],
"return": "float"
},
"draw_nine_slice": {
"module": "draw",
"symbol": "nine_slice",
"header": "twn_draw.h",
"params": [
{ "name": "texture", "type": "char *" },
{ "name": "corners", "type": "Vec2" },
{ "name": "rect", "type": "Rect" },
{ "name": "border_thickness", "type": "float", "default": 0 },
{ "name": "color", "type": "Color", "default": { "r": 255, "g": 255, "b": 255, "a": 255 } }
]
},
"draw_triangle": {
"module": "draw",
"symbol": "triangle",
"header": "twn_draw.h",
"params": [
{ "name": "texture", "type": "char *" },
{ "name": "v0", "type": "Vec3" },
{ "name": "v1", "type": "Vec3" },
{ "name": "v2", "type": "Vec3" },
{ "name": "uv0", "type": "Vec2" },
{ "name": "uv1", "type": "Vec2" },
{ "name": "uv2", "type": "Vec2" },
{ "name": "c0", "type": "Color", "default": { "r": 255, "g": 255, "b": 255, "a": 255 } },
{ "name": "c1", "type": "Color", "default": { "r": 255, "g": 255, "b": 255, "a": 255 } },
{ "name": "c2", "type": "Color", "default": { "r": 255, "g": 255, "b": 255, "a": 255 } }
]
},
"draw_quad": {
"module": "draw",
"symbol": "quad",
"header": "twn_draw.h",
"params": [
{ "name": "texture", "type": "char *" },
{ "name": "v0", "type": "Vec3" },
{ "name": "v1", "type": "Vec3" },
{ "name": "v2", "type": "Vec3" },
{ "name": "v3", "type": "Vec3" },
{ "name": "texture_region", "type": "Rect" },
{ "name": "color", "type": "Color", "default": { "r": 255, "g": 255, "b": 255, "a": 255 } }
]
},
"draw_billboard": {
"module": "draw",
"symbol": "billboard",
"header": "twn_draw.h",
"params": [
{ "name": "texture", "type": "char *" },
{ "name": "position", "type": "Vec3" },
{ "name": "size", "type": "Vec2" },
{ "name": "color", "type": "Color", "default": { "r": 255, "g": 255, "b": 255, "a": 255 } },
{ "name": "cylindrical", "type": "bool", "default": false }
]
},
"draw_camera": {
"module": "draw",
"symbol": "camera",
"header": "twn_draw.h",
"params": [
{ "name": "position", "type": "Vec3" },
{ "name": "fov", "type": "float" },
{ "name": "up", "type": "Vec3" },
{ "name": "direction", "type": "Vec3" }
]
},
"draw_camera_from_principal_axes": {
"module": "draw",
"symbol": "camera_from_principal_axes",
"header": "twn_draw.h",
"params": [
{ "name": "position", "type": "Vec3" },
{ "name": "fov", "type": "float" },
{ "name": "roll", "type": "float" },
{ "name": "pitch", "type": "float" },
{ "name": "yaw", "type": "float" }
],
"return": {
"fields": [
{ "name": "direction", "type": "Vec3" },
{ "name": "up", "type": "Vec3" }
],
"c_type": "DrawCameraFromPrincipalAxesResult"
}
},
"draw_skybox": {
"module": "draw",
"symbol": "skybox",
"header": "twn_draw.h",
"params": [
{ "name": "textures", "type": "char *", "default": {} }
]
},
"audio_play": {
"module": "audio",
"symbol": "play",
"header": "twn_audio.h",
"params": [
{ "name": "audio", "type": "char *" },
{ "name": "channel", "type": "char *", "default": {} },
{ "name": "repeat", "type": "bool", "default": false },
{ "name": "volume", "type": "float", "default": 1.0 },
{ "name": "panning", "type": "float", "default": 0.0 }
]
},
"audio_parameter": {
"module": "audio",
"symbol": "parameter",
"header": "twn_audio.h",
"params": [
{ "name": "channel", "type": "char *" },
{ "name": "parameter", "type": "char *" },
{ "name": "value", "type": "float" }
]
}
},
"types": {
"Vec2": {
"fields": [
{ "name": "x", "type": "float" },
{ "name": "y", "type": "float" }
],
"c_type": "Vec2"
},
"Vec3": {
"fields": [
{ "name": "x", "type": "float" },
{ "name": "y", "type": "float" },
{ "name": "z", "type": "float" }
],
"c_type": "Vec3"
},
"Color": {
"fields": [
{ "name": "r", "type": "uint8_t" },
{ "name": "g", "type": "uint8_t" },
{ "name": "b", "type": "uint8_t" },
{ "name": "a", "type": "uint8_t" }
],
"c_type": "Color"
},
"Rect": {
"fields": [
{ "name": "x", "type": "float" },
{ "name": "y", "type": "float" },
{ "name": "w", "type": "float" },
{ "name": "h", "type": "float" }
],
"c_type": "Rect"
},
"Context": {
"fields": [
{ "name": "frame_number", "type": "float" },
{ "name": "frame_duration", "type": "float" },
{ "name": "fog_density", "type": "float" },
{ "name": "fog_color", "type": "Color" },
{ "name": "resolution", "type": "Vec2" },
{ "name": "mouse_position", "type": "Vec2" },
{ "name": "mouse_movement", "type": "Vec2" },
{ "name": "random_seed", "type": "float" },
{ "name": "debug", "type": "bool" },
{ "name": "initialization_needed", "type": "bool" },
{ "name": "mouse_capture", "type": "bool" }
],
"c_type": "Context"
},
"Control": {
"enums": {
"A": 4,
"B": 5,
"C": 6,
"D": 7,
"E": 8,
"F": 9,
"G": 10,
"H": 11,
"I": 12,
"J": 13,
"K": 14,
"L": 15,
"M": 16,
"N": 17,
"O": 18,
"P": 19,
"Q": 20,
"R": 21,
"S": 22,
"T": 23,
"U": 24,
"V": 25,
"W": 26,
"X": 27,
"Y": 28,
"Z": 29,
"1": 30,
"2": 31,
"3": 32,
"4": 33,
"5": 34,
"6": 35,
"7": 36,
"8": 37,
"9": 38,
"0": 39,
"RETURN": 40,
"ESCAPE": 41,
"BACKSPACE": 42,
"TAB": 43,
"SPACE": 44,
"MINUS": 45,
"EQUALS": 46,
"LEFTBRACKET": 47,
"RIGHTBRACKET": 48,
"BACKSLASH": 49,
"NONUSHASH": 50,
"SEMICOLON": 51,
"APOSTROPHE": 52,
"GRAVE": 53,
"COMMA": 54,
"PERIOD": 55,
"SLASH": 56,
"CAPSLOCK": 57,
"F1": 58,
"F2": 59,
"F3": 60,
"F4": 61,
"F5": 62,
"F6": 63,
"F7": 64,
"F8": 65,
"F9": 66,
"F10": 67,
"F11": 68,
"F12": 69,
"PRINTSCREEN": 70,
"SCROLLLOCK": 71,
"PAUSE": 72,
"INSERT": 73,
"HOME": 74,
"PAGEUP": 75,
"DELETE": 76,
"END": 77,
"PAGEDOWN": 78,
"RIGHT": 79,
"LEFT": 80,
"DOWN": 81,
"UP": 82,
"NUMLOCKCLEAR": 83,
"KP_DIVIDE": 84,
"KP_MULTIPLY": 85,
"KP_MINUS": 86,
"KP_PLUS": 87,
"KP_ENTER": 88,
"KP_1": 89,
"KP_2": 90,
"KP_3": 91,
"KP_4": 92,
"KP_5": 93,
"KP_6": 94,
"KP_7": 95,
"KP_8": 96,
"KP_9": 97,
"KP_0": 98,
"KP_PERIOD": 99,
"NONUSBACKSLASH": 100,
"APPLICATION": 101,
"POWER": 102,
"KP_EQUALS": 103,
"F13": 104,
"F14": 105,
"F15": 106,
"F16": 107,
"F17": 108,
"F18": 109,
"F19": 110,
"F20": 111,
"F21": 112,
"F22": 113,
"F23": 114,
"F24": 115,
"EXECUTE": 116,
"HELP": 117,
"MENU": 118,
"SELECT": 119,
"STOP": 120,
"AGAIN": 121,
"UNDO": 122,
"CUT": 123,
"COPY": 124,
"PASTE": 125,
"FIND": 126,
"MUTE": 127,
"VOLUMEUP": 128,
"VOLUMEDOWN": 129,
"KP_COMMA": 133,
"KP_EQUALSAS400": 134,
"INTERNATIONAL1": 135,
"INTERNATIONAL2": 136,
"INTERNATIONAL3": 137,
"INTERNATIONAL4": 138,
"INTERNATIONAL5": 139,
"INTERNATIONAL6": 140,
"INTERNATIONAL7": 141,
"INTERNATIONAL8": 142,
"INTERNATIONAL9": 143,
"LANG1": 144,
"LANG2": 145,
"LANG3": 146,
"LANG4": 147,
"LANG5": 148,
"LANG6": 149,
"LANG7": 150,
"LANG8": 151,
"LANG9": 152,
"ALTERASE": 153,
"SYSREQ": 154,
"CANCEL": 155,
"CLEAR": 156,
"PRIOR": 157,
"RETURN2": 158,
"SEPARATOR": 159,
"OUT": 160,
"OPER": 161,
"CLEARAGAIN": 162,
"CRSEL": 163,
"EXSEL": 164,
"KP_00": 176,
"KP_000": 177,
"THOUSANDSSEPARATOR": 178,
"DECIMALSEPARATOR": 179,
"CURRENCYUNIT": 180,
"CURRENCYSUBUNIT": 181,
"KP_LEFTPAREN": 182,
"KP_RIGHTPAREN": 183,
"KP_LEFTBRACE": 184,
"KP_RIGHTBRACE": 185,
"KP_TAB": 186,
"KP_BACKSPACE": 187,
"KP_A": 188,
"KP_B": 189,
"KP_C": 190,
"KP_D": 191,
"KP_E": 192,
"KP_F": 193,
"KP_XOR": 194,
"KP_POWER": 195,
"KP_PERCENT": 196,
"KP_LESS": 197,
"KP_GREATER": 198,
"KP_AMPERSAND": 199,
"KP_DBLAMPERSAND": 200,
"KP_VERTICALBAR": 201,
"KP_DBLVERTICALBAR": 202,
"KP_COLON": 203,
"KP_HASH": 204,
"KP_SPACE": 205,
"KP_AT": 206,
"KP_EXCLAM": 207,
"KP_MEMSTORE": 208,
"KP_MEMRECALL": 209,
"KP_MEMCLEAR": 210,
"KP_MEMADD": 211,
"KP_MEMSUBTRACT": 212,
"KP_MEMMULTIPLY": 213,
"KP_MEMDIVIDE": 214,
"KP_PLUSMINUS": 215,
"KP_CLEAR": 216,
"KP_CLEARENTRY": 217,
"KP_BINARY": 218,
"KP_OCTAL": 219,
"KP_DECIMAL": 220,
"KP_HEXADECIMAL": 221,
"LCTRL": 224,
"LSHIFT": 225,
"LALT": 226,
"LGUI": 227,
"RCTRL": 228,
"RSHIFT": 229,
"RALT": 230,
"RGUI": 231,
"MODE": 257,
"KBDILLUMTOGGLE": 278,
"KBDILLUMDOWN": 279,
"KBDILLUMUP": 280,
"EJECT": 281,
"SLEEP": 282,
"APP1": 283,
"APP2": 284,
"AUDIOREWIND": 285,
"AUDIOFASTFORWARD": 286,
"SOFTLEFT": 287,
"SOFTRIGHT": 288,
"CALL": 289,
"ENDCALL": 290,
"LEFT_MOUSE": 513,
"RIGHT_MOUSE": 515,
"MIDDLE_MOUSE": 514,
"X1": 516,
"X2": 517
}
}
}
}

View File

@ -1,6 +1,7 @@
#include "twn_game_object_c.h"
#include "twn_engine_context_c.h"
#include "twn_util_c.h"
#include "twn_util.h"
#include <x-watcher.h>
#include <SDL2/SDL.h>
@ -18,7 +19,7 @@ static void (*game_end_callback)(void);
static x_watcher *watcher;
static void *handle = NULL;
static uint64_t last_tick_modified;
static float last_tick_modified;
static bool loaded_after_modification = true;
static SDL_mutex *lock;
@ -58,7 +59,7 @@ static void load_game_object(void) {
handle = new_handle;
if (ctx.game.frame_number != 0)
if (fabsf(0.0f - ctx.game.frame_number) > 0.00001f)
log_info("Game object was reloaded\n");
return;

View File

@ -1,6 +1,7 @@
#include "twn_game_object_c.h"
#include "twn_engine_context_c.h"
#include "twn_util_c.h"
#include "twn_util.h"
#include <errhandlingapi.h>
#include <libloaderapi.h>

View File

@ -0,0 +1,185 @@
#include "twn_draw.h"
#include "twn_draw_c.h"
#include "twn_engine_context_c.h"
#include "twn_textures_c.h"
#include "twn_types.h"
#include "twn_util_c.h"
#include "twn_vec.h"
#include <stb_ds.h>
void draw_billboard(const char *texture,
Vec3 position,
Vec2 size,
Color color,
bool cylindrical)
{
// TODO: order drawing by atlas id as well, so that texture rebinding is not as common
const TextureKey texture_key = textures_get_key(&ctx.texture_cache, texture);
struct MeshBatchItem *batch_p = hmgetp_null(ctx.billboard_batches, texture_key);
if (!batch_p) {
struct MeshBatch item = {0};
hmput(ctx.billboard_batches, texture_key, item);
batch_p = &ctx.billboard_batches[hmlenu(ctx.billboard_batches) - 1]; /* TODO: can last index be used? */
}
struct SpaceBillboard billboard = {
.color = color,
.cylindrical = cylindrical,
.position = position,
.size = size,
};
struct SpaceBillboard *billboards = (struct SpaceBillboard *)(void *)batch_p->value.primitives;
arrpush(billboards, billboard);
batch_p->value.primitives = (uint8_t *)billboards;
}
/* reused for all billboards */
static Vec3 right_plus_up;
static Vec3 right_minus_up;
static Vec3 right_plus_up_cylindrical;
static Vec3 right_minus_up_cylindrical;
/* precalculate (right + up) and (right - up) that are used with all batches */
static void calculate_intermediates(void) {
Vec3 const right = { camera_look_at_matrix.row[0].x, camera_look_at_matrix.row[1].x, camera_look_at_matrix.row[2].x };
Vec3 const up = { camera_look_at_matrix.row[0].y, camera_look_at_matrix.row[1].y, camera_look_at_matrix.row[2].y };
right_plus_up = m_vec_add(right, up);
right_minus_up = m_vec_sub(right, up);
Vec3 const up_cylindrical = { 0, 1, 0 };
right_plus_up_cylindrical = m_vec_add(right, up_cylindrical);
right_minus_up_cylindrical = m_vec_sub(right, up_cylindrical);
}
/* http://www.lighthouse3d.com/opengl/billboarding/index.php?billCheat2 */
void finally_draw_billboard_batch(struct MeshBatch const *batch,
TextureKey texture_key)
{
const size_t primitives_len = arrlenu(batch->primitives);
/* nothing to do */
if (primitives_len == 0)
return;
/* TODO: only do it once per frame */
calculate_intermediates();
const Rect srcrect = textures_get_srcrect(&ctx.texture_cache, texture_key);
const Rect dims = textures_get_dims(&ctx.texture_cache, texture_key);
const float wr = srcrect.w / dims.w;
const float hr = srcrect.h / dims.h;
const float xr = srcrect.x / dims.w;
const float yr = srcrect.y / dims.h;
const Vec2 uv0 = { xr + wr, yr };
const Vec2 uv1 = { xr + wr, yr + hr };
const Vec2 uv2 = { xr, yr + hr };
const Vec2 uv3 = { xr, yr };
for (size_t batch_n = 0; batch_n <= (primitives_len - 1) / QUAD_ELEMENT_BUFFER_LENGTH; batch_n++) {
/* emit vertex data */
VertexBuffer const buffer = get_scratch_vertex_array();
VertexBufferBuilder builder = build_vertex_buffer(
buffer,
sizeof (ElementIndexedBillboard) * MIN(primitives_len - batch_n * QUAD_ELEMENT_BUFFER_LENGTH, QUAD_ELEMENT_BUFFER_LENGTH));
for (size_t i = 0; i < primitives_len; ++i) {
struct SpaceBillboard const billboard = ((SpaceBillboard *)(void *)batch->primitives)[batch_n * QUAD_ELEMENT_BUFFER_LENGTH + i];
/* a = (right + up) * size, b = (right - up) * size*/
Vec3 a, b;
if (billboard.cylindrical) {
a = vec3_mul(right_plus_up_cylindrical, ((Vec3){billboard.size.x, billboard.size.y, billboard.size.x }));
b = vec3_mul(right_minus_up_cylindrical, ((Vec3){billboard.size.x, billboard.size.y, billboard.size.x }));
} else {
a = vec3_mul(right_plus_up, ((Vec3){billboard.size.x, billboard.size.y, billboard.size.x }));
b = vec3_mul(right_minus_up, ((Vec3){billboard.size.x, billboard.size.y, billboard.size.x }));
}
struct ElementIndexedBillboard const payload = {
/* flat shading is assumed, so we can skip setting the duplicates */
.c0 = billboard.color,
// .c1 = billboard.color,
.c2 = billboard.color,
// .c3 = billboard.color,
.uv0 = uv0,
.uv1 = uv1,
.uv2 = uv2,
.uv3 = uv3,
.v0 = vec3_sub(billboard.position, b),
.v1 = vec3_sub(billboard.position, a),
.v2 = vec3_add(billboard.position, b),
.v3 = vec3_add(billboard.position, a),
};
((struct ElementIndexedBillboard *)builder.base)[i] = payload;
}
finish_vertex_builder(&builder);
/* commit to drawing */
DeferredCommandDraw command = {0};
command.vertices = (AttributeArrayPointer) {
.arity = 3,
.type = TWN_FLOAT,
.stride = offsetof(ElementIndexedBillboard, v1),
.offset = offsetof(ElementIndexedBillboard, v0),
.buffer = buffer
};
command.texcoords = (AttributeArrayPointer) {
.arity = 2,
.type = TWN_FLOAT,
.stride = offsetof(ElementIndexedBillboard, v1),
.offset = offsetof(ElementIndexedBillboard, uv0),
.buffer = buffer
};
command.colors = (AttributeArrayPointer) {
.arity = 4,
.type = TWN_UNSIGNED_BYTE,
.stride = offsetof(ElementIndexedBillboard, v1),
.offset = offsetof(ElementIndexedBillboard, c0),
.buffer = buffer
};
command.textured = true;
command.texture_key = texture_key;
command.element_buffer = get_quad_element_buffer();
command.element_count = 6 * (uint32_t)primitives_len;
command.range_end = 6 * (uint32_t)primitives_len;
/* TODO: support alpha blended case, with distance sort */
TextureMode mode = textures_get_mode(&ctx.texture_cache, texture_key);
if (mode == TEXTURE_MODE_GHOSTLY)
mode = TEXTURE_MODE_SEETHROUGH;
command.texture_mode = mode;
command.pipeline = PIPELINE_SPACE;
command.depth_range_high = depth_range_high;
command.depth_range_low = depth_range_low;
DeferredCommand final_command = {
.type = DEFERRED_COMMAND_TYPE_DRAW,
.draw = command
};
arrpush(deferred_commands, final_command);
}
}

View File

@ -1,6 +1,7 @@
#include "twn_engine_context_c.h"
#include "twn_draw_c.h"
#include "twn_draw.h"
#include "twn_util_c.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
@ -27,8 +28,10 @@ void create_circle_geometry(Vec2 position,
size_t num_vertices,
Vec2 vertices[])
{
SDL_assert(num_vertices <= CIRCLE_VERTICES_MAX);
/* the angle (in radians) to rotate by on each iteration */
float seg_rotation_angle = (360.0f / (float)num_vertices) * ((float)M_PI / 180);
float seg_rotation_angle = (360.0f / (float)(num_vertices - 2)) * ((float)M_PI / 180);
vertices[0].x = (float)position.x;
vertices[0].y = (float)position.y;
@ -37,7 +40,7 @@ void create_circle_geometry(Vec2 position,
float start_x = 0.0f - radius;
float start_y = 0.0f;
for (size_t i = 1; i < num_vertices + 1; ++i) {
for (size_t i = 1; i < num_vertices - 1; ++i) {
float final_seg_rotation_angle = (float)i * seg_rotation_angle;
float c, s;
@ -49,4 +52,76 @@ void create_circle_geometry(Vec2 position,
vertices[i].x += position.x;
vertices[i].y += position.y;
}
// place a redundant vertex to make proper circling over shared element buffer
{
float final_seg_rotation_angle = (float)1 * seg_rotation_angle;
float c, s;
sincosf(final_seg_rotation_angle, &s, &c);
vertices[num_vertices - 1].x = c * start_x - s * start_y;
vertices[num_vertices - 1].y = c * start_y + s * start_x;
vertices[num_vertices - 1].x += position.x;
vertices[num_vertices - 1].y += position.y;
}
}
void render_circle(const CirclePrimitive *circle) {
static Vec2 vertices[CIRCLE_VERTICES_MAX];
static int prev_num_vertices = 0;
static Vec2 prev_position = {0};
int const num_vertices = MIN((int)circle->radius, CIRCLE_VERTICES_MAX);
if (prev_num_vertices != num_vertices) {
create_circle_geometry(circle->position,
circle->radius,
num_vertices,
vertices);
prev_num_vertices = num_vertices;
prev_position = circle->position;
} else {
/* reuse the data, but offset it by difference with previously generated position */
/* no evil cos sin ops this way, if radius is shared in sequential calls */
Vec2 const d = { prev_position.x - circle->position.x, prev_position.y - circle->position.y };
for (int i = 0; i < num_vertices; ++i)
vertices[i] = (Vec2){ vertices[i].x - d.x, vertices[i].y - d.y };
prev_position = circle->position;
}
VertexBuffer buffer = get_scratch_vertex_array();
specify_vertex_buffer(buffer, vertices, sizeof (Vec2) * num_vertices);
DeferredCommandDraw command = {0};
command.vertices = (AttributeArrayPointer) {
.arity = 2,
.type = TWN_FLOAT,
.stride = sizeof (Vec2),
.offset = 0,
.buffer = buffer
};
command.constant_colored = true;
command.color = circle->color;
command.element_buffer = get_circle_element_buffer();
command.element_count = (num_vertices - 2) * 3;
command.range_end = (num_vertices - 2) * 3;
command.texture_mode = circle->color.a == 255 ? TEXTURE_MODE_OPAQUE : TEXTURE_MODE_GHOSTLY;
command.pipeline = PIPELINE_2D;
command.depth_range_high = depth_range_high;
command.depth_range_low = depth_range_low;
DeferredCommand final_command = {
.type = DEFERRED_COMMAND_TYPE_DRAW,
.draw = command
};
arrpush(deferred_commands, final_command);
}

View File

@ -0,0 +1,87 @@
#ifndef TWN_DEFERRED_COMMANDS_H
#define TWN_DEFERRED_COMMANDS_H
#include "twn_types.h"
#include "twn_gpu_texture_c.h"
#include "twn_textures_c.h"
#include <stddef.h>
#include <stdbool.h>
typedef enum {
PIPELINE_NO,
PIPELINE_SPACE,
PIPELINE_2D, /* TODO: rename to PIPELINE_PLANE? */
} Pipeline;
typedef struct {
size_t offset;
uint32_t type;
uint32_t stride;
uint32_t buffer;
uint8_t arity; /* leave at 0 to signal pointer as unused */
} AttributeArrayPointer;
/* allows us to have generic way to issue draws as well as */
/* deferring new draw calls while previous frame is still being drawn */
typedef struct {
AttributeArrayPointer vertices;
AttributeArrayPointer texcoords;
union {
AttributeArrayPointer colors;
Color color;
};
double depth_range_low, depth_range_high;
Pipeline pipeline;
TextureMode texture_mode;
TextureKey texture_key;
GPUTexture gpu_texture;
/* could be either `element_count` with supplied `element_buffer`, or this, but not both */
uint32_t primitive_count;
uint32_t element_buffer;
uint32_t element_count;
uint32_t range_start, range_end;
bool constant_colored;
bool textured, texture_repeat, uses_gpu_key;
} DeferredCommandDraw;
typedef struct {
char *paths;
} DeferredCommandDrawSkybox;
typedef struct {
Color color;
bool clear_color;
bool clear_depth;
bool clear_stencil;
} DeferredCommandClear;
typedef struct {
enum DeferredCommandType {
DEFERRED_COMMAND_TYPE_DRAW,
DEFERRED_COMMAND_TYPE_DRAW_SKYBOX,
DEFERRED_COMMAND_TYPE_CLEAR,
} type;
union {
DeferredCommandDraw draw;
DeferredCommandDrawSkybox draw_skybox;
DeferredCommandClear clear;
};
} DeferredCommand;
extern DeferredCommand *deferred_commands;
#endif

View File

@ -1,26 +1,29 @@
#include "twn_draw_c.h"
#include "twn_draw.h"
#include "twn_engine_context_c.h"
#include "twn_camera.h"
#include "twn_camera_c.h"
#include "twn_types.h"
#include "twn_util.h"
#include "twn_vec.h"
#include "twn_deferred_commands.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
#ifdef EMSCRIPTEN
#include <GLES2/gl2.h>
#else
#include <glad/glad.h>
#endif
#include <stddef.h>
#include <math.h>
#include <tgmath.h>
DeferredCommand *deferred_commands;
/* TODO: have a default initialized one */
/* TODO: with buffered render, don't we use camera of wrong frame right now ? */
Matrix4 camera_projection_matrix;
Matrix4 camera_look_at_matrix;
double depth_range_low, depth_range_high;
void render_queue_clear(void) {
text_cache_reset_arena(&ctx.text_cache);
@ -30,13 +33,17 @@ void render_queue_clear(void) {
/* and start overwriting the existing data */
arrsetlen(ctx.render_queue_2d, 0);
/* TODO: free memory if it isn't used for a while */
for (size_t i = 0; i < hmlenu(ctx.uncolored_mesh_batches); ++i)
arrsetlen(ctx.uncolored_mesh_batches[i].value.primitives, 0);
for (size_t i = 0; i < hmlenu(ctx.billboard_batches); ++i)
arrsetlen(ctx.billboard_batches[i].value.primitives, 0);
}
void draw_9slice(const char *texture_path, int texture_w, int texture_h, int border_thickness, Rect rect, Color color) {
const float bt = (float)border_thickness; /* i know! */
void draw_nine_slice(const char *texture, Vec2 corners, Rect rect, float border_thickness, Color color) {
const float bt = border_thickness;
const float bt2 = bt * 2; /* combined size of the two borders in an axis */
@ -48,7 +55,7 @@ void draw_9slice(const char *texture_path, int texture_w, int texture_h, int bor
};
m_sprite(
m_set(path, texture_path),
m_set(texture, texture),
m_set(rect, top_left),
m_opt(texture_region, ((Rect) { 0, 0, bt, bt })),
m_opt(color, color),
@ -63,9 +70,9 @@ void draw_9slice(const char *texture_path, int texture_w, int texture_h, int bor
};
m_sprite(
m_set(path, texture_path),
m_set(texture, texture),
m_set(rect, top_center),
m_opt(texture_region, ((Rect) { bt, 0, (float)texture_w - bt2, bt })),
m_opt(texture_region, ((Rect) { bt, 0, corners.x - bt2, bt })),
m_opt(color, color),
);
@ -78,9 +85,9 @@ void draw_9slice(const char *texture_path, int texture_w, int texture_h, int bor
};
m_sprite(
m_set(path, texture_path),
m_set(texture, texture),
m_set(rect, top_right),
m_opt(texture_region, ((Rect) { (float)texture_w - bt, 0, bt, bt })),
m_opt(texture_region, ((Rect) { corners.x - bt, 0, bt, bt })),
m_opt(color, color),
);
@ -93,9 +100,9 @@ void draw_9slice(const char *texture_path, int texture_w, int texture_h, int bor
};
m_sprite(
m_set(path, texture_path),
m_set(texture, texture),
m_set(rect, center_left),
m_opt(texture_region, ((Rect) { 0, bt, bt, (float)texture_h - bt2 })),
m_opt(texture_region, ((Rect) { 0, bt, bt, corners.y - bt2 })),
m_opt(color, color),
);
@ -108,9 +115,9 @@ void draw_9slice(const char *texture_path, int texture_w, int texture_h, int bor
};
m_sprite(
m_set(path, texture_path),
m_set(texture, texture),
m_set(rect, center_right),
m_opt(texture_region, ((Rect) { (float)texture_w - bt, bt, bt, (float)texture_h - bt2 })),
m_opt(texture_region, ((Rect) { corners.x - bt, bt, bt, corners.y - bt2 })),
m_opt(color, color),
);
@ -123,9 +130,9 @@ void draw_9slice(const char *texture_path, int texture_w, int texture_h, int bor
};
m_sprite(
m_set(path, texture_path),
m_set(texture, texture),
m_set(rect, bottom_left),
m_opt(texture_region, ((Rect) { 0, (float)texture_h - bt, bt, bt })),
m_opt(texture_region, ((Rect) { 0, corners.y - bt, bt, bt })),
m_opt(color, color),
);
@ -138,9 +145,9 @@ void draw_9slice(const char *texture_path, int texture_w, int texture_h, int bor
};
m_sprite(
m_set(path, texture_path),
m_set(texture, texture),
m_set(rect, bottom_center),
m_opt(texture_region, ((Rect) { bt, (float)texture_h - bt, (float)texture_w - bt2, bt })),
m_opt(texture_region, ((Rect) { bt, corners.y - bt, corners.x - bt2, bt })),
m_opt(color, color),
);
@ -153,9 +160,9 @@ void draw_9slice(const char *texture_path, int texture_w, int texture_h, int bor
};
m_sprite(
m_set(path, texture_path),
m_set(texture, texture),
m_set(rect, bottom_right),
m_opt(texture_region, ((Rect) { (float)texture_w - bt, (float)texture_h - bt, bt, bt })),
m_opt(texture_region, ((Rect) { corners.x - bt, corners.y - bt, bt, bt })),
m_opt(color, color),
);
@ -168,17 +175,40 @@ void draw_9slice(const char *texture_path, int texture_w, int texture_h, int bor
};
m_sprite(
m_set(path, texture_path),
m_set(texture, texture),
m_set(rect, center),
m_opt(texture_region, ((Rect) { bt, bt, (float)texture_w - bt2, (float)texture_h - bt2 })),
m_opt(texture_region, ((Rect) { bt, bt, corners.x - bt2, corners.y - bt2 })),
m_opt(color, color),
);
}
static void render_2d(void) {
use_2d_pipeline();
TWN_API void draw_quad(char const *texture,
Vec3 v0, /* upper-left */
Vec3 v1, /* bottom-left */
Vec3 v2, /* bottom-right */
Vec3 v3, /* upper-right */
Rect texture_region,
Color color)
{
Vec2 const uv0 = { texture_region.x, texture_region.y };
Vec2 const uv1 = { texture_region.x, texture_region.y + texture_region.h };
Vec2 const uv2 = { texture_region.x + texture_region.w, texture_region.y + texture_region.h };
Vec2 const uv3 = { texture_region.x + texture_region.w, texture_region.y };
draw_triangle(texture,
v0, v1, v3,
uv0, uv1, uv3,
color, color, color);
draw_triangle(texture,
v3, v1, v2,
uv3, uv1, uv2,
color, color, color);
}
static void render_2d(void) {
const size_t render_queue_len = arrlenu(ctx.render_queue_2d);
struct Render2DInvocation {
@ -255,6 +285,20 @@ static void render_2d(void) {
break;
}
/* TODO: batching */
case PRIMITIVE_2D_LINE: {
struct Render2DInvocation const invocation = {
.primitive = current,
.layer = layer,
};
if (current->line.color.a != 255)
arrput(ghostly_invocations, invocation);
else
arrput(opaque_invocations, invocation);
break;
}
case PRIMITIVE_2D_TEXT: {
struct Render2DInvocation const invocation = {
.primitive = current,
@ -291,6 +335,9 @@ static void render_2d(void) {
case PRIMITIVE_2D_CIRCLE:
render_circle(&invocation.primitive->circle);
break;
case PRIMITIVE_2D_LINE:
render_line(&invocation.primitive->line);
break;
case PRIMITIVE_2D_TEXT:
default:
SDL_assert(false);
@ -320,6 +367,9 @@ static void render_2d(void) {
case PRIMITIVE_2D_TEXT:
render_text(&invocation.primitive->text);
break;
case PRIMITIVE_2D_LINE:
render_line(&invocation.primitive->line);
break;
default:
SDL_assert(false);
}
@ -333,18 +383,18 @@ static void render_2d(void) {
static void render_space(void) {
/* nothing to do, abort */
/* as space pipeline isn't used we can have fewer changes and initialization costs */
if (hmlenu(ctx.uncolored_mesh_batches) == 0)
return;
if (hmlenu(ctx.uncolored_mesh_batches) != 0 || hmlenu(ctx.billboard_batches) != 0) {
for (size_t i = 0; i < hmlenu(ctx.uncolored_mesh_batches); ++i) {
finally_draw_uncolored_space_traingle_batch(&ctx.uncolored_mesh_batches[i].value,
ctx.uncolored_mesh_batches[i].key);
}
use_space_pipeline();
apply_fog();
for (size_t i = 0; i < hmlenu(ctx.uncolored_mesh_batches); ++i) {
draw_uncolored_space_traingle_batch(&ctx.uncolored_mesh_batches[i].value,
ctx.uncolored_mesh_batches[i].key);
for (size_t i = 0; i < hmlenu(ctx.billboard_batches); ++i) {
finally_draw_billboard_batch(&ctx.billboard_batches[i].value, ctx.billboard_batches[i].key);
}
}
pop_fog();
render_skybox(); /* after everything else, as to use depth buffer for early z rejection */
}
@ -353,37 +403,143 @@ void render(void) {
/* fit rendering context onto the resizable screen */
if (ctx.window_size_has_changed) {
if ((float)ctx.window_dims.x / (float)ctx.window_dims.y > (float)ctx.base_render_width / (float)ctx.base_render_height) {
float ratio = (float)ctx.window_dims.y / (float)ctx.base_render_height;
int w = (int)((float)ctx.base_render_width * ratio);
setup_viewport(
ctx.window_dims.x / 2 - w / 2,
0,
w,
ctx.window_dims.y
);
} else {
float ratio = (float)ctx.window_dims.x / (float)ctx.base_render_width;
int h = (int)((float)ctx.base_render_height * ratio);
setup_viewport(
0,
ctx.window_dims.y / 2 - h / 2,
ctx.window_dims.x,
h
);
}
setup_viewport((int)ctx.viewport_rect.x, (int)ctx.viewport_rect.y, (int)ctx.viewport_rect.w, (int)ctx.viewport_rect.h);
}
start_render_frame(); {
render_space();
render_skybox(); /* after space, as to use depth buffer for early z rejection */
render_2d();
} end_render_frame();
}
void draw_camera(const Camera *const camera) {
/* TODO: skip recaulculating if it's the same? */
camera_projection_matrix = camera_perspective(camera);
camera_look_at_matrix = camera_look_at(camera);
void draw_camera(Vec3 position, float fov, Vec3 up, Vec3 direction) {
if (fabsf(0.0f - fov) < 0.00001f || fov >= M_PIf)
log_warn("Invalid fov given (%f)", (double)fov);
Camera const camera = {
.fov = fov,
.pos = position,
.target = direction,
.up = up,
};
camera_projection_matrix = camera_perspective(&camera);
camera_look_at_matrix = camera_look_at(&camera);
}
/* TODO: https://stackoverflow.com/questions/62493770/how-to-add-roll-in-camera-class */
DrawCameraFromPrincipalAxesResult draw_camera_from_principal_axes(Vec3 position, float fov, float roll, float pitch, float yaw) {
if (fabsf(0.0f - fov) < 0.00001f || fov >= M_PIf)
log_warn("Invalid fov given (%f)", (double)fov);
(void)roll;
float yawc, yaws, pitchc, pitchs;
sincosf(yaw, &yaws, &yawc);
sincosf(pitch, &pitchs, &pitchc);
Camera const camera = {
.fov = fov,
.pos = position,
.target = m_vec_norm(((Vec3){
yawc * pitchc,
pitchs,
yaws * pitchc,
})),
.up = (Vec3){0, 1, 0},
};
camera_projection_matrix = camera_perspective(&camera);
camera_look_at_matrix = camera_look_at(&camera);
return (DrawCameraFromPrincipalAxesResult) {
.direction = camera.target,
.up = camera.up,
};
}
void set_depth_range(double low, double high) {
depth_range_low = low;
depth_range_high = high;
}
void clear_draw_buffer(void) {
/* TODO: we can optimize a rectangle drawn over whole window to a clear color call*/
DeferredCommand command = {
.type = DEFERRED_COMMAND_TYPE_CLEAR,
.clear = (DeferredCommandClear) {
.clear_color = true,
.clear_depth = true,
.clear_stencil = true,
.color = (Color) { 230, 230, 230, 1 }
}
};
arrpush(deferred_commands, command);
}
void issue_deferred_draw_commands(void) {
for (size_t i = 0; i < arrlenu(deferred_commands); ++i) {
switch (deferred_commands[i].type) {
case DEFERRED_COMMAND_TYPE_CLEAR: {
finally_clear_draw_buffer(deferred_commands[i].clear);
break;
}
case DEFERRED_COMMAND_TYPE_DRAW: {
finally_draw_command(deferred_commands[i].draw);
break;
}
case DEFERRED_COMMAND_TYPE_DRAW_SKYBOX: {
finally_render_skybox(deferred_commands[i].draw_skybox);
break;
}
default:
SDL_assert(false);
}
}
}
/* TODO: Support thickness */
void draw_line(Vec2 start,
Vec2 finish,
float thickness,
Color color)
{
if (fabsf(1.0f - thickness) >= 0.00001f)
log_warn("Thickness isn't yet implemented for line drawing (got %f)", (double)thickness);
LinePrimitive line = {
.start = start,
.finish = finish,
.thickness = thickness,
.color = color,
};
Primitive2D primitive = {
.type = PRIMITIVE_2D_LINE,
.line = line,
};
arrput(ctx.render_queue_2d, primitive);
}
void draw_box(Rect rect,
float thickness,
Color color)
{
draw_line((Vec2){rect.x, rect.y}, (Vec2){rect.x + rect.w, rect.y}, thickness, color);
draw_line((Vec2){rect.x + rect.w, rect.y}, (Vec2){rect.x + rect.w, rect.y + rect.h}, thickness, color);
draw_line((Vec2){rect.x + rect.w, rect.y + rect.h}, (Vec2){rect.x, rect.y + rect.h}, thickness, color);
draw_line((Vec2){rect.x, rect.y + rect.h}, (Vec2){rect.x, rect.y}, thickness, color);
}

View File

@ -1,36 +1,44 @@
#ifndef TWN_DRAW_C_H
#define TWN_DRAW_C_H
/* TODO: structure more categorically */
#include "twn_textures_c.h"
#include "twn_types_c.h"
#include "twn_text_c.h"
#include "twn_util.h"
#include "twn_option.h"
#include "twn_deferred_commands.h"
#include <SDL2/SDL.h>
#include <stb_truetype.h>
#ifdef EMSCRIPTEN
#include <GLES2/gl2.h>
#else
#include <glad/glad.h>
#endif
#include <stdbool.h>
extern Matrix4 camera_projection_matrix;
extern Matrix4 camera_look_at_matrix;
extern double depth_range_low, depth_range_high;
#define QUAD_ELEMENT_BUFFER_LENGTH (65536 / 6)
#define CIRCLE_VERTICES_MAX 2048
#define CIRCLE_ELEMENT_BUFFER_LENGTH (CIRCLE_VERTICES_MAX * 3)
/* TODO: limit to only most necessary */
enum {
TWN_FLOAT,
TWN_INT,
TWN_SHORT,
TWN_UNSIGNED_SHORT,
TWN_UNSIGNED_INT,
TWN_BYTE,
TWN_UNSIGNED_BYTE,
};
typedef GLuint VertexBuffer;
typedef uint32_t VertexBuffer;
typedef struct VertexBufferBuilder {
size_t bytes_left;
void *mapping;
size_t size;
void *base;
} VertexBufferBuilder;
@ -39,45 +47,55 @@ typedef struct SpritePrimitive {
Color color;
float rotation;
TextureKey texture_key;
bool flip_x;
bool flip_y;
bool repeat;
m_option_list(
Rect, texture_region )
bool flip_x;
bool flip_y;
bool repeat;
} SpritePrimitive;
typedef struct LinePrimitive {
Vec2 start;
Vec2 finish;
float thickness;
Color color;
} LinePrimitive;
typedef struct RectPrimitive {
Rect rect;
Color color;
} RectPrimitive;
typedef struct CirclePrimitive {
Vec2 position;
float radius;
Color color;
Vec2 position;
} CirclePrimitive;
typedef struct TextPrimitive {
Color color;
Vec2 position;
char *text;
const char *font;
Color color;
int height_px;
} TextPrimitive;
typedef enum Primitive2DType {
PRIMITIVE_2D_SPRITE,
PRIMITIVE_2D_LINE,
PRIMITIVE_2D_RECT,
PRIMITIVE_2D_CIRCLE,
PRIMITIVE_2D_TEXT,
} Primitive2DType;
typedef struct Primitive2D {
Primitive2DType type;
Primitive2DType type; /* TODO: separate to structure of arrays for more efficient memory usage? */
union {
SpritePrimitive sprite;
LinePrimitive line;
RectPrimitive rect;
CirclePrimitive circle;
TextPrimitive text;
@ -85,34 +103,30 @@ typedef struct Primitive2D {
} Primitive2D;
/* union for in-place recalculation of texture coordinates */
union UncoloredSpaceTriangle {
/* pending for sending, uvs are not final as texture atlases could update */
struct UncoloredSpaceTrianglePrimitive {
Vec3 v0;
Vec2 uv0; /* in pixels */
Vec3 v1;
Vec2 uv1; /* in pixels */
Vec3 v2;
Vec2 uv2; /* in pixels */
} primitive;
/* needs to be later resolved in texture atlas */
typedef struct UncoloredSpaceTriangle {
Vec3 v0;
Vec2 uv0; /* in pixels */
Vec3 v1;
Vec2 uv1; /* in pixels */
Vec3 v2;
Vec2 uv2; /* in pixels */
} UncoloredSpaceTriangle;
/* TODO: have it packed? */
/* structure that is passed in opengl vertex array */
struct UncoloredSpaceTrianglePayload {
Vec3 v0;
Vec2 uv0;
Vec3 v1;
Vec2 uv1;
Vec3 v2;
Vec2 uv2;
} payload;
};
typedef struct SpaceBillboard {
Vec3 position;
Vec2 size;
Color color;
// TextureKey texture; /* is assumed from other places */
bool cylindrical;
} SpaceBillboard;
/* batch of primitives with overlapping properties */
typedef struct MeshBatch {
uint8_t *primitives;
uint8_t *primitives; /* note: interpretation of it is arbitrary */
} MeshBatch;
/* TODO: use atlas id instead */
typedef struct MeshBatchItem {
TextureKey key;
struct MeshBatch value;
@ -123,6 +137,95 @@ typedef struct TextCache {
} TextCache;
/* TODO: try using the fact we utilize edge coloring and step virtual color attributes to bogus points */
/* this is only doable is we take out color attribute to separate array or a portion of it */
/* interleaved vertex array data */
typedef struct ElementIndexedQuad {
/* upper-left */
Vec2 v0;
Vec2 uv0;
Color c0;
/* bottom-left */
Vec2 v1;
Vec2 uv1;
Color c1;
/* bottom-right */
Vec2 v2;
Vec2 uv2;
Color c2;
/* upper-right */
Vec2 v3;
Vec2 uv3;
Color c3;
} ElementIndexedQuad;
typedef struct ElementIndexedQuadWithoutColor {
/* upper-left */
Vec2 v0;
Vec2 uv0;
/* bottom-left */
Vec2 v1;
Vec2 uv1;
/* bottom-right */
Vec2 v2;
Vec2 uv2;
/* upper-right */
Vec2 v3;
Vec2 uv3;
} ElementIndexedQuadWithoutColor;
typedef struct ElementIndexedQuadWithoutTexture {
/* upper-left */
Vec2 v0;
Color c0;
/* bottom-left */
Vec2 v1;
Color c1;
/* bottom-right */
Vec2 v2;
Color c2;
/* upper-right */
Vec2 v3;
Color c3;
} ElementIndexedQuadWithoutTexture;
typedef struct ElementIndexedQuadWithoutColorWithoutTexture {
/* upper-left */
Vec2 v0;
/* bottom-left */
Vec2 v1;
/* bottom-right */
Vec2 v2;
/* upper-right */
Vec2 v3;
} ElementIndexedQuadWithoutColorWithoutTexture;
/* TODO: no color variant */
typedef struct ElementIndexedBillboard {
/* upper-left */
Vec3 v0;
Vec2 uv0;
Color c0;
/* bottom-left */
Vec3 v1;
Vec2 uv1;
Color c1;
/* bottom-right */
Vec3 v2;
Vec2 uv2;
Color c2;
/* upper-right */
Vec3 v3;
Vec2 uv3;
Color c3;
} ElementIndexedBillboard;
bool render_init(void);
/* renders the background, then the primitives in all render queues */
void render(void);
@ -138,6 +241,7 @@ void create_circle_geometry(Vec2 position,
struct QuadBatch {
size_t size; /* how many primitives are in current batch */
TextureKey texture_key;
TextureMode mode; /* how color should be applied */
bool constant_colored; /* whether colored batch is uniformly colored */
bool repeat; /* whether repeat is needed */
@ -149,9 +253,6 @@ struct QuadBatch collect_rect_batch(const Primitive2D primitives[], size_t len);
void render_sprite_batch(const Primitive2D primitives[], struct QuadBatch batch);
void render_rect_batch(const Primitive2D primitives[], struct QuadBatch batch);
void draw_uncolored_space_traingle_batch(MeshBatch *batch,
TextureKey texture_key);
/* text */
void render_text(const TextPrimitive *text);
@ -166,6 +267,8 @@ void text_cache_reset_arena(TextCache *cache);
VertexBuffer create_vertex_buffer(void);
void restart_scratch_vertex_arrays(void);
VertexBuffer get_scratch_vertex_array(void);
void delete_vertex_buffer(VertexBuffer buffer);
@ -175,16 +278,14 @@ void specify_vertex_buffer(VertexBuffer buffer, void const *data, size_t bytes);
/* uses present in 1.5 buffer mapping feature */
VertexBufferBuilder build_vertex_buffer(VertexBuffer buffer, size_t bytes);
/* collects bytes for sending to the gpu until all is pushed, which is when false is returned */
bool push_to_vertex_buffer_builder(VertexBufferBuilder *builder,
void const *bytes,
size_t size);
void finish_vertex_builder(VertexBufferBuilder *builder);
/* state */
void setup_viewport(int x, int y, int width, int height);
void clear_draw_buffer(void);
void finally_clear_draw_buffer(DeferredCommandClear command);
void swap_buffers(void);
@ -196,35 +297,28 @@ VertexBuffer get_circle_element_buffer(void);
void render_circle(const CirclePrimitive *circle);
void render_line(const LinePrimitive *line);
void render_rectangle(const RectPrimitive *rectangle);
void use_space_pipeline(void);
void use_2d_pipeline(void);
void use_texture_mode(TextureMode mode);
void finally_render_quads(Primitive2D const primitives[],
struct QuadBatch batch,
VertexBuffer buffer);
size_t get_quad_payload_size(struct QuadBatch batch);
bool push_quad_payload_to_vertex_buffer_builder(struct QuadBatch batch,
void push_quad_payload_to_vertex_buffer_builder(struct QuadBatch batch,
size_t index,
VertexBufferBuilder *builder,
Vec2 v0, Vec2 v1, Vec2 v2, Vec2 v3,
Vec2 uv0, Vec2 uv1, Vec2 uv2, Vec2 uv3,
Color color);
void finally_draw_uncolored_space_traingle_batch(MeshBatch const *batch,
TextureKey texture_key,
VertexBuffer buffer);
TextureKey texture_key);
size_t get_text_payload_size(void);
bool push_text_payload_to_vertex_buffer_builder(FontData const *font_data,
VertexBufferBuilder *builder,
stbtt_aligned_quad quad);
void finally_draw_billboard_batch(MeshBatch const *batch,
TextureKey texture_key);
void finally_draw_text(FontData const *font_data,
size_t len,
@ -232,19 +326,13 @@ void finally_draw_text(FontData const *font_data,
VertexBuffer buffer);
void render_skybox(void);
void finally_render_skybox(char *paths_in_use);
void apply_fog(void);
void finally_apply_fog(float start, float end, float density, Color color);
void pop_fog(void);
void finally_pop_fog(void);
void finally_render_skybox(DeferredCommandDrawSkybox);
void start_render_frame(void);
void end_render_frame(void);
void finally_draw_command(DeferredCommandDraw command);
void issue_deferred_draw_commands(void);
#endif

View File

@ -1,34 +0,0 @@
#include "twn_draw.h"
#include "twn_draw_c.h"
#include "twn_util.h"
#include <stdbool.h>
static float start_cache, end_cache, density_cache;
static Color color_cache;
static bool fog_used = false;
void draw_fog(float start, float end, float density, Color color) {
start_cache = start;
end_cache = end;
density_cache = density;
color_cache = color;
fog_used = true;
}
void apply_fog(void) {
if (!fog_used)
return;
finally_apply_fog(start_cache, end_cache, density_cache, color_cache);
}
void pop_fog(void) {
if (!fog_used)
return;
finally_pop_fog();
}

View File

@ -1,71 +0,0 @@
#include "twn_gpu_texture_c.h"
#include "twn_util_c.h"
GPUTexture create_gpu_texture(TextureFilter filter, bool generate_mipmaps) {
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, (GLboolean)generate_mipmaps);
if (filter == TEXTURE_FILTER_NEAREAST) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
} else if (filter == TEXTURE_FILTER_LINEAR) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
#if !defined(EMSCRIPTEN)
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
#endif
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
void delete_gpu_texture(GPUTexture texture) {
glDeleteTextures(1, &texture);
}
void upload_gpu_texture(GPUTexture texture, void *pixels, int channels, int width, int height) {
glBindTexture(GL_TEXTURE_2D, texture);
int format_internal, format;
if (channels == 4) {
format_internal = GL_RGBA8;
format = GL_RGBA;
} else if (channels == 3) {
format_internal = GL_RGBA8;
format = GL_RGB;
} else if (channels == 1) {
format_internal = GL_ALPHA;
format = GL_ALPHA;
} else {
CRY("upload_gpu_texture", "Unsupported channel count");
return;
}
glTexImage2D(GL_TEXTURE_2D,
0,
format_internal,
width,
height,
0,
format,
GL_UNSIGNED_BYTE,
pixels);
glBindTexture(GL_TEXTURE_2D, 0);
}
void bind_gpu_texture(GPUTexture texture) {
glBindTexture(GL_TEXTURE_2D, texture);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,8 @@
#include "twn_draw_c.h"
#include "twn_engine_context_c.h"
#include "twn_util_c.h"
#include "twn_draw_c.h"
#include <stb_ds.h>
#ifdef EMSCRIPTEN
#include <GLES2/gl2.h>
@ -42,16 +44,15 @@ VertexBuffer get_quad_element_buffer(void) {
VertexBufferBuilder builder = build_vertex_buffer(buffer, sizeof (GLshort) * QUAD_ELEMENT_BUFFER_LENGTH * 6 );
for (size_t i = 0; i < QUAD_ELEMENT_BUFFER_LENGTH; ++i) {
GLshort indices[6];
indices[0] = (GLshort)(i * 4 + 0);
indices[1] = (GLshort)(i * 4 + 1);
indices[2] = (GLshort)(i * 4 + 2);
indices[3] = (GLshort)(i * 4 + 2);
indices[4] = (GLshort)(i * 4 + 3);
indices[5] = (GLshort)(i * 4 + 0);
push_to_vertex_buffer_builder(&builder, indices, sizeof indices);
((GLshort *)builder.base)[i * 6 + 0] = (GLshort)(i * 4 + 0);
((GLshort *)builder.base)[i * 6 + 1] = (GLshort)(i * 4 + 1);
((GLshort *)builder.base)[i * 6 + 2] = (GLshort)(i * 4 + 2);
((GLshort *)builder.base)[i * 6 + 3] = (GLshort)(i * 4 + 2);
((GLshort *)builder.base)[i * 6 + 4] = (GLshort)(i * 4 + 3);
((GLshort *)builder.base)[i * 6 + 5] = (GLshort)(i * 4 + 0);
}
finish_vertex_builder(&builder);
}
SDL_assert_always(buffer);
@ -65,27 +66,135 @@ VertexBuffer get_circle_element_buffer(void) {
if (buffer == 0) {
buffer = create_vertex_buffer();
VertexBufferBuilder builder = build_vertex_buffer(buffer, sizeof (GLshort) * CIRCLE_ELEMENT_BUFFER_LENGTH);
VertexBufferBuilder builder = build_vertex_buffer(buffer, sizeof (GLshort) * (CIRCLE_VERTICES_MAX - 2) * 3);
for (size_t i = 1; i < CIRCLE_VERTICES_MAX; ++i) {
for (size_t i = 1; i < CIRCLE_VERTICES_MAX - 1; ++i) {
/* first one is center point index, always zero */
GLshort indices[3];
indices[0] = 0;
((GLshort *)builder.base)[(i - 1) * 3 + 0] = 0;
/* generated point index */
indices[1] = (GLshort)i;
size_t index = (i + 1) % (CIRCLE_VERTICES_MAX - 1);
if (index == 0) /* don't use center for outer ring */
index = (CIRCLE_VERTICES_MAX - 1);
indices[2] = (GLshort)index;
push_to_vertex_buffer_builder(&builder, indices, sizeof indices);
((GLshort *)builder.base)[(i - 1) * 3 + 1] = (GLshort)i;
((GLshort *)builder.base)[(i - 1) * 3 + 2] = (GLshort)i + 1;
}
finish_vertex_builder(&builder);
}
SDL_assert_always(buffer);
return buffer;
}
/* potentially double buffered array of vertex array handles */
/* we assume they will be refilled fully each frame */
static size_t scratch_va_front_used, scratch_va_back_used;
static GLuint *front_scratch_vertex_arrays, *back_scratch_vertex_arrays;
static GLuint **current_scratch_vertex_array = &front_scratch_vertex_arrays;
void restart_scratch_vertex_arrays(void) {
scratch_va_front_used = 0;
scratch_va_back_used = 0;
if (ctx.render_double_buffered) {
current_scratch_vertex_array = current_scratch_vertex_array == &front_scratch_vertex_arrays ?
&back_scratch_vertex_arrays : &front_scratch_vertex_arrays;
}
}
GLuint get_scratch_vertex_array(void) {
size_t *used = current_scratch_vertex_array == &front_scratch_vertex_arrays ?
&scratch_va_front_used : &scratch_va_back_used;
if (arrlenu(*current_scratch_vertex_array) <= *used) {
GLuint handle;
glGenBuffers(1, &handle);
arrpush(*current_scratch_vertex_array, handle);
}
(*used)++;
return (*current_scratch_vertex_array)[*used - 1];
}
GPUTexture create_gpu_texture(TextureFilter filter, bool generate_mipmaps) {
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
#if !defined(EMSCRIPTEN)
glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, (GLboolean)generate_mipmaps);
#else
if (generate_mipmaps)
glGenerateMipmap(GL_TEXTURE_2D);
#endif
if (filter == TEXTURE_FILTER_NEAREAST) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
} else if (filter == TEXTURE_FILTER_LINEAR) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
#if !defined(EMSCRIPTEN)
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
#endif
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
void delete_gpu_texture(GPUTexture texture) {
glDeleteTextures(1, &texture);
}
void upload_gpu_texture(GPUTexture texture, void *pixels, int channels, int width, int height) {
glBindTexture(GL_TEXTURE_2D, texture);
int format_internal, format;
if (channels == 4) {
#ifdef EMSCRIPTEN
format_internal = GL_RGBA;
#else
format_internal = GL_RGBA8;
#endif
format = GL_RGBA;
} else if (channels == 3) {
#ifdef EMSCRIPTEN
format_internal = GL_RGBA;
#else
format_internal = GL_RGBA8;
#endif
format = GL_RGB;
} else if (channels == 1) {
format_internal = GL_ALPHA;
format = GL_ALPHA;
} else {
CRY("upload_gpu_texture", "Unsupported channel count");
return;
}
glTexImage2D(GL_TEXTURE_2D,
0,
format_internal,
width,
height,
0,
format,
GL_UNSIGNED_BYTE,
pixels);
glBindTexture(GL_TEXTURE_2D, 0);
}
void bind_gpu_texture(GPUTexture texture) {
glBindTexture(GL_TEXTURE_2D, texture);
}

View File

@ -2,8 +2,9 @@
#define TWN_GPU_TEXTURE_C_H
#include <stdbool.h>
#include <stdint.h>
typedef GLuint GPUTexture;
typedef uint32_t GPUTexture;
typedef enum TextureFilter {
TEXTURE_FILTER_NEAREAST,

View File

@ -1,30 +1,173 @@
#include "twn_draw_c.h"
#include <stb_ds.h>
#include <stddef.h>
struct QuadBatch collect_quad_batch(const Primitive2D primitives[], size_t len) {
if (primitives[0].type == PRIMITIVE_2D_SPRITE)
return collect_sprite_batch(primitives, len);
else if (primitives[0].type == PRIMITIVE_2D_RECT)
return collect_rect_batch(primitives, len);
else
SDL_assert(false);
return (struct QuadBatch){0};
}
/* assumes that orthogonal matrix setup is done already */
void render_quad_batch(const Primitive2D primitives[],
const struct QuadBatch batch)
void finally_render_quads(const Primitive2D primitives[],
const struct QuadBatch batch,
const VertexBuffer buffer)
{
if (primitives[0].type == PRIMITIVE_2D_SPRITE)
render_sprite_batch(primitives, batch);
else if (primitives[0].type == PRIMITIVE_2D_RECT)
render_rect_batch(primitives, batch);
else
SDL_assert(false);
DeferredCommandDraw command = {0};
return (struct QuadBatch){0};
uint32_t off = 0, voff = 0, uvoff = 0, coff = 0;
if (!batch.constant_colored && batch.textured) {
off = offsetof(ElementIndexedQuad, v1);
voff = offsetof(ElementIndexedQuad, v0);
uvoff = offsetof(ElementIndexedQuad, uv0);
coff = offsetof(ElementIndexedQuad, c0);
} else if (batch.constant_colored && batch.textured) {
off = offsetof(ElementIndexedQuadWithoutColor, v1);
voff = offsetof(ElementIndexedQuadWithoutColor, v0);
uvoff = offsetof(ElementIndexedQuadWithoutColor, uv0);
} else if (!batch.constant_colored && !batch.textured) {
off = offsetof(ElementIndexedQuadWithoutTexture, v1);
voff = offsetof(ElementIndexedQuadWithoutTexture, v0);
coff = offsetof(ElementIndexedQuad, c0);
} else if (batch.constant_colored && !batch.textured) {
off = offsetof(ElementIndexedQuadWithoutColorWithoutTexture, v1);
voff = offsetof(ElementIndexedQuadWithoutColorWithoutTexture, v0);
}
command.vertices = (AttributeArrayPointer) {
.arity = 2,
.type = TWN_FLOAT,
.stride = off,
.offset = voff,
.buffer = buffer
};
if (batch.textured)
command.texcoords = (AttributeArrayPointer) {
.arity = 2,
.type = TWN_FLOAT,
.stride = off,
.offset = uvoff,
.buffer = buffer
};
if (!batch.constant_colored) {
command.colors = (AttributeArrayPointer) {
.arity = 4,
.type = TWN_UNSIGNED_BYTE,
.stride = off,
.offset = coff,
.buffer = buffer
};
} else {
command.constant_colored = true;
command.color = primitives[0].sprite.color;
}
if (batch.textured) {
command.textured = true;
command.texture_key = batch.texture_key;
command.texture_repeat = batch.repeat;
}
command.element_buffer = get_quad_element_buffer();
command.element_count = 6 * (uint32_t)batch.size;
command.range_end = 6 * (uint32_t)batch.size;
command.texture_mode = batch.mode;
command.pipeline = PIPELINE_2D;
command.depth_range_high = depth_range_high;
command.depth_range_low = depth_range_low;
DeferredCommand final_command = {
.type = DEFERRED_COMMAND_TYPE_DRAW,
.draw = command
};
arrpush(deferred_commands, final_command);
}
size_t get_quad_payload_size(struct QuadBatch batch) {
if (batch.constant_colored && batch.textured)
return sizeof (ElementIndexedQuadWithoutColor);
else if (!batch.constant_colored && batch.textured)
return sizeof (ElementIndexedQuad);
else if (batch.constant_colored && !batch.textured)
return sizeof (ElementIndexedQuadWithoutColorWithoutTexture);
else if (!batch.constant_colored && !batch.textured)
return sizeof (ElementIndexedQuadWithoutTexture);
SDL_assert(false);
return 0;
}
void push_quad_payload_to_vertex_buffer_builder(struct QuadBatch batch,
size_t index,
VertexBufferBuilder *builder,
Vec2 v0, Vec2 v1, Vec2 v2, Vec2 v3,
Vec2 uv0, Vec2 uv1, Vec2 uv2, Vec2 uv3,
Color color)
{
if (!batch.constant_colored && batch.textured) {
ElementIndexedQuad const payload = {
.v0 = v0,
.v1 = v1,
.v2 = v2,
.v3 = v3,
.uv0 = uv0,
.uv1 = uv1,
.uv2 = uv2,
.uv3 = uv3,
/* equal for all (flat shaded) */
.c0 = color,
// .c1 = color,
.c2 = color,
// .c3 = color,
};
((ElementIndexedQuad *)builder->base)[index] = payload;
} else if (batch.constant_colored && batch.textured) {
ElementIndexedQuadWithoutColor const payload = {
.v0 = v0,
.v1 = v1,
.v2 = v2,
.v3 = v3,
.uv0 = uv0,
.uv1 = uv1,
.uv2 = uv2,
.uv3 = uv3,
};
((ElementIndexedQuadWithoutColor *)builder->base)[index] = payload;
} else if (!batch.constant_colored && !batch.textured) {
ElementIndexedQuadWithoutTexture const payload = {
.v0 = v0,
.v1 = v1,
.v2 = v2,
.v3 = v3,
/* equal for all (flat shaded) */
.c0 = color,
// .c1 = color,
.c2 = color,
// .c3 = color,
};
((ElementIndexedQuadWithoutTexture *)builder->base)[index] = payload;
} else if (batch.constant_colored && !batch.textured) {
ElementIndexedQuadWithoutColorWithoutTexture const payload = {
.v0 = v0,
.v1 = v1,
.v2 = v2,
.v3 = v3,
};
((ElementIndexedQuadWithoutColorWithoutTexture *)builder->base)[index] = payload;
}
}

View File

@ -1,10 +1,6 @@
#include "twn_draw.h"
#include "twn_draw_c.h"
#include "twn_engine_context_c.h"
#include "twn_util.h"
#include "twn_util_c.h"
#include "twn_textures_c.h"
#include "twn_option.h"
#include <stb_ds.h>
@ -36,7 +32,7 @@ struct QuadBatch collect_rect_batch(const Primitive2D primitives[], size_t len)
.constant_colored = true,
};
const uint32_t uniform_color = *(const uint32_t *)&primitives[0].rect.color;
const uint32_t uniform_color = *(const uint32_t *)(void const *)&primitives[0].rect.color;
/* batch size is clamped so that reallocated short indices could be used */
if (len >= QUAD_ELEMENT_BUFFER_LENGTH)
@ -54,7 +50,7 @@ struct QuadBatch collect_rect_batch(const Primitive2D primitives[], size_t len)
break;
/* if all are modulated the same we can skip sending the color data */
if (*(const uint32_t *)&current->rect.color != uniform_color)
if (*(const uint32_t *)(void const *)&current->rect.color != uniform_color)
batch.constant_colored = false;
++batch.size;
@ -74,8 +70,6 @@ void render_rect_batch(const Primitive2D primitives[],
/* single vertex array is used for every batch with NULL glBufferData() trick at the end */
VertexBuffer const vertex_array = get_scratch_vertex_array();
use_texture_mode(batch.mode);
/* vertex population over a vertex buffer builder interface */
{
VertexBufferBuilder payload = build_vertex_buffer(vertex_array, get_quad_payload_size(batch) * batch.size);
@ -85,13 +79,13 @@ void render_rect_batch(const Primitive2D primitives[],
const size_t cur = batch.mode == TEXTURE_MODE_GHOSTLY ? i : batch.size - i - 1;
const RectPrimitive rect = primitives[cur].rect;
Vec2 v0 = { rect.rect.x, rect.rect.y };
Vec2 v1 = { rect.rect.x, rect.rect.y + rect.rect.h };
Vec2 v0 = { rect.rect.x, rect.rect.y };
Vec2 v1 = { rect.rect.x, rect.rect.y + rect.rect.h };
Vec2 v2 = { rect.rect.x + rect.rect.w, rect.rect.y + rect.rect.h };
Vec2 v3 = { rect.rect.x + rect.rect.w, rect.rect.y };
push_quad_payload_to_vertex_buffer_builder(
batch, &payload,
batch, i, &payload,
v0, v1, v2, v3,
(Vec2){0}, (Vec2){0}, (Vec2){0}, (Vec2){0},
rect.color);

View File

@ -2,6 +2,7 @@
#include "twn_draw_c.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
static char *paths_in_use;
@ -21,6 +22,14 @@ void render_skybox(void) {
return;
/* note: ownership of 'paths_in_use' goes there */
finally_render_skybox(paths_in_use);
DeferredCommand command = {
.type = DEFERRED_COMMAND_TYPE_DRAW_SKYBOX,
.draw_skybox = (DeferredCommandDrawSkybox){
.paths = paths_in_use
}
};
arrpush(deferred_commands, command);
paths_in_use = NULL;
}

View File

@ -56,7 +56,7 @@ void draw_sprite_args(const DrawSpriteArgs args) {
bool const stretch = m_or(args, stretch, false);
Rect const *texture_region = m_is_set(args, texture_region) ? &args.texture_region_opt : NULL;
draw_sprite(args.path, args.rect, texture_region, color, rotation, flip_x, flip_y, stretch);
draw_sprite(args.texture, args.rect, texture_region, color, rotation, flip_x, flip_y, stretch);
}
@ -69,12 +69,13 @@ struct QuadBatch collect_sprite_batch(const Primitive2D primitives[], size_t len
struct QuadBatch batch = {
.mode = textures_get_mode(&ctx.texture_cache, primitives[0].sprite.texture_key),
.texture_key = primitives[0].sprite.texture_key,
.constant_colored = true,
.repeat = primitives[0].sprite.repeat,
.textured = true,
};
const uint32_t uniform_color = *(const uint32_t *)&primitives[0].sprite.color;
const uint32_t uniform_color = *(const uint32_t *)(void const*)&primitives[0].sprite.color;
/* batch size is clamped so that reallocated short indices could be used */
if (len >= QUAD_ELEMENT_BUFFER_LENGTH)
@ -108,7 +109,7 @@ struct QuadBatch collect_sprite_batch(const Primitive2D primitives[], size_t len
}
/* if all are modulated the same we can skip sending the color data */
if (*(const uint32_t *)&current->sprite.color != uniform_color)
if (*(const uint32_t *)(void const *)&current->sprite.color != uniform_color)
batch.constant_colored = false;
++batch.size;
@ -131,7 +132,7 @@ void render_sprite_batch(const Primitive2D primitives[],
textures_get_dims(&ctx.texture_cache, primitives->sprite.texture_key);
/* cached srcrect */
Rect cached_srcrect;
Rect cached_srcrect = {0};
TextureKey cached_srcrect_key = TEXTURE_KEY_INVALID;
/* vertex population over a vertex buffer builder interface */
@ -215,7 +216,7 @@ void render_sprite_batch(const Primitive2D primitives[],
#pragma GCC diagnostic pop
const Vec2 c = frect_center(sprite.rect);
const Vec2 c = rect_center(sprite.rect);
const Vec2 t = fast_cossine(sprite.rotation + (float)M_PI_4);
const Vec2 d = {
.x = t.x * sprite.rect.w * (float)M_SQRT1_2,
@ -230,7 +231,7 @@ void render_sprite_batch(const Primitive2D primitives[],
} else {
/* rotated non-square case*/
const Vec2 c = frect_center(sprite.rect);
const Vec2 c = rect_center(sprite.rect);
const Vec2 t = fast_cossine(sprite.rotation);
const Vec2 h = { sprite.rect.w / 2, sprite.rect.h / 2 };
@ -241,7 +242,7 @@ void render_sprite_batch(const Primitive2D primitives[],
v3 = (Vec2){ c.x + t.x * +h.x - t.y * -h.y, c.y + t.y * +h.x + t.x * -h.y };
}
push_quad_payload_to_vertex_buffer_builder(batch, &payload, v0, v1, v2, v3, uv0, uv1, uv2, uv3, sprite.color);
push_quad_payload_to_vertex_buffer_builder(batch, i, &payload, v0, v1, v2, v3, uv0, uv1, uv2, uv3, sprite.color);
}
}

View File

@ -175,7 +175,7 @@ static void text_draw_with(FontData* font_data, char* text, Vec2 position, Color
const size_t len = SDL_strlen(text);
VertexBufferBuilder payload = build_vertex_buffer(vertex_array, get_text_payload_size() * len);
VertexBufferBuilder builder = build_vertex_buffer(vertex_array, sizeof (ElementIndexedQuadWithoutColor) * len);
for (size_t i = 0; i < len; ++i) {
const char c = text[i];
@ -204,9 +204,22 @@ static void text_draw_with(FontData* font_data, char* text, Vec2 position, Color
quad.y0 += (float)font_data->ascent;
quad.y1 += (float)font_data->ascent;
push_text_payload_to_vertex_buffer_builder(font_data, &payload, quad);
ElementIndexedQuadWithoutColor const payload = {
.v0 = (Vec2){ quad.x0, quad.y0 },
.v1 = (Vec2){ quad.x1, quad.y0 },
.v2 = (Vec2){ quad.x1, quad.y1 },
.v3 = (Vec2){ quad.x0, quad.y1 },
.uv0 = (Vec2){ quad.s0, quad.t0 },
.uv1 = (Vec2){ quad.s1, quad.t0 },
.uv2 = (Vec2){ quad.s1, quad.t1 },
.uv3 = (Vec2){ quad.s0, quad.t1 },
};
((ElementIndexedQuadWithoutColor *)builder.base)[i] = payload;
}
finish_vertex_builder(&builder);
finally_draw_text(font_data, len, color, vertex_array);
}
@ -275,8 +288,8 @@ void text_cache_reset_arena(TextCache *cache) {
}
void draw_text(const char *string, Vec2 position, int height_px, Color color, const char *font_path) {
ensure_font_cache(font_path, height_px);
void draw_text(const char *string, Vec2 position, float height, Color color, const char *font) {
ensure_font_cache(font, (int)height);
/* the original string might not be around by the time it's used, so copy it */
size_t str_length = SDL_strlen(string) + 1;
@ -290,8 +303,8 @@ void draw_text(const char *string, Vec2 position, int height_px, Color color, co
.color = color,
.position = position,
.text = dup_string,
.font = font_path,
.height_px = height_px,
.font = font,
.height_px = (int)height,
};
Primitive2D primitive = {
@ -303,9 +316,9 @@ void draw_text(const char *string, Vec2 position, int height_px, Color color, co
}
int draw_text_width(const char *string, int height_px, const char *font_path) {
ensure_font_cache(font_path, height_px);
FontData *font_data = get_font_data(font_path, height_px);
float draw_text_width(const char *string, float height, const char *font) {
ensure_font_cache(font, (int)height);
FontData *font_data = get_font_data(font, (int)height);
int length = 0;
for (const char *p = string; *p != '\0'; ++p) {
@ -316,5 +329,57 @@ int draw_text_width(const char *string, int height_px, const char *font_path) {
length += advance_width;
}
return (int)((float)length * font_data->scale_factor);
return (float)length * font_data->scale_factor;
}
void finally_draw_text(FontData const *font_data,
size_t len,
Color color,
VertexBuffer buffer)
{
DeferredCommandDraw command = {0};
command.vertices = (AttributeArrayPointer) {
.arity = 2,
.type = TWN_FLOAT,
.stride = offsetof(ElementIndexedQuadWithoutColor, v1),
.offset = offsetof(ElementIndexedQuadWithoutColor, v0),
.buffer = buffer
};
command.texcoords = (AttributeArrayPointer) {
.arity = 2,
.type = TWN_FLOAT,
.stride = offsetof(ElementIndexedQuadWithoutColor, v1),
.offset = offsetof(ElementIndexedQuadWithoutColor, uv0),
.buffer = buffer
};
command.constant_colored = true;
command.color = color;
command.gpu_texture = font_data->texture;
command.uses_gpu_key = true;
command.textured = true;
command.element_buffer = get_quad_element_buffer();
command.element_count = 6 * (uint32_t)len;
command.range_end = 6 * (uint32_t)len;
command.texture_mode = TEXTURE_MODE_GHOSTLY;
command.pipeline = PIPELINE_2D;
command.depth_range_high = depth_range_high;
command.depth_range_low = depth_range_low;
DeferredCommand final_command = {
.type = DEFERRED_COMMAND_TYPE_DRAW,
.draw = command
};
arrpush(deferred_commands, final_command);
/* TODO: why doesn't it get restored if not placed here? */
// glDepthMask(GL_TRUE);
}

View File

@ -15,44 +15,51 @@ void draw_triangle(const char *path,
Vec3 v2,
Vec2 uv0,
Vec2 uv1,
Vec2 uv2)
Vec2 uv2,
Color c0,
Color c1,
Color c2)
{
// TODO: support color
(void)c0; (void)c1; (void)c2;
// TODO: order drawing by atlas id as well, so that texture rebinding is not as common
const TextureKey texture_key = textures_get_key(&ctx.texture_cache, path);
struct MeshBatchItem *batch_p = hmgetp_null(ctx.uncolored_mesh_batches, texture_key);
if (!batch_p) {
struct MeshBatch item = {0};
hmput(ctx.uncolored_mesh_batches, texture_key, item);
batch_p = &ctx.uncolored_mesh_batches[hmlenu(ctx.uncolored_mesh_batches) - 1]; /* TODO: can last index be used? */
batch_p = &ctx.uncolored_mesh_batches[hmlenu(ctx.uncolored_mesh_batches) - 1];
}
union UncoloredSpaceTriangle triangle = { .primitive = {
UncoloredSpaceTriangle const triangle = {
.v0 = v0,
.v1 = v1,
.v2 = v2,
.uv1 = uv1,
.uv0 = uv0,
.uv2 = uv2,
}};
};
union UncoloredSpaceTriangle *triangles = (union UncoloredSpaceTriangle *)(void *)batch_p->value.primitives;
UncoloredSpaceTriangle *triangles = (UncoloredSpaceTriangle *)(void *)batch_p->value.primitives;
arrpush(triangles, triangle);
batch_p->value.primitives = (uint8_t *)triangles;
}
void draw_uncolored_space_traingle_batch(struct MeshBatch *batch,
TextureKey texture_key)
void finally_draw_uncolored_space_traingle_batch(const MeshBatch *batch,
const TextureKey texture_key)
{
VertexBuffer const vertex_array = get_scratch_vertex_array();
const size_t primitives_len = arrlenu(batch->primitives);
/* nothing to do */
if (primitives_len == 0)
return;
VertexBuffer const buffer = get_scratch_vertex_array();
const Rect srcrect = textures_get_srcrect(&ctx.texture_cache, texture_key);
const Rect dims = textures_get_dims(&ctx.texture_cache, texture_key);
@ -63,8 +70,8 @@ void draw_uncolored_space_traingle_batch(struct MeshBatch *batch,
/* update pixel-based uvs to correspond with texture atlases */
for (size_t i = 0; i < primitives_len; ++i) {
struct UncoloredSpaceTrianglePayload *payload =
&((union UncoloredSpaceTriangle *)(void *)batch->primitives)[i].payload;
UncoloredSpaceTriangle *payload =
&((UncoloredSpaceTriangle *)(void *)batch->primitives)[i];
payload->uv0.x = xr + ((float)payload->uv0.x / srcrect.w) * wr;
payload->uv0.y = yr + ((float)payload->uv0.y / srcrect.h) * hr;
@ -74,7 +81,46 @@ void draw_uncolored_space_traingle_batch(struct MeshBatch *batch,
payload->uv2.y = yr + ((float)payload->uv2.y / srcrect.h) * hr;
}
specify_vertex_buffer(vertex_array, batch->primitives, primitives_len * sizeof (struct UncoloredSpaceTrianglePayload));
specify_vertex_buffer(buffer, batch->primitives, primitives_len * sizeof (UncoloredSpaceTriangle));
finally_draw_uncolored_space_traingle_batch(batch, texture_key, vertex_array);
DeferredCommandDraw command = {0};
command.vertices = (AttributeArrayPointer) {
.arity = 3,
.type = TWN_FLOAT,
.stride = offsetof(UncoloredSpaceTriangle, v1),
.offset = offsetof(UncoloredSpaceTriangle, v0),
.buffer = buffer
};
command.texcoords = (AttributeArrayPointer) {
.arity = 2,
.type = TWN_FLOAT,
.stride = offsetof(UncoloredSpaceTriangle, v1),
.offset = offsetof(UncoloredSpaceTriangle, uv0),
.buffer = buffer
};
command.textured = true;
command.texture_key = texture_key;
command.primitive_count = (uint32_t)(3 * primitives_len);
/* TODO: support alpha blended case, with distance sort */
TextureMode mode = textures_get_mode(&ctx.texture_cache, texture_key);
if (mode == TEXTURE_MODE_GHOSTLY)
mode = TEXTURE_MODE_SEETHROUGH;
command.texture_mode = mode;
command.pipeline = PIPELINE_SPACE;
command.depth_range_high = depth_range_high;
command.depth_range_low = depth_range_low;
DeferredCommand final_command = {
.type = DEFERRED_COMMAND_TYPE_DRAW,
.draw = command
};
arrpush(deferred_commands, final_command);
}

View File

@ -12,9 +12,10 @@
#include "rendering/twn_circles.c"
#include "rendering/twn_draw.c"
#include "rendering/twn_fog.c"
#include "rendering/twn_skybox.c"
#include "rendering/twn_sprites.c"
#include "rendering/twn_rects.c"
#include "rendering/twn_text.c"
#include "rendering/twn_quads.c"
#include "rendering/twn_triangles.c"
#include "rendering/twn_billboards.c"

View File

@ -2,10 +2,12 @@
#include "twn_engine_context_c.h"
#include "twn_util.h"
#include "twn_util_c.h"
#include "twn_audio.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
#include <physfs.h>
#include <physfsrwops.h>
#define STB_VORBIS_NO_PUSHDATA_API
#define STB_VORBIS_HEADER_ONLY
@ -16,9 +18,17 @@
static const char *audio_exts[AUDIO_FILE_TYPE_COUNT] = {
".ogg", /* AUDIO_FILE_TYPE_OGG */
".wav", /* AUDIO_FILE_TYPE_WAV */
".xm", /* AUDIO_FILE_TYPE_XM */
};
static const uint8_t audio_exts_len[AUDIO_FILE_TYPE_COUNT] = {
sizeof ".ogg" - 1,
sizeof ".wav" - 1,
sizeof ".xm" - 1,
};
/* TODO: allow for vectorization and packed vectors (alignment care and alike) */
/* TODO: count frames without use, free the memory when threshold is met */
/* TODO: count repeated usages for sound effect cases with rendering to ram? */
@ -55,14 +65,10 @@ static int64_t get_audio_data(const char *path, unsigned char **data) {
static AudioFileType infer_audio_file_type(const char *path) {
size_t path_len = SDL_strlen(path);
size_t const path_len = SDL_strlen(path);
for (int i = 0; i < AUDIO_FILE_TYPE_COUNT; ++i) {
size_t ext_length = SDL_strlen(audio_exts[i]);
if (path_len <= ext_length)
continue;
if (SDL_strcmp(&path[path_len - ext_length], audio_exts[i]) == 0)
if (SDL_strncmp(&path[path_len - audio_exts_len[i]], audio_exts[i], audio_exts_len[i]) == 0)
return (AudioFileType)i;
}
@ -72,6 +78,7 @@ static AudioFileType infer_audio_file_type(const char *path) {
/* TODO: error propagation and clearing of resources on partial success? */
/* or should we expect things to simply fail? */
/* TODO: reuse often used decoded/decompressed data */
static union AudioContext init_audio_context(const char *path, AudioFileType type) {
switch (type) {
case AUDIO_FILE_TYPE_OGG: {
@ -101,6 +108,54 @@ static union AudioContext init_audio_context(const char *path, AudioFileType typ
};
}
/* TODO: transform to destination format immediately? */
case AUDIO_FILE_TYPE_WAV: {
SDL_AudioSpec spec;
uint8_t *data;
uint32_t len;
if (!SDL_LoadWAV_RW(PHYSFSRWOPS_openRead(path), 1, &spec, &data, &len)) {
CRY_SDL("Cannot load .wav file:");
break;
}
SDL_AudioCVT cvt;
int conv = SDL_BuildAudioCVT(&cvt,
spec.format,
spec.channels,
spec.freq,
AUDIO_F32,
2,
AUDIO_FREQUENCY);
if (conv < 0) {
CRY_SDL("Cannot resample .wav:");
break;
}
if (conv != 0) {
data = SDL_realloc(data, len * cvt.len_mult);
cvt.buf = data;
cvt.len = len;
if (SDL_ConvertAudio(&cvt) < 0) {
CRY_SDL("Error resampling .wav:");
break;
}
spec.channels = 2;
spec.freq = AUDIO_FREQUENCY;
/* TODO: test this */
spec.samples = (uint16_t)((size_t)(SDL_floor((double)len * cvt.len_ratio)) / sizeof (float) / 2);
} else {
spec.samples = (uint16_t)((size_t)(SDL_floor((double)len * cvt.len_ratio)) / sizeof (float) / 2);
}
return (union AudioContext) {
.wav = {
.position = 0,
.samples = data,
.spec = spec,
}
};
}
case AUDIO_FILE_TYPE_XM: {
unsigned char *data;
int64_t len = get_audio_data(path, &data);
@ -137,6 +192,29 @@ static union AudioContext init_audio_context(const char *path, AudioFileType typ
}
static void free_audio_channel(AudioChannel channel) {
switch (channel.file_type) {
case AUDIO_FILE_TYPE_OGG: {
SDL_free(channel.context.vorbis.data);
break;
}
case AUDIO_FILE_TYPE_WAV: {
SDL_free(channel.context.wav.samples);
break;
}
case AUDIO_FILE_TYPE_XM: {
xm_free_context(channel.context.xm.handle);
break;
}
case AUDIO_FILE_TYPE_COUNT:
case AUDIO_FILE_TYPE_UNKNOWN:
default:
SDL_assert_always(false);
break;
}
}
static void repeat_audio(AudioChannel *channel) {
switch (channel->file_type) {
case AUDIO_FILE_TYPE_OGG: {
@ -144,6 +222,11 @@ static void repeat_audio(AudioChannel *channel) {
break;
}
case AUDIO_FILE_TYPE_WAV: {
channel->context.wav.position = 0;
break;
}
case AUDIO_FILE_TYPE_XM: {
xm_restart(channel->context.xm.handle);
break;
@ -164,71 +247,110 @@ void audio_play(const char *path,
float volume,
float panning)
{
AudioChannelItem *pair = shgetp_null(ctx.audio_channels, channel);
if (!ctx.audio_initialized) {
profile_start("audio initialization");
/* create a channel if it doesn't exist */
if (!pair) {
AudioFileType file_type = infer_audio_file_type(path);
SDL_AudioSpec request, got;
SDL_zero(request);
request.freq = AUDIO_FREQUENCY;
request.format = AUDIO_F32;
request.channels = 2;
#ifndef TWN_FEATURE_PUSH_AUDIO
request.callback = audio_callback;
#endif
/* TODO: check for errors */
ctx.audio_device = SDL_OpenAudioDevice(NULL, 0, &request, &got, 0);
ctx.audio_stream_format = got.format;
ctx.audio_stream_frequency = got.freq;
ctx.audio_stream_channel_count = got.channels;
/* TODO: relax this */
SDL_assert_always(got.freq == AUDIO_FREQUENCY);
SDL_assert_always(got.format == AUDIO_F32);
SDL_assert_always(got.channels == 2);
SDL_PauseAudioDevice(ctx.audio_device, 0);
profile_end("audio initialization");
ctx.audio_initialized = true;
}
if (channel) {
AudioChannelItem *pair = shgetp_null(ctx.audio_channels, channel);
/* create a channel if it doesn't exist */
if (!pair) {
AudioFileType const file_type = infer_audio_file_type(path);
AudioChannel new_channel = {
.file_type = file_type,
.context = init_audio_context(path, file_type),
.path = path,
.name = channel,
.repeat = repeat,
.volume = volume,
.panning = panning,
};
shput(ctx.audio_channels, channel, new_channel);
pair = shgetp_null(ctx.audio_channels, channel);
}
/* TODO: destroy and create new context when channel is reused for different file */
/* works for both restarts and new audio */
if (strcmp(pair->value.path, path) == 0)
repeat_audio(&pair->value);
} else {
/* audio without channel plays without repeat and ability to change parameters over its course, nor stop it */
AudioFileType const file_type = infer_audio_file_type(path);
AudioChannel new_channel = {
.file_type = file_type,
.context = init_audio_context(path, file_type),
.path = path,
.name = channel,
.repeat = repeat,
.name = NULL,
.repeat = false,
.volume = volume,
.panning = panning,
};
shput(ctx.audio_channels, channel, new_channel);
pair = shgetp_null(ctx.audio_channels, channel);
if (repeat)
log_warn("Cannot repeat audio played on unnamed scratch channel (for %s)", path);
arrpush(ctx.unnamed_audio_channels, new_channel);
}
/* TODO: destroy and create new context when channel is reused for different file */
/* works for both restarts and new audio */
if (strcmp(pair->value.path, path) == 0)
repeat_audio(&pair->value);
}
TWN_API void audio_set(const char *channel, AudioParam param, float value) {
TWN_API void audio_parameter(const char *channel, const char *param, float value) {
AudioChannelItem *pair = shgetp_null(ctx.audio_channels, channel);
if (!pair) {
log_warn("No channel by the name of %s to set a parameter for", channel);
return;
}
switch (param) {
case AUDIO_PARAM_REPEAT:
if (SDL_strncmp(param, "repeat", sizeof "repeat" - 1) == 0) {
pair->value.repeat = (bool)value;
break;
case AUDIO_PARAM_VOLUME:
if (value > 1.0f) {
} else if (SDL_strncmp(param, "volume", sizeof "volume" - 1) == 0) {
if (value > 1.0f || value < 0.0f) {
log_warn("Out of range volume for channel %s set", channel);
value = 1.0f;
}
if (value < 0.0f) {
log_warn("Out of range volume for channel %s set", channel);
value = 0.0f;
value = clampf(value, 0.0f, 1.0f);
}
pair->value.volume = value;
break;
case AUDIO_PARAM_PANNING:
if (value > 1.0f) {
} else if (SDL_strncmp(param, "panning", sizeof "panning" - 1) == 0) {
if (value > 1.0f || value < -1.0f) {
log_warn("Out of range panning for channel %s set", channel);
value = 1.0f;
}
if (value < -1.0f) {
log_warn("Out of range panning for channel %s set", channel);
value = -1.0f;
value = clampf(value, -1.0f, +1.0f);
}
pair->value.panning = value;
break;
default:
} else
CRY("Audio channel parameter setting failed", "Invalid parameter enum given");
}
}
/* TODO: handle it more properly in regards to clipping and alike */
/* this assumes float based streams */
static void audio_mixin_streams(const AudioChannel *channel,
uint8_t *restrict a,
@ -241,37 +363,39 @@ static void audio_mixin_streams(const AudioChannel *channel,
const float left_panning = fminf(fabsf(channel->panning - 1.0f), 1.0f);
const float right_panning = fminf(fabsf(channel->panning + 1.0f), 1.0f);
for (size_t s = 0; s < frames; s += 2) {
for (size_t s = 0; s < frames; ++s) {
/* left channel */
sa[s] += (float)(sb[s] * channel->volume * left_panning);
sa[s * 2 + 0] += (float)(sb[s * 2 + 0] * channel->volume * left_panning);
sa[s * 2 + 0] *= 1 / (float)M_2_SQRTPI;
/* right channel */
sa[s + 1] += (float)(sb[s + 1] * channel->volume * right_panning);
sa[s * 2 + 1] += (float)(sb[s * 2 + 1] * channel->volume * right_panning);
sa[s * 2 + 1] *= 1 / (float)M_2_SQRTPI;
}
}
/* remember: sample is data for all channels where frame is a part of it */
static void audio_sample_and_mixin_channel(const AudioChannel *channel,
/* remember: frame consists of sample * channel_count */
static void audio_sample_and_mixin_channel(AudioChannel *channel,
uint8_t *stream,
int len)
{
static uint8_t buffer[16384];
const int float_buffer_frames = sizeof (buffer) / sizeof (float);
const int stream_frames = len / (int)(sizeof (float));
static uint8_t buffer[16384]; /* TODO: better make it a growable scratch instead, which will simplify things */
const size_t float_buffer_frames = sizeof (buffer) / sizeof (float) / 2;
const size_t stream_frames = len / sizeof (float) / 2;
switch (channel->file_type) {
case AUDIO_FILE_TYPE_OGG: {
/* feed stream for needed conversions */
for (int i = 0; i < stream_frames; ) {
const int n_frames = (stream_frames - i) > float_buffer_frames ?
for (size_t i = 0; i < stream_frames; ) {
const size_t n_frames = (stream_frames - i) > float_buffer_frames ?
float_buffer_frames : stream_frames - i;
const int samples_per_channel = stb_vorbis_get_samples_float_interleaved(
const size_t samples_per_channel = stb_vorbis_get_samples_float_interleaved(
channel->context.vorbis.handle,
channel->context.vorbis.channel_count,
2,
(float *)buffer,
n_frames);
(int)n_frames * 2);
/* handle end of file */
if (samples_per_channel == 0) {
@ -279,30 +403,61 @@ static void audio_sample_and_mixin_channel(const AudioChannel *channel,
/* seek to start and try sampling some more */
stb_vorbis_seek_start(channel->context.vorbis.handle);
continue;
} else
} else {
/* leave silence */
channel->finished = true;
break;
}
}
/* panning and mixing */
audio_mixin_streams(channel,
&stream[i * sizeof(float)], buffer,
samples_per_channel * 2);
&stream[i * sizeof(float) * 2], buffer,
samples_per_channel);
i += samples_per_channel * 2;
i += samples_per_channel;
}
break;
}
case AUDIO_FILE_TYPE_WAV: {
/* feed stream for needed conversions */
for (size_t i = 0; i < stream_frames; ) {
const size_t limit = MIN(stream_frames - i, channel->context.wav.spec.samples - channel->context.wav.position);
/* same format, just feed it directly */
audio_mixin_streams(channel,
&stream[i * sizeof(float) * 2],
&((uint8_t *)channel->context.wav.samples)[channel->context.wav.position * sizeof (float) * 2],
limit);
channel->context.wav.position += limit;
if (channel->context.wav.position >= channel->context.wav.spec.samples) {
if (channel->repeat)
channel->context.wav.position = 0;
else {
/* leave silence */
channel->finished = true;
break;
}
}
i += limit;
}
break;
}
case AUDIO_FILE_TYPE_XM: {
for (int i = 0; i < stream_frames; ) {
const int n_frames = (stream_frames - i) > float_buffer_frames ?
for (size_t i = 0; i < stream_frames; ) {
const size_t n_frames = (stream_frames - i) > float_buffer_frames ?
float_buffer_frames : stream_frames - i;
const int samples_per_channel = xm_generate_samples(channel->context.xm.handle,
(float *)buffer,
n_frames / 2);
const size_t samples_per_channel = xm_generate_samples(channel->context.xm.handle,
(float *)buffer,
n_frames);
/* handle end of file */
if (samples_per_channel == 0) {
@ -310,18 +465,20 @@ static void audio_sample_and_mixin_channel(const AudioChannel *channel,
/* seek to start and try sampling some more */
xm_restart(channel->context.xm.handle);
continue;
} else
} else {
channel->finished = true;
/* leave silence */
break;
}
}
/* panning and mixing */
audio_mixin_streams(channel,
&stream[i * sizeof(float)],
&stream[i * sizeof(float) * 2],
buffer,
samples_per_channel * 2);
samples_per_channel);
i += samples_per_channel * 2;
i += samples_per_channel;
}
break;
@ -355,8 +512,23 @@ void audio_callback(void *userdata, uint8_t *stream, int len) {
sanity_check_channel(&ctx.audio_channels[i].value);
audio_sample_and_mixin_channel(&ctx.audio_channels[i].value, stream, len);
}
for (int i = 0; i < arrlen(ctx.unnamed_audio_channels); ++i) {
sanity_check_channel(&ctx.unnamed_audio_channels[i]);
audio_sample_and_mixin_channel(&ctx.unnamed_audio_channels[i], stream, len);
}
/* ditch finished unnamed */
int i = 0;
while (i < arrlen(ctx.unnamed_audio_channels)) {
if (ctx.unnamed_audio_channels[i].finished) {
free_audio_channel(ctx.unnamed_audio_channels[i]);
arrdelswap(ctx.unnamed_audio_channels, i);
} else i++;
}
}
TWN_API void audio_play_args(PlayAudioArgs args) {
const char *channel = m_or(args, channel, NULL);
const bool repeat = m_or(args, repeat, false);

View File

@ -1,8 +1,6 @@
#ifndef TWN_AUDIO_C_H
#define TWN_AUDIO_C_H
#include "twn_audio.h"
#include <SDL2/SDL_audio.h>
#define STB_VORBIS_HEADER_ONLY
@ -15,9 +13,12 @@
#define AUDIO_FREQUENCY 48000
/* TODO: specify which PCM formats are usable with WAV */
/* TODO: specify limitations of libxm */
/* TODO: specify limitations of stb_vorbis */
typedef enum AudioFileType {
AUDIO_FILE_TYPE_OGG,
AUDIO_FILE_TYPE_WAV,
AUDIO_FILE_TYPE_XM,
AUDIO_FILE_TYPE_COUNT,
AUDIO_FILE_TYPE_UNKNOWN,
@ -32,6 +33,12 @@ union AudioContext {
uint8_t channel_count;
} vorbis;
struct {
void *samples;
SDL_AudioSpec spec;
size_t position;
} wav;
struct {
xm_context_t *handle;
} xm;
@ -46,6 +53,7 @@ typedef struct AudioChannel {
bool repeat;
float volume;
float panning;
bool finished;
} AudioChannel;

View File

@ -1,4 +1,4 @@
#include "twn_camera.h"
#include "twn_camera_c.h"
#include "twn_vec.h"
#include "twn_engine_context_c.h"
@ -28,9 +28,10 @@ Matrix4 camera_look_at(const Camera *const camera) {
result.row[3].x = -m_vec_dot(r, camera->pos);
result.row[3].y = -m_vec_dot(u, camera->pos);
result.row[3].z = m_vec_dot(camera->target, camera->pos);
result.row[0].w = result.row[1].w = result.row[2].w = 0.0f;
result.row[3].w = 1.0f;
result.row[0].w = result.row[1].w = result.row[2].w = 0.0f;
return result;
}
@ -38,7 +39,7 @@ Matrix4 camera_perspective(const Camera *const camera) {
/* from cglm */
Matrix4 result = {0};
const float aspect = (float)(ctx.base_render_width / ctx.base_render_height);
const float aspect = ((float)ctx.base_render_width / (float)ctx.base_render_height);
const float f = 1.0f / tanf(camera->fov * 0.5f);
const float fn = 1.0f / (CAMERA_NEAR_Z - CAMERA_FAR_Z);

View File

@ -2,7 +2,7 @@
#define TWN_CAMERA_H
#include "twn_types.h"
#include "twn_engine_api.h"
#include "twn_types_c.h"
/* TODO: make it cached? */
/* for example, perspective matrix only needs recaluclation on FOV change */
@ -12,11 +12,11 @@ typedef struct Camera {
Vec3 pos; /* eye position */
Vec3 target; /* normalized target vector */
Vec3 up; /* normalized up vector */
float fov; /* field of view, in radians */
float fov; /* field of view, in radians */
} Camera;
TWN_API Matrix4 camera_look_at(const Camera *camera);
Matrix4 camera_look_at(const Camera *camera);
TWN_API Matrix4 camera_perspective(const Camera *const camera);
Matrix4 camera_perspective(const Camera *const camera);
#endif

View File

@ -28,8 +28,11 @@ typedef struct EngineContext {
char **argv;
/* where the app was run from, used as the root for packs */
char *base_dir;
char *title;
Vec2i window_dims;
Vec2 window_dims;
Rect viewport_rect;
float viewport_scale;
/* configuration */
toml_table_t *config_table;
@ -45,11 +48,13 @@ typedef struct EngineContext {
/* rendering */
Primitive2D *render_queue_2d;
MeshBatchItem *uncolored_mesh_batches;
MeshBatchItem *billboard_batches;
TextCache text_cache;
TextureCache texture_cache;
/* audio */
AudioChannelItem *audio_channels;
AudioChannel *unnamed_audio_channels;
SDL_AudioDeviceID audio_device;
int audio_stream_frequency;
SDL_AudioFormat audio_stream_format;
@ -64,11 +69,6 @@ typedef struct EngineContext {
int64_t delta_averager_residual;
int64_t time_averager[4];
/* this should be a multiple of the current ticks per second */
/* use it to simulate low framerate (e.g. at 60 tps, set to 2 for 30 fps) */
/* it can be changed at runtime; any resulting logic anomalies are bugs */
uint32_t update_multiplicity;
SDL_GLContext *gl_context;
SDL_Window *window;
uint32_t window_id;
@ -78,6 +78,9 @@ typedef struct EngineContext {
bool resync_flag;
bool was_successful;
bool render_double_buffered;
/* signals mouse focus, used to disable mouse capture */
bool window_mouse_resident;
bool audio_initialized;
} EngineContext;
/* TODO: does it need to be marked with TWN_API? */

View File

@ -1,13 +1,14 @@
#include "twn_input_c.h"
#include "twn_util.h"
#include "twn_util_c.h"
#include "twn_control.h"
#include "twn_engine_context_c.h"
#include "twn_input.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
#include <stdbool.h>
#include <stdlib.h>
static void update_action_pressed_state(InputState *input, Action *action) {
@ -44,8 +45,8 @@ static void update_action_pressed_state(InputState *input, Action *action) {
else {
action->just_changed = !action->is_pressed;
action->is_pressed = true;
action->position.x = (float)input->mouse_window_position.x;
action->position.y = (float)input->mouse_window_position.y;
action->position.x = ((float)input->mouse_window_position.x - ctx.viewport_rect.x) / ctx.viewport_scale;
action->position.y = ((float)input->mouse_window_position.y - ctx.viewport_rect.y) / ctx.viewport_scale;
/* TODO: */
/*
@ -69,20 +70,40 @@ static void update_action_pressed_state(InputState *input, Action *action) {
}
static ActionHashItem *input_add_action(char const *action_name) {
SDL_assert(action_name);
Action new_action = { 0 };
new_action.bindings = SDL_calloc(ctx.keybind_slots, sizeof *new_action.bindings);
shput(ctx.input.action_hash, action_name, new_action);
return shgetp(ctx.input.action_hash, action_name);
}
static void input_delete_action(char const *action_name) {
SDL_assert(action_name);
ActionHashItem *action = shgetp_null(ctx.input.action_hash, action_name);
SDL_assert(action);
SDL_free(action->value.bindings);
shdel(ctx.input.action_hash, action_name);
}
static void input_bind_code_to_action(InputState *input,
char const *action_name,
ButtonSource source,
union ButtonCode code)
{
ActionHashItem *action_item = shgetp_null(input->action_hash, action_name);
if (action_item == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return;
}
if (!action_item)
action_item = input_add_action(action_name);
Action *action = &action_item->value;
/* check every binding to make sure this code isn't already bound */
for (size_t i = 0; i < (uint64_t)ctx.keybind_slots; ++i) {
for (size_t i = 0; i < action->num_bindings; ++i) {
Button *binding = &action->bindings[i];
if (binding->source != source)
@ -109,7 +130,8 @@ static void input_bind_code_to_action(InputState *input,
}
if (is_already_bound) {
log_warn("(%s) Code already bound to action \"%s\".", __func__, action_name);
/* keep it alive */
binding->in_use = true;
return;
}
}
@ -117,13 +139,14 @@ static void input_bind_code_to_action(InputState *input,
/* if we're at max bindings, forget the first element and shift the rest */
if (action->num_bindings == (uint64_t)ctx.keybind_slots) {
--action->num_bindings;
size_t shifted_size = (sizeof action->bindings) - (sizeof action->bindings[0]);
size_t shifted_size = sizeof action->bindings[0] * (ctx.keybind_slots - 1);
SDL_memmove(action->bindings, action->bindings + 1, shifted_size);
}
action->bindings[action->num_bindings++] = (Button) {
.source = source,
.code = code,
.in_use = true,
};
}
@ -134,16 +157,15 @@ static void input_unbind_code_from_action(InputState *input,
union ButtonCode code)
{
ActionHashItem *action_item = shgetp_null(input->action_hash, action_name);
if (action_item == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return;
}
if (!action_item)
action_item = input_add_action(action_name);
Action *action = &action_item->value;
/* check every binding to make sure this code is bound */
size_t index = 0;
bool is_bound = false;
for (index = 0; index < (uint64_t)ctx.keybind_slots; ++index) {
for (index = 0; index < action->num_bindings; ++index) {
Button *binding = &action->bindings[index];
if (binding->source != source)
@ -173,10 +195,8 @@ static void input_unbind_code_from_action(InputState *input,
break;
}
if (!is_bound) {
log_warn("(%s) Code is not bound to action \"%s\".", __func__, action_name);
if (!is_bound)
return;
}
/* remove the element to unbind and shift the rest so there isn't a gap */
size_t elements_after_index = action->num_bindings - index;
@ -196,25 +216,54 @@ void input_state_deinit(InputState *input) {
}
void input_state_update_postframe(InputState *input) {
/* TODO: don't spam it if it happens */
if (SDL_SetRelativeMouseMode(ctx.game_copy.mouse_capture && ctx.window_mouse_resident) != 0)
log_warn("(%s) Mouse capture isn't supported.", __func__);
}
void input_state_update(InputState *input) {
int x, y;
input->keyboard_state = SDL_GetKeyboardState(NULL);
input->mouse_state = SDL_GetMouseState(&input->mouse_window_position.x,
&input->mouse_window_position.y);
input->mouse_state = SDL_GetMouseState(&x, &y);
input->mouse_window_position = (Vec2){ (float)x, (float)y };
SDL_GetRelativeMouseState(&input->mouse_relative_position.x,
&input->mouse_relative_position.y);
SDL_GetRelativeMouseState(&x, &y);
input->mouse_relative_position = (Vec2){ (float)x, (float)y };
ctx.game.mouse_position = input->mouse_window_position;
ctx.game.mouse_movement = input->mouse_relative_position;
ctx.game.mouse_position.x = ((float)input->mouse_window_position.x - ctx.viewport_rect.x) / ctx.viewport_scale;
ctx.game.mouse_position.y = ((float)input->mouse_window_position.y - ctx.viewport_rect.y) / ctx.viewport_scale;
if (ctx.window_mouse_resident)
ctx.game.mouse_movement = input->mouse_relative_position;
else
ctx.game.mouse_movement = (Vec2){0};
for (size_t i = 0; i < shlenu(input->action_hash); ++i) {
Action *action = &input->action_hash[i].value;
/* collect unused */
for (size_t u = 0; u < action->num_bindings; ++u) {
Button *button = &action->bindings[u];
if (!button->in_use)
input_unbind_code_from_action(input, input->action_hash[i].key, button->source, button->code);
else
button->in_use = false;
}
update_action_pressed_state(input, action);
}
size_t removed = 0;
for (size_t i = 0; i < shlenu(input->action_hash); ++i) {
if (input->action_hash[i - removed].value.num_bindings == 0)
input_delete_action(input->action_hash[i - removed].key);
}
}
void input_bind_action_control(char const *action_name,
void input_action(char const *action_name,
Control control)
{
SDL_assert_always(action_name);
@ -236,57 +285,7 @@ void input_bind_action_control(char const *action_name,
}
void input_unbind_action_control(char const *action_name,
Control control)
{
SDL_assert_always(action_name);
if (CONTROL_SCANCODE_START <= control && control < CONTROL_SCANCODE_LIMIT)
input_unbind_code_from_action(&ctx.input,
action_name,
BUTTON_SOURCE_KEYBOARD_PHYSICAL,
(union ButtonCode) { .scancode = (SDL_Scancode)control });
else if (CONTROL_MOUSECODE_START <= control && control < CONTROL_MOUSECODE_LIMIT) {
uint8_t const mouse_button = (uint8_t)(control - CONTROL_MOUSECODE_START);
input_unbind_code_from_action(&ctx.input,
action_name,
BUTTON_SOURCE_MOUSE,
(union ButtonCode) { .mouse_button = (uint8_t)SDL_BUTTON(mouse_button)});
} else
log_warn("(%s) Invalid control value given: %i.", __func__, control);
}
void input_add_action(char const *action_name) {
SDL_assert_always(action_name);
if (shgeti(ctx.input.action_hash, action_name) >= 0) {
log_warn("(%s) Action \"%s\" is already registered.", __func__, action_name);
return;
}
Action new_action = { 0 };
new_action.bindings = ccalloc(ctx.keybind_slots, sizeof *new_action.bindings);
shput(ctx.input.action_hash, action_name, new_action);
}
void input_delete_action(char const *action_name) {
SDL_assert_always(action_name);
ActionHashItem *action = shgetp_null(ctx.input.action_hash, action_name);
if (action == NULL) {
log_warn("(%s) Action \"%s\" is not registered.", __func__, action_name);
return;
}
SDL_free(action->value.bindings);
shdel(ctx.input.action_hash, action_name);
}
bool input_is_action_pressed(char const *action_name) {
bool input_action_pressed(char const *action_name) {
SDL_assert_always(action_name);
ActionHashItem *action = shgetp_null(ctx.input.action_hash, action_name);
@ -298,7 +297,7 @@ bool input_is_action_pressed(char const *action_name) {
}
bool input_is_action_just_pressed(char const *action_name) {
bool input_action_just_pressed(char const *action_name) {
SDL_assert_always(action_name);
ActionHashItem *action = shgetp_null(ctx.input.action_hash, action_name);
@ -310,7 +309,7 @@ bool input_is_action_just_pressed(char const *action_name) {
}
bool input_is_action_just_released(char const *action_name) {
bool input_action_just_released(char const *action_name) {
SDL_assert_always(action_name);
ActionHashItem *action = shgetp_null(ctx.input.action_hash, action_name);
@ -322,7 +321,7 @@ bool input_is_action_just_released(char const *action_name) {
}
Vec2 input_get_action_position(char const *action_name) {
Vec2 input_action_position(char const *action_name) {
SDL_assert_always(action_name);
ActionHashItem *action = shgetp_null(ctx.input.action_hash, action_name);
@ -335,17 +334,6 @@ Vec2 input_get_action_position(char const *action_name) {
}
void input_set_mouse_captured(bool enabled) {
if (SDL_SetRelativeMouseMode(enabled) != 0)
log_warn("(%s) Mouse capture isn't supported.", __func__);
}
bool input_is_mouse_captured(void) {
return SDL_GetRelativeMouseMode();
}
void input_reset_state(InputState *input) {
for (size_t i = 0; i < shlenu(input->action_hash); ++i) {
Action *action = &input->action_hash[i].value;

View File

@ -1,11 +1,12 @@
#ifndef TWN_INPUT_C_H
#define TWN_INPUT_C_H
#include "twn_input.h"
#include "twn_vec.h"
#include "twn_types.h"
#include <SDL2/SDL.h>
#include <stdbool.h>
#define KEYBIND_SLOTS_DEFAULT 3
@ -32,6 +33,7 @@ typedef enum ButtonSource {
typedef struct Button {
enum ButtonSource source;
union ButtonCode code;
bool in_use;
} Button;
@ -59,8 +61,8 @@ typedef struct ActionHashItem {
typedef struct InputState {
const uint8_t *keyboard_state; /* array of booleans indexed by scancode */
ActionHashItem *action_hash;
Vec2i mouse_window_position;
Vec2i mouse_relative_position;
Vec2 mouse_window_position;
Vec2 mouse_relative_position;
uint32_t mouse_state; /* SDL mouse button bitmask */
ButtonSource last_active_source;
bool is_anything_just_pressed;
@ -74,6 +76,8 @@ void input_state_deinit(InputState *input);
void input_state_update(InputState *input);
void input_state_update_postframe(InputState *input);
void input_reset_state(InputState *input);
#endif

View File

@ -4,7 +4,6 @@
#include "twn_util.h"
#include "twn_util_c.h"
#include "twn_game_object_c.h"
#include "twn_audio_c.h"
#include "twn_textures_c.h"
#include <SDL2/SDL.h>
@ -12,13 +11,6 @@
#include <stb_ds.h>
#include <toml.h>
/* TODO: should not be used here directly */
#ifdef EMSCRIPTEN
#include <GLES2/gl2.h>
#else
#include <glad/glad.h>
#endif
#include <stdbool.h>
#include <limits.h>
@ -27,6 +19,9 @@
#define PACKAGE_EXTENSION "btw"
static SDL_Thread *opengl_load_thread;
static int event_callback(void *userdata, SDL_Event *event) {
(void)userdata;
@ -37,11 +32,21 @@ static int event_callback(void *userdata, SDL_Event *event) {
switch (event->window.event) {
case SDL_WINDOWEVENT_SIZE_CHANGED:
ctx.window_dims.x = event->window.data1;
ctx.window_dims.y = event->window.data2;
ctx.window_dims.x = (float)event->window.data1;
ctx.window_dims.y = (float)event->window.data2;
ctx.resync_flag = true;
break;
case SDL_WINDOWEVENT_FOCUS_LOST: {
ctx.window_mouse_resident = false;
break;
}
case SDL_WINDOWEVENT_FOCUS_GAINED: {
ctx.window_mouse_resident = true;
break;
}
default:
break;
}
@ -90,33 +95,36 @@ static void poll_events(void) {
}
#ifndef EMSCRIPTEN
static void APIENTRY opengl_log(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar* message,
const void* userParam)
{
(void)source;
(void)type;
(void)id;
(void)severity;
(void)userParam;
log_info("OpenGL: %.*s\n", length, message);
}
#endif
static void preserve_persistent_ctx_fields(void) {
ctx.game.udata = ctx.game_copy.udata;
}
static void update_viewport(void) {
Rect new_viewport;
float new_scale;
if ((float)ctx.window_dims.x / (float)ctx.window_dims.y > (float)ctx.base_render_width / (float)ctx.base_render_height) {
float ratio = (float)ctx.window_dims.y / (float)ctx.base_render_height;
float w = ((float)ctx.base_render_width * ratio);
new_viewport.x = ctx.window_dims.x / 2 - w / 2;
new_viewport.y = 0;
new_viewport.w = w;
new_viewport.h = ctx.window_dims.y;
new_scale = ratio;
} else {
float ratio = (float)ctx.window_dims.x / (float)ctx.base_render_width;
float h = ((float)ctx.base_render_height * ratio);
new_viewport.x = 0;
new_viewport.y = ctx.window_dims.y / 2 - h / 2;
new_viewport.w = ctx.window_dims.x;
new_viewport.h = h;
new_scale = ratio;
}
ctx.viewport_rect = new_viewport;
ctx.viewport_scale = new_scale;
}
static void main_loop(void) {
/*
if (!ctx.is_running) {
@ -198,20 +206,36 @@ static void main_loop(void) {
/* finally, let's get to work */
int frames = 0;
while (ctx.frame_accumulator >= ctx.desired_frametime * ctx.update_multiplicity) {
while (ctx.frame_accumulator >= ctx.desired_frametime) {
frames += 1;
for (size_t i = 0; i < ctx.update_multiplicity; ++i) {
/* TODO: disable rendering pushes on not-last ? */
render_queue_clear();
poll_events();
input_state_update(&ctx.input);
game_object_tick();
preserve_persistent_ctx_fields();
/* TODO: disable rendering pushes on not-last ? */
render_queue_clear();
poll_events();
if (ctx.window_size_has_changed)
update_viewport();
input_state_update(&ctx.input);
game_object_tick();
input_state_update_postframe(&ctx.input);
ctx.frame_accumulator -= ctx.desired_frametime;
ctx.game.frame_number = (ctx.game.frame_number % ULLONG_MAX) + 1;
ctx.game.initialization_needed = false;
/* TODO: make it works when ctx.ticks_per_second != 60 */
#ifdef TWN_FEATURE_PUSH_AUDIO
static uint8_t audio_buffer[(AUDIO_FREQUENCY / 60) * sizeof (float) * 2];
if (ctx.audio_initialized) {
audio_callback(NULL, audio_buffer, sizeof audio_buffer);
if (SDL_QueueAudio(ctx.audio_device, audio_buffer, sizeof audio_buffer))
CRY_SDL("Error queueing audio: ");
}
#endif
/* TODO: ctx.game_copy = ctx.game should be placed after it, but it messes with state used in render() */
preserve_persistent_ctx_fields();
ctx.frame_accumulator -= ctx.desired_frametime;
ctx.game.frame_number++;
/* TODO: should we ask for reinitialization in such case? */
if (ctx.game.frame_number > 16777216)
ctx.game.frame_number = 0;
ctx.game.initialization_needed = false;
}
/* TODO: in some cases machine might want to assume frames will be fed as much as possible */
@ -320,19 +344,25 @@ ERR_PACK_MANIFEST_PATH_ALLOC_FAIL:
}
static bool initialize(void) {
if (SDL_Init(SDL_INIT_EVERYTHING & ~SDL_INIT_HAPTIC) == -1) {
CRY_SDL("SDL initialization failed.");
return false;
}
static int opengl_load_thread_fn(void *data) {
(void)data;
SDL_GL_LoadLibrary(NULL);
return 0;
}
static bool initialize(void) {
/* first things first, most things here will be loaded from the config file */
/* it's expected to be present in the data directory, no matter what */
/* that is why PhysicsFS is initialized before anything else */
toml_set_memutil(SDL_malloc, SDL_free);
profile_start("pack dependency resolution");
/* time to orderly resolve any dependencies present */
resolve_pack_dependencies("data");
profile_end("pack dependency resolution");
/* load the config file into an opaque table */
{
@ -396,11 +426,7 @@ static bool initialize(void) {
/* debug mode defaults to being enabled */
/* pass --debug or --release to force a mode, ignoring configuration */
toml_datum_t datum_debug = toml_bool_in(game, "debug");
if (!datum_debug.ok) {
ctx.game.debug = true;
} else {
ctx.game.debug = datum_debug.u.b;
}
ctx.game.debug = datum_debug.ok ? datum_debug.u.b : true;
#ifdef EMSCRIPTEN
/* emscripten interpretes those as GL ES version against WebGL */
@ -424,135 +450,61 @@ static bool initialize(void) {
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1);
/* init got far enough to create a window */
{
toml_datum_t datum_title = toml_string_in(about, "title");
if (!datum_title.ok) {
CRY("Initialization failed", "Valid about.title expected in configuration file");
goto fail;
}
/* not yet used
toml_datum_t datum_developer = toml_string_in(about, "developer");
if (!datum_developer.ok) {
CRY("Initialization failed", "Valid about.developer expected in configuration file");
goto fail;
}
*/
toml_datum_t datum_base_render_width = toml_int_in(game, "base_render_width");
toml_datum_t datum_title = toml_string_in(about, "title");
if (!datum_title.ok)
datum_title.u.s = SDL_strdup("townengine project");
ctx.title = datum_title.u.s;
/* not yet used
toml_datum_t datum_developer = toml_string_in(about, "developer");
if (!datum_developer.ok) {
CRY("Initialization failed", "Valid about.developer expected in configuration file");
goto fail;
}
*/
toml_array_t *datum_resolution = toml_array_in(game, "resolution");
if (datum_resolution) {
toml_datum_t datum_base_render_width = toml_int_at(datum_resolution, 0);
if (!datum_base_render_width.ok) {
CRY("Initialization failed", "Valid game.base_render_width expected in configuration file");
CRY("Initialization failed", "Valid game.resolution expected in configuration file");
goto fail;
}
ctx.base_render_width = datum_base_render_width.u.i;
ctx.game.resolution.x = (int)ctx.base_render_width;
toml_datum_t datum_base_render_height = toml_int_in(game, "base_render_height");
toml_datum_t datum_base_render_height = toml_int_at(datum_resolution, 1);
if (!datum_base_render_height.ok) {
CRY("Initialization failed", "Valid game.base_render_height expected in configuration file");
CRY("Initialization failed", "Valid game.resolution expected in configuration file");
goto fail;
}
ctx.base_render_width = datum_base_render_width.u.i;
ctx.base_render_height = datum_base_render_height.u.i;
ctx.game.resolution.y = (int)ctx.base_render_height;
ctx.window = SDL_CreateWindow(datum_title.u.s,
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
(int)datum_base_render_width.u.i,
(int)datum_base_render_height.u.i,
//SDL_WINDOW_ALLOW_HIGHDPI |
SDL_WINDOW_RESIZABLE |
SDL_WINDOW_OPENGL);
SDL_free(datum_title.u.s);
//SDL_free(datum_developer.u.s);
} else {
ctx.base_render_width = 640;
ctx.base_render_height = 360;
}
if (ctx.window == NULL) {
CRY_SDL("Window creation failed.");
goto fail;
}
ctx.game.resolution.x = (float)ctx.base_render_width;
ctx.game.resolution.y = (float)ctx.base_render_height;
ctx.gl_context = SDL_GL_CreateContext(ctx.window);
if (!ctx.gl_context) {
CRY_SDL("GL context creation failed.");
goto fail;
}
if (SDL_GL_MakeCurrent(ctx.window, ctx.gl_context)) {
CRY_SDL("GL context binding failed.");
goto fail;
}
if (SDL_GL_SetSwapInterval(-1))
SDL_GL_SetSwapInterval(1);
#ifndef EMSCRIPTEN
if (gladLoadGL() == 0) {
CRY("Init", "GLAD failed");
goto fail;
}
#endif
log_info("OpenGL context: %s\n", glGetString(GL_VERSION));
#ifndef EMSCRIPTEN
glHint(GL_TEXTURE_COMPRESSION_HINT, GL_NICEST);
glHint(GL_GENERATE_MIPMAP_HINT, GL_NICEST);
glHint(GL_FOG_HINT, GL_FASTEST);
#endif
/* might need this to have multiple windows */
ctx.window_id = SDL_GetWindowID(ctx.window);
glViewport(0, 0, (GLsizei)ctx.base_render_width, (GLsizei)ctx.base_render_height);
//SDL_free(datum_developer.u.s);
/* TODO: */
// SDL_GetRendererOutputSize(ctx.renderer, &ctx.window_w, &ctx.window_h);
ctx.window_dims.x = (int)ctx.base_render_width;
ctx.window_dims.y = (int)ctx.base_render_height;
ctx.window_dims.x = (float)ctx.base_render_width;
ctx.window_dims.y = (float)ctx.base_render_height;
/* add a watcher for immediate updates on window size */
SDL_AddEventWatch(event_callback, NULL);
/* audio initialization */
{
SDL_AudioSpec request, got;
SDL_zero(request);
request.freq = AUDIO_FREQUENCY;
request.format = AUDIO_F32;
request.channels = 2;
request.callback = audio_callback;
/* TODO: check for errors */
ctx.audio_device = SDL_OpenAudioDevice(NULL, 0, &request, &got, 0);
ctx.audio_stream_format = got.format;
ctx.audio_stream_frequency = got.freq;
ctx.audio_stream_channel_count = got.channels;
SDL_assert_always(got.format == AUDIO_F32);
SDL_assert_always(got.channels == 2);
SDL_PauseAudioDevice(ctx.audio_device, 0);
}
/* you could change this at runtime if you wanted */
ctx.update_multiplicity = 1;
#ifndef EMSCRIPTEN
/* hook up opengl debugging callback */
if (ctx.game.debug) {
glEnable(GL_DEBUG_OUTPUT);
glDebugMessageCallback(opengl_log, NULL);
}
#endif
/* random seeding */
/* SDL_GetPerformanceCounter returns some platform-dependent number. */
/* it should vary between game instances. i checked! random enough for me. */
ctx.game.random_seed = SDL_GetPerformanceCounter();
srand((unsigned int)ctx.game.random_seed);
stbds_rand_seed(ctx.game.random_seed);
ctx.game.random_seed = (float)(SDL_GetPerformanceCounter() % 16777216);
srand((unsigned int)(SDL_GetPerformanceCounter()));
stbds_rand_seed((size_t)(SDL_GetPerformanceCounter()));
/* main loop machinery */
toml_datum_t datum_ticks_per_second = toml_int_in(engine, "ticks_per_second");
@ -620,9 +572,9 @@ static bool initialize(void) {
if (!datum_font_filtering.ok) {
ctx.font_filtering = TEXT_FONT_FILTERING_DEFAULT;
} else {
if (SDL_strcmp(datum_font_filtering.u.s, "nearest") == 0) {
if (SDL_strncmp(datum_font_filtering.u.s, "nearest", sizeof "nearest" - 1) == 0) {
ctx.font_filtering = TEXTURE_FILTER_NEAREAST;
} else if (SDL_strcmp(datum_font_filtering.u.s, "linear") == 0) {
} else if (SDL_strncmp(datum_font_filtering.u.s, "linear", sizeof "linear" - 1) == 0) {
ctx.font_filtering = TEXTURE_FILTER_LINEAR;
} else {
ctx.font_filtering = TEXT_FONT_FILTERING_DEFAULT;
@ -635,9 +587,6 @@ static bool initialize(void) {
ctx.render_queue_2d = NULL;
ctx.uncolored_mesh_batches = NULL;
textures_cache_init(&ctx.texture_cache, ctx.window);
text_cache_init(&ctx.text_cache);
/* input */
toml_datum_t datum_keybind_slots = toml_int_in(engine, "keybind_slots");
if (!datum_keybind_slots.ok) {
@ -651,14 +600,67 @@ static bool initialize(void) {
}
input_state_init(&ctx.input);
/* scripting */
/*
if (!scripting_init(ctx)) {
ctx.render_double_buffered = true;
ctx.window_mouse_resident = true;
ctx.game.fog_color = (Color){ 255, 255, 255, 255 }; /* TODO: pick some grey? */
update_viewport();
profile_start("game object load");
/* now we can actually start doing stuff */
game_object_load();
profile_end("game object load");
/* delayed as further as possible so that more work is done before we have to wait */
SDL_WaitThread(opengl_load_thread, NULL);
profile_end("opengl loading");
/* TODO: investigate viability of detached thread driver and window creation, as it's the worst load time offender */
profile_start("window creation");
ctx.window = SDL_CreateWindow(ctx.title,
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
(int)ctx.base_render_width,
(int)ctx.base_render_height,
//SDL_WINDOW_ALLOW_HIGHDPI |
SDL_WINDOW_RESIZABLE |
SDL_WINDOW_OPENGL);
profile_end("window creation");
if (ctx.window == NULL) {
CRY_SDL("Window creation failed.");
goto fail;
}
*/
ctx.render_double_buffered = true;
profile_start("opengl context creation");
ctx.gl_context = SDL_GL_CreateContext(ctx.window);
if (!ctx.gl_context) {
CRY_SDL("GL context creation failed.");
goto fail;
}
if (SDL_GL_MakeCurrent(ctx.window, ctx.gl_context)) {
CRY_SDL("GL context binding failed.");
goto fail;
}
/* TODO: figure out what we ultimately prefer */
SDL_GL_SetSwapInterval(0);
if (!render_init())
goto fail;
setup_viewport(0, 0, (int)ctx.base_render_width, (int)ctx.base_render_height);
profile_end("opengl context creation");
/* might need this to have multiple windows */
ctx.window_id = SDL_GetWindowID(ctx.window);
profile_start("texture and text cache initialization");
textures_cache_init(&ctx.texture_cache, ctx.window);
text_cache_init(&ctx.text_cache);
profile_end("texture and text cache initialization");
return true;
@ -670,10 +672,6 @@ fail:
/* will not be called on an abnormal exit */
static void clean_up(void) {
/*
scripting_deinit(ctx);
*/
input_state_deinit(&ctx.input);
text_cache_deinit(&ctx.text_cache);
textures_cache_deinit(&ctx.texture_cache);
@ -686,7 +684,9 @@ static void clean_up(void) {
PHYSFS_deinit();
SDL_free(ctx.base_dir);
SDL_free(ctx.title);
SDL_GL_DeleteContext(ctx.gl_context);
SDL_GL_UnloadLibrary();
SDL_Quit();
}
@ -698,6 +698,22 @@ static void reset_state(void) {
int enter_loop(int argc, char **argv) {
profile_start("startup");
profile_start("SDL initialization");
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS) == -1) {
CRY_SDL("SDL initialization failed.");
return EXIT_FAILURE;
}
profile_end("SDL initialization");
profile_start("opengl loading");
opengl_load_thread = SDL_CreateThread(opengl_load_thread_fn, "opengl loader", NULL);
if (!opengl_load_thread) {
CRY_SDL("Cannot create opengl loading thread: ");
return EXIT_FAILURE;
}
ctx.argc = argc;
ctx.argv = argv;
ctx.base_dir = SDL_GetBasePath();
@ -716,7 +732,7 @@ int enter_loop(int argc, char **argv) {
for (int i = 1; i < argc; ++i) {
/* override data directory */
if (SDL_strcmp(argv[i], "--data-dir") == 0) {
if (SDL_strncmp(argv[i], "--data-dir", sizeof "--data-dir" - 1) == 0) {
if (argv[i+1] == NULL || SDL_strncmp(argv[i+1], "--", 2) == 0) {
CRY("Data dir mount override failed.", "No arguments passed (expected 1).");
return EXIT_FAILURE;
@ -733,14 +749,14 @@ int enter_loop(int argc, char **argv) {
}
/* force debug mode */
if (SDL_strcmp(argv[i], "--debug") == 0) {
if (SDL_strncmp(argv[i], "--debug", sizeof "--debug" - 1) == 0) {
force_debug = true;
continue;
}
/* force release mode */
if (SDL_strcmp(argv[i], "--release") == 0) {
force_release = false;
if (SDL_strncmp(argv[i], "--release", sizeof "--release" - 1) == 0) {
force_release = true;
continue;
}
}
@ -773,12 +789,11 @@ int enter_loop(int argc, char **argv) {
ctx.game.debug = false;
}
/* now we can actually start doing stuff */
game_object_load();
ctx.was_successful = true;
ctx.game.initialization_needed = true;
profile_end("startup");
while (ctx.is_running) {
if (game_object_try_reloading()) {
ctx.game.initialization_needed = true;
@ -788,6 +803,9 @@ int enter_loop(int argc, char **argv) {
main_loop();
}
if (ctx.game.debug)
profile_list_stats();
/* loop is over */
game_object_unload();

View File

@ -1,6 +1,8 @@
#include "twn_loop.h"
#ifndef EMSCRIPTEN
#define SDL_MAIN_HANDLED
#endif
#include <SDL2/SDL.h>

View File

@ -40,6 +40,44 @@ static int load_eof_callback(void *user) {
return context->position == context->size;
}
static SDL_Surface *missing_texture_surface;
static uint16_t missing_texture_id;
static SDL_Surface *gen_missing_texture_surface(void) {
Uint32 rmask, gmask, bmask;
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
rmask = 0xff000000;
gmask = 0x00ff0000;
bmask = 0x0000ff00;
#else
rmask = 0x000000ff;
gmask = 0x0000ff00;
bmask = 0x00ff0000;
#endif
if (!missing_texture_surface) {
uint8_t *data = SDL_malloc(64 * 64 * 3);
for (int y = 0; y < 64; ++y) {
for (int x = 0; x < 64; ++x) {
/* diagonal stripes, chosen so that asked pixel uvs are corresponding to final output */
data[(y * 64 + x) * 3 + 0] = (x / 2 + y / 2) % 2 == 0 ? 175 : 0;
data[(y * 64 + x) * 3 + 1] = (x / 2 + y / 2) % 2 == 0 ? 0 : 0;
data[(y * 64 + x) * 3 + 2] = (x / 2 + y / 2) % 2 == 0 ? 175 : 0;
}
}
missing_texture_surface = SDL_CreateRGBSurfaceFrom(data, 64, 64,
3 * 8,
64 * 3,
rmask, gmask, bmask, 0);
}
return missing_texture_surface;
}
SDL_Surface *textures_load_surface(const char *path) {
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
if (handle == NULL)
@ -93,8 +131,9 @@ ERR_CANNOT_CREATE_SURFACE:
ERR_CANNOT_READ_IMAGE:
ERR_CANNOT_OPEN_FILE:
CRY(path, "Failed to load image. Aborting...");
die_abruptly();
/* something didn't worked out, use a stub texture */
log_warn("Cannot open image: %s, using a stub", path);
return gen_missing_texture_surface();
}
@ -147,6 +186,8 @@ static void upload_texture_from_surface(GPUTexture texture, SDL_Surface *surface
static void recreate_current_atlas_texture(TextureCache *cache) {
profile_start("atlas recreation");
/* TODO: should surfaces be freed after they cannot be referenced in atlas builing? */
/* for example, if full page of 64x64 tiles was already filled, there's no real reason to process them further */
SDL_Surface *atlas_surface = cache->atlas_surfaces[cache->atlas_index];
@ -177,6 +218,8 @@ static void recreate_current_atlas_texture(TextureCache *cache) {
/* texturize it! */
upload_texture_from_surface(cache->atlas_textures[cache->atlas_index], atlas_surface);
profile_end("atlas recreation");
}
@ -268,10 +311,9 @@ void textures_cache_init(TextureCache *cache, SDL_Window *window) {
cache->window = window;
sh_new_arena(cache->hash);
cache->node_buffer = SDL_calloc(ctx.texture_atlas_size, sizeof *cache->node_buffer);
cache->node_buffer = SDL_malloc(ctx.texture_atlas_size * sizeof *cache->node_buffer);
add_new_atlas(cache);
recreate_current_atlas_texture(cache);
}
@ -290,7 +332,10 @@ void textures_cache_deinit(TextureCache *cache) {
/* free cache hashes */
for (size_t i = 0; i < shlenu(cache->hash); ++i) {
stbi_image_free(cache->hash[i].value.data->pixels);
if (missing_texture_surface && cache->hash[i].value.data->pixels != missing_texture_surface->pixels)
stbi_image_free(cache->hash[i].value.data->pixels);
else
SDL_free(cache->hash[i].value.data->pixels);
SDL_FreeSurface(cache->hash[i].value.data);
}
shfree(cache->hash);
@ -334,6 +379,9 @@ static TextureKey textures_load(TextureCache *cache, const char *path) {
return (TextureKey){ (uint16_t)i };
SDL_Surface *surface = textures_load_surface(path);
if (surface == missing_texture_surface && missing_texture_id != 0)
return (TextureKey){ missing_texture_id };
Texture new_texture = {
.data = surface,
.mode = infer_texture_mode(surface),
@ -350,13 +398,22 @@ static TextureKey textures_load(TextureCache *cache, const char *path) {
new_texture.loner_texture = create_gpu_texture(TEXTURE_FILTER_NEAREAST, true);
upload_texture_from_surface(new_texture.loner_texture, surface);
new_texture.srcrect = (Rect) { .w = (float)surface->w, .h = (float)surface->h };
} else {
/* will be fully populated as the atlas updates */
new_texture.atlas_index = cache->atlas_index;
cache->is_dirty = true;
}
shput(cache->hash, path, new_texture);
return (TextureKey){ (uint16_t)shgeti(cache->hash, path) };
uint16_t const id = (uint16_t)shlenu(cache->hash) - 1;
/* reuse this id for every later missing texture */
if (surface == missing_texture_surface)
missing_texture_id = id;
return (TextureKey){ id };
}
@ -439,6 +496,8 @@ TextureKey textures_get_key(TextureCache *cache, const char *path) {
}
}
/* TODO: this will be bad when dynamic strings are involved */
/* to mitigate that we could free ptr_to_texture each frame */
/* try loading */
last_texture = textures_load(cache, path);
hmput(ptr_to_texture, path, last_texture);
@ -533,7 +592,7 @@ void textures_bind_repeating(const TextureCache *cache, TextureKey key) {
upload_gpu_texture(repeating_texture,
texture.data->pixels,
4,
texture.data->format->BytesPerPixel,
texture.data->w,
texture.data->h);

View File

@ -1,7 +1,7 @@
#ifndef TWN_TEXTURES_C_H
#define TWN_TEXTURES_C_H
#include "twn_util.h"
#include "twn_types.h"
#include "twn_texture_modes.h"
#include "rendering/twn_gpu_texture_c.h"
@ -17,8 +17,8 @@
typedef struct Texture {
Rect srcrect; /* position in atlas */
SDL_Surface *data; /* original image data */
Rect srcrect; /* position in atlas */
SDL_Surface *data; /* original image data */
int atlas_index;
GPUTexture loner_texture; /* stored directly for loners, == 0 means atlas_index should be used */
GPUTexture repeating_texture; /* separately allocated Texture, for loners == loner_texture */

19
src/twn_types_c.h Normal file
View File

@ -0,0 +1,19 @@
#ifndef TWN_TYPES_C_H
#define TWN_TYPES_C_H
#include <stdint.h>
typedef struct Vec4 {
float x;
float y;
float z;
float w;
} Vec4;
typedef struct Matrix4 {
Vec4 row[4];
} Matrix4;
#endif

View File

@ -9,6 +9,17 @@
#include <stdarg.h>
static struct ProfileItem {
char const *key;
struct Profile {
uint64_t tick_start;
uint64_t tick_accum;
uint64_t sample_count;
uint64_t worst_tick;
} value;
} *profiles;
void cry_impl(const char *file, const int line, const char *title, const char *text) {
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
"TEARS AT %s:%d: %s ... %s", file, line, title, text);
@ -197,59 +208,27 @@ bool strends(const char *str, const char *suffix) {
}
bool overlap_rect(const Recti *a, const Recti *b, Recti *result) {
SDL_Rect a_sdl = { a->x, a->y, a->w, a->h };
SDL_Rect b_sdl = { b->x, b->y, b->w, b->h };
SDL_Rect result_sdl = { 0 };
bool intersection = SDL_IntersectRect(&a_sdl, &b_sdl, &result_sdl);
if (result != NULL)
*result = (Recti){ result_sdl.x, result_sdl.y, result_sdl.w, result_sdl.h };
return intersection;
}
bool overlap_frect(const Rect *a, const Rect *b, Rect *result) {
SDL_FRect a_sdl = { a->x, a->y, a->w, a->h };
SDL_FRect b_sdl = { b->x, b->y, b->w, b->h };
/* TODO: have our own */
Rect rect_overlap(const Rect a, const Rect b) {
SDL_FRect a_sdl = { a.x, a.y, a.w, a.h };
SDL_FRect b_sdl = { b.x, b.y, b.w, b.h };
SDL_FRect result_sdl = { 0 };
bool intersection = SDL_IntersectFRect(&a_sdl, &b_sdl, &result_sdl);
(void)SDL_IntersectFRect(&a_sdl, &b_sdl, &result_sdl);
if (result != NULL)
*result = (Rect){ result_sdl.x, result_sdl.y, result_sdl.w, result_sdl.h };
return intersection;
return (Rect){ result_sdl.x, result_sdl.y, result_sdl.w, result_sdl.h };
}
bool intersect_rect(const Recti *a, const Recti *b) {
SDL_Rect a_sdl = { a->x, a->y, a->w, a->h };
SDL_Rect b_sdl = { b->x, b->y, b->w, b->h };
return SDL_HasIntersection(&a_sdl, &b_sdl);
}
bool intersect_frect(const Rect *a, const Rect *b) {
SDL_FRect a_sdl = { a->x, a->y, a->w, a->h };
SDL_FRect b_sdl = { b->x, b->y, b->w, b->h };
/* TODO: have our own */
bool rect_intersects(const Rect a, const Rect b) {
SDL_FRect a_sdl = { a.x, a.y, a.w, a.h };
SDL_FRect b_sdl = { b.x, b.y, b.w, b.h };
return SDL_HasIntersectionF(&a_sdl, &b_sdl);
}
Rect to_frect(Recti rect) {
return (Rect) {
.h = (float)rect.h,
.w = (float)rect.w,
.x = (float)rect.x,
.y = (float)rect.y,
};
}
Vec2 frect_center(Rect rect) {
Vec2 rect_center(Rect rect) {
return (Vec2){
.x = rect.x + rect.w / 2,
.y = rect.y + rect.h / 2,
@ -257,25 +236,52 @@ Vec2 frect_center(Rect rect) {
}
void tick_timer(int *value) {
*value = MAX(*value - 1, 0);
int32_t timer_tick_frames(int32_t frames_left) {
SDL_assert(frames_left >= 0);
return MAX(frames_left - 1, 0);
}
void tick_ftimer(float *value) {
*value = MAX(*value - ((float)ctx.delta_time / (float)ctx.clocks_per_second), 0.0f);
float timer_tick_seconds(float seconds_left) {
SDL_assert(seconds_left >= 0);
return MAX(seconds_left - ((float)ctx.delta_time / (float)ctx.clocks_per_second), 0.0f);
}
bool repeat_ftimer(float *value, float at) {
*value -= (float)ctx.delta_time / (float)ctx.clocks_per_second;
if (*value < 0.0f) {
*value += at;
return true;
TimerElapseFramesResult timer_elapse_frames(int32_t frames_left, int32_t interval) {
SDL_assert(frames_left >= 0);
SDL_assert(interval > 0);
frames_left -= 1;
bool elapsed = false;
if (frames_left <= 0) {
elapsed = true;
frames_left += interval;
}
return false;
return (TimerElapseFramesResult) {
.elapsed = elapsed,
.frames_left = frames_left
};
}
TimerElapseSecondsResult timer_elapse_seconds(float seconds_left, float interval) {
SDL_assert(seconds_left >= 0);
SDL_assert(interval > 0);
seconds_left -= (float)ctx.delta_time / (float)ctx.clocks_per_second;
bool elapsed = false;
if (seconds_left <= 0.0f) {
elapsed = true;
seconds_left += interval;
}
return (TimerElapseSecondsResult) {
.elapsed = elapsed,
.seconds_left = seconds_left
};
}
/* TODO: handle utf8 */
char *expand_asterisk(const char *mask, const char *to) {
const char *offset = SDL_strchr(mask, '*');
@ -292,3 +298,80 @@ char *expand_asterisk(const char *mask, const char *to) {
SDL_memcpy(str + (offset - mask) + to_len, offset + 1, mask_len - (offset - mask));
return str;
}
void profile_start(char profile[const static 1]) {
uint64_t tick_accum = 0, sample_count = 0, worst_tick = 0;
struct ProfileItem const *p = shgetp_null(profiles, profile);
if (p) {
tick_accum = p->value.tick_accum;
sample_count = p->value.sample_count;
worst_tick = p->value.worst_tick;
}
shput(profiles, profile, ((struct Profile) {
.tick_start = SDL_GetPerformanceCounter(),
.tick_accum = tick_accum,
.sample_count = sample_count,
.worst_tick = worst_tick,
}));
}
void profile_end(char profile[const static 1]) {
struct ProfileItem *p = shgetp_null(profiles, profile);
if (!p) {
log_warn("Profile %s wasn't started!", profile);
return;
}
uint64_t took = SDL_GetPerformanceCounter() - p->value.tick_start;
p->value.tick_accum += took;
p->value.sample_count++;
if (p->value.worst_tick < took)
p->value.worst_tick = took;
}
void profile_list_stats(void) {
for (size_t i = 0; i < shlenu(profiles); ++i) {
if (profiles[i].value.sample_count == 0) {
log_warn("Profile %s was started, but not once finished.", profiles[i].key);
}
else if (profiles[i].value.sample_count == 1) {
log_info("Profile '%s' took: %fs",
profiles[i].key,
(double)profiles[i].value.tick_accum / (double)(SDL_GetPerformanceFrequency()));
}
else if (profiles[i].value.sample_count > 1) {
log_info("Profile '%s' on average took: %fs, worst case: %fs, sample count: %llu",
profiles[i].key,
(double)profiles[i].value.tick_accum /
(double)profiles[i].value.sample_count /
(double)(SDL_GetPerformanceFrequency()),
(double)profiles[i].value.worst_tick / (double)(SDL_GetPerformanceFrequency()),
profiles[i].value.sample_count);
}
}
}
void log_vec2(Vec2 vector, char const *message) {
if (!message) message = "Vec2";
log_info("%s = (%f, %f)", message, (double)vector.x, (double)vector.y);
}
void log_vec3(Vec3 vector, char const *message) {
if (!message) message = "Vec3";
log_info("%s = (%f, %f, %f)", message, (double)vector.x, (double)vector.y, (double)vector.z);
}
void log_rect(Rect rect, char const *message) {
if (!message) message = "Rect";
log_info("%s = (%f, %f, %f, %f)", message, (double)rect.x, (double)rect.y, (double)rect.w, (double)rect.h);
}

View File

@ -6,7 +6,7 @@
#include <SDL2/SDL.h>
#include <stdbool.h>
#include <math.h>
#define MAX SDL_max
#define MIN SDL_min

View File

@ -1,4 +1,4 @@
CMAKE_MINIMUM_REQUIRED(VERSION 2.8)
CMAKE_MINIMUM_REQUIRED(VERSION 3.5)
PROJECT(libxm C)
FUNCTION(OPTION_AND_DEFINE name description default_value)

View File

@ -11,7 +11,7 @@
set(PHYSFS_VERSION 3.2.0)
cmake_minimum_required(VERSION 3.0)
cmake_minimum_required(VERSION 3.5)
project(PhysicsFS VERSION ${PHYSFS_VERSION} LANGUAGES C )

View File

@ -548,7 +548,7 @@ extern void * stbds_shmode_func(size_t elemsize, int mode);
#define stbds_arraddnindex(a,n)(stbds_arrmaybegrow(a,n), (n) ? (stbds_header(a)->length += (n), stbds_header(a)->length-(n)) : stbds_arrlen(a))
#define stbds_arraddnoff stbds_arraddnindex
#define stbds_arrlast(a) ((a)[stbds_header(a)->length-1])
#define stbds_arrfree(a) ((void) ((a) ? STBDS_FREE(NULL,stbds_header(a)) : (void)0), (a)=NULL)
#define stbds_arrfree(a) ((void) ((a) ? stbds_arrfreef(a) : (void)0), (a)=NULL)
#define stbds_arrdel(a,i) stbds_arrdeln(a,i,1)
#define stbds_arrdeln(a,i,n) (memmove(&(a)[i], &(a)[(i)+(n)], sizeof *(a) * (stbds_header(a)->length-(n)-(i))), stbds_header(a)->length -= (n))
#define stbds_arrdelswap(a,i) ((a)[i] = stbds_arrlast(a), stbds_header(a)->length -= 1)