From 67feb5974ae4f46a592a3473155c1b81e1fa1892 Mon Sep 17 00:00:00 2001 From: veclavtalica Date: Fri, 7 Mar 2025 07:10:44 +0300 Subject: [PATCH] /apps/tools/twndel: start the thing --- apps/tools/twndel/CMakeLists.txt | 16 +++ apps/tools/twndel/data/point.png | 3 + apps/tools/twndel/data/twn.toml | 12 +++ apps/tools/twndel/state.h | 79 +++++++++++++++ apps/tools/twndel/tool.c | 162 +++++++++++++++++++++++++++++++ 5 files changed, 272 insertions(+) create mode 100644 apps/tools/twndel/CMakeLists.txt create mode 100644 apps/tools/twndel/data/point.png create mode 100644 apps/tools/twndel/data/twn.toml create mode 100644 apps/tools/twndel/state.h create mode 100644 apps/tools/twndel/tool.c diff --git a/apps/tools/twndel/CMakeLists.txt b/apps/tools/twndel/CMakeLists.txt new file mode 100644 index 0000000..a95bb1f --- /dev/null +++ b/apps/tools/twndel/CMakeLists.txt @@ -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}) diff --git a/apps/tools/twndel/data/point.png b/apps/tools/twndel/data/point.png new file mode 100644 index 0000000..ceb77a6 --- /dev/null +++ b/apps/tools/twndel/data/point.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a94d5677f37025bb62bd91709e8f3066ff2ec9dbcf194089a62f1a06e275c8a +size 1217 diff --git a/apps/tools/twndel/data/twn.toml b/apps/tools/twndel/data/twn.toml new file mode 100644 index 0000000..8fc4527 --- /dev/null +++ b/apps/tools/twndel/data/twn.toml @@ -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] diff --git a/apps/tools/twndel/state.h b/apps/tools/twndel/state.h new file mode 100644 index 0000000..fa9a745 --- /dev/null +++ b/apps/tools/twndel/state.h @@ -0,0 +1,79 @@ +#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) + + +typedef struct Operation { + enum { + OPERATION_MOVE_POINT, + } kind; + union { + struct { + uint16_t id; + int16_t delta_x; + int16_t delta_y; + } move_point; + } data; +} Operation; + +typedef struct Point { + int16_t x, y, z; +} Point; + +/* triangles have p3 = INVALID_POINT */ +/* lines have p2, p3 = INVALID_POINT */ +/* points have p1, p2, p3 = INVALID_POINT */ +typedef struct Face { + uint16_t p0, p1, p2, p3; + uint8_t texture; +} Face; + +typedef struct Object { + char *name; + bool is_invisible; + Point position; + char *textures[TEXTURE_LIMIT]; + uint8_t textures_sz; + Point rotation; + Face faces[FACE_LIMIT]; + uint16_t faces_sz; +} Object; + +typedef struct State { + Operation undo_stack[UNDO_STACK_SIZE]; + uint32_t undo_stack_ptr; + + 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; + uint8_t objects_sz; +} State; + + +#endif diff --git a/apps/tools/twndel/tool.c b/apps/tools/twndel/tool.c new file mode 100644 index 0000000..f2c2ab7 --- /dev/null +++ b/apps/tools/twndel/tool.c @@ -0,0 +1,162 @@ +#include "twn_game_api.h" +#include "state.h" +#include "twn_vec.h" + +#include + +/* 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 */ +/* texture painting */ +/* bones with mesh animations, snapping to grid, with no weights */ +/* billboard render to specified angles */ +/* 1 point light primitive lighting */ + +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 = SDL_realloc(state.objects, state.objects_sz * sizeof (*state.objects)); + SDL_memset(&state.objects[state.objects_sz-1], 0, sizeof (Object)); + 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) { + Object *o = &state.objects[object]; + o->faces_sz++; + o->faces[o->faces_sz-1] = (Face) {p0, p1, p2, p3, texture}; + return o->faces_sz-1; +} + + +static uint8_t push_texture(uint8_t object, char *texture) { + /* TODO: search and combine if it already exists */ + Object *o = &state.objects[object]; + o->textures_sz++; + o->textures[o->textures_sz-1] = SDL_strdup(texture); + return o->textures_sz-1; +} + + +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]; + /* quads */ + if (f->p3 != INVALID_POINT) { + Vec3 p0 = (Vec3){(float)POINTS_PER_METER / (o->position.x + state.points[f->p0].x), + (float)POINTS_PER_METER / (o->position.y + state.points[f->p0].y), + (float)POINTS_PER_METER / (o->position.z + state.points[f->p0].z) }; + Vec3 p1 = (Vec3){(float)POINTS_PER_METER / (o->position.x + state.points[f->p1].x), + (float)POINTS_PER_METER / (o->position.y + state.points[f->p1].y), + (float)POINTS_PER_METER / (o->position.z + state.points[f->p1].z) }; + Vec3 p2 = (Vec3){(float)POINTS_PER_METER / (o->position.x + state.points[f->p2].x), + (float)POINTS_PER_METER / (o->position.y + state.points[f->p2].y), + (float)POINTS_PER_METER / (o->position.z + state.points[f->p2].z) }; + Vec3 p3 = (Vec3){(float)POINTS_PER_METER / (o->position.x + state.points[f->p3].x), + (float)POINTS_PER_METER / (o->position.y + state.points[f->p3].y), + (float)POINTS_PER_METER / (o->position.z + state.points[f->p3].z) }; + + if (state.solid_display_mode) + draw_quad(o->textures[f->texture], + p0, p1, p2, p3, + (Rect){0}, + (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}); + draw_line_3d(p2, p3, 1, (Color){255,255,255,255}); + draw_line_3d(p3, p0, 1, (Color){255,255,255,255}); + } + } else { + /* triangles */ + /* TODO: */ + 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 txt = push_texture(object, "/data/default.png"); + push_face(object, p0, p1, p2, p3, txt); + push_face(object, p7, p6, p5, p4, txt); + push_face(object, p4, p5, p1, p0, txt); + push_face(object, p6, p7, p3, p2, txt); + push_face(object, p2, p1, p5, p6, txt); + push_face(object, p0, p3, p7, p4, txt); + + return object; +} + + +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){10,5,10}; + state.camera_direction = vec3_norm(((Vec3){-10,-5,-10})); + init = true; + } + + input_action("toggle_display_mode", "C"); + + if (input_action_just_pressed("toggle_display_mode")) + state.solid_display_mode = !state.solid_display_mode; + + for (uint8_t obj = 0; obj < state.objects_sz; ++obj) + render_object(obj); + + draw_text("twndel\x03", (Vec2){0, 0}, 32, (Color){255,255,255,255}, 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.objects_sz != 0) + SDL_free(state.objects); +}