diff options
| author | KonAki <konaki@anokonaki.org> | 2026-10-10 23:43:00 +0800 |
|---|---|---|
| committer | KonAki <konaki@anokonaki.org> | 2026-10-10 23:43:00 +0800 |
| commit | 2d7261e95fa1abead95564c340f383c1aafdc6e6 (patch) | |
| tree | 7218b6d6ff4ac562a264d87e7f1840d8bab1de3b /kon | |
| parent | 625108a3c9d4ebf99be30296aef10d60b272b7a7 (diff) | |
add 3d kit: first person body and tilemap level drawing
Diffstat (limited to 'kon')
| -rw-r--r-- | kon/kon3d.c | 212 | ||||
| -rw-r--r-- | kon/kon3d.h | 84 |
2 files changed, 296 insertions, 0 deletions
diff --git a/kon/kon3d.c b/kon/kon3d.c new file mode 100644 index 0000000..8184039 --- /dev/null +++ b/kon/kon3d.c @@ -0,0 +1,212 @@ +/* SPDX-License-Identifier: MIT */ +/* + * kon3d.c + * + * Copyright (c) 2026 KonAki + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "kon3d.h" + +#include <math.h> +#include <stdio.h> +#include <stdlib.h> + +/*** private ***/ + +static void kon_quad_(kon_framebuffer_t *fb, kon_mat4_t vp, const kon_material_t *material, const kon_light_t *light, + kon_vec3_t a, kon_vec3_t b, kon_vec3_t c, kon_vec3_t d, kon_vec3_t normal, kon_vec2_t uvA, kon_vec2_t uvC) +{ + kon_vertex_t va = { a, normal, uvA }, vb = { b, normal, KON_VEC2(uvC.x, uvA.y) }; + kon_vertex_t vc = { c, normal, uvC }, vd = { d, normal, KON_VEC2(uvA.x, uvC.y) }; + + kon_drawTriangle3D(fb, vp, va, vb, vc, material, light, KON_RENDER_DEFAULT); + kon_drawTriangle3D(fb, vp, va, vc, vd, material, light, KON_RENDER_DEFAULT); +} + +/* a wall face seen from its open side, left and right are the bottom corners as the viewer sees them */ +static void kon_wall_(kon_framebuffer_t *fb, kon_mat4_t vp, const kon_level3d_t *level, const kon_light_t *light, + kon_vec3_t left, kon_vec3_t right, kon_vec3_t normal) +{ + float h = level->wallHeight; + kon_vec3_t topRight = KON_VEC3(right.x, h, right.z), topLeft = KON_VEC3(left.x, h, left.z); + + kon_quad_(fb, vp, level->wall, light, left, right, topRight, topLeft, normal, KON_VEC2(0, h), KON_VEC2(1, 0)); +} + +static int kon_open_(const kon_level3d_t *level, int column, int row) +{ + uint16_t id = kon_tilemapAt(level->map, column, row); + + return id >= level->solidCount || !level->solid[id]; +} + +/*** body ***/ + +void kon_fpInit(kon_fp_t *fp, const kon_tilemap_t *map, int column, int row) +{ + float size = 12.0f; + + fp->eyeHeight = 0.6f; + fp->yaw = 0.0f; + fp->pitch = 0.0f; + fp->sensitivity = 0.0015f; + fp->speed = 2.5f; + fp->turnSpeed = 2.0f; + fp->fov = KON_DEG2RAD(60); + fp->forward = fp->strafe = fp->turn = 0; + fp->body = KON_BOX2(((float)column + 0.5f) * (float)map->tileWidth - size * 0.5f, ((float)row + 0.5f) * (float)map->tileHeight - size * 0.5f, size, size); +} + +int kon_fpEvent(kon_fp_t *fp, kon_program_t *program, const kon_event_t *ev) +{ + int down; + + if (ev->type == KON_EVENT_MOUSE_MOVE && kon_isMouseCaptured(program->window)) { + fp->yaw += (float)ev->mouseDX * fp->sensitivity; + fp->pitch -= (float)ev->mouseDY * fp->sensitivity; + if (fp->pitch > 1.2f) fp->pitch = 1.2f; + if (fp->pitch < -1.2f) fp->pitch = -1.2f; + return 1; + } + if (ev->type == KON_EVENT_MOUSE_DOWN && !kon_isMouseCaptured(program->window)) { + kon_setMouseCapture(program->window, true); + return 1; + } + if (ev->type == KON_EVENT_KEY_DOWN && ev->key == KON_KEY_TAB) { + kon_setMouseCapture(program->window, !kon_isMouseCaptured(program->window)); + return 1; + } + if (ev->type != KON_EVENT_KEY_DOWN && ev->type != KON_EVENT_KEY_UP) return 0; + down = ev->type == KON_EVENT_KEY_DOWN; + + switch (ev->key) { + case KON_KEY_W: case KON_KEY_UP: fp->forward = down ? 1 : (fp->forward > 0 ? 0 : fp->forward); break; + case KON_KEY_S: case KON_KEY_DOWN: fp->forward = down ? -1 : (fp->forward < 0 ? 0 : fp->forward); break; + case KON_KEY_D: fp->strafe = down ? 1 : (fp->strafe > 0 ? 0 : fp->strafe); break; + case KON_KEY_A: fp->strafe = down ? -1 : (fp->strafe < 0 ? 0 : fp->strafe); break; + case KON_KEY_RIGHT: fp->turn = down ? 1 : (fp->turn > 0 ? 0 : fp->turn); break; + case KON_KEY_LEFT: fp->turn = down ? -1 : (fp->turn < 0 ? 0 : fp->turn); break; + default: return 0; + } + return 1; +} + +void kon_fpUpdate(kon_fp_t *fp, kon_program_t *program, const kon_tilemap_t *map, const uint8_t *solid, int solidCount, double dt) +{ + float step = fp->speed * (float)dt * (float)map->tileWidth; + float fx = sinf(fp->yaw), fz = -cosf(fp->yaw); + float dx = (fx * (float)fp->forward - fz * (float)fp->strafe) * step; + float dz = (fz * (float)fp->forward + fx * (float)fp->strafe) * step; + + if (kon_debugConsoleOpen(program->debug) && kon_isMouseCaptured(program->window)) kon_setMouseCapture(program->window, false); + + fp->yaw += (float)fp->turn * fp->turnSpeed * (float)dt; + kon_tilemapMove(map, &fp->body, dx, dz, solid, solidCount); +} + +kon_vec3_t kon_fpEye(const kon_fp_t *fp, const kon_tilemap_t *map) +{ + return KON_VEC3((fp->body.x + fp->body.w * 0.5f) / (float)map->tileWidth, fp->eyeHeight, (fp->body.y + fp->body.h * 0.5f) / (float)map->tileHeight); +} + +kon_camera_t kon_fpCamera(const kon_fp_t *fp, const kon_tilemap_t *map) +{ + kon_camera_t camera = kon_cameraDefault(); + kon_vec3_t eye = kon_fpEye(fp, map); + + camera.position = eye; + camera.target = KON_VEC3(eye.x + sinf(fp->yaw) * cosf(fp->pitch), eye.y + sinf(fp->pitch), eye.z - cosf(fp->yaw) * cosf(fp->pitch)); + camera.fov = fp->fov; + camera.nearZ = 0.05f; + camera.farZ = 40.0f; + return camera; +} + +#if defined(KON_DEVELOPER) && defined(KON_MODULE_DEBUG) + +static void kon_cmdWhere_(kon_debug_t *debug, void *user, int argc, char **argv) +{ + kon_fp_t *fp = user; + + (void)argc; (void)argv; + kon_debugLog(debug, "x %.1f y %.1f yaw %.2f pitch %.2f", (double)fp->body.x, (double)fp->body.y, (double)fp->yaw, (double)fp->pitch); +} + +void kon_fpDebug(kon_fp_t *fp, kon_debug_t *debug) +{ + kon_debugVar(debug, "sens", KON_VAR_FLOAT, &fp->sensitivity); + kon_debugVar(debug, "speed", KON_VAR_FLOAT, &fp->speed); + kon_debugVar(debug, "eye", KON_VAR_FLOAT, &fp->eyeHeight); + kon_debugCommand(debug, "where", "print where you stand in map pixels", kon_cmdWhere_, fp); +} + +#else + +void kon_fpDebug(kon_fp_t *fp, kon_debug_t *debug) +{ + (void)fp; (void)debug; +} + +#endif + +/*** drawing ***/ + +void kon_drawLevel(kon_framebuffer_t *fb, const kon_camera_t *camera, const kon_level3d_t *level, const kon_light_t *light) +{ + const kon_tilemap_t *map = level->map; + kon_mat4_t vp = kon_cameraViewProjection(camera, fb->width, fb->height); + float fx = camera->target.x - camera->position.x, fz = camera->target.z - camera->position.z; + float flat = sqrtf(fx * fx + fz * fz), reach = camera->farZ + 1.0f; + float w = (float)map->columns, d = (float)map->rows, h = level->wallHeight; + int row, column; + + /* looking straight up or down there is no direction to cull against */ + if (flat > 0.001f) { + fx /= flat; + fz /= flat; + } + + for (row = 0; row < map->rows; row++) { + for (column = 0; column < map->columns; column++) { + float x = (float)column, z = (float)row; + float dx = x + 0.5f - camera->position.x, dz = z + 0.5f - camera->position.z; + + if (kon_open_(level, column, row)) continue; + if (dx * dx + dz * dz > reach * reach) continue; + if (flat > 0.001f && dx * fx + dz * fz < -2.0f) continue; + + if (kon_open_(level, column, row - 1)) kon_wall_(fb, vp, level, light, KON_VEC3(x + 1, 0, z), KON_VEC3(x, 0, z), KON_VEC3(0, 0, -1)); + if (kon_open_(level, column, row + 1)) kon_wall_(fb, vp, level, light, KON_VEC3(x, 0, z + 1), KON_VEC3(x + 1, 0, z + 1), KON_VEC3(0, 0, 1)); + if (kon_open_(level, column - 1, row)) kon_wall_(fb, vp, level, light, KON_VEC3(x, 0, z), KON_VEC3(x, 0, z + 1), KON_VEC3(-1, 0, 0)); + if (kon_open_(level, column + 1, row)) kon_wall_(fb, vp, level, light, KON_VEC3(x + 1, 0, z + 1), KON_VEC3(x + 1, 0, z), KON_VEC3(1, 0, 0)); + } + } + + if (level->floor) kon_quad_(fb, vp, level->floor, light, KON_VEC3(0, 0, 0), KON_VEC3(0, 0, d), KON_VEC3(w, 0, d), KON_VEC3(w, 0, 0), + KON_VEC3(0, 1, 0), KON_VEC2(0, 0), KON_VEC2(w, d)); + if (level->ceiling) kon_quad_(fb, vp, level->ceiling, light, KON_VEC3(0, h, 0), KON_VEC3(w, h, 0), KON_VEC3(w, h, d), KON_VEC3(0, h, d), + KON_VEC3(0, -1, 0), KON_VEC2(0, 0), KON_VEC2(w, d)); +} + +float kon_distanceFade(float distance, float falloff) +{ + return 1.0f / (1.0f + distance * falloff); +} diff --git a/kon/kon3d.h b/kon/kon3d.h new file mode 100644 index 0000000..e4ffc6b --- /dev/null +++ b/kon/kon3d.h @@ -0,0 +1,84 @@ +/* SPDX-License-Identifier: MIT */ +/* + * kon3d.h + * + * Copyright (c) 2026 KonAki + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef KON3D_H +#define KON3D_H + +#include "konprogram.h" +#include "kontile.h" + +/*** definition ***/ + +/* a first person body on a tilemap. the world is one unit per tile, y is up, yaw 0 looks up the map (towards row 0) */ +typedef struct kon_fp { + kon_box2_t body; /* map pixels, this is what bumps into walls */ + float eyeHeight; /* world units, 0.6 */ + float yaw, pitch; + float sensitivity; /* radians per mouse pixel, 0.0015 */ + float speed; /* tiles per second, 2.5 */ + float turnSpeed; /* radians per second for the arrow keys, 2 */ + float fov; /* vertical, radians, 60 degrees */ + int forward, strafe, turn; /* -1, 0 or 1, set by kon_fpEvent */ +} kon_fp_t; + +/* what to draw for a map. solid is indexed by tile id like in kon_tilemapMove, solid tiles become walls. + ceiling can be NULL, that leaves the sky colour */ +typedef struct kon_level3d { + const kon_tilemap_t *map; + const uint8_t *solid; + int solidCount; + float wallHeight; + const kon_material_t *wall, *floor, *ceiling; +} kon_level3d_t; + +/*** body ***/ + +/* stands in the middle of the tile, looking up the map */ +void kon_fpInit(kon_fp_t *fp, const kon_tilemap_t *map, int column, int row); + +/* returns 1 when the event was used. wasd and the arrows walk, the mouse looks once it is captured, + a click grabs the mouse and tab lets go */ +int kon_fpEvent(kon_fp_t *fp, kon_program_t *program, const kon_event_t *event); + +/* walks and turns, slides along solid tiles. lets the mouse go while the debug console is open */ +void kon_fpUpdate(kon_fp_t *fp, kon_program_t *program, const kon_tilemap_t *map, const uint8_t *solid, int solidCount, double dt); + +/* where the eyes are in world units */ +kon_vec3_t kon_fpEye(const kon_fp_t *fp, const kon_tilemap_t *map); +kon_camera_t kon_fpCamera(const kon_fp_t *fp, const kon_tilemap_t *map); + +/* adds the variables sens, speed and eye and the command where */ +void kon_fpDebug(kon_fp_t *fp, kon_debug_t *debug); + +/*** drawing ***/ + +/* walls, floor and ceiling. walls behind the camera and far away ones are skipped, floor and ceiling go last + so the depth test throws out what the walls already cover */ +void kon_drawLevel(kon_framebuffer_t *fb, const kon_camera_t *camera, const kon_level3d_t *level, const kon_light_t *light); + +/* 1 up close and less the further away, for dimming sprites. falloff 0.18 is gentle */ +float kon_distanceFade(float distance, float falloff); + +#endif |
