Compare commits
88 Commits
a223506a5f
...
main
Author | SHA1 | Date | |
---|---|---|---|
75890b1a71 | |||
73db3e57dc | |||
2975aa2dfb | |||
6726faf719 | |||
183dfa6be5 | |||
e974194af0 | |||
8607aa48ec | |||
f6600dfbda | |||
bdabd04388 | |||
0e075ec334 | |||
b256fc903a | |||
b52ecaeaa0 | |||
37e46e9a7e | |||
a472e6af52 | |||
66b2f04d9d | |||
90f4097070 | |||
829ff4780c | |||
8e15c9ec3c | |||
474ea84a77 | |||
7b8b9416ba | |||
8ed8158ae6 | |||
48e3a4c233 | |||
56530f9864 | |||
f86f3dd41a | |||
adae6be7e5 | |||
cd3033f9c4 | |||
e11e63f273 | |||
75737b738f | |||
ce2c2513aa | |||
36c0af9953 | |||
826622cd58 | |||
78b6a26de9 | |||
5f7b8bac6d | |||
6d6230c6a1 | |||
c07e16490e | |||
f5e55bb997 | |||
1e6e323fe1 | |||
dbf9599fe5 | |||
923cd81571 | |||
733a1786ab | |||
a03e1d885d | |||
67feb5974a | |||
5be4ed4645 | |||
4a41f47a58 | |||
35bb26705a | |||
13bc71a28d | |||
b97a155de4 | |||
5df80addeb | |||
787977b747 | |||
f90b973d86 | |||
32675c012c | |||
a97515e948 | |||
ed8e826b94 | |||
4e5ff9433c | |||
55829a1bef | |||
119bd52c51 | |||
5abd1ced1c | |||
80db96672d | |||
2f6f7852be | |||
307d5552f6 | |||
5911cbd980 | |||
e47b761a2c | |||
844283c2fb | |||
09eac707c3 | |||
5e89710458 | |||
4bc1feb826 | |||
1c3973c6a2 | |||
da5bdb4fae | |||
ed2afec5a7 | |||
6812c7c13d | |||
8c0f43ec34 | |||
23fbd45564 | |||
a36459397e | |||
5f3920fdba | |||
f57525cea6 | |||
6b2901be28 | |||
9f0d15b9f6 | |||
b46331e08d | |||
d2938da8e2 | |||
9134e51817 | |||
d66eda1894 | |||
a88392b9e9 | |||
05f85062e8 | |||
d5aec5e6e1 | |||
62866d33ae | |||
ce7240d423 | |||
7a38f7bcf3 | |||
affaf7f557 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,6 +12,7 @@
|
||||
**/*.js
|
||||
**/*.wasm
|
||||
**/*.wasm.map
|
||||
**/*.data
|
||||
**/*.so
|
||||
**/*.dll
|
||||
**/*.7z
|
||||
|
@ -106,6 +106,7 @@ set(TWN_NONOPT_SOURCE_FILES
|
||||
src/rendering/twn_circles.c
|
||||
src/rendering/twn_skybox.c
|
||||
src/rendering/twn_models.c
|
||||
src/rendering/twn_lines.c
|
||||
)
|
||||
|
||||
set(TWN_SOURCE_FILES
|
||||
@ -167,8 +168,7 @@ function(give_options_without_warnings target)
|
||||
-g3
|
||||
-gdwarf
|
||||
-fno-omit-frame-pointer
|
||||
$<$<BOOL:${TWN_SANITIZE}>:-fstack-protector-all -fsanitize=undefined -fsanitize=address>
|
||||
$<$<BOOL:${EMSCRIPTEN}>:-gsource-map>)
|
||||
$<$<BOOL:${TWN_SANITIZE}>:-fstack-protector-all -fsanitize=undefined -fsanitize=address>)
|
||||
|
||||
set(LINK_FLAGS
|
||||
-Bsymbolic-functions
|
||||
@ -287,6 +287,24 @@ function(link_deps target)
|
||||
endfunction()
|
||||
|
||||
|
||||
function(put_townengine output_directory)
|
||||
cmake_path(GET TWN_OUT_DIR STEM LAST_ONLY target)
|
||||
|
||||
add_executable(${target} ${TWN_ROOT_DIR}/src/twn_main.c)
|
||||
target_link_options(${target} PRIVATE $<$<BOOL:${LINUX}>:-Wl,-rpath,$ORIGIN/>)
|
||||
set_target_properties(${TWN_TARGET} PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY ${output_directory})
|
||||
set_target_properties(${target} PROPERTIES
|
||||
OUTPUT_NAME ${target}
|
||||
LIBRARY_OUTPUT_DIRECTORY ${output_directory}
|
||||
RUNTIME_OUTPUT_DIRECTORY ${output_directory})
|
||||
give_options(${target})
|
||||
include_deps(${target})
|
||||
link_deps(${target})
|
||||
target_link_libraries(${target} PUBLIC ${TWN_TARGET})
|
||||
endfunction()
|
||||
|
||||
|
||||
function(use_townengine sources output_directory)
|
||||
cmake_path(GET TWN_OUT_DIR STEM LAST_ONLY target)
|
||||
|
||||
@ -354,5 +372,18 @@ link_deps(twn_third_parties)
|
||||
give_options(${TWN_TARGET})
|
||||
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)
|
||||
target_link_libraries(${TWN_TARGET} PUBLIC
|
||||
twn_third_parties
|
||||
${CMAKE_CURRENT_BINARY_DIR}/font.o)
|
||||
|
||||
# embed resources
|
||||
# TODO: think of a portable way to compress/decompress them
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/font.o
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
COMMAND ${CMAKE_COMMAND} -E env CMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR} sh bin/prep-embed.sh
|
||||
DEPENDS share/assets/Dernyns256.ttf)
|
||||
|
||||
add_custom_target(asset-compilation ALL DEPENDS
|
||||
${CMAKE_CURRENT_BINARY_DIR}/font.o)
|
||||
|
@ -1,3 +1,3 @@
|
||||
[[deps]]
|
||||
source = "../../../common-data"
|
||||
source = "../../../data"
|
||||
name = "common-data"
|
||||
|
1
apps/demos/crawl/data/assets/LICENSES
Normal file
1
apps/demos/crawl/data/assets/LICENSES
Normal file
@ -0,0 +1 @@
|
||||
castledoors.png - https://opengameart.org/content/castle-door - CC-BY 3.0
|
@ -1,4 +1,3 @@
|
||||
require("string")
|
||||
require("level")
|
||||
require("render")
|
||||
|
||||
@ -29,19 +28,23 @@ function game_tick()
|
||||
input_action { control = "S", name = "walk_backward" }
|
||||
|
||||
if input_action_just_released { name = "turn_left" } then
|
||||
ctx.udata.player.direction = { x = ctx.udata.player.direction.z, y = ctx.udata.player.direction.y, z = -ctx.udata.player.direction.x }
|
||||
ctx.udata.player.direction = { x = ctx.udata.player.direction.z,
|
||||
y = ctx.udata.player.direction.y,
|
||||
z =-ctx.udata.player.direction.x }
|
||||
end
|
||||
|
||||
if input_action_just_released { name = "turn_right" } then
|
||||
ctx.udata.player.direction = { x = -ctx.udata.player.direction.z, y = ctx.udata.player.direction.y, z = ctx.udata.player.direction.x }
|
||||
ctx.udata.player.direction = { x =-ctx.udata.player.direction.z,
|
||||
y = ctx.udata.player.direction.y,
|
||||
z = ctx.udata.player.direction.x }
|
||||
end
|
||||
|
||||
local move = { x = 0, y = 0 }
|
||||
if input_action_just_released { name = "walk_forward" } then
|
||||
move = { x = move.x + ctx.udata.player.direction.z, y = move.y + ctx.udata.player.direction.x }
|
||||
move = { x = move.x + ctx.udata.player.direction.x, y = move.y + ctx.udata.player.direction.z }
|
||||
end
|
||||
if input_action_just_released { name = "walk_backward" } then
|
||||
move = { x = move.x - ctx.udata.player.direction.z, y = move.y - ctx.udata.player.direction.x }
|
||||
move = { x = move.x - ctx.udata.player.direction.x, y = move.y - ctx.udata.player.direction.z }
|
||||
end
|
||||
|
||||
if ctx.udata.level.grid[ctx.udata.player.position.y + move.y][ctx.udata.player.position.x + move.x].solid ~= nil then
|
||||
@ -57,9 +60,9 @@ function game_tick()
|
||||
|
||||
draw_camera {
|
||||
position = {
|
||||
x = ctx.udata.player.position_lerp.y * 2 + 1 - ctx.udata.player.direction.x / 2,
|
||||
y = 1,
|
||||
z = ctx.udata.player.position_lerp.x * 2 + 1 - ctx.udata.player.direction.z / 2,
|
||||
x = ctx.udata.player.position_lerp.x + 0.5 - ctx.udata.player.direction.x / 2,
|
||||
y = 0.5,
|
||||
z = ctx.udata.player.position_lerp.y + 0.5 - ctx.udata.player.direction.z / 2,
|
||||
},
|
||||
direction = ctx.udata.player.direction_lerp,
|
||||
}
|
||||
|
@ -3,14 +3,20 @@ function load_level(file)
|
||||
local f = file_read { file = file }
|
||||
|
||||
local result = {
|
||||
-- templates to fill the grid with
|
||||
classes = {
|
||||
-- predefined empty tile
|
||||
void = { },
|
||||
},
|
||||
-- symbol to class lookup table
|
||||
glossary = {
|
||||
[" "] = "void",
|
||||
},
|
||||
-- grid consists of expanded classes, of size dimensions
|
||||
grid = {},
|
||||
-- map consists of original rows of symbols that the grid is constructed from
|
||||
map = {},
|
||||
-- maximum extends of the map, unspecified tiles are filled with "void"
|
||||
size = { x = 0, y = 0 },
|
||||
}
|
||||
|
||||
@ -28,14 +34,10 @@ function load_level(file)
|
||||
section = line:sub(2); subsection = "none"
|
||||
-- decode map one line at a time
|
||||
elseif section == "map" then
|
||||
local l = #result.map + 1
|
||||
if result.size.x < #line then
|
||||
result.size.x = #line
|
||||
end
|
||||
result.map[l] = {}
|
||||
for i = 1, #line do
|
||||
result.map[l][i] = line:sub(i,i)
|
||||
end
|
||||
result.map[#result.map + 1] = line
|
||||
-- templates to expand
|
||||
elseif section == "classes" then
|
||||
-- properties
|
||||
@ -61,14 +63,16 @@ function load_level(file)
|
||||
from = limit + 1
|
||||
start, limit = string.find(f, "\n", from)
|
||||
end
|
||||
-- post process
|
||||
-- post process, expand map to grid
|
||||
for y = 1, #result.map do
|
||||
result.grid[y] = {}
|
||||
for x = 1, result.size.x do
|
||||
-- past defined for line
|
||||
local symbol
|
||||
if x > #result.map[y] then symbol = " "
|
||||
else symbol = result.map[y][x]
|
||||
if x > #result.map[y] then
|
||||
symbol = " "
|
||||
else
|
||||
symbol = result.map[y]:sub(x,x)
|
||||
end
|
||||
local class = result.classes[result.glossary[symbol]]
|
||||
if class["unique"] ~= nil then
|
||||
|
@ -1,56 +1,57 @@
|
||||
|
||||
-- if this is too wasteful, one could check nerby tiles to see whether faces could be visible
|
||||
-- more robust solution would be to travel the level from observer point of view
|
||||
function render_dungeon(dungeon)
|
||||
for y = 1, dungeon.size.y do
|
||||
for x = 1, dungeon.size.x do
|
||||
if dungeon.grid[y][x].wall_texture ~= nil then
|
||||
draw_quad {
|
||||
texture = dungeon.grid[y][x].wall_texture,
|
||||
v3 = { x = y * 2, y = 2, z = x * 2 },
|
||||
v2 = { x = y * 2, y = 0, z = x * 2 },
|
||||
v1 = { x = y * 2 + 2, y = 0, z = x * 2 },
|
||||
v0 = { x = y * 2 + 2, y = 2, z = x * 2 },
|
||||
v3 = { x = x, y = 1, z = y },
|
||||
v2 = { x = x, y = 0, z = y },
|
||||
v1 = { x = x + 1, y = 0, z = y },
|
||||
v0 = { x = x + 1, y = 1, z = y },
|
||||
texture_region = { w = 128, h = 128 },
|
||||
}
|
||||
draw_quad {
|
||||
texture = dungeon.grid[y][x].wall_texture,
|
||||
v3 = { x = y * 2 + 2, y = 2, z = x * 2 },
|
||||
v2 = { x = y * 2 + 2, y = 0, z = x * 2 },
|
||||
v1 = { x = y * 2 + 2, y = 0, z = x * 2 + 2 },
|
||||
v0 = { x = y * 2 + 2, y = 2, z = x * 2 + 2 },
|
||||
v3 = { x = x + 1, y = 1, z = y },
|
||||
v2 = { x = x + 1, y = 0, z = y },
|
||||
v1 = { x = x + 1, y = 0, z = y + 1 },
|
||||
v0 = { x = x + 1, y = 1, z = y + 1 },
|
||||
texture_region = { w = 128, h = 128 },
|
||||
}
|
||||
draw_quad {
|
||||
texture = dungeon.grid[y][x].wall_texture,
|
||||
v3 = { x = y * 2 + 2, y = 2, z = x * 2 + 2 },
|
||||
v2 = { x = y * 2 + 2, y = 0, z = x * 2 + 2 },
|
||||
v1 = { x = y * 2, y = 0, z = x * 2 + 2 },
|
||||
v0 = { x = y * 2, y = 2, z = x * 2 + 2 },
|
||||
v3 = { x = x + 1, y = 1, z = y + 1 },
|
||||
v2 = { x = x + 1, y = 0, z = y + 1 },
|
||||
v1 = { x = x, y = 0, z = y + 1 },
|
||||
v0 = { x = x, y = 1, z = y + 1 },
|
||||
texture_region = { w = 128, h = 128 },
|
||||
}
|
||||
draw_quad {
|
||||
texture = dungeon.grid[y][x].wall_texture,
|
||||
v3 = { x = y * 2, y = 2, z = x * 2 + 2 },
|
||||
v2 = { x = y * 2, y = 0, z = x * 2 + 2 },
|
||||
v1 = { x = y * 2, y = 0, z = x * 2 },
|
||||
v0 = { x = y * 2, y = 2, z = x * 2 },
|
||||
v3 = { x = x, y = 1, z = y + 1 },
|
||||
v2 = { x = x, y = 0, z = y + 1 },
|
||||
v1 = { x = x, y = 0, z = y },
|
||||
v0 = { x = x, y = 1, z = y },
|
||||
texture_region = { w = 128, h = 128 },
|
||||
}
|
||||
|
||||
elseif dungeon.grid[y][x].tile_texture ~= nil then
|
||||
draw_quad {
|
||||
texture = dungeon.grid[y][x].tile_texture,
|
||||
v0 = { x = y * 2 + 2, y = 0, z = x * 2 },
|
||||
v1 = { x = y * 2, y = 0, z = x * 2 },
|
||||
v2 = { x = y * 2, y = 0, z = x * 2 + 2 },
|
||||
v3 = { x = y * 2 + 2, y = 0, z = x * 2 + 2},
|
||||
v0 = { x = x + 1, y = 0, z = y },
|
||||
v1 = { x = x, y = 0, z = y },
|
||||
v2 = { x = x, y = 0, z = y + 1 },
|
||||
v3 = { x = x + 1, y = 0, z = y + 1},
|
||||
texture_region = { w = 128, h = 128 },
|
||||
}
|
||||
draw_quad {
|
||||
texture = dungeon.grid[y][x].tile_texture,
|
||||
v3 = { x = y * 2 + 2, y = 2, z = x * 2 },
|
||||
v2 = { x = y * 2, y = 2, z = x * 2 },
|
||||
v1 = { x = y * 2, y = 2, z = x * 2 + 2 },
|
||||
v0 = { x = y * 2 + 2, y = 2, z = x * 2 + 2},
|
||||
v3 = { x = x + 1, y = 1, z = y },
|
||||
v2 = { x = x, y = 1, z = y },
|
||||
v1 = { x = x, y = 1, z = y + 1 },
|
||||
v0 = { x = x + 1, y = 1, z = y + 1},
|
||||
texture_region = { w = 128, h = 128 },
|
||||
}
|
||||
end
|
||||
@ -59,25 +60,26 @@ function render_dungeon(dungeon)
|
||||
if dungeon.grid[y][x].face == "horizon" then
|
||||
draw_quad {
|
||||
texture = dungeon.grid[y][x].face_texture,
|
||||
v3 = { x = y * 2, y = 2, z = x * 2 + 1 },
|
||||
v2 = { x = y * 2, y = 0, z = x * 2 + 1 },
|
||||
v1 = { x = y * 2 + 2, y = 0, z = x * 2 + 1 },
|
||||
v0 = { x = y * 2 + 2, y = 2, z = x * 2 + 1 },
|
||||
texture_region = { w = 64, h = 96 },
|
||||
v3 = { x = x + 1, y = 1, z = y },
|
||||
v2 = { x = x + 1, y = 0, z = y },
|
||||
v1 = { x = x + 1, y = 0, z = y + 1 },
|
||||
v0 = { x = x + 1, y = 1, z = y + 1 },
|
||||
texture_region = { w = 64, h = 64 },
|
||||
}
|
||||
draw_quad {
|
||||
texture = dungeon.grid[y][x].face_texture,
|
||||
v3 = { x = y * 2 + 2, y = 2, z = x * 2 + 1 },
|
||||
v2 = { x = y * 2 + 2, y = 0, z = x * 2 + 1 },
|
||||
v1 = { x = y * 2, y = 0, z = x * 2 + 1 },
|
||||
v0 = { x = y * 2, y = 2, z = x * 2 + 1 },
|
||||
texture_region = { w = 64, h = 96 },
|
||||
v3 = { x = x, y = 1, z = y + 1 },
|
||||
v2 = { x = x, y = 0, z = y + 1 },
|
||||
v1 = { x = x, y = 0, z = y },
|
||||
v0 = { x = x, y = 1, z = y },
|
||||
texture_region = { w = 64, h = 64 },
|
||||
}
|
||||
|
||||
elseif dungeon.grid[y][x].face == "observer" then
|
||||
draw_billboard {
|
||||
texture = dungeon.grid[y][x].face_texture,
|
||||
position = { x = y * 2 + 1, y = 1, z = x * 2 + 1 },
|
||||
size = { x = 1, y = 1 },
|
||||
position = { x = x + 0.5, y = 0.5, z = y + 0.5 },
|
||||
size = { x = 0.5, y = 0.5 },
|
||||
}
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,3 @@
|
||||
[[deps]]
|
||||
source = "../../../common-data" # where does it come from, might be an url
|
||||
source = "../../../data" # where does it come from, might be an url
|
||||
name = "common-data" # should be globally unique
|
||||
|
@ -29,10 +29,6 @@ void game_tick(void) {
|
||||
ctx.debug = !ctx.debug;
|
||||
}
|
||||
|
||||
if (input_action_just_pressed("debug_dump_atlases")) {
|
||||
textures_dump_atlases();
|
||||
}
|
||||
|
||||
state->scene->tick(state);
|
||||
|
||||
/* there's a scene switch pending, we can do it now that the tick is done */
|
||||
|
@ -13,7 +13,6 @@ set(SOURCE_FILES
|
||||
state.h
|
||||
|
||||
scenes/scene.c scenes/scene.h
|
||||
scenes/title.c scenes/title.h
|
||||
scenes/ingame.c scenes/ingame.h
|
||||
)
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
[[deps]]
|
||||
source = "../../../common-data" # where does it come from, might be an url
|
||||
source = "../../../data" # where does it come from, might be an url
|
||||
name = "common-data" # should be globally unique
|
||||
|
@ -1,6 +1,5 @@
|
||||
#include "state.h"
|
||||
#include "scenes/scene.h"
|
||||
#include "scenes/title.h"
|
||||
#include "scenes/ingame.h"
|
||||
|
||||
#include "twn_game_api.h"
|
||||
@ -18,7 +17,7 @@ void game_tick(void) {
|
||||
|
||||
State *state = ctx.udata;
|
||||
state->ctx = &ctx;
|
||||
state->scene = title_scene(state);
|
||||
state->scene = ingame_scene(state);
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,10 +30,6 @@ void game_tick(void) {
|
||||
ctx.debug = !ctx.debug;
|
||||
}
|
||||
|
||||
if (input_action_just_pressed("debug_dump_atlases")) {
|
||||
textures_dump_atlases();
|
||||
}
|
||||
|
||||
state->scene->tick(state);
|
||||
|
||||
/* there's a scene switch pending, we can do it now that the tick is done */
|
||||
|
@ -1,5 +1,4 @@
|
||||
#include "ingame.h"
|
||||
#include "title.h"
|
||||
#include "scene.h"
|
||||
|
||||
#include "twn_game_api.h"
|
||||
@ -13,21 +12,298 @@
|
||||
|
||||
|
||||
#define TERRAIN_FREQUENCY 0.15f
|
||||
#define TERRAIN_DISTANCE 64
|
||||
#define TERRAIN_RADIUS 128
|
||||
#define GRASS_RADIUS 16
|
||||
#define TERRAIN_DISTANCE (TERRAIN_RADIUS * 2)
|
||||
#define HALF_TERRAIN_DISTANCE ((float)TERRAIN_DISTANCE / 2)
|
||||
#define PLAYER_HEIGHT 0.6f
|
||||
#define TREE_DENSITY 0.03f
|
||||
|
||||
#define G_CONST 10.0f
|
||||
|
||||
/* TODO: pregenerate grid of levels of detail */
|
||||
static float heightmap[TERRAIN_DISTANCE][TERRAIN_DISTANCE];
|
||||
|
||||
|
||||
/* vehicle sim ! */
|
||||
/* https://www.youtube.com/watch?v=pwbwFdWBkU0 */
|
||||
/* representation is a "jelly" box with spring configuration trying to make it coherent */
|
||||
|
||||
/* == springs == */
|
||||
/* damped spring: F = -kx - cv */
|
||||
/* x = length(p1-p0) - at_rest_length */
|
||||
/* v = dot(v1 - v0, unit(p1-p0)) */
|
||||
/* F = (-kx - cv) * unit(p1-p0) */
|
||||
/* v += (F/m)*t */
|
||||
/* one points gains positive F, other negative F, to come together */
|
||||
|
||||
/* == ground interaction == */
|
||||
/* if point is under terrain, then apply this: */
|
||||
/* x = y difference on point and ground */
|
||||
/* -x(n) = x * normal */
|
||||
/* -v(n) = dot(v, normal) */
|
||||
/* -F = (-kx(n)-cv(n)) * normal */
|
||||
|
||||
/* == friction == */
|
||||
/* v(o)/F(o) are perpendicular to slope (x - x(n)) */
|
||||
/* if v(o) == 0, then */
|
||||
/* -- at rest, static friction overcomes */
|
||||
/* if F(o) <= f(s)*x(n), F(o) = 0 */
|
||||
/* else, F(o) -= f(k) * x(n) */
|
||||
/* else if length(v(o) + (F(o)/m)*t) <= (f(k)*x(n)*t), v(o) = 0 */
|
||||
/* else, F = -unit(v(o)*f(k)*x(n)) */
|
||||
|
||||
#define VEHICLE_MASS 200.0f
|
||||
#define VEHICLE_LENGTH 3.0f
|
||||
#define VEHICLE_WIDTH 1.7f
|
||||
#define VEHICLE_HEIGHT 1.3f
|
||||
/* spring constant */
|
||||
#define VEHICLE_SPRING_K 22000.0f
|
||||
#define VEHICLE_SPRING_K_SHOCK 18000.0f
|
||||
#define VEHICLE_SPRING_GK 70000.0f
|
||||
/* damping constant */
|
||||
#define VEHICLE_SPRING_C 800.0f
|
||||
#define VEHICLE_SPRING_C_SHOCK 500.0f
|
||||
#define VEHICLE_SPRING_GC 100.0f
|
||||
#define VEHICLE_FRICTION_S 1000.0f
|
||||
#define VEHICLE_FRICTION_K 110.0f
|
||||
#define VEHICLE_FRICTION_V 4000.0f
|
||||
|
||||
/* TODO: shock springs, that are more loose, which are used to simulate the wheels */
|
||||
/* initial, ideal corner positions */
|
||||
static const Vec3 vbpi[8] = {
|
||||
[0] = { 0, 0, 0 },
|
||||
[1] = { VEHICLE_LENGTH, 0, 0 },
|
||||
[2] = { VEHICLE_LENGTH, 0, VEHICLE_WIDTH },
|
||||
[3] = { 0, 0, VEHICLE_WIDTH },
|
||||
[4] = { 0, VEHICLE_HEIGHT, 0 },
|
||||
[5] = { VEHICLE_LENGTH, VEHICLE_HEIGHT, 0 },
|
||||
[6] = { VEHICLE_LENGTH, VEHICLE_HEIGHT, VEHICLE_WIDTH },
|
||||
[7] = { 0, VEHICLE_HEIGHT, VEHICLE_WIDTH },
|
||||
};
|
||||
/* corner positions in simulation */
|
||||
static Vec3 vbp[8] = { vbpi[0], vbpi[1], vbpi[2], vbpi[3],
|
||||
vbpi[4], vbpi[5], vbpi[6], vbpi[7], };
|
||||
/* corner velocities */
|
||||
static Vec3 vbv[8];
|
||||
/* springs */
|
||||
static uint8_t vbs[28][2] = {
|
||||
{0, 1}, {4, 5}, {0, 4},
|
||||
{1, 2}, {5, 6}, {1, 5},
|
||||
{2, 3}, {6, 7}, {2, 6},
|
||||
{3, 0}, {7, 4}, {3, 7},
|
||||
|
||||
{0, 2}, {0, 5}, {0, 7},
|
||||
{1, 3}, {1, 6}, {1, 4},
|
||||
{4, 6}, {2, 7}, {2, 5},
|
||||
{5, 7}, {3, 4}, {3, 6},
|
||||
|
||||
{0, 6}, {1, 7}, {2, 4}, {3, 5},
|
||||
};
|
||||
/* ackermann steering geometry */
|
||||
static float vehicle_turning_extend;
|
||||
static float vehicle_turning_speed = 0.12f;
|
||||
static float vehicle_turning_extend_limit = VEHICLE_WIDTH * 1.75f;
|
||||
|
||||
static float height_at(SceneIngame *scn, Vec2 position);
|
||||
static Vec3 normal_at(SceneIngame *scn, Vec2 position);
|
||||
|
||||
|
||||
static inline float clampf(float f, float min, float max) {
|
||||
const float t = f < min ? min : f;
|
||||
return t > max ? max : t;
|
||||
}
|
||||
|
||||
|
||||
static void draw_vehicle(SceneIngame *scn) {
|
||||
for (size_t i = 0; i < 12; ++i)
|
||||
draw_line_3d(vbp[vbs[i][0]], vbp[vbs[i][1]], 1, (Color){255, 255, 255, 255});
|
||||
for (size_t i = 12; i < 24; ++i)
|
||||
draw_line_3d(vbp[vbs[i][0]], vbp[vbs[i][1]], 1, (Color){200, 200, 200, 255});
|
||||
for (size_t i = 24; i < 28; ++i)
|
||||
draw_line_3d(vbp[vbs[i][0]], vbp[vbs[i][1]], 1, (Color){255, 125, 125, 255});
|
||||
}
|
||||
|
||||
|
||||
static void process_vehicle(SceneIngame *scn) {
|
||||
/* apply gravity */
|
||||
Vec3 Facc[8] = {0};
|
||||
|
||||
/* steering */
|
||||
bool steered = false;
|
||||
if (input_action_pressed("player_left")) {
|
||||
vehicle_turning_extend -= vehicle_turning_speed;
|
||||
steered = true;
|
||||
}
|
||||
|
||||
if (input_action_pressed("player_right")) {
|
||||
vehicle_turning_extend += vehicle_turning_speed;
|
||||
steered = true;
|
||||
}
|
||||
|
||||
if (!steered)
|
||||
vehicle_turning_extend -= copysignf(vehicle_turning_speed * 0.9f, vehicle_turning_extend);
|
||||
|
||||
vehicle_turning_extend = clampf(vehicle_turning_extend, -vehicle_turning_extend_limit, vehicle_turning_extend_limit);
|
||||
if (fabsf(vehicle_turning_extend) <= 0.11f)
|
||||
vehicle_turning_extend = 0;
|
||||
|
||||
Vec3 const Fg = { .y = -VEHICLE_MASS * G_CONST };
|
||||
for (size_t i = 0; i < 8; ++i)
|
||||
Facc[i] = vec3_add(Facc[i], Fg);
|
||||
|
||||
/* apply springs */
|
||||
for (size_t i = 0; i < 28; ++i) {
|
||||
Vec3 const p0 = vbp[vbs[i][0]];
|
||||
Vec3 const p1 = vbp[vbs[i][1]];
|
||||
Vec3 const v0 = vbv[vbs[i][0]];
|
||||
Vec3 const v1 = vbv[vbs[i][1]];
|
||||
Vec3 const pd = vec3_sub(p1, p0);
|
||||
Vec3 const pn = vec3_norm(pd);
|
||||
/* TODO: length at rest could be precalculated */
|
||||
float const lar = vec3_length(vec3_sub(vbpi[vbs[i][1]], vbpi[vbs[i][0]]));
|
||||
float const x = vec3_length(pd) - lar;
|
||||
float const v = vec3_dot(vec3_sub(v1, v0), pn);
|
||||
float const spring_k = i == 2 | i == 5 || i == 8 || i == 11 ? VEHICLE_SPRING_K_SHOCK : VEHICLE_SPRING_K;
|
||||
float const spring_c = i == 2 | i == 5 || i == 8 || i == 11 ? VEHICLE_SPRING_C_SHOCK : VEHICLE_SPRING_C;
|
||||
Vec3 const Fs = vec3_scale(pn, -spring_k * x - spring_c * v);
|
||||
Facc[vbs[i][0]] = vec3_sub(Facc[vbs[i][0]], Fs);
|
||||
Facc[vbs[i][1]] = vec3_add(Facc[vbs[i][1]], Fs);
|
||||
}
|
||||
|
||||
/* spring and friction against the ground */
|
||||
for (size_t i = 0; i < 8; ++i) {
|
||||
Vec3 const p = vbp[i];
|
||||
Vec3 const v = vbv[i];
|
||||
float const h = height_at(scn, (Vec2){ p.x, p.z });
|
||||
Vec3 const fwd = vec3_norm(vec3_sub(vbp[1], vbp[0]));
|
||||
if (h >= p.y) {
|
||||
/* back wheel processing: acceleration */
|
||||
if (i == 0 || i == 3) {
|
||||
float scale = 0;
|
||||
if (scn->camera_mode == 2 && input_action_pressed("player_forward"))
|
||||
scale += 1;
|
||||
if (scn->camera_mode == 2 && input_action_pressed("player_backward"))
|
||||
scale -= 1;
|
||||
Facc[i] = vec3_add(Facc[i], vec3_scale(fwd, 6500 * scale));
|
||||
}
|
||||
|
||||
/* normal force, for displacement */
|
||||
Vec3 const n = normal_at(scn, (Vec2){ p.x, p.z });
|
||||
float const xn = (h - p.y) * n.y;
|
||||
float const vn = vec3_dot(v, n);
|
||||
Vec3 const Fn = vec3_scale(n, -VEHICLE_SPRING_GK * xn - VEHICLE_SPRING_GC * vn);
|
||||
Facc[i] = vec3_sub(Facc[i], Fn);
|
||||
|
||||
/* friction force, perpendicular to normal force */
|
||||
/* TODO: is it right? aren't vn+vol should be = |v| */
|
||||
Vec3 const von = vec3_norm(vec3_cross(n, vec3_cross(v, n)));
|
||||
Vec3 const vo = vec3_scale(vec3_scale(von, vec3_length(v) - vn), -1);
|
||||
float const vol = vec3_length(vo);
|
||||
Vec3 const Fon = vec3_norm(vec3_cross(n, vec3_cross(Facc[i], n)));
|
||||
Vec3 Fo = vec3_scale(vec3_scale(Fon, vec3_length(Facc[i]) - vec3_dot(Facc[i], n)), -1);
|
||||
/* portion of total force along the surface */
|
||||
float const Fol = vec3_length(Fo);
|
||||
float const fkxn = VEHICLE_FRICTION_K * xn;
|
||||
/* at rest, might want to start moving */
|
||||
if (fabsf(0.0f - vol) <= 0.0001f) {
|
||||
/* cannot overcome static friction, force along the surface is zeroed */
|
||||
if (Fol <= VEHICLE_FRICTION_S * xn) { Fo = vec3_scale(Fo, -1);}
|
||||
/* resist the force by friction, while starting to move */
|
||||
else { Fo = vec3_sub(Fo, vec3_scale(von, fkxn));}
|
||||
/* not at rest, stop accelerating along the surface */
|
||||
} else if (vol + (Fol / VEHICLE_MASS) * ctx.frame_duration <= fkxn * ctx.frame_duration * 2) {
|
||||
/* ugh ... */
|
||||
vbv[i] = vec3_add(v, vo);
|
||||
/* just apply friction */
|
||||
} else {
|
||||
Fo = vec3_scale(von, -fkxn * 400);
|
||||
}
|
||||
Facc[i] = vec3_add(Facc[i], Fo);
|
||||
|
||||
/* rear wheel friction */
|
||||
if (i == 0 || i == 3) {
|
||||
Vec3 const pn = vec3_cross(fwd, n);
|
||||
Vec3 const Fp = vec3_scale(pn, vec3_dot(v, pn) * -VEHICLE_FRICTION_V);
|
||||
Facc[i] = vec3_add(Facc[i], Fp);
|
||||
}
|
||||
|
||||
/* front wheel processing */
|
||||
if (i == 1 || i == 2) {
|
||||
/* steering influences "center of turning", which is a point */
|
||||
/* laying on line defined by rear axle */
|
||||
/* front arms are rotated to be perpendicular to center of turning, */
|
||||
/* which then are used to dissipate forces, thus providing control */
|
||||
Vec3 const rear_bar = vec3_sub(vbp[0], vbp[3]);
|
||||
Vec3 const rear_center = vec3_scale(vec3_add(vbp[0], vbp[3]), 0.5);
|
||||
Vec3 a, b, r;
|
||||
if (i == 1) {
|
||||
a = vec3_sub(vbp[3], vbp[2]);
|
||||
b = vec3_sub(rear_center, vbp[2]);
|
||||
r = vbp[2];
|
||||
} else {
|
||||
a = vec3_sub(vbp[0], vbp[1]);
|
||||
b = vec3_sub(rear_center, vbp[1]);
|
||||
r = vbp[1];
|
||||
}
|
||||
|
||||
float const arm_angle = vec3_angle(a, b);
|
||||
Vec3 const turn_center = vec3_add(rear_center, vec3_scale(vec3_norm(rear_bar), vehicle_turning_extend));
|
||||
Vec3 const arm = vec3_sub(r, turn_center);
|
||||
Vec3 const n = vec3_norm(vec3_cross(a, b));
|
||||
Vec3 const wheel = vec3_norm(vec3_rotate(arm, -arm_angle, n));
|
||||
Vec3 const p = vec3_norm(vec3_cross(wheel, n));
|
||||
draw_line_3d(r, vec3_add(r, p), 1, (Color){0,255,255,255});
|
||||
|
||||
Vec3 const Fp = vec3_scale(p, vec3_dot(v, p) * -VEHICLE_FRICTION_V);
|
||||
Facc[i] = vec3_add(Facc[i], Fp);
|
||||
}
|
||||
}
|
||||
|
||||
Vec3 vd = vec3_scale(vec3_scale(Facc[i], (1.0f / VEHICLE_MASS)), ctx.frame_duration);
|
||||
vbv[i] = vec3_add(vbv[i], vd);
|
||||
vbp[i] = vec3_add(vbp[i], vec3_scale(vbv[i], ctx.frame_duration));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void process_vehicle_mode(State *state) {
|
||||
SceneIngame *scn = (SceneIngame *)state->scene;
|
||||
|
||||
Vec3 const top_center = vec3_sub(vbp[4], vec3_scale(vec3_sub(vbp[4], vbp[6]), 1.0f / 2.0f));
|
||||
// Vec3 const front_center = vec3_add(vbp[4], vec3_scale(vec3_sub(vbp[4], vbp[7]), 1.0f / 2.0f));
|
||||
// Vec3 const facing_direction = vec3_sub(top_center, front_center);
|
||||
|
||||
float yawc, yaws, pitchc, pitchs;
|
||||
sincosf(scn->yaw + (float)M_PI_2, &yaws, &yawc);
|
||||
sincosf(scn->pitch, &pitchs, &pitchc);
|
||||
|
||||
Vec3 const looking_direction = vec3_norm(((Vec3){
|
||||
yawc * pitchc,
|
||||
pitchs,
|
||||
yaws * pitchc,
|
||||
}));
|
||||
|
||||
Vec3 const orbit = vec3_sub(top_center, vec3_scale(looking_direction, 7.5));
|
||||
|
||||
draw_camera(orbit, looking_direction, (Vec3){0,1,0}, (float)M_PI_2 * 0.8f, 1, TERRAIN_RADIUS * sqrtf(3));
|
||||
|
||||
scn->looking_direction = looking_direction;
|
||||
|
||||
scn->pos = top_center;
|
||||
}
|
||||
|
||||
|
||||
static void process_fly_mode(State *state) {
|
||||
SceneIngame *scn = (SceneIngame *)state->scene;
|
||||
|
||||
DrawCameraFromPrincipalAxesResult dir_and_up =
|
||||
draw_camera_from_principal_axes(scn->pos, scn->roll, scn->pitch, scn->yaw, (float)M_PI_2 * 0.8f, 1);
|
||||
draw_camera_from_principal_axes(scn->pos, scn->roll, scn->pitch, scn->yaw, (float)M_PI_2 * 0.8f, 1, TERRAIN_RADIUS * sqrtf(3));
|
||||
|
||||
scn->looking_direction = 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.04f; /* TODO: put this in a better place */
|
||||
const float speed = 0.1f; /* TODO: put this in a better place */
|
||||
if (input_action_pressed("player_left"))
|
||||
scn->pos = vec3_sub(scn->pos, m_vec_scale(right, speed));
|
||||
|
||||
@ -47,33 +323,39 @@ static void process_fly_mode(State *state) {
|
||||
scn->pos.y -= speed;
|
||||
}
|
||||
|
||||
/* TODO: could be baked in map format */
|
||||
static Vec3 normal_at(SceneIngame *scn, Vec2 position) {
|
||||
int const x = (int)(floorf(position.x - scn->world_center.x));
|
||||
int const y = (int)(floorf(position.y - scn->world_center.y));
|
||||
|
||||
float const height0 = heightmap[x][y];
|
||||
float const height1 = heightmap[x + 1][y];
|
||||
float const height2 = heightmap[x][y + 1];
|
||||
|
||||
Vec3 const a = { .x = 1, .y = height0 - height1, .z = 0 };
|
||||
Vec3 const b = { .x = 0, .y = height0 - height2, .z = -1 };
|
||||
|
||||
return vec3_norm(vec3_cross(a, b));
|
||||
}
|
||||
|
||||
/* TODO: don't operate on triangles, instead interpolate on quads */
|
||||
static float height_at(SceneIngame *scn, Vec2 position) {
|
||||
float height0, height1, height2, weight0, weight1, weight2;
|
||||
int const x = (int)(floorf(position.x - scn->world_center.x));
|
||||
int const y = (int)(floorf(position.y - scn->world_center.y));
|
||||
|
||||
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];
|
||||
float const height0 = heightmap[x][y];
|
||||
float const height1 = heightmap[x + 1][y];
|
||||
float const height2 = heightmap[x][y + 1];
|
||||
float const height3 = 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));
|
||||
float const weight0 = (1 - incell.x) * (1 - incell.y);
|
||||
float const weight1 = ( incell.x) * (1 - incell.y);
|
||||
float const weight2 = (1 - incell.x) * ( incell.y);
|
||||
float const weight3 = ( incell.x) * ( incell.y);
|
||||
|
||||
/* 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));
|
||||
}
|
||||
|
||||
return (height0 * weight0 + height1 * weight1 + height2 * weight2) / (weight0 + weight1 + weight2);
|
||||
return (height0 * weight0 + height1 * weight1 + height2 * weight2 + height3 * weight3) / (weight0 + weight1 + weight2 + weight3);
|
||||
}
|
||||
|
||||
|
||||
@ -81,13 +363,15 @@ static void process_ground_mode(State *state) {
|
||||
SceneIngame *scn = (SceneIngame *)state->scene;
|
||||
|
||||
DrawCameraFromPrincipalAxesResult dir_and_up =
|
||||
draw_camera_from_principal_axes(scn->pos, scn->roll, scn->pitch, scn->yaw, (float)M_PI_2 * 0.8f, 1);
|
||||
draw_camera_from_principal_axes(scn->pos, scn->roll, scn->pitch, scn->yaw, (float)M_PI_2 * 0.8f, 1, TERRAIN_RADIUS * sqrtf(3));
|
||||
|
||||
scn->looking_direction = dir_and_up.direction;
|
||||
|
||||
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 */
|
||||
const float speed = 0.20f; /* TODO: put this in a better place */
|
||||
|
||||
Vec3 target = scn->pos;
|
||||
|
||||
@ -96,7 +380,7 @@ static void process_ground_mode(State *state) {
|
||||
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;
|
||||
target.y = target.y - 0.6f;
|
||||
|
||||
if (target.y < height + PLAYER_HEIGHT)
|
||||
target.y = height + PLAYER_HEIGHT;
|
||||
@ -135,50 +419,99 @@ static void generate_terrain(SceneIngame *scn) {
|
||||
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;
|
||||
height += stb_perlin_noise3((float)x * TERRAIN_FREQUENCY / 10, (float)y * TERRAIN_FREQUENCY / 10, 0, 0, 0, 0) * 20 - 1;
|
||||
|
||||
heightmap[lx][ly] = height;
|
||||
}
|
||||
}
|
||||
|
||||
scn->world_center = (Vec2){ floorf(scn->pos.x - HALF_TERRAIN_DISTANCE), floorf(scn->pos.z - HALF_TERRAIN_DISTANCE) };
|
||||
}
|
||||
|
||||
|
||||
static int32_t ceil_sqrt(int32_t const n) {
|
||||
int32_t res = 1;
|
||||
while(res * res < n)
|
||||
res++;
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
static uint32_t adler32(const void *buf, size_t buflength) {
|
||||
const uint8_t *buffer = (const uint8_t*)buf;
|
||||
|
||||
uint32_t s1 = 1;
|
||||
uint32_t s2 = 0;
|
||||
|
||||
for (size_t n = 0; n < buflength; n++) {
|
||||
s1 = (s1 + buffer[n]) % 65521;
|
||||
s2 = (s2 + s1) % 65521;
|
||||
}
|
||||
return (s2 << 16) | s1;
|
||||
}
|
||||
|
||||
|
||||
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));
|
||||
/* used to cull invisible tiles over field of view (to horizon) */
|
||||
Vec2 const d = vec2_norm((Vec2){ .x = scn->looking_direction.x, .y = scn->looking_direction.z });
|
||||
float const c = cosf((float)M_PI_2 * 0.8f * 0.8f);
|
||||
|
||||
/* draw terrain in circle */
|
||||
int32_t const rsi = (int32_t)TERRAIN_RADIUS * (int32_t)TERRAIN_RADIUS;
|
||||
for (int32_t iy = -(int32_t)TERRAIN_RADIUS; iy <= (int32_t)TERRAIN_RADIUS - 1; ++iy) {
|
||||
int32_t const dx = ceil_sqrt(rsi - (iy + (iy <= 0)) * (iy + (iy <= 0)));
|
||||
for (int32_t ix = -dx; ix < dx - 1; ++ix) {
|
||||
int32_t lx = ix + TERRAIN_RADIUS;
|
||||
int32_t ly = iy + TERRAIN_RADIUS;
|
||||
|
||||
float x = (float)(floorf(scn->pos.x - HALF_TERRAIN_DISTANCE + (float)lx));
|
||||
float y = (float)(floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly));
|
||||
|
||||
/* cull tiles outside of vision */
|
||||
if (vec2_dot(vec2_norm((Vec2){x - scn->pos.x + d.x * 2, y - scn->pos.z + d.y * 2}), d) < c)
|
||||
continue;
|
||||
|
||||
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",
|
||||
draw_quad("/assets/grass2.png",
|
||||
(Vec3){ (float)x, d0, (float)y },
|
||||
(Vec3){ (float)x + 1, d1, (float)y },
|
||||
(Vec3){ (float)x, d3, (float)y - 1 },
|
||||
(Vec2){ 128, 128 },
|
||||
(Vec2){ 128, 0 },
|
||||
(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 },
|
||||
(Vec3){ (float)x + 1, d2, (float)y - 1 },
|
||||
(Vec3){ (float)x, d3, (float)y - 1 },
|
||||
(Vec2){ 128, 0 },
|
||||
(Vec2){ 0, 0 },
|
||||
(Vec2){ 0, 128 },
|
||||
(Color){255, 255, 255, 255},
|
||||
(Color){255, 255, 255, 255},
|
||||
(Rect){ .w = 128, .h = 128 },
|
||||
(Color){255, 255, 255, 255});
|
||||
|
||||
draw_billboard("/assets/grasses/10.png",
|
||||
(Vec3){ (float)x, d0 + 0.15f, (float)y },
|
||||
(Vec2){0.3f, 0.3f},
|
||||
if (((float)(adler32(&((Vec2){x, y}), sizeof (Vec2)) % 100) / 100) <= TREE_DENSITY)
|
||||
draw_billboard("/assets/trreez.png",
|
||||
(Vec3){ (float)x, d0 + 1.95f, (float)y },
|
||||
(Vec2){2.f, 2.f},
|
||||
(Rect){0},
|
||||
(Color){255, 255, 255, 255}, true);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t const rsi_g = (int32_t)GRASS_RADIUS * (int32_t)GRASS_RADIUS;
|
||||
for (int32_t iy = -(int32_t)GRASS_RADIUS; iy <= (int32_t)GRASS_RADIUS - 1; ++iy) {
|
||||
int32_t const dx = ceil_sqrt(rsi_g - (iy + (iy <= 0)) * (iy + (iy <= 0)));
|
||||
for (int32_t ix = -dx; ix < dx; ++ix) {
|
||||
int32_t lx = ix + TERRAIN_RADIUS;
|
||||
int32_t ly = iy + TERRAIN_RADIUS;
|
||||
|
||||
float x = (float)(floorf(scn->pos.x - HALF_TERRAIN_DISTANCE + (float)lx));
|
||||
float y = (float)(floorf(scn->pos.z - HALF_TERRAIN_DISTANCE + (float)ly));
|
||||
|
||||
float d = heightmap[lx][ly];
|
||||
|
||||
draw_billboard("/assets/grasses/25.png",
|
||||
(Vec3){
|
||||
(float)x + (float)((adler32(&((Vec2){x, y}), sizeof (Vec2))) % 32) / 64.0f,
|
||||
d + 0.2f,
|
||||
(float)y + (float)((adler32(&((Vec2){y, x}), sizeof (Vec2))) % 32) / 64.0f
|
||||
},
|
||||
(Vec2){0.4f, 0.4f},
|
||||
(Rect){0},
|
||||
(Color){255, 255, 255, 255}, true);
|
||||
}
|
||||
@ -198,24 +531,22 @@ static void ingame_tick(State *state) {
|
||||
input_action("mouse_capture_toggle", "ESCAPE");
|
||||
input_action("toggle_camera_mode", "C");
|
||||
|
||||
// draw_model("models/test.obj", (Vec3){0}, (Vec3){0,0,1}, (Vec3){1.f / 64,1.f / 64,1.f / 64});
|
||||
// draw_model("models/test2.obj", (Vec3){0}, (Vec3){0,0,1}, (Vec3){1.f / 64,1.f / 64,1.f / 64});
|
||||
// draw_model("models/bunny.obj", (Vec3){0}, (Vec3){0,0,1}, (Vec3){4.,4.,4.});
|
||||
|
||||
if (scn->mouse_captured) {
|
||||
const float sensitivity = 0.4f * (float)DEG2RAD; /* TODO: put this in a better place */
|
||||
const float sensitivity = 0.4f * (float)(M_PI / 180); /* 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;
|
||||
scn->camera_mode = scn->camera_mode == 2 ? 0 : scn->camera_mode + 1;
|
||||
|
||||
if (scn->flying_camera) {
|
||||
if (scn->camera_mode == 1) {
|
||||
process_fly_mode(state);
|
||||
} else {
|
||||
} else if (scn->camera_mode == 0) {
|
||||
process_ground_mode(state);
|
||||
} else if (scn->camera_mode) {
|
||||
process_vehicle_mode(state);
|
||||
}
|
||||
|
||||
/* toggle mouse capture with end key */
|
||||
@ -225,12 +556,15 @@ static void ingame_tick(State *state) {
|
||||
ctx.mouse_capture = scn->mouse_captured;
|
||||
|
||||
generate_terrain(scn);
|
||||
process_vehicle(scn);
|
||||
|
||||
draw_terrain(scn);
|
||||
draw_vehicle(scn);
|
||||
|
||||
draw_skybox("/assets/miramar/miramar_*.tga");
|
||||
|
||||
ctx.fog_color = (Color){ 140, 147, 160, 255 };
|
||||
ctx.fog_density = 0.03f;
|
||||
ctx.fog_density = 0.015f;
|
||||
}
|
||||
|
||||
|
||||
@ -248,7 +582,7 @@ Scene *ingame_scene(State *state) {
|
||||
|
||||
new_scene->mouse_captured = true;
|
||||
|
||||
m_audio(m_set(path, "music/mod65.xm"),
|
||||
m_audio(m_set(path, "music/woah.ogg"),
|
||||
m_opt(channel, "soundtrack"),
|
||||
m_opt(repeat, true));
|
||||
|
||||
|
@ -12,13 +12,16 @@
|
||||
typedef struct SceneIngame {
|
||||
Scene base;
|
||||
|
||||
Vec3 looking_direction;
|
||||
Vec2 world_center;
|
||||
|
||||
Vec3 pos;
|
||||
float yaw;
|
||||
float pitch;
|
||||
float roll;
|
||||
|
||||
bool mouse_captured;
|
||||
bool flying_camera;
|
||||
int camera_mode;
|
||||
} SceneIngame;
|
||||
|
||||
|
||||
|
@ -1,61 +0,0 @@
|
||||
#include "title.h"
|
||||
#include "ingame.h"
|
||||
|
||||
#include "twn_game_api.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
|
||||
static void title_tick(State *state) {
|
||||
SceneTitle *scn = (SceneTitle *)state->scene;
|
||||
(void)scn;
|
||||
|
||||
input_action("ui_accept", "RETURN");
|
||||
|
||||
if (input_action_just_pressed("ui_accept")) {
|
||||
switch_to(state, ingame_scene);
|
||||
return;
|
||||
}
|
||||
|
||||
m_sprite("/assets/title.png", ((Rect) {
|
||||
((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", (unsigned long long)state->ctx->frame_number) + 1;
|
||||
char *text_str = malloc(text_str_len);
|
||||
snprintf(text_str, text_str_len, "%llu", (unsigned long long)state->ctx->frame_number);
|
||||
|
||||
const char *font = "/fonts/kenney-pixel.ttf";
|
||||
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,
|
||||
},
|
||||
(Color) { 0, 0, 0, 255 }
|
||||
);
|
||||
|
||||
draw_text(text_str, (Vec2){ 0, 0 }, text_h, (Color) { 255, 255, 255, 255 }, font);
|
||||
free(text_str);
|
||||
}
|
||||
|
||||
|
||||
static void title_end(State *state) {
|
||||
free(state->scene);
|
||||
}
|
||||
|
||||
|
||||
Scene *title_scene(State *state) {
|
||||
(void)state;
|
||||
|
||||
SceneTitle *new_scene = calloc(1, sizeof *new_scene);
|
||||
new_scene->base.tick = title_tick;
|
||||
new_scene->base.end = title_end;
|
||||
|
||||
return (Scene *)new_scene;
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
#ifndef TITLE_H
|
||||
#define TITLE_H
|
||||
|
||||
#include "../state.h"
|
||||
#include "scene.h"
|
||||
|
||||
|
||||
typedef struct SceneTitle {
|
||||
Scene base;
|
||||
} SceneTitle;
|
||||
|
||||
|
||||
Scene *title_scene(State *state);
|
||||
|
||||
|
||||
#endif
|
@ -13,4 +13,4 @@ set(SOURCE_FILES
|
||||
state.h
|
||||
)
|
||||
|
||||
use_townengine(${PROJECT_NAME} "${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
use_townengine("${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
@ -116,11 +116,13 @@ void game_tick(void) {
|
||||
input_action("up", "LCLICK");
|
||||
input_action("down", "RCLICK");
|
||||
|
||||
if (input_action_just_pressed("up"))
|
||||
if (input_action_pressed("up"))
|
||||
state->r += 1;
|
||||
if (input_action_just_pressed("down"))
|
||||
if (input_action_pressed("down") && state->r > 2)
|
||||
state->r -= 1;
|
||||
|
||||
draw_circle(mouse_snap, state->r * 8, (Color){125, 125, 125, 125});
|
||||
|
||||
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) {
|
||||
@ -133,9 +135,8 @@ void game_tick(void) {
|
||||
}
|
||||
}
|
||||
|
||||
draw_circle(mouse_snap, state->r * 8, (Color){125, 125, 125, 125});
|
||||
|
||||
benchmark(state);
|
||||
/* uncomment to see performance difference between variants */
|
||||
// benchmark(state);
|
||||
}
|
||||
|
||||
|
||||
|
BIN
apps/templates/c/data/twn.png
(Stored with Git LFS)
BIN
apps/templates/c/data/twn.png
(Stored with Git LFS)
Binary file not shown.
@ -17,11 +17,6 @@ void game_tick(void) {
|
||||
|
||||
struct state *state = ctx.udata;
|
||||
++state->counter;
|
||||
|
||||
m_sprite("twn.png",
|
||||
(Rect) { .w = 128, .h = 64, },
|
||||
m_opt(stretch, true)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
13
apps/templates/lua/.gitignore
vendored
13
apps/templates/lua/.gitignore
vendored
@ -3,10 +3,15 @@
|
||||
!*.*
|
||||
!*/
|
||||
|
||||
*.so
|
||||
*.dll
|
||||
*.exe
|
||||
*.trace
|
||||
**/*.so
|
||||
**/*.dll
|
||||
**/*.exe
|
||||
**/*.trace
|
||||
**/*.js
|
||||
**/*.wasm
|
||||
**/*.wasm.map
|
||||
**/*.data
|
||||
**/*.html
|
||||
|
||||
data/scripts/twnapi.lua
|
||||
build/
|
||||
|
@ -1,53 +0,0 @@
|
||||
---@type { frame_number: number, frame_duration: number, fog_density: number, fog_color: { r: number, g: number, b: number, a: number }, resolution: { x: number, y: number }, mouse_position: { x: number, y: number }, mouse_movement: { x: number, y: number }, random_seed: number, debug: boolean, initialization_needed: boolean, mouse_capture: boolean, udata: table }
|
||||
ctx = nil
|
||||
---@alias Control '"A"'|'"B"'|'"C"'|'"D"'|'"E"'|'"F"'|'"G"'|'"H"'|'"I"'|'"J"'|'"K"'|'"L"'|'"M"'|'"N"'|'"O"'|'"P"'|'"Q"'|'"R"'|'"S"'|'"T"'|'"U"'|'"V"'|'"W"'|'"X"'|'"Y"'|'"Z"'|'"1"'|'"2"'|'"3"'|'"4"'|'"5"'|'"6"'|'"7"'|'"8"'|'"9"'|'"0"'|'"RETURN"'|'"ESCAPE"'|'"BACKSPACE"'|'"TAB"'|'"SPACE"'|'"MINUS"'|'"EQUALS"'|'"LEFTBRACKET"'|'"RIGHTBRACKET"'|'"BACKSLASH"'|'"NONUSHASH"'|'"SEMICOLON"'|'"APOSTROPHE"'|'"GRAVE"'|'"COMMA"'|'"PERIOD"'|'"SLASH"'|'"CAPSLOCK"'|'"F1"'|'"F2"'|'"F3"'|'"F4"'|'"F5"'|'"F6"'|'"F7"'|'"F8"'|'"F9"'|'"F10"'|'"F11"'|'"F12"'|'"PRINTSCREEN"'|'"SCROLLLOCK"'|'"PAUSE"'|'"INSERT"'|'"HOME"'|'"PAGEUP"'|'"DELETE"'|'"END"'|'"PAGEDOWN"'|'"RIGHT"'|'"LEFT"'|'"DOWN"'|'"UP"'|'"NUMLOCKCLEAR"'|'"KP_DIVIDE"'|'"KP_MULTIPLY"'|'"KP_MINUS"'|'"KP_PLUS"'|'"KP_ENTER"'|'"KP_1"'|'"KP_2"'|'"KP_3"'|'"KP_4"'|'"KP_5"'|'"KP_6"'|'"KP_7"'|'"KP_8"'|'"KP_9"'|'"KP_0"'|'"KP_PERIOD"'|'"NONUSBACKSLASH"'|'"APPLICATION"'|'"POWER"'|'"KP_EQUALS"'|'"F13"'|'"F14"'|'"F15"'|'"F16"'|'"F17"'|'"F18"'|'"F19"'|'"F20"'|'"F21"'|'"F22"'|'"F23"'|'"F24"'|'"EXECUTE"'|'"HELP"'|'"MENU"'|'"SELECT"'|'"STOP"'|'"AGAIN"'|'"UNDO"'|'"CUT"'|'"COPY"'|'"PASTE"'|'"FIND"'|'"MUTE"'|'"VOLUMEUP"'|'"VOLUMEDOWN"'|'"KP_COMMA"'|'"KP_EQUALSAS400"'|'"INTERNATIONAL1"'|'"INTERNATIONAL2"'|'"INTERNATIONAL3"'|'"INTERNATIONAL4"'|'"INTERNATIONAL5"'|'"INTERNATIONAL6"'|'"INTERNATIONAL7"'|'"INTERNATIONAL8"'|'"INTERNATIONAL9"'|'"LANG1"'|'"LANG2"'|'"LANG3"'|'"LANG4"'|'"LANG5"'|'"LANG6"'|'"LANG7"'|'"LANG8"'|'"LANG9"'|'"ALTERASE"'|'"SYSREQ"'|'"CANCEL"'|'"CLEAR"'|'"PRIOR"'|'"RETURN2"'|'"SEPARATOR"'|'"OUT"'|'"OPER"'|'"CLEARAGAIN"'|'"CRSEL"'|'"EXSEL"'|'"KP_00"'|'"KP_000"'|'"THOUSANDSSEPARATOR"'|'"DECIMALSEPARATOR"'|'"CURRENCYUNIT"'|'"CURRENCYSUBUNIT"'|'"KP_LEFTPAREN"'|'"KP_RIGHTPAREN"'|'"KP_LEFTBRACE"'|'"KP_RIGHTBRACE"'|'"KP_TAB"'|'"KP_BACKSPACE"'|'"KP_A"'|'"KP_B"'|'"KP_C"'|'"KP_D"'|'"KP_E"'|'"KP_F"'|'"KP_XOR"'|'"KP_POWER"'|'"KP_PERCENT"'|'"KP_LESS"'|'"KP_GREATER"'|'"KP_AMPERSAND"'|'"KP_DBLAMPERSAND"'|'"KP_VERTICALBAR"'|'"KP_DBLVERTICALBAR"'|'"KP_COLON"'|'"KP_HASH"'|'"KP_SPACE"'|'"KP_AT"'|'"KP_EXCLAM"'|'"KP_MEMSTORE"'|'"KP_MEMRECALL"'|'"KP_MEMCLEAR"'|'"KP_MEMADD"'|'"KP_MEMSUBTRACT"'|'"KP_MEMMULTIPLY"'|'"KP_MEMDIVIDE"'|'"KP_PLUSMINUS"'|'"KP_CLEAR"'|'"KP_CLEARENTRY"'|'"KP_BINARY"'|'"KP_OCTAL"'|'"KP_DECIMAL"'|'"KP_HEXADECIMAL"'|'"LCTRL"'|'"LSHIFT"'|'"LALT"'|'"LGUI"'|'"RCTRL"'|'"RSHIFT"'|'"RALT"'|'"RGUI"'|'"MODE"'|'"KBDILLUMTOGGLE"'|'"KBDILLUMDOWN"'|'"KBDILLUMUP"'|'"EJECT"'|'"SLEEP"'|'"APP1"'|'"APP2"'|'"AUDIOREWIND"'|'"AUDIOFASTFORWARD"'|'"SOFTLEFT"'|'"SOFTRIGHT"'|'"CALL"'|'"ENDCALL"'|'"LEFT_MOUSE"'|'"RIGHT_MOUSE"'|'"MIDDLE_MOUSE"'|'"X1"'|'"X2"'
|
||||
---@param args { name: string, control: Control }
|
||||
function input_action(args) end
|
||||
---@param args { name: string }
|
||||
function input_action_pressed(args) end
|
||||
---@param args { name: string }
|
||||
function input_action_just_pressed(args) end
|
||||
---@param args { name: string }
|
||||
function input_action_just_released(args) end
|
||||
---@param args { name: string }
|
||||
function input_action_position(args) end
|
||||
---@param args { texture: string, rect: { x: number, y: number, w: number, h: number }, texture_region: { x: number, y: number, w: number, h: number }?, color: { r: number, g: number, b: number, a: number }?, rotation: number?, flip_x: boolean?, flip_y: boolean?, stretch: boolean? }
|
||||
function draw_sprite(args) end
|
||||
---@param args { rect: { x: number, y: number, w: number, h: number }, color: { r: number, g: number, b: number, a: number }? }
|
||||
function draw_rectangle(args) end
|
||||
---@param args { position: { x: number, y: number }, radius: number, color: { r: number, g: number, b: number, a: number }? }
|
||||
function draw_circle(args) end
|
||||
---@param args { string: string, position: { x: number, y: number }, height: number?, color: { r: number, g: number, b: number, a: number }?, font: string? }
|
||||
function draw_text(args) end
|
||||
---@param args { string: string, height: number?, font: string? }
|
||||
function draw_text_width(args) end
|
||||
---@param args { texture: string, corners: { x: number, y: number }, rect: { x: number, y: number, w: number, h: number }, border_thickness: number?, color: { r: number, g: number, b: number, a: number }? }
|
||||
function draw_nine_slice(args) end
|
||||
---@param args { start: { x: number, y: number }, finish: { x: number, y: number }, thickness: number?, color: { r: number, g: number, b: number, a: number }? }
|
||||
function draw_line(args) end
|
||||
---@param args { texture: string, v0: { x: number, y: number, z: number }, v1: { x: number, y: number, z: number }, v2: { x: number, y: number, z: number }, uv0: { x: number, y: number }, uv1: { x: number, y: number }, uv2: { x: number, y: number }, c0: { r: number, g: number, b: number, a: number }?, c1: { r: number, g: number, b: number, a: number }?, c2: { r: number, g: number, b: number, a: number }? }
|
||||
function draw_triangle(args) end
|
||||
---@param args { texture: string, v0: { x: number, y: number, z: number }, v1: { x: number, y: number, z: number }, v2: { x: number, y: number, z: number }, v3: { x: number, y: number, z: number }, texture_region: { x: number, y: number, w: number, h: number }, color: { r: number, g: number, b: number, a: number }? }
|
||||
function draw_quad(args) end
|
||||
---@param args { texture: string, position: { x: number, y: number, z: number }, size: { x: number, y: number }, color: { r: number, g: number, b: number, a: number }?, cylindrical: boolean? }
|
||||
function draw_billboard(args) end
|
||||
---@param args { position: { x: number, y: number, z: number }, direction: { x: number, y: number, z: number }?, up: { x: number, y: number, z: number }?, fov: number?, zoom: number? }
|
||||
function draw_camera(args) end
|
||||
---@param args { position: { x: number, y: number, z: number }, roll: number?, pitch: number?, yaw: number?, fov: number?, zoom: number? }
|
||||
function draw_camera_from_principal_axes(args) end
|
||||
---@param args { textures: string? }
|
||||
function draw_skybox(args) end
|
||||
---@param args { audio: string, channel: string?, loops: boolean?, volume: number?, panning: number? }
|
||||
function audio_play(args) end
|
||||
---@param args { channel: string, parameter: string, value: number }
|
||||
function audio_parameter(args) end
|
||||
---@param args { value: { x: number, y: number }, identity: string }
|
||||
function log_vec2(args) end
|
||||
---@param args { value: { x: number, y: number, z: number }, identity: string }
|
||||
function log_vec3(args) end
|
||||
---@param args { value: { x: number, y: number, w: number, h: number }, identity: string }
|
||||
function log_rect(args) end
|
||||
---@param args { profile: string }
|
||||
function profile_start(args) end
|
||||
---@param args { profile: string }
|
||||
function profile_end(args) end
|
19
apps/templates/zig/.gitignore
vendored
Normal file
19
apps/templates/zig/.gitignore
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
# ignore executables
|
||||
*
|
||||
!*.*
|
||||
!*/
|
||||
|
||||
**/*.so
|
||||
**/*.dll
|
||||
**/*.exe
|
||||
**/*.trace
|
||||
**/*.js
|
||||
**/*.wasm
|
||||
**/*.wasm.map
|
||||
**/*.data
|
||||
**/*.html
|
||||
|
||||
data/scripts/twnapi.lua
|
||||
build/
|
||||
.zig-cache/
|
||||
zig-out/
|
26
apps/templates/zig/CMakeLists.txt
Normal file
26
apps/templates/zig/CMakeLists.txt
Normal file
@ -0,0 +1,26 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
project(twngame LANGUAGES C)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Debug)
|
||||
endif()
|
||||
|
||||
add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
|
||||
|
||||
cmake_path(GET CMAKE_SOURCE_DIR STEM LAST_ONLY GAME_PROJECT_NAME)
|
||||
put_townengine(${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
file(GLOB_RECURSE zig-sources ${CMAKE_CURRENT_SOURCE_DIR}/src/*.zig)
|
||||
|
||||
# TODO: support static build
|
||||
# TODO: propagate release switches
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/libgame.so
|
||||
COMMAND env zig build
|
||||
DEPENDS ${TWN_TARGET} ${zig-sources}
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
zig-step ALL
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/libgame.so
|
||||
)
|
58
apps/templates/zig/build.zig
Normal file
58
apps/templates/zig/build.zig
Normal file
@ -0,0 +1,58 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Although this function looks imperative, note that its job is to
|
||||
// declaratively construct a build graph that will be executed by an external
|
||||
// runner.
|
||||
pub fn build(b: *std.Build) void {
|
||||
// Standard target options allows the person running `zig build` to choose
|
||||
// what target to build for. Here we do not override the defaults, which
|
||||
// means any target is allowed, and the default is native. Other options
|
||||
// for restricting supported target set are available.
|
||||
const target = b.standardTargetOptions(.{});
|
||||
|
||||
// Standard optimization options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// This creates a "module", which represents a collection of source files alongside
|
||||
// some compilation options, such as optimization mode and linked system libraries.
|
||||
// Every executable or library we compile will be based on one or more modules.
|
||||
const lib_mod = b.createModule(.{
|
||||
// `root_source_file` is the Zig "entry point" of the module. If a module
|
||||
// only contains e.g. external object files, you can make this `null`.
|
||||
// In this case the main source file is merely a path, however, in more
|
||||
// complicated build scripts, this could be a generated file.
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
});
|
||||
|
||||
lib_mod.addIncludePath(b.path("../../../"));
|
||||
lib_mod.addIncludePath(b.path("../../../include/"));
|
||||
lib_mod.addLibraryPath(b.path("./"));
|
||||
|
||||
// Now, we will create a static library based on the module we created above.
|
||||
// This creates a `std.Build.Step.Compile`, which is the build step responsible
|
||||
// for actually invoking the compiler.
|
||||
const lib = b.addLibrary(.{
|
||||
.linkage = .dynamic,
|
||||
.name = "game",
|
||||
.root_module = lib_mod,
|
||||
});
|
||||
|
||||
lib.linkSystemLibrary("townengine");
|
||||
|
||||
// This declares intent for the library to be installed into the standard
|
||||
// location when the user invokes the "install" step (the default step when
|
||||
// running `zig build`).
|
||||
const install_artifact = b.addInstallArtifact(lib, .{
|
||||
.dest_dir = .{
|
||||
.override = .{
|
||||
.custom = "../",
|
||||
},
|
||||
},
|
||||
});
|
||||
b.getInstallStep().dependOn(&install_artifact.step);
|
||||
}
|
27
apps/templates/zig/data/twn.toml
Normal file
27
apps/templates/zig/data/twn.toml
Normal file
@ -0,0 +1,27 @@
|
||||
# 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 = "Zig Awesomeness"
|
||||
developer = "notwanp"
|
||||
app_id = "yourzigthing"
|
||||
dev_id = "definatelynotwanp"
|
||||
|
||||
# Game runtime details
|
||||
[game]
|
||||
resolution = [ 640, 480 ]
|
||||
background_color = [ 255, 125, 0, 255 ]
|
||||
#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"
|
24
apps/templates/zig/src/root.zig
Normal file
24
apps/templates/zig/src/root.zig
Normal file
@ -0,0 +1,24 @@
|
||||
const std = @import("std");
|
||||
const c = @cImport({
|
||||
@cInclude("twn_game_api.h");
|
||||
});
|
||||
|
||||
export fn game_tick() void {
|
||||
tick() catch |err| {
|
||||
std.log.err(@errorName(err));
|
||||
};
|
||||
}
|
||||
|
||||
export fn game_end() void {
|
||||
end() catch |err| {
|
||||
std.log.err(@errorName(err));
|
||||
};
|
||||
}
|
||||
|
||||
fn tick() !void {
|
||||
if (c.ctx.initialization_needed) {
|
||||
std.debug.print("lmao\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
fn end() !void {}
|
16
apps/tools/twndel/CMakeLists.txt
Normal file
16
apps/tools/twndel/CMakeLists.txt
Normal file
@ -0,0 +1,16 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
project(twndel LANGUAGES C)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Debug)
|
||||
endif()
|
||||
|
||||
add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
|
||||
|
||||
set(SOURCE_FILES
|
||||
tool.c
|
||||
state.h
|
||||
)
|
||||
|
||||
cmake_path(GET CMAKE_SOURCE_DIR STEM LAST_ONLY GAME_PROJECT_NAME)
|
||||
use_townengine("${SOURCE_FILES}" ${CMAKE_CURRENT_SOURCE_DIR})
|
BIN
apps/tools/twndel/data/bong.ogg
(Stored with Git LFS)
Normal file
BIN
apps/tools/twndel/data/bong.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/camera.png
(Stored with Git LFS)
Normal file
BIN
apps/tools/twndel/data/camera.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/center.png
(Stored with Git LFS)
Normal file
BIN
apps/tools/twndel/data/center.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/click.wav
Normal file
BIN
apps/tools/twndel/data/click.wav
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/drop.ogg
(Stored with Git LFS)
Normal file
BIN
apps/tools/twndel/data/drop.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/grab.png
(Stored with Git LFS)
Normal file
BIN
apps/tools/twndel/data/grab.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/point.png
(Stored with Git LFS)
Normal file
BIN
apps/tools/twndel/data/point.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/pop.wav
Normal file
BIN
apps/tools/twndel/data/pop.wav
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/rip.wav
Normal file
BIN
apps/tools/twndel/data/rip.wav
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/selectin.ogg
(Stored with Git LFS)
Normal file
BIN
apps/tools/twndel/data/selectin.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/selectout.ogg
(Stored with Git LFS)
Normal file
BIN
apps/tools/twndel/data/selectout.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/snip.wav
Normal file
BIN
apps/tools/twndel/data/snip.wav
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/spooncap.wav
Normal file
BIN
apps/tools/twndel/data/spooncap.wav
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/switch.ogg
(Stored with Git LFS)
Normal file
BIN
apps/tools/twndel/data/switch.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
12
apps/tools/twndel/data/twn.toml
Normal file
12
apps/tools/twndel/data/twn.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[about]
|
||||
title = "Townengine Modeling Tool"
|
||||
developer = "twnteam"
|
||||
app_id = "twndel"
|
||||
dev_id = "twnteam"
|
||||
|
||||
# Game runtime details
|
||||
[game]
|
||||
resolution = [ 640, 480 ]
|
||||
background_color = [ 255, 125, 0, 255 ]
|
||||
|
||||
[engine]
|
BIN
apps/tools/twndel/data/x.png
(Stored with Git LFS)
Normal file
BIN
apps/tools/twndel/data/x.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/y.png
(Stored with Git LFS)
Normal file
BIN
apps/tools/twndel/data/y.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
apps/tools/twndel/data/z.png
(Stored with Git LFS)
Normal file
BIN
apps/tools/twndel/data/z.png
(Stored with Git LFS)
Normal file
Binary file not shown.
118
apps/tools/twndel/state.h
Normal file
118
apps/tools/twndel/state.h
Normal file
@ -0,0 +1,118 @@
|
||||
#ifndef STATE_H
|
||||
#define STATE_H
|
||||
|
||||
#include "twn_game_api.h"
|
||||
|
||||
|
||||
#define POINTS_PER_METER 128
|
||||
#define UNDO_STACK_SIZE 32
|
||||
#define CAMERA_FOV ((float)M_PI_2 * 0.75f)
|
||||
|
||||
#define POINT_LIMIT 65534
|
||||
#define FACE_LIMIT 2048
|
||||
#define OBJECT_LIMIT 16
|
||||
#define TEXTURE_LIMIT 32
|
||||
|
||||
#define INVALID_POINT (POINT_LIMIT+1)
|
||||
#define INVALID_FACE (FACE_LIMIT+1)
|
||||
#define INVALID_OBJECT (OBJECT_LIMIT+1)
|
||||
#define INVALID_TEXTURE (TEXTURE_LIMIT+1)
|
||||
|
||||
#define CAMERA_ROTATION_SPEED 0.04f
|
||||
#define CAMERA_TRANSLATION_SPEED 0.04f
|
||||
#define SELECTION_SPHERE_RADIUS 32
|
||||
/* should be an odd number */
|
||||
#define SNAP_LINES_SHOW 7
|
||||
#define SNAP_LINES_WIDTH 1.0f
|
||||
#define SNAP_LINES_COLOR ((Color){200,200,200,150})
|
||||
|
||||
typedef struct Operation {
|
||||
enum {
|
||||
OPERATION_MOVE_POINT,
|
||||
OPERATION_SET_TEXTURE,
|
||||
OPERATION_TRIANGULATE,
|
||||
} kind;
|
||||
union {
|
||||
struct {
|
||||
uint16_t point;
|
||||
int16_t delta_x;
|
||||
int16_t delta_y;
|
||||
int16_t delta_z;
|
||||
uint8_t object;
|
||||
} move_point;
|
||||
|
||||
struct {
|
||||
uint16_t face;
|
||||
int16_t delta_texture;
|
||||
uint8_t object;
|
||||
} set_texture;
|
||||
|
||||
struct {
|
||||
uint16_t old_face;
|
||||
uint16_t new_face;
|
||||
uint8_t object;
|
||||
} triangulate;
|
||||
} data;
|
||||
|
||||
bool chained;
|
||||
} Operation;
|
||||
|
||||
typedef struct Point {
|
||||
int16_t x, y, z;
|
||||
} Point;
|
||||
|
||||
/* TODO: store topology in terms on edge connections? might be bad, as it's stateful */
|
||||
/* triangles have p3 = INVALID_POINT */
|
||||
/* lines have p2, p3 = INVALID_POINT */
|
||||
/* billboards have p1, p2, p3 = INVALID_POINT */
|
||||
typedef struct Face {
|
||||
uint16_t p[4];
|
||||
/* texture origin, as point on face plane, in absolute coordinates */
|
||||
int16_t tex_x, tex_y;
|
||||
uint8_t texture;
|
||||
uint8_t tex_scale;
|
||||
} Face;
|
||||
|
||||
typedef struct Object {
|
||||
char *name;
|
||||
bool is_invisible;
|
||||
Point position;
|
||||
char *textures[TEXTURE_LIMIT + 1];
|
||||
uint8_t textures_sz;
|
||||
Point rotation;
|
||||
Face faces[FACE_LIMIT];
|
||||
uint16_t faces_sz;
|
||||
} Object;
|
||||
|
||||
typedef struct State {
|
||||
Operation op_stack[UNDO_STACK_SIZE];
|
||||
uint32_t op_stack_ptr;
|
||||
bool op_active;
|
||||
|
||||
Vec3 active_center;
|
||||
Vec3 camera_position;
|
||||
Vec3 camera_direction;
|
||||
float camera_zoom;
|
||||
bool camera_is_orthographic;
|
||||
/* defaults to wireframe */
|
||||
bool solid_display_mode;
|
||||
/* positions skipped */
|
||||
uint8_t grid_snap_granularity;
|
||||
|
||||
Point points[POINT_LIMIT];
|
||||
uint16_t points_sz;
|
||||
|
||||
Object objects[OBJECT_LIMIT];
|
||||
uint8_t objects_sz;
|
||||
|
||||
/* which axes are blocked and which are not */
|
||||
/* order: x, y, z */
|
||||
bool axis_mask[3];
|
||||
char *current_texture;
|
||||
|
||||
uint8_t current_hovered_obj;
|
||||
uint16_t current_hovered_face;
|
||||
} State;
|
||||
|
||||
|
||||
#endif
|
972
apps/tools/twndel/tool.c
Normal file
972
apps/tools/twndel/tool.c
Normal file
@ -0,0 +1,972 @@
|
||||
#include "twn_game_api.h"
|
||||
#include "state.h"
|
||||
#include "twn_vec.h"
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
/* planned features: */
|
||||
/* grid-based, bounded space (65536 / POINTS_PER_METER meters), which allows for efficient storage */
|
||||
/* 65534 point limit, 16 object limit, 2048 faces per object, 32 textures per object */
|
||||
/* triangles and quads only */
|
||||
/* support for billboards and flat two sided quads */
|
||||
/* texture painting */
|
||||
/* bones with mesh animations, snapping to grid, with no weights */
|
||||
/* billboard render to specified angles */
|
||||
/* 1 point light primitive lighting */
|
||||
/* live edited textures are capped at 128x128 */
|
||||
|
||||
/* assumptions: */
|
||||
/* up is always (0,1,0) */
|
||||
/* preallocations everywhere */
|
||||
|
||||
static State state;
|
||||
static bool init;
|
||||
|
||||
|
||||
static uint8_t new_object(const char *name) {
|
||||
if (state.objects_sz >= OBJECT_LIMIT)
|
||||
return INVALID_OBJECT;
|
||||
state.objects_sz++;
|
||||
state.objects[state.objects_sz-1].name = SDL_strdup(name);
|
||||
return state.objects_sz-1;
|
||||
}
|
||||
|
||||
|
||||
static uint16_t new_point(int16_t x, int16_t y, int16_t z) {
|
||||
if (state.points_sz >= POINT_LIMIT)
|
||||
return INVALID_POINT;
|
||||
state.points_sz++;
|
||||
state.points[state.points_sz-1] = (Point){x, y, z};
|
||||
return state.points_sz-1;
|
||||
}
|
||||
|
||||
|
||||
static uint16_t push_face(uint8_t object,
|
||||
uint16_t p0, uint16_t p1, uint16_t p2, uint16_t p3,
|
||||
uint8_t texture, uint8_t tex_scale, int16_t tex_x, int16_t tex_y)
|
||||
{
|
||||
Object *o = &state.objects[object];
|
||||
o->faces_sz++;
|
||||
o->faces[o->faces_sz-1] = (Face) {
|
||||
.p = {p0, p1, p2, p3},
|
||||
.texture = texture,
|
||||
.tex_scale = tex_scale,
|
||||
.tex_x = tex_x,
|
||||
.tex_y = tex_y,
|
||||
};
|
||||
return o->faces_sz-1;
|
||||
}
|
||||
|
||||
|
||||
static uint8_t push_texture(uint8_t object, char *texture) {
|
||||
Object *o = &state.objects[object];
|
||||
|
||||
/* check whether it's already here */
|
||||
for (uint8_t i = 0; i < o->textures_sz; ++i)
|
||||
if (SDL_strcmp(o->textures[i], texture) == 0)
|
||||
return i;
|
||||
|
||||
o->textures_sz++;
|
||||
o->textures[o->textures_sz-1] = SDL_strdup(texture);
|
||||
return o->textures_sz-1;
|
||||
}
|
||||
|
||||
|
||||
/* TODO: use tombstones instead? it would be easier to maintain, by a lot */
|
||||
/* note: make sure nothing depends on none */
|
||||
static void pop_face(uint8_t object, uint16_t face) {
|
||||
Object *o = &state.objects[object];
|
||||
if (face != o->faces_sz-1)
|
||||
o->faces[face] = o->faces[o->faces_sz-1];
|
||||
o->faces_sz--;
|
||||
}
|
||||
|
||||
|
||||
|
||||
static void push_operation(Operation operation, bool active) {
|
||||
state.op_stack_ptr++;
|
||||
uint8_t op = state.op_stack_ptr % UNDO_STACK_SIZE;
|
||||
state.op_stack[op] = operation;
|
||||
state.op_active = active;
|
||||
}
|
||||
|
||||
|
||||
static void extend_operation(Operation operation) {
|
||||
uint8_t op = state.op_stack_ptr % UNDO_STACK_SIZE;
|
||||
Operation ext = state.op_stack[op];
|
||||
ext.chained = true;
|
||||
state.op_stack[op] = operation;
|
||||
push_operation(ext, state.op_active);
|
||||
}
|
||||
|
||||
|
||||
static inline Vec3 point_to_vec3(uint8_t object, uint16_t point) {
|
||||
Object *o = &state.objects[object];
|
||||
return (Vec3){ (o->position.x + state.points[point].x) / (float)POINTS_PER_METER,
|
||||
(o->position.y + state.points[point].y) / (float)POINTS_PER_METER,
|
||||
(o->position.z + state.points[point].z) / (float)POINTS_PER_METER };
|
||||
}
|
||||
|
||||
|
||||
/* TODO: something is wrong, figure out how to introduce rotation to this. */
|
||||
static inline Vec2 project_texture_coordinate(Vec2 origin, Vec3 plane, Vec3 point, float scale) {
|
||||
Vec3 right = vec3_norm(vec3_cross(plane, fabsf(0.0f - plane.y) < 1e-9f ? (Vec3){0,1,0} : (Vec3){1,0,0}));
|
||||
Vec3 up = vec3_norm(vec3_cross(right, plane));
|
||||
Vec2 pp = { vec3_dot(point, right), vec3_dot(point, up) };
|
||||
return vec2_scale(vec2_sub(origin, pp), scale);
|
||||
}
|
||||
|
||||
|
||||
static void render_object(uint8_t object) {
|
||||
Object *o = &state.objects[object];
|
||||
|
||||
if (o->is_invisible)
|
||||
return;
|
||||
|
||||
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
|
||||
Face *f = &o->faces[fi];
|
||||
|
||||
if (f->p[2] != INVALID_POINT) {
|
||||
Vec3 p0 = point_to_vec3(object, f->p[0]);
|
||||
Vec3 p1 = point_to_vec3(object, f->p[1]);
|
||||
Vec3 p2 = point_to_vec3(object, f->p[2]);
|
||||
|
||||
if (state.solid_display_mode) {
|
||||
Vec3 n = vec3_norm(vec3_cross(vec3_sub(p1, p0), vec3_sub(p2, p0)));
|
||||
Vec2 to = { f->tex_x / (float)POINTS_PER_METER, f->tex_y / (float)POINTS_PER_METER, };
|
||||
|
||||
Vec2 tul = project_texture_coordinate(to, n, p0, (float)f->tex_scale);
|
||||
Vec2 tdl = project_texture_coordinate(to, n, p1, (float)f->tex_scale);
|
||||
Vec2 tdr = project_texture_coordinate(to, n, p2, (float)f->tex_scale);
|
||||
|
||||
draw_triangle(o->textures[f->texture],
|
||||
p0, p1, p2,
|
||||
tul, tdl, tdr,
|
||||
(Color){255,255,255,255},
|
||||
(Color){255,255,255,255},
|
||||
(Color){255,255,255,255});
|
||||
|
||||
if (f->p[3] != INVALID_POINT) {
|
||||
Vec3 p3 = point_to_vec3(object, f->p[3]);
|
||||
Vec2 tur = project_texture_coordinate(to, n, p3, (float)f->tex_scale);
|
||||
draw_triangle(o->textures[f->texture],
|
||||
p2, p3, p0,
|
||||
tdr, tur, tul,
|
||||
(Color){255,255,255,255},
|
||||
(Color){255,255,255,255},
|
||||
(Color){255,255,255,255});
|
||||
}
|
||||
|
||||
} else {
|
||||
draw_line_3d(p0, p1, 1, (Color){255,255,255,255});
|
||||
draw_line_3d(p1, p2, 1, (Color){255,255,255,255});
|
||||
|
||||
if (f->p[3] == INVALID_POINT)
|
||||
draw_line_3d(p2, p0, 1, (Color){255,255,255,255});
|
||||
else {
|
||||
Vec3 p3 = point_to_vec3(object, f->p[3]);
|
||||
draw_line_3d(p2, p3, 1, (Color){255,255,255,255});
|
||||
draw_line_3d(p3, p0, 1, (Color){255,255,255,255});
|
||||
}
|
||||
}
|
||||
|
||||
} else
|
||||
SDL_assert_always(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static uint8_t new_cube(Point pos, Point size) {
|
||||
uint8_t object = new_object("cube");
|
||||
|
||||
uint16_t p0 = new_point(pos.x - size.x / 2, pos.y - size.y / 2, pos.z - size.z / 2);
|
||||
uint16_t p1 = new_point(pos.x - size.x / 2, pos.y + size.y / 2, pos.z - size.z / 2);
|
||||
uint16_t p2 = new_point(pos.x + size.x / 2, pos.y + size.y / 2, pos.z - size.z / 2);
|
||||
uint16_t p3 = new_point(pos.x + size.x / 2, pos.y - size.y / 2, pos.z - size.z / 2);
|
||||
uint16_t p4 = new_point(pos.x - size.x / 2, pos.y - size.y / 2, pos.z + size.z / 2);
|
||||
uint16_t p5 = new_point(pos.x - size.x / 2, pos.y + size.y / 2, pos.z + size.z / 2);
|
||||
uint16_t p6 = new_point(pos.x + size.x / 2, pos.y + size.y / 2, pos.z + size.z / 2);
|
||||
uint16_t p7 = new_point(pos.x + size.x / 2, pos.y - size.y / 2, pos.z + size.z / 2);
|
||||
|
||||
uint8_t tex = push_texture(object, "/data/placeholder.png");
|
||||
push_face(object, p2, p3, p0, p1, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
|
||||
push_face(object, p5, p4, p7, p6, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
|
||||
push_face(object, p1, p0, p4, p5, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
|
||||
push_face(object, p6, p7, p3, p2, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
|
||||
push_face(object, p2, p1, p5, p6, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
|
||||
push_face(object, p0, p3, p7, p4, tex, 128, pos.x - size.x / 2, pos.y - size.y / 2);
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
|
||||
static void process_camera_rotation(void) {
|
||||
float horizontal_rotation = 0;
|
||||
float vertical_rotation = 0;
|
||||
|
||||
if (input_action_pressed("camera_rotate_left"))
|
||||
horizontal_rotation -= CAMERA_ROTATION_SPEED;
|
||||
if (input_action_pressed("camera_rotate_right"))
|
||||
horizontal_rotation += CAMERA_ROTATION_SPEED;
|
||||
if (input_action_pressed("camera_rotate_up"))
|
||||
vertical_rotation -= CAMERA_ROTATION_SPEED;
|
||||
if (input_action_pressed("camera_rotate_down"))
|
||||
vertical_rotation += CAMERA_ROTATION_SPEED;
|
||||
|
||||
Vec3 front = vec3_cross(state.camera_direction, (Vec3){0,1,0});
|
||||
Vec3 local_position = vec3_sub(state.active_center, state.camera_position);
|
||||
Vec3 new_local_position = vec3_rotate(local_position, horizontal_rotation, (Vec3){0,1,0});
|
||||
|
||||
state.camera_direction = vec3_rotate(state.camera_direction, horizontal_rotation, (Vec3){0,1,0});
|
||||
Vec3 new_rot = vec3_rotate(state.camera_direction, vertical_rotation, front);
|
||||
|
||||
/* only apply if it's in limits */
|
||||
float d = vec3_dot(new_rot, (Vec3){0,-1,0});
|
||||
if (fabsf(d) <= 0.999f) {
|
||||
new_local_position = vec3_rotate(new_local_position, vertical_rotation, front);
|
||||
state.camera_direction = new_rot;
|
||||
}
|
||||
|
||||
state.camera_position = vec3_sub(state.active_center, new_local_position);
|
||||
}
|
||||
|
||||
|
||||
static void process_camera_translation(void) {
|
||||
Vec3 right = vec3_norm(vec3_cross(state.camera_direction, (Vec3){0,1,0}));
|
||||
Vec3 up = vec3_norm(vec3_cross(state.camera_direction, right));
|
||||
Vec3 was = state.camera_position;
|
||||
|
||||
if (input_action_pressed("camera_rotate_left"))
|
||||
state.camera_position = vec3_sub(state.camera_position, vec3_scale(right, CAMERA_TRANSLATION_SPEED));
|
||||
if (input_action_pressed("camera_rotate_right"))
|
||||
state.camera_position = vec3_add(state.camera_position, vec3_scale(right, CAMERA_TRANSLATION_SPEED));
|
||||
if (input_action_pressed("camera_rotate_up"))
|
||||
state.camera_position = vec3_sub(state.camera_position, vec3_scale(up, CAMERA_TRANSLATION_SPEED));
|
||||
if (input_action_pressed("camera_rotate_down"))
|
||||
state.camera_position = vec3_add(state.camera_position, vec3_scale(up, CAMERA_TRANSLATION_SPEED));
|
||||
|
||||
state.active_center = vec3_add(state.active_center, vec3_sub(state.camera_position, was));
|
||||
|
||||
draw_billboard("/data/camera.png",
|
||||
vec3_add(state.camera_position, vec3_scale(state.camera_direction, vec3_length(state.camera_position))),
|
||||
(Vec2){0.2f,0.2f},
|
||||
(Rect){0},
|
||||
(Color){255,255,255,255},
|
||||
false);
|
||||
|
||||
/* show relation to origin */
|
||||
draw_billboard("/data/center.png",
|
||||
(Vec3){0},
|
||||
(Vec2){0.1f,0.1f},
|
||||
(Rect){0},
|
||||
(Color){255,255,255,255},
|
||||
false);
|
||||
|
||||
draw_line_3d((Vec3){0}, state.active_center, 1, (Color){255,255,255,255});
|
||||
}
|
||||
|
||||
|
||||
static void process_camera_movement(void) {
|
||||
input_action("camera_rotate_left", "A");
|
||||
input_action("camera_rotate_right", "D");
|
||||
input_action("camera_rotate_up", "W");
|
||||
input_action("camera_rotate_down", "S");
|
||||
input_action("camera_lock_rotation", "SPACE");
|
||||
|
||||
if (input_action_pressed("camera_lock_rotation")) {
|
||||
process_camera_translation();
|
||||
} else {
|
||||
process_camera_rotation();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static inline DrawCameraUnprojectResult unproject_point(Vec2 point) {
|
||||
return draw_camera_unproject(
|
||||
point,
|
||||
state.camera_position,
|
||||
state.camera_direction,
|
||||
(Vec3){0, 1, 0},
|
||||
state.camera_is_orthographic ? 0 : CAMERA_FOV,
|
||||
state.camera_zoom,
|
||||
100 );
|
||||
}
|
||||
|
||||
|
||||
static bool find_closest_point(uint8_t* object_result, uint16_t *point_result) {
|
||||
DrawCameraUnprojectResult pos_and_ray = unproject_point(ctx.mouse_position);
|
||||
|
||||
/* step over every selectable object and find points closest to the view ray */
|
||||
/* by constructing triangles and finding their height, from perpendicular */
|
||||
uint16_t closest_point = INVALID_POINT;
|
||||
uint8_t closest_obj = INVALID_OBJECT;
|
||||
float closest_distance = INFINITY;
|
||||
|
||||
for (uint8_t obj = 0; obj < state.objects_sz; ++obj) {
|
||||
Object *o = &state.objects[obj];
|
||||
|
||||
if (o->is_invisible)
|
||||
continue;
|
||||
|
||||
/* TODO: is it possible to skip repeated points? does it matter? */
|
||||
/* as we limit the point could we could actually have bool array preallocated for this */
|
||||
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
|
||||
Face *f = &o->faces[fi];
|
||||
for (uint16_t pi = 0; pi < 4; ++pi) {
|
||||
if (f->p[pi] == INVALID_POINT) break;
|
||||
Vec3 p = point_to_vec3(obj, f->p[pi]);
|
||||
Vec3 d = vec3_sub(pos_and_ray.position, p);
|
||||
Vec3 b = vec3_cross(d, pos_and_ray.direction);
|
||||
float ray_dist = vec3_length(b);
|
||||
if (ray_dist > ((float)SELECTION_SPHERE_RADIUS / POINTS_PER_METER))
|
||||
continue;
|
||||
float dist = vec3_length(vec3_sub(pos_and_ray.position, vec3_add(p, b)));
|
||||
if (dist < closest_distance) {
|
||||
closest_distance = dist;
|
||||
closest_obj = obj;
|
||||
closest_point = f->p[pi];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (closest_point == INVALID_POINT)
|
||||
return false;
|
||||
|
||||
if (object_result)
|
||||
*object_result = closest_obj;
|
||||
if (point_result)
|
||||
*point_result = closest_point;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/* o = vector origin */
|
||||
/* v = vector direction, normalized */
|
||||
/* p = any point on plane */
|
||||
/* n = normal of a plane */
|
||||
static bool vector_plane_intersection(Vec3 o, Vec3 v, Vec3 p, Vec3 n, Vec3 *out) {
|
||||
float dot = vec3_dot(n, v);
|
||||
if (fabsf(dot) > FLT_EPSILON) {
|
||||
Vec3 w = vec3_sub(o, p);
|
||||
float fac = -vec3_dot(n, w) / dot;
|
||||
*out = vec3_add(o, vec3_scale(v, fac));
|
||||
return true;
|
||||
}
|
||||
/* vector and plane are perpendicular, assume that it lies exactly on it */
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static bool find_closest_face(uint8_t* object_result, uint16_t *face_result) {
|
||||
DrawCameraUnprojectResult cam = unproject_point(ctx.mouse_position);
|
||||
|
||||
uint8_t closest_obj = INVALID_OBJECT;
|
||||
uint16_t closest_face = INVALID_FACE;
|
||||
float closest_distance = INFINITY;
|
||||
|
||||
for (uint8_t oi = 0; oi < state.objects_sz; ++oi) {
|
||||
Object *o = &state.objects[oi];
|
||||
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
|
||||
Face *f = &o->faces[fi];
|
||||
|
||||
if (f->p[1] == INVALID_POINT) continue;
|
||||
|
||||
Vec3 p0 = point_to_vec3(oi, f->p[0]);
|
||||
Vec3 p1 = point_to_vec3(oi, f->p[1]);
|
||||
Vec3 p2 = point_to_vec3(oi, f->p[2]);
|
||||
|
||||
Vec3 n = vec3_norm(vec3_cross(vec3_sub(p1, p0), vec3_sub(p2, p0)));
|
||||
|
||||
/* culling */
|
||||
if (vec3_dot(state.camera_direction, n) >= 0) continue;
|
||||
|
||||
Vec3 i;
|
||||
if (!vector_plane_intersection(cam.position, cam.direction, p0, n, &i))
|
||||
continue;
|
||||
|
||||
float dist = vec3_length(vec3_sub(p0, i));
|
||||
if (dist >= closest_distance) continue;
|
||||
|
||||
/* left normals are used to determine whether point lies to the left for all forming lines */
|
||||
Vec3 ln0 = vec3_norm(vec3_cross(n, vec3_sub(p1, p0)));
|
||||
if (vec3_dot(ln0, vec3_sub(p0, i)) >= 0) continue;
|
||||
|
||||
Vec3 ln1 = vec3_norm(vec3_cross(n, vec3_sub(p2, p1)));
|
||||
if (vec3_dot(ln1, vec3_sub(p1, i)) >= 0) continue;
|
||||
|
||||
if (f->p[3] == INVALID_POINT) {
|
||||
/* triangle */
|
||||
Vec3 ln2 = vec3_norm(vec3_cross(n, vec3_sub(p0, p2)));
|
||||
if (vec3_dot(ln2, vec3_sub(p2, i)) >= 0) continue;
|
||||
|
||||
} else {
|
||||
/* quad */
|
||||
Vec3 p3 = point_to_vec3(oi, f->p[3]);
|
||||
Vec3 ln2 = vec3_norm(vec3_cross(n, vec3_sub(p3, p2)));
|
||||
if (vec3_dot(ln2, vec3_sub(p2, i)) >= 0) continue;
|
||||
|
||||
Vec3 ln3 = vec3_norm(vec3_cross(n, vec3_sub(p0, p3)));
|
||||
if (vec3_dot(ln3, vec3_sub(p3, i)) >= 0) continue;
|
||||
}
|
||||
|
||||
closest_distance = dist;
|
||||
closest_face = fi;
|
||||
closest_obj = oi;
|
||||
}
|
||||
}
|
||||
|
||||
if (closest_face == INVALID_FACE)
|
||||
return false;
|
||||
|
||||
if (object_result)
|
||||
*object_result = closest_obj;
|
||||
if (face_result)
|
||||
*face_result = closest_face;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static void show_snap_lines(Vec3 p) {
|
||||
float step = 1.0f / ((float)POINTS_PER_METER / state.grid_snap_granularity);
|
||||
int const lines_per_side = (SNAP_LINES_SHOW - 1) / 2;
|
||||
|
||||
for (int l = -lines_per_side; l <= lines_per_side; ++l) {
|
||||
if (!state.axis_mask[0]) {
|
||||
Vec3 c = vec3_add(p, vec3_scale((Vec3){1,0,0}, step * (float)l));
|
||||
draw_line_3d(
|
||||
vec3_add(c, vec3_scale((Vec3){0,0,1}, SNAP_LINES_WIDTH)),
|
||||
vec3_add(c, vec3_scale((Vec3){0,0,1}, -SNAP_LINES_WIDTH)),
|
||||
1,
|
||||
SNAP_LINES_COLOR);
|
||||
}
|
||||
|
||||
if (!state.axis_mask[1]) {
|
||||
Vec3 axis = fabsf(vec3_dot(state.camera_direction, (Vec3){0,0,1})) >= 0.5f ? (Vec3){1,0,0} : (Vec3){0,0,1};
|
||||
Vec3 c = vec3_add(p, vec3_scale((Vec3){0,1,0}, step * (float)l));
|
||||
draw_line_3d(
|
||||
vec3_add(c, vec3_scale(axis, SNAP_LINES_WIDTH)),
|
||||
vec3_add(c, vec3_scale(axis, -SNAP_LINES_WIDTH)),
|
||||
1,
|
||||
SNAP_LINES_COLOR);
|
||||
}
|
||||
|
||||
if (!state.axis_mask[2]) {
|
||||
Vec3 c = vec3_add(p, vec3_scale((Vec3){0,0,1}, step * (float)l));
|
||||
draw_line_3d(
|
||||
vec3_add(c, vec3_scale((Vec3){1,0,0}, SNAP_LINES_WIDTH)),
|
||||
vec3_add(c, vec3_scale((Vec3){1,0,0}, -SNAP_LINES_WIDTH)),
|
||||
1,
|
||||
SNAP_LINES_COLOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static bool process_operation_move_point(Operation *op) {
|
||||
/* finish dragging around */
|
||||
/* TODO: dont keep empty ops on stack? */
|
||||
if (input_action_just_released("select")) {
|
||||
state.op_active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
float size = sinf(ctx.frame_number / 10) * 0.05f + 0.05f;
|
||||
draw_billboard("/data/grab.png",
|
||||
point_to_vec3(op->data.move_point.object, op->data.move_point.point),
|
||||
(Vec2){size, size},
|
||||
(Rect){0},
|
||||
(Color){255,255,255,255},
|
||||
false);
|
||||
|
||||
DrawCameraUnprojectResult cam = unproject_point(ctx.mouse_position);
|
||||
Vec3 p = point_to_vec3(op->data.move_point.object, op->data.move_point.point);
|
||||
bool point_moved = false;
|
||||
|
||||
/* figure out which planes are angled acutely against the viewing direction */
|
||||
bool y_closer_than_horizon = fabsf(vec3_dot(state.camera_direction, (Vec3){0,1,0})) >= 0.5f;
|
||||
|
||||
Vec3 plane_normal_x = y_closer_than_horizon ? (Vec3){0,1,0} : (Vec3){0,0,1};
|
||||
Vec3 plane_normal_z = y_closer_than_horizon ? (Vec3){0,1,0} : (Vec3){1,0,0};
|
||||
|
||||
/* show snapping in lines */
|
||||
show_snap_lines(p);
|
||||
|
||||
Vec3 s;
|
||||
if (!state.axis_mask[0]) {
|
||||
if (vector_plane_intersection(cam.position, cam.direction, p, plane_normal_x, &s)) {
|
||||
int16_t xch = (int16_t)(roundf(vec3_dot(vec3_sub(s, p), (Vec3){1,0,0}) * (float)POINTS_PER_METER));
|
||||
xch -= xch % state.grid_snap_granularity;
|
||||
state.points[op->data.move_point.point].x += xch;
|
||||
op->data.move_point.delta_x += xch;
|
||||
if (xch != 0) point_moved = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.axis_mask[1]) {
|
||||
if (vector_plane_intersection(cam.position, cam.direction, p, (Vec3){0,0,1}, &s)) {
|
||||
int16_t ych = (int16_t)(roundf(vec3_dot(vec3_sub(s, p), (Vec3){0,1,0}) * (float)POINTS_PER_METER));
|
||||
ych -= ych % state.grid_snap_granularity;
|
||||
state.points[op->data.move_point.point].y += ych;
|
||||
op->data.move_point.delta_y += ych;
|
||||
if (ych != 0) point_moved = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.axis_mask[2]) {
|
||||
if (vector_plane_intersection(cam.position, cam.direction, p, plane_normal_z, &s)) {
|
||||
int16_t zch = (int16_t)(roundf(vec3_dot(vec3_sub(s, p), (Vec3){0,0,1}) * (float)POINTS_PER_METER));
|
||||
zch -= zch % state.grid_snap_granularity;
|
||||
state.points[op->data.move_point.point].z += zch;
|
||||
op->data.move_point.delta_z += zch;
|
||||
if (zch != 0) point_moved = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (point_moved) {
|
||||
audio_play("/data/bong.ogg", NULL, false, 0.12f, 0.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static void reverse_operation_move_point(Operation *op) {
|
||||
SDL_assert(op->kind == OPERATION_MOVE_POINT);
|
||||
state.points[op->data.move_point.point].x -= op->data.move_point.delta_x;
|
||||
state.points[op->data.move_point.point].y -= op->data.move_point.delta_y;
|
||||
state.points[op->data.move_point.point].z -= op->data.move_point.delta_z;
|
||||
audio_play("/data/drop.ogg", NULL, false, 0.4f, 0.0f);
|
||||
}
|
||||
|
||||
|
||||
static void reverse_operation_set_texture(Operation *op) {
|
||||
SDL_assert(op->kind == OPERATION_SET_TEXTURE);
|
||||
Object *o = &state.objects[op->data.set_texture.object];
|
||||
o->faces[op->data.set_texture.face].texture += op->data.set_texture.delta_texture;
|
||||
audio_play("/data/drop.ogg", NULL, false, 0.4f, 0.0f);
|
||||
}
|
||||
|
||||
|
||||
static void reverse_triangulation(Operation *op) {
|
||||
SDL_assert(op->kind == OPERATION_TRIANGULATE);
|
||||
Object *o = &state.objects[op->data.set_texture.object];
|
||||
Face *fn = &o->faces[op->data.triangulate.new_face];
|
||||
Face *fo = &o->faces[op->data.triangulate.old_face];
|
||||
fo->p[3] = fo->p[2];
|
||||
fo->p[2] = fn->p[1];
|
||||
pop_face(op->data.set_texture.object, op->data.triangulate.new_face);
|
||||
}
|
||||
|
||||
|
||||
/* TODO: reverse of this */
|
||||
static void try_subdividing_from_moving(uint8_t object, uint16_t point) {
|
||||
Object *o = &state.objects[object];
|
||||
bool not_first = false;
|
||||
|
||||
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
|
||||
Face *f = &o->faces[fi];
|
||||
if (f->p[3] == INVALID_POINT) continue;
|
||||
for (uint16_t pi = 0; pi < 4; ++pi) {
|
||||
if (f->p[pi] == point) {
|
||||
Face new0 = *f;
|
||||
new0.p[0] = f->p[pi];
|
||||
new0.p[1] = f->p[(pi + 1) % 4];
|
||||
new0.p[2] = f->p[(pi + 3) % 4];
|
||||
new0.p[3] = INVALID_POINT;
|
||||
|
||||
uint16_t newf = push_face(
|
||||
object,
|
||||
f->p[(pi + 1) % 4],
|
||||
f->p[(pi + 2) % 4],
|
||||
f->p[(pi + 3) % 4],
|
||||
INVALID_POINT,
|
||||
f->texture,
|
||||
f->tex_scale,
|
||||
f->tex_x,
|
||||
f->tex_y);
|
||||
|
||||
*f = new0;
|
||||
|
||||
extend_operation((Operation){
|
||||
.kind = OPERATION_TRIANGULATE,
|
||||
.data = { .triangulate = { .new_face = newf, .old_face = fi, .object = object} },
|
||||
.chained = not_first,
|
||||
});
|
||||
|
||||
not_first = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void process_undo(void) {
|
||||
if (state.op_active)
|
||||
state.op_active = false;
|
||||
|
||||
/* TODO: checks and defined limit */
|
||||
Operation *op = &state.op_stack[state.op_stack_ptr % UNDO_STACK_SIZE];
|
||||
state.op_stack_ptr--;
|
||||
|
||||
switch (op->kind) {
|
||||
case OPERATION_MOVE_POINT: {
|
||||
reverse_operation_move_point(op);
|
||||
break;
|
||||
}
|
||||
|
||||
case OPERATION_SET_TEXTURE: {
|
||||
reverse_operation_set_texture(op);
|
||||
break;
|
||||
}
|
||||
|
||||
case OPERATION_TRIANGULATE: {
|
||||
reverse_triangulation(op);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
(void)0;
|
||||
}
|
||||
|
||||
/* pop another if they're chained together */
|
||||
if (op->chained) process_undo();
|
||||
}
|
||||
|
||||
|
||||
static void process_operations(void) {
|
||||
if (input_action_just_pressed("undo"))
|
||||
process_undo();
|
||||
|
||||
if (!state.op_active) {
|
||||
/* point dragging */
|
||||
if (!state.current_texture) {
|
||||
uint16_t point_select; uint8_t obj_select;
|
||||
if (find_closest_point(&obj_select, &point_select)) {
|
||||
draw_billboard("/data/point.png",
|
||||
point_to_vec3(obj_select, point_select),
|
||||
(Vec2){0.05f, 0.05f},
|
||||
(Rect){0},
|
||||
(Color){255,255,255,255},
|
||||
false);
|
||||
|
||||
if (input_action_just_pressed("select"))
|
||||
push_operation((Operation){
|
||||
.kind = OPERATION_MOVE_POINT,
|
||||
.data = { .move_point = { .point = point_select, .object = obj_select } },
|
||||
}, true );
|
||||
}
|
||||
|
||||
/* texture setting */
|
||||
} else {
|
||||
uint8_t obj_select; uint16_t face_select;
|
||||
if (find_closest_face(&obj_select, &face_select)) {
|
||||
state.current_hovered_face = face_select;
|
||||
state.current_hovered_obj = obj_select;
|
||||
|
||||
if (input_action_just_pressed("rotate")) {
|
||||
Face *f = &state.objects[obj_select].faces[face_select];
|
||||
int16_t tex_x = f->tex_x;
|
||||
int16_t tex_y = f->tex_y;
|
||||
f->tex_x = -tex_y;
|
||||
f->tex_y = tex_x;
|
||||
}
|
||||
|
||||
if (input_action_pressed("select") && state.current_texture) {
|
||||
uint8_t new_tex = push_texture(obj_select, state.current_texture);
|
||||
uint8_t cur_tex = state.objects[obj_select].faces[face_select].texture;
|
||||
|
||||
if (new_tex != cur_tex) {
|
||||
state.objects[obj_select].faces[face_select].texture = new_tex;
|
||||
audio_play("/data/bong.ogg", NULL, false, 0.5f, 0.0f);
|
||||
|
||||
push_operation((Operation){
|
||||
.kind = OPERATION_SET_TEXTURE,
|
||||
.data = { .set_texture = {
|
||||
.face = face_select,
|
||||
.object = obj_select,
|
||||
.delta_texture = cur_tex - new_tex,
|
||||
}},
|
||||
}, false );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.op_active) {
|
||||
Operation *op = &state.op_stack[state.op_stack_ptr % UNDO_STACK_SIZE];
|
||||
switch (op->kind) {
|
||||
case OPERATION_MOVE_POINT: {
|
||||
bool update = process_operation_move_point(op);
|
||||
|
||||
if (update)
|
||||
try_subdividing_from_moving(op->data.move_point.object, op->data.move_point.point);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OPERATION_SET_TEXTURE:
|
||||
case OPERATION_TRIANGULATE:
|
||||
default:
|
||||
(void)0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void draw_axes(void) {
|
||||
/* axis helpers */
|
||||
/* idea: double selection of axes for diagonal edits */
|
||||
draw_line_3d(state.active_center, (Vec3){256,0,0}, 1, state.axis_mask[0] ? (Color){0,0,0,75} : (Color){255,0,0,125});
|
||||
draw_billboard("/data/x.png",
|
||||
vec3_add(state.active_center, (Vec3){2,0,0}),
|
||||
(Vec2){0.1f, 0.1f},
|
||||
(Rect){0},
|
||||
state.axis_mask[0] ? (Color){0,0,0,255} : (Color){255,0,0,255},
|
||||
false);
|
||||
|
||||
draw_line_3d(state.active_center, (Vec3){0,256,0}, 1, state.axis_mask[1] ? (Color){0,0,0,75} : (Color){75,125,25,125});
|
||||
draw_billboard("/data/y.png",
|
||||
vec3_add(state.active_center, (Vec3){0,1.5f,0}),
|
||||
(Vec2){0.1f, 0.1f},
|
||||
(Rect){0},
|
||||
state.axis_mask[1] ? (Color){0,0,0,255} : (Color){75,125,25,255},
|
||||
false);
|
||||
|
||||
draw_line_3d(state.active_center, (Vec3){0,0,256}, 1, state.axis_mask[2] ? (Color){0,0,0,75} : (Color){0,0,255,125});
|
||||
draw_billboard("/data/z.png",
|
||||
vec3_add(state.active_center, (Vec3){0,0,2}),
|
||||
(Vec2){0.1f, 0.1f},
|
||||
(Rect){0},
|
||||
state.axis_mask[2] ? (Color){0,0,0,255} : (Color){0,0,255,255},
|
||||
false);
|
||||
}
|
||||
|
||||
|
||||
static void draw_hovered_face_border(void) {
|
||||
if (state.current_hovered_obj == INVALID_OBJECT || state.current_hovered_face == INVALID_FACE)
|
||||
return;
|
||||
|
||||
Object *o = &state.objects[state.current_hovered_obj];
|
||||
Face *f = &o->faces[state.current_hovered_face];
|
||||
|
||||
if (f->p[3] != INVALID_POINT) {
|
||||
Vec3 p0 = point_to_vec3(state.current_hovered_obj, f->p[0]);
|
||||
Vec3 p1 = point_to_vec3(state.current_hovered_obj, f->p[1]);
|
||||
Vec3 p2 = point_to_vec3(state.current_hovered_obj, f->p[2]);
|
||||
Vec3 p3 = point_to_vec3(state.current_hovered_obj, f->p[3]);
|
||||
Vec3 center = vec3_scale(vec3_add(p2, p0), 0.5);
|
||||
float size = sinf(ctx.frame_number / 10) * 0.05f + 0.1f;
|
||||
Vec3 c0 = vec3_add(p0, vec3_scale(vec3_sub(p0, center), size));
|
||||
Vec3 c1 = vec3_add(p1, vec3_scale(vec3_sub(p1, center), size));
|
||||
Vec3 c2 = vec3_add(p2, vec3_scale(vec3_sub(p2, center), size));
|
||||
Vec3 c3 = vec3_add(p3, vec3_scale(vec3_sub(p3, center), size));
|
||||
draw_line_3d(c0, c1, 1, (Color){255,255,255,255});
|
||||
draw_line_3d(c1, c2, 1, (Color){255,255,255,255});
|
||||
draw_line_3d(c2, c3, 1, (Color){255,255,255,255});
|
||||
draw_line_3d(c3, c0, 1, (Color){255,255,255,255});
|
||||
} else {
|
||||
Vec3 p0 = point_to_vec3(state.current_hovered_obj, f->p[0]);
|
||||
Vec3 p1 = point_to_vec3(state.current_hovered_obj, f->p[1]);
|
||||
Vec3 p2 = point_to_vec3(state.current_hovered_obj, f->p[2]);
|
||||
Vec3 center = vec3_scale(vec3_add(p0, vec3_add(p1, p2)), 0.33f);
|
||||
float size = sinf(ctx.frame_number / 10) * 0.05f + 0.1f;
|
||||
Vec3 c0 = vec3_add(p0, vec3_scale(vec3_sub(p0, center), size));
|
||||
Vec3 c1 = vec3_add(p1, vec3_scale(vec3_sub(p1, center), size));
|
||||
Vec3 c2 = vec3_add(p2, vec3_scale(vec3_sub(p2, center), size));
|
||||
draw_line_3d(c0, c1, 1, (Color){255,255,255,255});
|
||||
draw_line_3d(c1, c2, 1, (Color){255,255,255,255});
|
||||
draw_line_3d(c2, c0, 1, (Color){255,255,255,255});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void display_texture_selection(void) {
|
||||
String list = file_read("/data/assets/", ":images");
|
||||
if (!list.data)
|
||||
return;
|
||||
|
||||
draw_rectangle((Rect){0, 480 - 50, 640, 480}, (Color){0,0,0,126});
|
||||
|
||||
char *selected = NULL;
|
||||
char *saveptr = NULL;
|
||||
int count = 0;
|
||||
char const *part = SDL_strtokr(list.data, "\n", &saveptr);
|
||||
do {
|
||||
Rect box = (Rect){(float)count * 50, 480 - 48, 48, 48};
|
||||
draw_sprite(part,
|
||||
box,
|
||||
(Rect){0,0,64,64},
|
||||
(Color){255,255,255,255},
|
||||
0, false, false, true);
|
||||
count++;
|
||||
|
||||
if (state.current_texture && SDL_strcmp(part, state.current_texture) == 0)
|
||||
draw_box(box, 1, (Color){255,255,255,255});
|
||||
|
||||
if (rect_intersects(box, (Rect){ctx.mouse_position.x, ctx.mouse_position.y, 1, 1}))
|
||||
selected = SDL_strdup(part);
|
||||
|
||||
} while ((part = SDL_strtokr(NULL, "\n", &saveptr)));
|
||||
|
||||
if (selected) {
|
||||
draw_text(selected, (Vec2){0, 480 - 48 - 24}, 24, (Color){255,255,255,255}, NULL);
|
||||
if (input_action_just_pressed("select")) {
|
||||
if (state.current_texture) SDL_free(state.current_texture);
|
||||
state.current_texture = selected;
|
||||
} else
|
||||
SDL_free(selected);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void process_camera_inputs(void) {
|
||||
if (input_action_just_pressed("toggle_display_mode")) {
|
||||
audio_play("/data/click.wav", NULL, false, 0.7f, 0.0f);
|
||||
state.solid_display_mode = !state.solid_display_mode;
|
||||
if (state.current_texture) {
|
||||
SDL_free(state.current_texture);
|
||||
state.current_texture = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (input_action_just_pressed("toggle_projection")) {
|
||||
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
|
||||
state.camera_is_orthographic = !state.camera_is_orthographic;
|
||||
}
|
||||
|
||||
if (input_action_just_pressed("toggle_x_axis")) {
|
||||
state.axis_mask[0] = 0; state.axis_mask[1] = 1; state.axis_mask[2] = 1;
|
||||
}
|
||||
|
||||
if (input_action_just_pressed("toggle_y_axis")) {
|
||||
state.axis_mask[0] = 1; state.axis_mask[1] = 0; state.axis_mask[2] = 1;
|
||||
}
|
||||
|
||||
if (input_action_just_pressed("toggle_z_axis")) {
|
||||
state.axis_mask[0] = 1; state.axis_mask[1] = 1; state.axis_mask[2] = 0;
|
||||
}
|
||||
|
||||
/* TODO: determine bounding box for this? */
|
||||
/* TODO: no idea whether it's all correct in terms of directions. */
|
||||
if (input_action_just_pressed("camera_front")) {
|
||||
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
|
||||
state.camera_is_orthographic = true;
|
||||
state.camera_direction = (Vec3){0,0,-1};
|
||||
state.camera_position = vec3_add(state.active_center, (Vec3){0,0,2});
|
||||
}
|
||||
|
||||
if (input_action_just_pressed("camera_back")) {
|
||||
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
|
||||
state.camera_is_orthographic = true;
|
||||
state.camera_direction = (Vec3){0,0,1};
|
||||
state.camera_position = vec3_add(state.active_center, (Vec3){0,0,-2});
|
||||
}
|
||||
|
||||
if (input_action_just_pressed("camera_left")) {
|
||||
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
|
||||
state.camera_is_orthographic = true;
|
||||
state.camera_direction = (Vec3){1,0,0};
|
||||
state.camera_position = vec3_add(state.active_center, (Vec3){-2,0,0});
|
||||
}
|
||||
|
||||
if (input_action_just_pressed("camera_right")) {
|
||||
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
|
||||
state.camera_is_orthographic = true;
|
||||
state.camera_direction = (Vec3){-1,0,0};
|
||||
state.camera_position = vec3_add(state.active_center, (Vec3){2,0,0});
|
||||
}
|
||||
|
||||
/* TODO: broken */
|
||||
if (input_action_just_pressed("camera_above")) {
|
||||
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
|
||||
state.camera_is_orthographic = true;
|
||||
state.camera_direction = (Vec3){0,-1,0};
|
||||
state.camera_position = vec3_add(state.active_center, (Vec3){0,2,0});
|
||||
}
|
||||
|
||||
/* TODO: broken */
|
||||
if (input_action_just_pressed("camera_below")) {
|
||||
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
|
||||
state.camera_is_orthographic = true;
|
||||
state.camera_direction = (Vec3){0,1,0};
|
||||
state.camera_position = vec3_add(state.active_center, (Vec3){0,-2,0});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void game_tick(void) {
|
||||
if (!init) {
|
||||
/* default state */
|
||||
new_cube((Point){0,0,0}, (Point){POINTS_PER_METER,POINTS_PER_METER,POINTS_PER_METER});
|
||||
state.camera_position = (Vec3){2,1,2};
|
||||
state.camera_direction = vec3_norm(((Vec3){-2,-1,-2}));
|
||||
state.camera_zoom = 0.5f;
|
||||
state.grid_snap_granularity = 16;
|
||||
state.axis_mask[1] = 1; state.axis_mask[2] = 1;
|
||||
init = true;
|
||||
}
|
||||
|
||||
state.current_hovered_face = INVALID_FACE;
|
||||
state.current_hovered_obj = INVALID_OBJECT;
|
||||
|
||||
input_action("toggle_display_mode", "Q");
|
||||
input_action("toggle_projection", "TAB");
|
||||
|
||||
input_action("toggle_x_axis", "Z");
|
||||
input_action("toggle_y_axis", "X");
|
||||
input_action("toggle_z_axis", "C");
|
||||
|
||||
input_action("select", "LCLICK");
|
||||
input_action("rotate", "R");
|
||||
input_action("undo", "F");
|
||||
|
||||
input_action("camera_front", "KP0");
|
||||
input_action("camera_back", "KP1");
|
||||
input_action("camera_left", "KP2");
|
||||
input_action("camera_right", "KP3");
|
||||
input_action("camera_above", "KP4");
|
||||
input_action("camera_below", "KP5");
|
||||
|
||||
process_camera_inputs();
|
||||
process_camera_movement();
|
||||
process_operations();
|
||||
|
||||
/* helpres */
|
||||
draw_axes();
|
||||
draw_hovered_face_border();
|
||||
|
||||
for (uint8_t obj = 0; obj < state.objects_sz; ++obj)
|
||||
render_object(obj);
|
||||
|
||||
if (state.solid_display_mode)
|
||||
display_texture_selection();
|
||||
|
||||
draw_text("twndel\x03", (Vec2){0, 2}, 32, (Color){255,255,255,200}, NULL);
|
||||
draw_camera(
|
||||
state.camera_position,
|
||||
state.camera_direction,
|
||||
(Vec3){0, 1, 0},
|
||||
state.camera_is_orthographic ? 0 : CAMERA_FOV,
|
||||
state.camera_zoom,
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void game_end(void) {
|
||||
for (uint8_t obj = 0; obj < state.objects_sz; ++obj) {
|
||||
Object *o = &state.objects[obj];
|
||||
for (uint8_t t = 0; t < o->textures_sz; ++t)
|
||||
SDL_free(o->textures[t]);
|
||||
SDL_free(o->name);
|
||||
}
|
||||
if (state.current_texture) {
|
||||
SDL_free(state.current_texture);
|
||||
state.current_texture = NULL;
|
||||
}
|
||||
}
|
@ -9,10 +9,6 @@ endif()
|
||||
|
||||
add_subdirectory($ENV{TWNROOT} ${CMAKE_BINARY_DIR}/twn)
|
||||
|
||||
set(FLAGS
|
||||
$<$<NOT:$<BOOL:${TWN_FEATURE_DYNLIB_GAME}>>:--no-dynlib-game>
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/game.c
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/bindgen.py $ENV{TWNROOT}/share/twn_api.json ${FLAGS} > ${CMAKE_CURRENT_SOURCE_DIR}/luabind.c
|
||||
|
@ -112,6 +112,9 @@ for procedure, procedure_desc in api["procedures"].items():
|
||||
binding += " lua_pushnumber(L, (float)(%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 procedure_desc["return"] == "String":
|
||||
binding += " String result = %s(%s);\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
|
||||
binding += " lua_pushlstring(L, result.data, (int)result.length);\n"
|
||||
elif type(procedure_desc["return"]) is dict or procedure_desc["return"] in api["types"]:
|
||||
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"]))
|
||||
|
@ -1,3 +1,3 @@
|
||||
[[deps]]
|
||||
source = "../../common-data" # where does it come from, might be an url
|
||||
source = "../../data" # where does it come from, might be an url
|
||||
name = "common-data" # should be globally unique
|
||||
|
@ -29,8 +29,8 @@ def to_lua_type_annot(typedesc):
|
||||
return "unknown"
|
||||
# raise BaseException("Unhandled type for annotation: %s" % typedesc)
|
||||
|
||||
print("---@meta twn")
|
||||
print("---@diagnostic disable")
|
||||
print("error(\"townengine lua api file is not supposed to be imported!\")")
|
||||
|
||||
type_annotations = {}
|
||||
type_annotations["ctx"] = r"{ %s, udata: table }" % \
|
||||
|
@ -26,7 +26,7 @@ static int physfs_loader(lua_State *L) {
|
||||
|
||||
static const char *name_breaker = NULL;
|
||||
if (name_breaker && SDL_strcmp(name, name_breaker) == 0) {
|
||||
log_critical("Recursive load on itself from lua module (%s)", name_breaker);
|
||||
log_string(name_breaker, "Recursive load on itself from lua module");
|
||||
return 0;
|
||||
} name_breaker = name;
|
||||
|
||||
@ -40,26 +40,26 @@ static int physfs_loader(lua_State *L) {
|
||||
SDL_asprintf(&final_path, "/scripts/%s.lua", path_copy);
|
||||
SDL_free(path_copy);
|
||||
|
||||
if (!file_exists(final_path)) {
|
||||
if (SDL_strcmp(file_read(final_path, ":exists").data, "yes") == 0) {
|
||||
char *error_message = NULL;
|
||||
SDL_asprintf(&error_message, "could not find module %s in filesystem", name);
|
||||
lua_pushstring(L, error_message);
|
||||
free(error_message);
|
||||
SDL_free(error_message);
|
||||
|
||||
free(final_path);
|
||||
SDL_free(final_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
unsigned char *buf = NULL;
|
||||
int64_t buf_size = file_to_bytes(final_path, &buf);
|
||||
free(final_path);
|
||||
char *final_path_binary = NULL;
|
||||
SDL_asprintf(&final_path_binary, "%s%s", final_path, ":binary");
|
||||
|
||||
String file = file_read(final_path, ":binary");
|
||||
SDL_free(final_path);
|
||||
|
||||
/* TODO: use reader interface for streaming instead */
|
||||
int const result = luaL_loadbuffer(L, (char *)buf, buf_size, name);
|
||||
free(buf);
|
||||
|
||||
int const result = luaL_loadbuffer(L, file.data, (size_t)file.length, name);
|
||||
if (result != LUA_OK)
|
||||
log_critical("%s", lua_tostring(L, -1));
|
||||
log_string(lua_tostring(L, -1), NULL);
|
||||
|
||||
return result == LUA_OK;
|
||||
}
|
||||
@ -110,7 +110,7 @@ static void *custom_alloc(void *ud, void *ptr, size_t osize, size_t nsize) {
|
||||
|
||||
static void exchange_lua_states(lua_State *from, lua_State *to, int level, int index) {
|
||||
if (level >= UDATA_NESTING_LIMIT) {
|
||||
log_critical("ctx.udata nesting limit is reached (%u)", UDATA_NESTING_LIMIT);
|
||||
log_string("ctx.udata nesting limit is reached", NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ static void exchange_lua_states(lua_State *from, lua_State *to, int level, int i
|
||||
break;
|
||||
default:
|
||||
/* TODO: provide a path and type of it for better diagnostic */
|
||||
log_warn("Unserializable udata found and is ignored");
|
||||
log_string("Unserializable udata found and is ignored", NULL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -167,7 +167,6 @@ void game_tick(void) {
|
||||
SDL_assert(!lua_isnoneornil(state->L, -1));
|
||||
SDL_assert(!lua_isnoneornil(state->L, -2));
|
||||
if (!lua_isnoneornil(state->L, -1)) {
|
||||
log_info("Exchanging lua states...");
|
||||
lua_newtable(new_state);
|
||||
exchange_lua_states(state->L, new_state, 0, -1);
|
||||
lua_setfield(new_state, -2, "udata");
|
||||
@ -216,24 +215,21 @@ void game_tick(void) {
|
||||
bindgen_load_twn(state->L);
|
||||
|
||||
/* now finally get to running the code */
|
||||
unsigned char *game_buf = NULL;
|
||||
size_t game_buf_size = file_to_bytes("/scripts/game.lua", &game_buf);
|
||||
String file = file_read("/scripts/game.lua", ":binary");
|
||||
/* TODO: use reader interface for streaming instead */
|
||||
if (luaL_loadbuffer(state->L, (char *)game_buf, game_buf_size, "game.lua") == LUA_OK) {
|
||||
if (luaL_loadbuffer(state->L, file.data, (size_t)file.length, "game.lua") == LUA_OK) {
|
||||
if (lua_pcall(state->L, 0, 0, 0) != LUA_OK) {
|
||||
log_critical("%s", lua_tostring(state->L, -1));
|
||||
log_string(luaL_tolstring(state->L, -1, NULL), "Error executing /scripts/game.lua entry");
|
||||
lua_pop(state->L, 1);
|
||||
} else
|
||||
state->loaded_successfully = true;
|
||||
} else {
|
||||
/* got some sort of error, it should be pushed on top of the stack */
|
||||
SDL_assert(lua_isstring(state->L, -1));
|
||||
log_critical("Error loading /scripts/game.lua entry: %s", luaL_tolstring(state->L, -1, NULL));
|
||||
log_string(luaL_tolstring(state->L, -1, NULL), "Error loading /scripts/game.lua entry");
|
||||
lua_pop(state->L, 1);
|
||||
}
|
||||
|
||||
SDL_free(game_buf);
|
||||
|
||||
/* from this point we have access to everything defined in lua */
|
||||
}
|
||||
|
||||
@ -251,7 +247,7 @@ void game_tick(void) {
|
||||
|
||||
lua_getglobal(state->L, "game_tick");
|
||||
if (lua_pcall(state->L, 0, 0, 0) != LUA_OK) {
|
||||
log_critical("%s", lua_tostring(state->L, -1));
|
||||
log_string(luaL_tolstring(state->L, -1, NULL), "Error executing game_tick()");
|
||||
lua_pop(state->L, 1);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/bin/env sh
|
||||
# single header api generator with clang
|
||||
|
||||
set +e
|
||||
set -e
|
||||
|
||||
clang -I./ -P -E -nostdinc -nobuiltininc -DTWN_NOT_C $TWNROOT/include/twn_game_api.h 2> /dev/null | clang-format
|
||||
|
8
bin/prep-embed.sh
Normal file
8
bin/prep-embed.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#!/bin/env sh
|
||||
set -e
|
||||
# packages embedded resources with objcopy, so that it is more portable
|
||||
# ld.lld on windows doesn't recognize --format binary, sadly
|
||||
|
||||
objdump=$(objdump -i)
|
||||
bdfname=$(echo "$objdump" | sed -n 2p)
|
||||
objcopy -I binary -O "$bdfname" share/assets/Dernyns256.ttf "$CMAKE_CURRENT_BINARY_DIR/font.o"
|
5
bin/twn
5
bin/twn
@ -1,7 +1,7 @@
|
||||
#!/bin/env sh
|
||||
# townengine tooling interface
|
||||
|
||||
set +e
|
||||
set -e
|
||||
|
||||
exe="$(basename $PWD)"
|
||||
toolpath="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
|
||||
@ -57,6 +57,9 @@ case "$1" in
|
||||
fi
|
||||
;;
|
||||
|
||||
devcompl ) (cd "$TWNROOT" && "$toolpath"/twnbuild "--build_dir=$TWNROOT/build" "${@:2}")
|
||||
;;
|
||||
|
||||
* ) echo "Unknown command."
|
||||
;;
|
||||
esac
|
||||
|
@ -5,6 +5,7 @@ from os import getcwd
|
||||
from os.path import expandvars
|
||||
from pathlib import Path
|
||||
from sys import argv
|
||||
from functools import reduce
|
||||
import tomllib
|
||||
|
||||
#TODO: support for default pack override
|
||||
@ -16,15 +17,16 @@ has_clang = getoutput("command -v clang") != ""
|
||||
target_web = "--target=web" in argv
|
||||
|
||||
#TODO: infer what "native" means for current env
|
||||
build_dir = "build/web" if target_web else "build/native"
|
||||
build_dir += "/release" if "--release" in argv else "/debug"
|
||||
build_dir_arg = reduce(lambda c, n: c if c.startswith("--build_dir=") else n, argv + [""], "")
|
||||
build_dir = "build/web" if target_web else build_dir_arg.split("=")[1] if build_dir_arg else "build/native"
|
||||
build_dir += "" if build_dir_arg else "/release" if "--release" in argv else "/debug"
|
||||
|
||||
cmake = ["emcmake", "cmake"] if target_web else ["cmake"]
|
||||
# cmake configuration command
|
||||
command = []
|
||||
|
||||
# check whether clang is around (it's just better)
|
||||
if has_clang:
|
||||
if has_clang and not target_web:
|
||||
command += ["-DCMAKE_C_COMPILER=clang"]
|
||||
# check whether ninja is around (you better start running)
|
||||
if has_ninja:
|
||||
|
BIN
data/assets/dirt/1.png
(Stored with Git LFS)
Normal file
BIN
data/assets/dirt/1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
data/assets/dirt/2.png
(Stored with Git LFS)
Normal file
BIN
data/assets/dirt/2.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
data/assets/grass2.png
(Stored with Git LFS)
Normal file
BIN
data/assets/grass2.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
data/assets/grasses/25.png
(Stored with Git LFS)
Normal file
BIN
data/assets/grasses/25.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
data/assets/trreez.png
(Stored with Git LFS)
Normal file
BIN
data/assets/trreez.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
data/music/woah.ogg
(Stored with Git LFS)
Normal file
BIN
data/music/woah.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -53,6 +53,7 @@
|
||||
<li><b>T</b> for townengine; development and apis.
|
||||
<li><b>G</b> for gamedev; guides and FAQs on game making.
|
||||
</ul>
|
||||
<p>There's no reason not to pile up knowledge, refactoring is irrelevant.
|
||||
</blockquote>
|
||||
<p><a name="abi"></a><strong>T1.3 </strong><strong>Procedure Interface</strong>
|
||||
<blockquote>
|
||||
@ -76,5 +77,21 @@
|
||||
<li>Returns must be restricted/closed on being constant over frame, parameters or asset frame.
|
||||
</ul>
|
||||
</blockquote>
|
||||
<p><a name="dev-on"></a><strong>T1.4 </strong><strong>Developing</strong>
|
||||
<blockquote>
|
||||
<p>One of hard choices we employ is requirement on having more or less the same tools on all platforms in order to compile.
|
||||
If you're on Windows you need to get a MinGW environment. [cmake] and [python3] (>specify versions<) are also required.
|
||||
[clang] is preferred over [gcc], as it has superior lto, which matters a great deal with our architecture (literal day and night difference in frame time).
|
||||
[git] is used for version control. Git for Windows distribution could also be used for getting conforming cli environment, much recommended.
|
||||
<p>Inside $TWNROOT/bin folder the [twn] script is used for engine instrumentation.
|
||||
It could build and run your projects and even open this very wiki locally!
|
||||
You could execute [source hooks] inside $TWNROOT to add [twn] to your $PATH temporarily. Symlinking of it is also supported.
|
||||
Look inside the script to see what it can do, it's ever growing !
|
||||
<p>[twn init <template> <name>] could be used to quickly initialize a project.
|
||||
Look into $TWNROOT/apps/templates folder to see available templates.
|
||||
<p>Support for [clangd] completions is provided with [twn devcompl] command, it generates compile_commands.json inside $TWNROOT/build folder.
|
||||
Having workspace placed in $TWNROOT by your IDE should be enough to make it all work. Rerun [twn devcompl] if new files are added.
|
||||
<p>Web target requires [emsdk] as of now. Use [twn build --target=web] to use it.
|
||||
</blockquote>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<a>Awesomeness</a>
|
||||
<table style="padding-top:1em">
|
||||
<tr><td>T1.</strong> <a href="#about-townengine">About Townengnine</a></td>
|
||||
<td>G1.</strong> <a href="#making-2dot5d-shooters">Making 2.5D Shooters</a></td>
|
||||
<td>G1.</strong> <a href="#trigonometry">Trigonometry</a></td>
|
||||
</tr>
|
||||
<tr><td>T2.</strong> <a href="#input-system">Input System</a></td>
|
||||
</tr>
|
||||
@ -21,6 +21,7 @@
|
||||
<p style="margin:0">T1.1 <a href="about-townengine.html#introduction">Introduction</a></p>
|
||||
<p style="margin:0">T1.2 <a href="about-townengine.html#wiki">Wiki</a></p>
|
||||
<p style="margin:0">T1.3 <a href="about-townengine.html#abi">Procedure Interface</a></p>
|
||||
<p style="margin:0">T1.4 <a href="about-townengine.html#dev-on">Developing</a></p>
|
||||
</blockquote>
|
||||
<p style="margin-bottom:0"><a name="input-system"></a>T2. </strong><a href="input-system.html">Input System</strong></a></p>
|
||||
<blockquote style="margin-top:0">
|
||||
@ -31,7 +32,7 @@
|
||||
<blockquote style="margin-top:0">
|
||||
<p style="margin:0">T3.1 <a href="packaging.html#overview">Overview</a></p>
|
||||
</blockquote>
|
||||
<p style="margin-bottom:0"><a name="making-2dot5d-shooters"></a>G1. </strong><a href="making-2dot5d-shooters.html">Making 2.5D Shooters</strong></a></p>
|
||||
<p style="margin-bottom:0"><a name="trigonometry"></a>G1. </strong><a href="trigonometry.html">Trigonometry</strong></a></p>
|
||||
<blockquote style="margin-top:0">
|
||||
</blockquote>
|
||||
</body>
|
||||
|
@ -6,7 +6,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="margin-bottom:0in">X1. {About}<span style="float:right">{twn}</span></h1>
|
||||
<a href="index.html">Go back
|
||||
<a href="index.html">Go back</a>
|
||||
<p><a name="{Section}"></a><strong>X1.1 </strong><strong>{Section}</strong>
|
||||
<blockquote>
|
||||
<p>Text
|
||||
|
46
docs/wiki/trigonometry.html
Normal file
46
docs/wiki/trigonometry.html
Normal file
@ -0,0 +1,46 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Townengine Wiki : Trigonometry</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="margin-bottom:0in">G1. Trigonometry<span style="float:right">{twn}</span></h1>
|
||||
<a href="index.html">Go back</a>
|
||||
<blockquote>
|
||||
<p>Truth is, most games are played in continuous space, where geometry matters.
|
||||
Math classes turn out to be not so useless after all.
|
||||
But even if you don't know your numbers well, you can compose functions by their meaning, and not number logic.
|
||||
This page will try to help you with that, providing examples.
|
||||
</blockquote>
|
||||
|
||||
<p><strong>the Hell are Vectors</strong>
|
||||
<blockquote>
|
||||
<p>There are distinct meanings they posses: <i>position in space</i>, <i>travel between points</i> and <i>direction</i>.
|
||||
|
||||
Often you need to juggle those representation around.
|
||||
<p>For example, player and enemy positions by themselves don't tell you anything about their spatial relation.
|
||||
For that you need to compute the <i>difference between them</i>, which is plain per component subtraction.
|
||||
Resulted direction of travel depends on order of inputs, - it will point to position on the right side, the one you subtract with, but not from.
|
||||
|
||||
<p>It's important to understand that after subtraction the travel vector doesn't have relation with original positions,
|
||||
it is effectively in a <i>subspace</i>, where (0,0,0) is at position you subtracted with to get it.
|
||||
Operation is reversible by means of addition, resulting in position in space once again.
|
||||
|
||||
<p>I often forget what the direction is pointed at but here's a quick way to remind yourself: take two numbers, 10 and 0, and try subtracting them in different order.
|
||||
Sign of result will tell you which part is pointed at, +10 -> direction is to the 10, -10 -> direction is to the 0. (Draw it in your head)
|
||||
|
||||
<p>Sometimes direction is more important than the travel, for which you need to drop the length of it.
|
||||
This is called <i>normalization</i>, where result is called a <i>normal</i> (You probably heard those terms).
|
||||
This is achieved by dividing every component by the length (magnitude) of vector itself.
|
||||
Length is computed as <i>sqrt(x*x, y*y, z*z)</i>. This effectively creates a vector of length 1.
|
||||
As every direction sized the same, when graphed they trace a circle.
|
||||
</blockquote>
|
||||
|
||||
<p><strong>Center between two points</strong>
|
||||
<blockquote>
|
||||
<p><b>Method one. </b>Add two positions and divide them by two: (a + b) / 2. This is cheap, but harder to remember.
|
||||
<p><b>Method two. </b>Find the travel between points, divide it by two and then translate the origin: (a + (a - b) / 2)
|
||||
</blockquote>
|
||||
</body>
|
||||
</html>
|
@ -1,13 +1,14 @@
|
||||
#ifndef TWN_AUDIO_H
|
||||
#define TWN_AUDIO_H
|
||||
|
||||
#include "twn_engine_api.h"
|
||||
#include "twn_api.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/* 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 */
|
||||
/* mono or stereo only */
|
||||
TWN_API void audio_play(const char *audio,
|
||||
const char *channel, /* optional */
|
||||
bool repeat, /* default: false */
|
||||
|
@ -2,7 +2,7 @@
|
||||
#define TWN_CONTEXT_H
|
||||
|
||||
#include "twn_types.h"
|
||||
#include "twn_engine_api.h"
|
||||
#include "twn_api.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
@ -2,7 +2,7 @@
|
||||
#define TWN_DRAW_H
|
||||
|
||||
#include "twn_types.h"
|
||||
#include "twn_engine_api.h"
|
||||
#include "twn_api.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
@ -47,6 +47,13 @@ TWN_API void draw_line(Vec2 start,
|
||||
float thickness, /* optional, default: 1 */
|
||||
Color color); /* optional, default: all 255 */
|
||||
|
||||
/* intended for debugging and spatial reference */
|
||||
TWN_API void draw_line_3d(Vec3 start,
|
||||
Vec3 finish,
|
||||
float thickness, /* optional, default: 1 */
|
||||
Color color); /* optional, default: all 255 */
|
||||
|
||||
|
||||
/* TODO: combine with draw_rectangle()? */
|
||||
TWN_API void draw_box(Rect rect,
|
||||
float thickness, /* optional, default: 1 */
|
||||
@ -86,11 +93,26 @@ TWN_API void draw_camera_2d(Vec2 position, /* optional, default: (0, 0) */
|
||||
|
||||
/* sets a perspective 3d camera to be used for all 3d commands */
|
||||
/* fov = 0 corresponds to orthographic projection */
|
||||
TWN_API void draw_camera(Vec3 position,
|
||||
TWN_API void draw_camera(Vec3 position, /* optional, default: (0, 0, 0) */
|
||||
Vec3 direction, /* optional, default: (0, 0, -1) */
|
||||
Vec3 up, /* optional, default: (0, 1, 0) */
|
||||
float fov, /* optional, default: PI / 6 * 3 (90 degrees) */
|
||||
float zoom); /* optional, default: 1 */
|
||||
float zoom, /* optional, default: 1 */
|
||||
float draw_distance); /* optional, default: 100 */
|
||||
|
||||
/* find position and looking direction of a 2d point into 3d space */
|
||||
/* make sure it's called with matching parameters to last draw_camera(), if you need them to match */
|
||||
typedef struct DrawCameraUnprojectResult {
|
||||
Vec3 position;
|
||||
Vec3 direction;
|
||||
} DrawCameraUnprojectResult;
|
||||
TWN_API DrawCameraUnprojectResult draw_camera_unproject(Vec2 point,
|
||||
Vec3 position,
|
||||
Vec3 direction,
|
||||
Vec3 up,
|
||||
float fov,
|
||||
float zoom,
|
||||
float draw_distance);
|
||||
|
||||
/* 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) */
|
||||
@ -100,12 +122,13 @@ typedef struct DrawCameraFromPrincipalAxesResult {
|
||||
Vec3 up;
|
||||
} DrawCameraFromPrincipalAxesResult;
|
||||
TWN_API DrawCameraFromPrincipalAxesResult
|
||||
draw_camera_from_principal_axes(Vec3 position,
|
||||
draw_camera_from_principal_axes(Vec3 position, /* optional, default: (0, 0, 0) */
|
||||
float roll, /* optional, default: 0 */
|
||||
float pitch, /* optional, default: 0 */
|
||||
float yaw, /* optional, default: 0 */
|
||||
float fov, /* optional, default: PI / 6 * 3 (90 degrees) */
|
||||
float zoom); /* optional, default: 1 */
|
||||
float zoom, /* optional, default: 1 */
|
||||
float draw_distance); /* optional, default: 100 */
|
||||
|
||||
/* expects '*' masks that will be expanded to 6 names: 'up', 'down', 'east', 'west', 'north' and 'south' */
|
||||
TWN_API void draw_skybox(const char *textures);
|
||||
|
@ -5,7 +5,7 @@
|
||||
#include "twn_input.h"
|
||||
#include "twn_draw.h"
|
||||
#include "twn_audio.h"
|
||||
#include "twn_engine_api.h"
|
||||
#include "twn_api.h"
|
||||
#include "twn_util.h"
|
||||
#include "twn_context.h"
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
#ifndef TWN_INPUT_H
|
||||
#define TWN_INPUT_H
|
||||
|
||||
#include "twn_engine_api.h"
|
||||
#include "twn_api.h"
|
||||
#include "twn_types.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
@ -40,4 +40,12 @@ typedef struct Rect {
|
||||
} Rect;
|
||||
|
||||
|
||||
/* data packet exchanged between parties, assumed immutable */
|
||||
/* length is whole number */
|
||||
typedef struct String {
|
||||
char *data;
|
||||
float length;
|
||||
} String;
|
||||
|
||||
|
||||
#endif
|
||||
|
@ -2,54 +2,43 @@
|
||||
#define TWN_UTIL_H
|
||||
|
||||
#include "twn_types.h"
|
||||
#include "twn_engine_api.h"
|
||||
#include "twn_api.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* here as it's not part of standard C */
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846264338327950288 /**< pi */
|
||||
#endif
|
||||
|
||||
#ifndef TWN_NOT_C
|
||||
#include <math.h>
|
||||
/* read data from virtual filesystem, with assumption that you know what that underlying data is */
|
||||
/* this routine supports commands, which define operations performed on filepaths */
|
||||
/* empty result is returned when something goes wrong or file doesn't exist, with an error logged */
|
||||
/* defined commands: */
|
||||
/* <path:string> -- reads utf8 encoded file to null terminated string */
|
||||
/* <path:binary> -- reads arbitrary binary file, resulted encoding depends on environment (could be base64, for example) */
|
||||
/* <path:exists> -- returns "yes" or "no" ascii string */
|
||||
/* <path:images> -- returns newline separated utf8 list of image files in given path */
|
||||
TWN_API String file_read(char const *file, const char *operation);
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846264338327950288 /**< pi */
|
||||
#endif
|
||||
|
||||
/* multiply by these to convert degrees <---> radians */
|
||||
#define DEG2RAD (M_PI / 180)
|
||||
#define RAD2DEG (180 / M_PI)
|
||||
|
||||
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);
|
||||
|
||||
/* TODO: this is why generics were invented. sorry, i'm tired today */
|
||||
TWN_API float clampf(float f, float min, float 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 */
|
||||
|
||||
/* read file to null terminated string, it is freed when the frame ends */
|
||||
TWN_API char const *file_read(char const *file);
|
||||
/* commit write to a file, which meaning depends on interpretation */
|
||||
/* file_read should not be affected for current frame by this, operation is pending */
|
||||
/* defined commands: */
|
||||
/* <path:string> -- save utf8 file */
|
||||
/* <path:binary> -- save binary file */
|
||||
/* <path:image> -- write image data to a PNG file, potentially updating running textures used */
|
||||
/* -- format is: [width:2b][height:2][alpha:1][data:(3+alpha)*width*height] */
|
||||
TWN_API void file_write(char const *file, const char *operation, String string);
|
||||
|
||||
/* TODO: move to external templated lib */
|
||||
/* 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 rect_intersects(Rect a, Rect b);
|
||||
|
||||
/* TODO: move to external templated lib */
|
||||
/* 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 */
|
||||
@ -60,10 +49,13 @@ typedef struct TimerElapseSecondsResult {
|
||||
} TimerElapseSecondsResult;
|
||||
TWN_API TimerElapseSecondsResult timer_elapse_seconds(float seconds_left, float interval);
|
||||
|
||||
TWN_API void log_string(char const *value, char const *identity);
|
||||
TWN_API void log_float(float value, char const *identity);
|
||||
TWN_API void log_vec2(Vec2 value, char const *identity);
|
||||
TWN_API void log_vec3(Vec3 value, char const *identity);
|
||||
TWN_API void log_rect(Rect value, char const *identity);
|
||||
|
||||
/* IDEA: NES debugger style frame slices that show for how long and when certain profile range takes place */
|
||||
TWN_API void profile_start(char const *profile);
|
||||
TWN_API void profile_end(char const *profile);
|
||||
|
||||
|
@ -29,10 +29,20 @@ static inline Vec2 vec2_scale(Vec2 a, float s) {
|
||||
return (Vec2) { a.x * s, a.y * s };
|
||||
}
|
||||
|
||||
static inline float vec2_dot(Vec2 a, Vec2 b) {
|
||||
return a.x * b.x + a.y * b.y;
|
||||
}
|
||||
|
||||
static inline float vec2_length(Vec2 a) {
|
||||
return sqrtf(a.x * a.x + a.y * a.y);
|
||||
}
|
||||
|
||||
static inline Vec2 vec2_norm(Vec2 a) {
|
||||
const float n = sqrtf(vec2_dot(a, a));
|
||||
/* TODO: do we need truncating over epsilon as cglm does? */
|
||||
return vec2_scale(a, 1.0f / n);
|
||||
}
|
||||
|
||||
static inline Vec3 vec3_add(Vec3 a, Vec3 b) {
|
||||
return (Vec3) { a.x + b.x, a.y + b.y, a.z + b.z };
|
||||
}
|
||||
@ -76,6 +86,10 @@ static inline Vec3 vec3_norm(Vec3 a) {
|
||||
return vec3_scale(a, 1.0f / n);
|
||||
}
|
||||
|
||||
static inline float vec3_angle(Vec3 a, Vec3 b) {
|
||||
return acosf(vec3_dot(vec3_norm(a), vec3_norm(b)));
|
||||
}
|
||||
|
||||
static inline Vec3 vec3_rotate(Vec3 v, float angle, Vec3 axis) {
|
||||
/* from cglm */
|
||||
Vec3 v1, v2, k;
|
||||
|
BIN
share/assets/Dernyns256.ttf
Normal file
BIN
share/assets/Dernyns256.ttf
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user