diff --git a/.changeset/witty-lies-count.md b/.changeset/witty-lies-count.md new file mode 100644 index 0000000..26782a2 --- /dev/null +++ b/.changeset/witty-lies-count.md @@ -0,0 +1,5 @@ +--- +"@jolly-pixel/voxel.renderer": minor +--- + +Refactor FaceDefinition to include culling in addition to face (properly splitting responsability between both) diff --git a/packages/voxel-renderer/src/blocks/BlockShape.ts b/packages/voxel-renderer/src/blocks/BlockShape.ts index 9b0a0d2..2377d01 100644 --- a/packages/voxel-renderer/src/blocks/BlockShape.ts +++ b/packages/voxel-renderer/src/blocks/BlockShape.ts @@ -11,8 +11,19 @@ import type { * 3 vertices = triangle, 4 vertices = quad (triangulated via [0,1,2] + [0,2,3]). */ export interface FaceDefinition { - /** Axis-aligned culling direction used to find the neighbor to check. */ + /** + * Texture slot: which of the block's 6 face textures to sample. + * Also used as the culling direction when `cull` is not specified. + */ face: FACE; + /** + * Culling direction: which axis-aligned neighbor to check for occlusion. + * - Omitted → falls back to `face` (default behaviour). + * - `null` → always emit; skip neighbor culling entirely (use for interior + * faces such as stair risers that have no axis-aligned neighbor). + * - A `FACE` value → check that specific neighbor instead of `face`. + */ + cull?: FACE | null; /** Outward-pointing surface normal (need not be axis-aligned). */ normal: Vec3; /** 3 (triangle) or 4 (quad) positions in 0-1 block space. */ diff --git a/packages/voxel-renderer/src/blocks/shapes/RampCorner.ts b/packages/voxel-renderer/src/blocks/shapes/RampCorner.ts index 6baef75..09ade90 100644 --- a/packages/voxel-renderer/src/blocks/shapes/RampCorner.ts +++ b/packages/voxel-renderer/src/blocks/shapes/RampCorner.ts @@ -81,11 +81,9 @@ export class RampCornerInner implements BlockShape { uvs: [[0, 0], [0, 1], [1, 1]] }, { - // Diagonal slope face: rises from (0,0,0) to corner height. - // Vertices: (0,0,0), (0,1,1), (1,1,0) form the slope triangle. - // e1=[0,1,1], e2=[1,0,-1] → cross=[-1,1,-1] (points up-left-front, outward) - // Cull against PosY: hidden if block sits above. - face: 6 as FACE, + // Top cap triangle at y=1 closing the inner corner (always visible — interior face). + face: FACE.PosY, + cull: null, normal: [0, 1, 0], vertices: [[0, 1, 1], [1, 1, 1], [1, 1, 0]], uvs: [[0, 0], [0, 1], [1, 1]] diff --git a/packages/voxel-renderer/src/blocks/shapes/Stair.ts b/packages/voxel-renderer/src/blocks/shapes/Stair.ts index 4630633..c28069a 100644 --- a/packages/voxel-renderer/src/blocks/shapes/Stair.ts +++ b/packages/voxel-renderer/src/blocks/shapes/Stair.ts @@ -47,8 +47,9 @@ const kStairFaces: readonly FaceDefinition[] = [ uvs: [[0, 0.5], [0, 1], [1, 1], [1, 0.5]] }, { - // Inner riser at z=0.5, y=0.5..1, facing NegZ (always visible) - face: 6 as FACE, + // Inner riser at z=0.5, y=0.5..1, facing NegZ (always visible — interior face) + face: FACE.NegZ, + cull: null, normal: [0, 0, -1], vertices: [[1, 0.5, 0.5], [0, 0.5, 0.5], [0, 1, 0.5], [1, 1, 0.5]], uvs: [[1, 0], [0, 0], [0, 0.5], [1, 0.5]] @@ -249,15 +250,17 @@ const kStairCornerOuterFaces: readonly FaceDefinition[] = [ uvs: [[0.5, 0.5], [0.5, 1], [1, 1], [1, 0.5]] }, { - // Inner riser at x=0.5, y=0.5..1, z=0..0.5 (right side of upper block, facing PosX, always visible) - face: 6 as FACE, + // Inner riser at x=0.5, y=0.5..1, z=0..0.5 (right side of upper block, facing PosX, always visible — interior face) + face: FACE.PosX, + cull: null, normal: [1, 0, 0], vertices: [[0.5, 0.5, 0.5], [0.5, 0.5, 0], [0.5, 1, 0], [0.5, 1, 0.5]], uvs: [[0.5, 0.5], [0, 0.5], [0, 1], [0.5, 1]] }, { - // Inner riser at z=0.5, y=0.5..1, x=0..0.5 (back side of upper block, facing PosZ, always visible) - face: 6 as FACE, + // Inner riser at z=0.5, y=0.5..1, x=0..0.5 (back side of upper block, facing PosZ, always visible — interior face) + face: FACE.PosZ, + cull: null, normal: [0, 0, 1], vertices: [[0, 0.5, 0.5], [0.5, 0.5, 0.5], [0.5, 1, 0.5], [0, 1, 0.5]], uvs: [[0, 0.5], [0.5, 0.5], [0.5, 1], [0, 1]] diff --git a/packages/voxel-renderer/src/mesh/VoxelMeshBuilder.ts b/packages/voxel-renderer/src/mesh/VoxelMeshBuilder.ts index 05f9af3..af82e4e 100644 --- a/packages/voxel-renderer/src/mesh/VoxelMeshBuilder.ts +++ b/packages/voxel-renderer/src/mesh/VoxelMeshBuilder.ts @@ -122,16 +122,17 @@ export class VoxelMeshBuilder { const { rotation, flipX, flipZ, flipY } = unpackTransform(entry.transform); for (const faceDef of shape.faces) { - // Rotate the logical face direction to find the world-space neighbour. - let worldFace = rotateFace(faceDef.face, rotation); - if (flipY && worldFace !== undefined) { - worldFace = flipYFace(worldFace); - } + // Determine the culling direction. An explicit `cull` field overrides + // the default (which is to use `face`). `null` means always emit. + const cullFace = faceDef.cull === undefined ? faceDef.face : faceDef.cull; + + if (cullFace !== null) { + // Rotate the culling direction to world space and check the neighbour. + let worldFace = rotateFace(cullFace, rotation); + if (flipY) { + worldFace = flipYFace(worldFace); + } - // worldFace is undefined when faceDef.face is the sentinel value 6 - // (used by Stair/RampCorner shapes to mark "always emit" faces). - // In that case skip neighbour culling entirely. - if (worldFace !== undefined) { const offset = FACE_OFFSETS[worldFace]; const nx = wx + offset[0]; const ny = wy + offset[1]; @@ -151,6 +152,7 @@ export class VoxelMeshBuilder { } // Resolve the tile reference for this face. + // face is always a valid FACE (0-5) so the texture slot lookup is safe. const tileRef = blockDef.faceTextures[faceDef.face] ?? blockDef.defaultTexture; if (!tileRef) { // No texture configured — skip.