Skip to content

Commit 6781b69

Browse files
authored
refactor(voxel-renderer): split face & culling in FaceDefinition (#270)
1 parent 83b0dc4 commit 6781b69

File tree

5 files changed

+40
-21
lines changed

5 files changed

+40
-21
lines changed

.changeset/witty-lies-count.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@jolly-pixel/voxel.renderer": minor
3+
---
4+
5+
Refactor FaceDefinition to include culling in addition to face (properly splitting responsability between both)

packages/voxel-renderer/src/blocks/BlockShape.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,19 @@ import type {
1111
* 3 vertices = triangle, 4 vertices = quad (triangulated via [0,1,2] + [0,2,3]).
1212
*/
1313
export interface FaceDefinition {
14-
/** Axis-aligned culling direction used to find the neighbor to check. */
14+
/**
15+
* Texture slot: which of the block's 6 face textures to sample.
16+
* Also used as the culling direction when `cull` is not specified.
17+
*/
1518
face: FACE;
19+
/**
20+
* Culling direction: which axis-aligned neighbor to check for occlusion.
21+
* - Omitted → falls back to `face` (default behaviour).
22+
* - `null` → always emit; skip neighbor culling entirely (use for interior
23+
* faces such as stair risers that have no axis-aligned neighbor).
24+
* - A `FACE` value → check that specific neighbor instead of `face`.
25+
*/
26+
cull?: FACE | null;
1627
/** Outward-pointing surface normal (need not be axis-aligned). */
1728
normal: Vec3;
1829
/** 3 (triangle) or 4 (quad) positions in 0-1 block space. */

packages/voxel-renderer/src/blocks/shapes/RampCorner.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,9 @@ export class RampCornerInner implements BlockShape {
8181
uvs: [[0, 0], [0, 1], [1, 1]]
8282
},
8383
{
84-
// Diagonal slope face: rises from (0,0,0) to corner height.
85-
// Vertices: (0,0,0), (0,1,1), (1,1,0) form the slope triangle.
86-
// e1=[0,1,1], e2=[1,0,-1] → cross=[-1,1,-1] (points up-left-front, outward)
87-
// Cull against PosY: hidden if block sits above.
88-
face: 6 as FACE,
84+
// Top cap triangle at y=1 closing the inner corner (always visible — interior face).
85+
face: FACE.PosY,
86+
cull: null,
8987
normal: [0, 1, 0],
9088
vertices: [[0, 1, 1], [1, 1, 1], [1, 1, 0]],
9189
uvs: [[0, 0], [0, 1], [1, 1]]

packages/voxel-renderer/src/blocks/shapes/Stair.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ const kStairFaces: readonly FaceDefinition[] = [
4747
uvs: [[0, 0.5], [0, 1], [1, 1], [1, 0.5]]
4848
},
4949
{
50-
// Inner riser at z=0.5, y=0.5..1, facing NegZ (always visible)
51-
face: 6 as FACE,
50+
// Inner riser at z=0.5, y=0.5..1, facing NegZ (always visible — interior face)
51+
face: FACE.NegZ,
52+
cull: null,
5253
normal: [0, 0, -1],
5354
vertices: [[1, 0.5, 0.5], [0, 0.5, 0.5], [0, 1, 0.5], [1, 1, 0.5]],
5455
uvs: [[1, 0], [0, 0], [0, 0.5], [1, 0.5]]
@@ -249,15 +250,17 @@ const kStairCornerOuterFaces: readonly FaceDefinition[] = [
249250
uvs: [[0.5, 0.5], [0.5, 1], [1, 1], [1, 0.5]]
250251
},
251252
{
252-
// Inner riser at x=0.5, y=0.5..1, z=0..0.5 (right side of upper block, facing PosX, always visible)
253-
face: 6 as FACE,
253+
// 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)
254+
face: FACE.PosX,
255+
cull: null,
254256
normal: [1, 0, 0],
255257
vertices: [[0.5, 0.5, 0.5], [0.5, 0.5, 0], [0.5, 1, 0], [0.5, 1, 0.5]],
256258
uvs: [[0.5, 0.5], [0, 0.5], [0, 1], [0.5, 1]]
257259
},
258260
{
259-
// Inner riser at z=0.5, y=0.5..1, x=0..0.5 (back side of upper block, facing PosZ, always visible)
260-
face: 6 as FACE,
261+
// 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)
262+
face: FACE.PosZ,
263+
cull: null,
261264
normal: [0, 0, 1],
262265
vertices: [[0, 0.5, 0.5], [0.5, 0.5, 0.5], [0.5, 1, 0.5], [0, 1, 0.5]],
263266
uvs: [[0, 0.5], [0.5, 0.5], [0.5, 1], [0, 1]]

packages/voxel-renderer/src/mesh/VoxelMeshBuilder.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,17 @@ export class VoxelMeshBuilder {
122122
const { rotation, flipX, flipZ, flipY } = unpackTransform(entry.transform);
123123

124124
for (const faceDef of shape.faces) {
125-
// Rotate the logical face direction to find the world-space neighbour.
126-
let worldFace = rotateFace(faceDef.face, rotation);
127-
if (flipY && worldFace !== undefined) {
128-
worldFace = flipYFace(worldFace);
129-
}
125+
// Determine the culling direction. An explicit `cull` field overrides
126+
// the default (which is to use `face`). `null` means always emit.
127+
const cullFace = faceDef.cull === undefined ? faceDef.face : faceDef.cull;
128+
129+
if (cullFace !== null) {
130+
// Rotate the culling direction to world space and check the neighbour.
131+
let worldFace = rotateFace(cullFace, rotation);
132+
if (flipY) {
133+
worldFace = flipYFace(worldFace);
134+
}
130135

131-
// worldFace is undefined when faceDef.face is the sentinel value 6
132-
// (used by Stair/RampCorner shapes to mark "always emit" faces).
133-
// In that case skip neighbour culling entirely.
134-
if (worldFace !== undefined) {
135136
const offset = FACE_OFFSETS[worldFace];
136137
const nx = wx + offset[0];
137138
const ny = wy + offset[1];
@@ -151,6 +152,7 @@ export class VoxelMeshBuilder {
151152
}
152153

153154
// Resolve the tile reference for this face.
155+
// face is always a valid FACE (0-5) so the texture slot lookup is safe.
154156
const tileRef = blockDef.faceTextures[faceDef.face] ?? blockDef.defaultTexture;
155157
if (!tileRef) {
156158
// No texture configured — skip.

0 commit comments

Comments
 (0)