summaryrefslogtreecommitdiff
path: root/kon
diff options
context:
space:
mode:
authorKonAki <konaki@anokonaki.org>2026-10-10 23:43:00 +0800
committerKonAki <konaki@anokonaki.org>2026-10-10 23:43:00 +0800
commit2d7261e95fa1abead95564c340f383c1aafdc6e6 (patch)
tree7218b6d6ff4ac562a264d87e7f1840d8bab1de3b /kon
parent625108a3c9d4ebf99be30296aef10d60b272b7a7 (diff)
add 3d kit: first person body and tilemap level drawing
Diffstat (limited to 'kon')
-rw-r--r--kon/kon3d.c212
-rw-r--r--kon/kon3d.h84
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