From f17b0bfcfb1b22008d2e54e345780f9cc7c478ef Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sat, 27 Jun 2026 08:50:55 -0500 Subject: [PATCH] Tiles --- assets/chunks/0_0_0.dcf | Bin 63500 -> 63501 bytes .../asset/loader/chunk/assetchunkloader.c | 31 ++-- .../asset/loader/chunk/assetchunkloader.h | 5 +- src/dusk/rpg/overworld/chunk.h | 20 +-- src/dusk/rpg/overworld/map.c | 43 +++-- src/dusk/scene/overworld/sceneoverworld.c | 66 +++++--- src/dusk/scene/overworld/sceneoverworld.h | 10 +- tools/asset/chunk/__main__.py | 120 +++++++++++++ tools/asset/chunk/hillgen.py | 160 ++++++++++++++++++ 9 files changed, 390 insertions(+), 65 deletions(-) create mode 100644 tools/asset/chunk/__main__.py create mode 100644 tools/asset/chunk/hillgen.py diff --git a/assets/chunks/0_0_0.dcf b/assets/chunks/0_0_0.dcf index 7e7f4803f22dbe82792fc1c203e628610ddc65bc..436721689c226de592b7839be2954b218b14a59f 100644 GIT binary patch delta 24 fcmeDAz})+RnbpPFje%*S;P?883VfS68<;BqaaRa} delta 22 dcmeDEz})kJnc2nJZ6p8h`iXK}n>ia;DgbF}2zvkk diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.c b/src/dusk/asset/loader/chunk/assetchunkloader.c index 7b1a09ce..92a491d0 100644 --- a/src/dusk/asset/loader/chunk/assetchunkloader.c +++ b/src/dusk/asset/loader/chunk/assetchunkloader.c @@ -85,19 +85,30 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) { memoryCopy(out->tiles, data + offset, tileSize); offset += tileSize; - out->vertCount = endianLittleToHost32(*(uint32_t *)(data + offset)); - offset += sizeof(uint32_t); - + out->meshCount = data[offset]; + offset += sizeof(uint8_t); assertTrue( - out->vertCount <= CHUNK_VERTEX_COUNT, - "Chunk vertex count exceeds maximum." + out->meshCount <= CHUNK_MESH_COUNT_MAX, + "Chunk mesh count exceeds maximum." ); - memoryCopy( - out->vertices, - data + offset, - out->vertCount * sizeof(meshvertex_t) - ); + uint32_t poolOffset = 0; + for(uint8_t m = 0; m < out->meshCount; m++) { + uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(data + offset)); + offset += sizeof(uint32_t); + assertTrue( + poolOffset + vertCount <= CHUNK_VERTEX_COUNT, + "Chunk vertex data exceeds pool." + ); + out->meshVertCounts[m] = vertCount; + memoryCopy( + &out->vertices[poolOffset], + data + offset, + vertCount * sizeof(meshvertex_t) + ); + offset += vertCount * sizeof(meshvertex_t); + poolOffset += vertCount; + } memoryFree(data); loading->loading.chunk.data = NULL; diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.h b/src/dusk/asset/loader/chunk/assetchunkloader.h index 44e7636f..ecedef40 100644 --- a/src/dusk/asset/loader/chunk/assetchunkloader.h +++ b/src/dusk/asset/loader/chunk/assetchunkloader.h @@ -9,7 +9,7 @@ #include "asset/assetfile.h" #include "rpg/overworld/chunk.h" -#define ASSET_CHUNK_FILE_VERSION 1 +#define ASSET_CHUNK_FILE_VERSION 2 typedef struct assetloading_s assetloading_t; typedef struct assetentry_s assetentry_t; @@ -33,7 +33,8 @@ typedef struct { typedef struct { tile_t tiles[CHUNK_TILE_COUNT]; - uint32_t vertCount; + uint8_t meshCount; + uint32_t meshVertCounts[CHUNK_MESH_COUNT_MAX]; meshvertex_t vertices[CHUNK_VERTEX_COUNT]; } assetchunkoutput_t; diff --git a/src/dusk/rpg/overworld/chunk.h b/src/dusk/rpg/overworld/chunk.h index c19dbebd..a701d544 100644 --- a/src/dusk/rpg/overworld/chunk.h +++ b/src/dusk/rpg/overworld/chunk.h @@ -1,6 +1,6 @@ /** - * Copyright (c) 2025 Dominic Masters - * + * Copyright (c) 2026 Dominic Masters + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ @@ -8,24 +8,22 @@ #pragma once #include "rpg/overworld/tile.h" #include "worldpos.h" -#include "display/mesh/quad.h" +#include "display/mesh/mesh.h" #include "display/spritebatch/spritebatch.h" -// #define CHUNK_MESH_COUNT_MAX 3 -#define CHUNK_VERTEX_COUNT (QUAD_VERTEX_COUNT * CHUNK_WIDTH * CHUNK_HEIGHT * 2) +#define CHUNK_MESH_COUNT_MAX 10 +#define CHUNK_VERTEX_COUNT 8192 #define CHUNK_ENTITY_COUNT_MAX 10 typedef struct chunk_s { chunkpos_t position; tile_t tiles[CHUNK_TILE_COUNT]; + uint8_t meshCount; + uint32_t meshVertCounts[CHUNK_MESH_COUNT_MAX]; meshvertex_t vertices[CHUNK_VERTEX_COUNT]; - uint32_t vertCount; - mesh_t mesh; - - // uint8_t meshCount; - // meshvertex_t vertices[CHUNK_VERTEX_COUNT_MAX]; - // mesh_t meshes[CHUNK_MESH_COUNT_MAX]; + mesh_t meshes[CHUNK_MESH_COUNT_MAX]; + uint8_t entities[CHUNK_ENTITY_COUNT_MAX]; } chunk_t; diff --git a/src/dusk/rpg/overworld/map.c b/src/dusk/rpg/overworld/map.c index 806342cd..2f7598dc 100644 --- a/src/dusk/rpg/overworld/map.c +++ b/src/dusk/rpg/overworld/map.c @@ -20,12 +20,14 @@ errorret_t mapInit() { // Setup chunk meshes for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { chunk_t *chunk = &MAP.chunks[i]; - errorChain(meshInit( - &chunk->mesh, - MESH_PRIMITIVE_TYPE_TRIANGLES, - CHUNK_VERTEX_COUNT, - chunk->vertices - )); + for(uint8_t j = 0; j < CHUNK_MESH_COUNT_MAX; j++) { + errorChain(meshInit( + &chunk->meshes[j], + MESH_PRIMITIVE_TYPE_TRIANGLES, + CHUNK_VERTEX_COUNT, + chunk->vertices + )); + } } // Perform "initial load" @@ -196,7 +198,9 @@ void mapUpdate() { errorret_t mapDispose() { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { mapChunkUnload(&MAP.chunks[i]); - errorChain(meshDispose(&MAP.chunks[i].mesh)); + for(uint8_t j = 0; j < CHUNK_MESH_COUNT_MAX; j++) { + errorChain(meshDispose(&MAP.chunks[i].meshes[j])); + } } errorOk(); } @@ -216,14 +220,14 @@ void mapChunkUnload(chunk_t* chunk) { entity->type = ENTITY_TYPE_NULL; } } - chunk->vertCount = 0; + chunk->meshCount = 0; } errorret_t mapChunkLoad(chunk_t* chunk) { if(!mapIsLoaded()) errorThrow("No map loaded"); memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); - chunk->vertCount = 0; + chunk->meshCount = 0; char_t name[64]; stringFormat( @@ -248,19 +252,26 @@ errorret_t mapChunkLoad(chunk_t* chunk) { return ret; } + memoryCopy(chunk->tiles, entry->data.chunk.tiles, sizeof(chunk->tiles)); memoryCopy( - chunk->tiles, entry->data.chunk.tiles, sizeof(chunk->tiles) + chunk->vertices, entry->data.chunk.vertices, sizeof(chunk->vertices) ); - chunk->vertCount = entry->data.chunk.vertCount; memoryCopy( - chunk->vertices, - entry->data.chunk.vertices, - chunk->vertCount * sizeof(meshvertex_t) + chunk->meshVertCounts, + entry->data.chunk.meshVertCounts, + sizeof(chunk->meshVertCounts) ); + uint8_t meshCount = entry->data.chunk.meshCount; assetUnlockEntry(entry); - if(chunk->vertCount == 0) errorOk(); - errorChain(meshFlush(&chunk->mesh, 0, chunk->vertCount)); + if(meshCount == 0) errorOk(); + chunk->meshCount = meshCount; + for(uint8_t m = 0; m < meshCount; m++) { + if(chunk->meshVertCounts[m] == 0) continue; + errorChain(meshFlush( + &chunk->meshes[m], 0, (int32_t)chunk->meshVertCounts[m] + )); + } errorOk(); } diff --git a/src/dusk/scene/overworld/sceneoverworld.c b/src/dusk/scene/overworld/sceneoverworld.c index 3c5156f1..97d9e26a 100644 --- a/src/dusk/scene/overworld/sceneoverworld.c +++ b/src/dusk/scene/overworld/sceneoverworld.c @@ -97,31 +97,7 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) { errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, eye)); // Chunks - { - shadermaterial_t chunkMaterial = { - .unlit = { - .color = COLOR_WHITE, - .texture = &TEXTURE_CHUNK - } - }; - - uint32_t i = 0; - for(uint8_t x = 0; x < MAP_CHUNK_WIDTH; x++) { - for(uint8_t y = 0; y < MAP_CHUNK_HEIGHT; y++) { - for(uint8_t z = 0; z < MAP_CHUNK_DEPTH; z++) { - chunk_t *chunk = &MAP.chunks[i]; - if(chunk->vertCount == 0) { - i++; - continue; - } - - errorChain(shaderSetMaterial(&SHADER_UNLIT, &chunkMaterial)); - errorChain(meshDraw(&chunk->mesh, 0, chunk->vertCount)); - i++; - } - } - } - } + errorChain(sceneOverworldDrawChunks()); // Entities { @@ -156,6 +132,46 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) { errorOk(); } +errorret_t sceneOverworldDrawChunks() { + shadermaterial_t chunkMaterial = { + .unlit = { + .color = COLOR_WHITE, + .texture = &TEXTURE_CHUNK + } + }; + + // Pass 1: draw all base meshes with the shared chunk texture (no mid-loop + // texture swaps). + errorChain(shaderSetMaterial(&SHADER_UNLIT, &chunkMaterial)); + for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { + chunk_t *chunk = &MAP.chunks[i]; + if(chunk->meshCount == 0) continue; + if(chunk->meshVertCounts[0] == 0) continue; + errorChain(meshDraw( + &chunk->meshes[0], 0, (int32_t)chunk->meshVertCounts[0] + )); + } + + // Pass 2: draw each chunk's additional meshes (indices 1..meshCount-1). + // Vertices are packed sequentially in the pool, so accumulate the offset. + for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { + chunk_t *chunk = &MAP.chunks[i]; + uint32_t vertOffset = chunk->meshVertCounts[0]; + for(uint8_t m = 1; m < chunk->meshCount; m++) { + if(chunk->meshVertCounts[m] > 0) { + errorChain(meshDraw( + &chunk->meshes[m], + (int32_t)vertOffset, + (int32_t)chunk->meshVertCounts[m] + )); + } + vertOffset += chunk->meshVertCounts[m]; + } + } + + errorOk(); +} + errorret_t sceneOverworldDispose(scenedata_t *sceneData) { assertNotNull(sceneData, "Scene data cannot be null"); diff --git a/src/dusk/scene/overworld/sceneoverworld.h b/src/dusk/scene/overworld/sceneoverworld.h index efd12f1c..e6c7ce4e 100644 --- a/src/dusk/scene/overworld/sceneoverworld.h +++ b/src/dusk/scene/overworld/sceneoverworld.h @@ -28,9 +28,17 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData); */ errorret_t sceneOverworldUpdate(scenedata_t *sceneData); +/** + * Draws all loaded chunks in two passes: base meshes first (shared texture, + * no binds between chunks), then each chunk's additional meshes. + * + * @return An error if drawing failed, or errorOk() on success. + */ +errorret_t sceneOverworldDrawChunks(); + /** * Renders the overworld scene. - * + * * @param sceneData The scene data used for this scene. * @return An error if the render failed, or errorOk() if it succeeded. */ diff --git a/tools/asset/chunk/__main__.py b/tools/asset/chunk/__main__.py new file mode 100644 index 00000000..ce152a4d --- /dev/null +++ b/tools/asset/chunk/__main__.py @@ -0,0 +1,120 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +""" +Converts DCF chunk files from version 1 to version 2. + +Version 1 format (after 8-byte header + tiles): + uint32_t vertCount + meshvertex_t vertices[vertCount] + +Version 2 format (after 8-byte header + tiles): + uint8_t meshCount + for each mesh: + uint32_t vertCount + meshvertex_t vertices[vertCount] + +Usage: + python3 -m tools.asset.chunk [output.dcf] + If output is omitted the input file is updated in place. +""" + +import struct +import sys +import os + +# Must match src/dusk/rpg/overworld/chunk.h +CHUNK_WIDTH = 16 +CHUNK_HEIGHT = 16 +CHUNK_DEPTH = 32 +CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH # 8192 + +CHUNK_MESH_COUNT_MAX = 10 +CHUNK_VERTEX_COUNT = 8192 + +# C enum (int) = 4 bytes; meshvertex_t = uv[2]+pos[3] floats = 20 bytes +TILE_SIZE = 4 +VERTEX_SIZE = 20 # 2 floats UV + 3 floats pos, MESH_ENABLE_COLOR=0 + +FILE_MAGIC = b'DCF' +VERSION_IN = 1 +VERSION_OUT = 2 + + +def read_v1(path): + with open(path, 'rb') as f: + data = f.read() + + if data[:3] != FILE_MAGIC: + raise ValueError(f"{path}: not a DCF file") + + version = struct.unpack_from(' CHUNK_VERTEX_COUNT: + print( + f" Warning: {vert_count} vertices exceeds pool " + f"({CHUNK_VERTEX_COUNT}); truncating." + ) + vert_count = CHUNK_VERTEX_COUNT + verts = verts[:vert_count * VERTEX_SIZE] + + mesh_count = 1 if vert_count > 0 else 0 + + buf = bytearray() + buf += FILE_MAGIC + buf += b'\x00' + buf += struct.pack(' 0: + buf += struct.pack(' [output.dcf]") + sys.exit(1) + + src = args[0] + dst = args[1] if len(args) > 1 else src + + print(f"Reading {src} ...") + tiles, vert_count, verts = read_v1(src) + print(f" tiles={CHUNK_TILE_COUNT}, vertices={vert_count}") + + print(f"Writing {dst} ...") + write_v2(dst, tiles, vert_count, verts) + + +if __name__ == '__main__': + main() diff --git a/tools/asset/chunk/hillgen.py b/tools/asset/chunk/hillgen.py new file mode 100644 index 00000000..13ec593c --- /dev/null +++ b/tools/asset/chunk/hillgen.py @@ -0,0 +1,160 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +""" +Generates chunk 0_0_0.dcf with a small hill in the centre. + +Hill layout (tile coordinates, 0-based): + y=5: . . . . . . N N . . . . . . . . RAMP_NORTH (south slope) + y=6: . . . . . E H H W . . . . . . . Hill top (H), RAMP_EAST/WEST + y=7: . . . . . E H H W . . . . . . . + y=8: . . . . . . S S . . . . . . . . RAMP_SOUTH (north slope) + x=6 x=7 +""" + +import struct, os + +# Must match src/dusk/rpg/overworld/chunk.h and tile.h +CHUNK_WIDTH = 16 +CHUNK_HEIGHT = 16 +CHUNK_DEPTH = 32 +CHUNK_W_F = float(CHUNK_WIDTH) + +TILE_NULL = 0 +TILE_GROUND = 1 +TILE_RAMP_NORTH = 2 +TILE_RAMP_SOUTH = 3 +TILE_RAMP_EAST = 4 +TILE_RAMP_WEST = 5 + +TILE_SIZE = 4 # sizeof(tile_t) = sizeof(int) +VERT_SIZE = 20 # sizeof(meshvertex_t): uv[2] + pos[3] floats +FILE_VER = 2 + +# Hill geometry parameters +HILL_X = frozenset({6, 7}) +HILL_Y = frozenset({6, 7}) +HILL_H = 1.0 + + +def tile_idx(cx, cy, cz): + return cz * CHUNK_WIDTH * CHUNK_HEIGHT + cy * CHUNK_WIDTH + cx + + +def make_vert(u, v, px, py, pz): + return struct.pack('<5f', u, v, px, py, pz) + + +def quad_verts(cx, cy, z_sw, z_se, z_ne, z_nw): + """ + Build 6 vertices (2 triangles) for a tile quad. + Heights at each corner: SW=south-west, SE=south-east, + NE=north-east, NW=north-west. + UV formula (verified against existing DCF data): + u = (cy + within_x) / CHUNK_WIDTH where within_x in {0,1} + v = (cx + within_y) / CHUNK_HEIGHT where within_y in {0,1} + """ + u0 = cy / CHUNK_W_F + u1 = (cy + 1) / CHUNK_W_F + v0 = cx / CHUNK_W_F + v1 = (cx + 1) / CHUNK_W_F + x0, x1 = float(cx), float(cx + 1) + y0, y1 = float(cy), float(cy + 1) + + SW = make_vert(u0, v0, x0, y0, float(z_sw)) + SE = make_vert(u1, v0, x1, y0, float(z_se)) + NE = make_vert(u1, v1, x1, y1, float(z_ne)) + NW = make_vert(u0, v1, x0, y1, float(z_nw)) + + return SW + SE + NE + SW + NE + NW + + +def flat(cx, cy, z): + return quad_verts(cx, cy, z, z, z, z) + + +def ramp_north(cx, cy): + return quad_verts(cx, cy, 0, 0, HILL_H, HILL_H) + + +def ramp_south(cx, cy): + return quad_verts(cx, cy, HILL_H, HILL_H, 0, 0) + + +def ramp_east(cx, cy): + return quad_verts(cx, cy, 0, HILL_H, HILL_H, 0) + + +def ramp_west(cx, cy): + return quad_verts(cx, cy, HILL_H, 0, 0, HILL_H) + + +def generate(): + tiles = [TILE_GROUND] * (CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH) + + ramps_n = frozenset((cx, 5) for cx in HILL_X) + ramps_s = frozenset((cx, 8) for cx in HILL_X) + ramps_e = frozenset((5, cy) for cy in HILL_Y) + ramps_w = frozenset((8, cy) for cy in HILL_Y) + + for cx, cy in ramps_n: + tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_NORTH + for cx, cy in ramps_s: + tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_SOUTH + for cx, cy in ramps_e: + tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_EAST + for cx, cy in ramps_w: + tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_WEST + + for cx in HILL_X: + for cy in HILL_Y: + tiles[tile_idx(cx, cy, 1)] = TILE_GROUND + + verts = bytearray() + + for cx in range(CHUNK_WIDTH): + for cy in range(CHUNK_HEIGHT): + pos = (cx, cy) + if cx in HILL_X and cy in HILL_Y: + continue + if pos in ramps_n: + verts += ramp_north(cx, cy) + elif pos in ramps_s: + verts += ramp_south(cx, cy) + elif pos in ramps_e: + verts += ramp_east(cx, cy) + elif pos in ramps_w: + verts += ramp_west(cx, cy) + else: + verts += flat(cx, cy, 0) + + for cx in sorted(HILL_X): + for cy in sorted(HILL_Y): + verts += flat(cx, cy, HILL_H) + + vert_count = len(verts) // VERT_SIZE + tile_bytes = struct.pack(f'<{len(tiles)}i', *tiles) + + buf = bytearray() + buf += b'DCF\x00' + buf += struct.pack('