Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/witty-lies-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@jolly-pixel/voxel.renderer": minor
---

Refactor FaceDefinition to include culling in addition to face (properly splitting responsability between both)
13 changes: 12 additions & 1 deletion packages/voxel-renderer/src/blocks/BlockShape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
8 changes: 3 additions & 5 deletions packages/voxel-renderer/src/blocks/shapes/RampCorner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down
15 changes: 9 additions & 6 deletions packages/voxel-renderer/src/blocks/shapes/Stair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down Expand Up @@ -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]]
Expand Down
20 changes: 11 additions & 9 deletions packages/voxel-renderer/src/mesh/VoxelMeshBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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.
Expand Down
Loading