minecraft-renderer 0.1.89 → 0.1.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.89",
3
+ "version": "0.1.90",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -642,7 +642,7 @@ export function renderWasmOutputToGeometry(
642
642
  const doAO = (model as { ao?: boolean }).ao ?? cachedModel.boundingBox !== 'empty'
643
643
 
644
644
  let forceCullMask = 0
645
- if (world) {
645
+ if (world && cachedModel.isCube) {
646
646
  const shaderCubeFaceNameToIndex: Record<string, number> = {
647
647
  up: 0,
648
648
  down: 1,
@@ -787,7 +787,11 @@ export function renderWasmOutputToGeometry(
787
787
  const maxy = element.to[1]
788
788
  const maxz = element.to[2]
789
789
 
790
- if (faceIdx !== undefined && (block.visible_faces & (1 << faceIdx)) === 0) {
790
+ // `visible_faces` is a cell-boundary occlusion mask, so it may only cull faces
791
+ // that vanilla considers cullable at all — i.e. faces that declare `cullface`.
792
+ // Inset faces without it (a stair riser) are always drawn. Removing this guard
793
+ // is what broke issue #81 in 7ce1ebb.
794
+ if (matchingEFace.cullface && faceIdx !== undefined && (block.visible_faces & (1 << faceIdx)) === 0) {
791
795
  continue
792
796
  }
793
797
 
@@ -14,6 +14,7 @@ import { setBlockStatesData, getSectionGeometry } from '../../mesher-shared/mode
14
14
  import { resetFaceOcclusionCache } from '../../mesher-shared/faceOcclusion'
15
15
  import { convertChunkToWasm } from '../bridge/convertChunk'
16
16
  import { renderWasmOutputToGeometry } from '../bridge/render-from-wasm'
17
+ import type { ExportedSection } from '../../mesher-shared/exportedGeometryTypes'
17
18
 
18
19
  const VERSION = '1.18.2'
19
20
  const SECTION_Y = 0
@@ -130,6 +131,81 @@ function assertMesherParity(world: World, expectedQuads: number) {
130
131
  expect(wasmShader).toBe(expectedQuads)
131
132
  }
132
133
 
134
+ function renderWasmSection(world: World, shaderCubes = false): ExportedSection {
135
+ const column = world.getColumn(0, 0)!
136
+ const conversion = convertChunkToWasm(column, VERSION, 0, 0, SECTION_Y, SECTION_Y + SECTION_HEIGHT, SECTION_Y, SECTION_HEIGHT)
137
+ const wasmResult = wasmModule.generate_geometry(
138
+ 0,
139
+ SECTION_Y,
140
+ 0,
141
+ SECTION_HEIGHT,
142
+ SECTION_Y,
143
+ SECTION_Y + SECTION_HEIGHT,
144
+ SECTION_Y,
145
+ conversion.blockStates,
146
+ conversion.blockLight,
147
+ conversion.skyLight,
148
+ conversion.biomesArray,
149
+ conversion.invisibleBlocks,
150
+ conversion.transparentBlocks,
151
+ conversion.noAoBlocks,
152
+ conversion.cullIdenticalBlocks,
153
+ conversion.occludingBlocks,
154
+ true,
155
+ false,
156
+ 15
157
+ )
158
+ return renderWasmOutputToGeometry(wasmResult, VERSION, '0,0,0', { x: 8, y: 8, z: 8 }, world, {
159
+ sectionHeight: SECTION_HEIGHT,
160
+ shaderCubes
161
+ })
162
+ }
163
+
164
+ /** Inset stair riser in cell (bx, by, bz): normal along expectedNx, all four verts on the cell mid-x-plane and within the cell AABB. */
165
+ function hasInsetHorizontalRiser(section: ExportedSection, bx: number, by: number, bz: number, expectedNx: 1 | -1): boolean {
166
+ const pos = section.geometry.positions
167
+ const norm = section.geometry.normals
168
+ const idx = section.geometry.indices
169
+ const eps = 1e-3
170
+ const minX = (bx & 15) - 8
171
+ const maxX = minX + 1
172
+ const minY = (by & 15) - 8
173
+ const maxY = minY + 1
174
+ const minZ = (bz & 15) - 8
175
+ const maxZ = minZ + 1
176
+ const riserX = minX + 0.5
177
+
178
+ for (let i = 0; i < idx.length; i += 6) {
179
+ const vertIndices = [...new Set([idx[i], idx[i + 1], idx[i + 2], idx[i + 3], idx[i + 4], idx[i + 5]])]
180
+ if (vertIndices.length !== 4) continue
181
+
182
+ const nx = norm[vertIndices[0]! * 3]!
183
+ const ny = norm[vertIndices[0]! * 3 + 1]!
184
+ const nz = norm[vertIndices[0]! * 3 + 2]!
185
+ if (Math.abs(ny) > eps || Math.abs(nz) > eps) continue
186
+ if (Math.abs(nx - expectedNx) > eps) continue
187
+
188
+ const onRiserPlane = vertIndices.every(vi => Math.abs(pos[vi * 3]! - riserX) < eps)
189
+ if (!onRiserPlane) continue
190
+
191
+ const inCell = vertIndices.every(vi => {
192
+ const px = pos[vi * 3]!
193
+ const py = pos[vi * 3 + 1]!
194
+ const pz = pos[vi * 3 + 2]!
195
+ return px >= minX - eps && px <= maxX + eps && py >= minY - eps && py <= maxY + eps && pz >= minZ - eps && pz <= maxZ + eps
196
+ })
197
+ if (inCell) return true
198
+ }
199
+ return false
200
+ }
201
+
202
+ function stairWithStoneNeighbor(facing: 'east' | 'west' | 'south' | 'north', stone: { x: number; y: number; z: number }): BlockSpec[] {
203
+ return [
204
+ { x: 0, y: 0, z: 0, name: 'cut_copper_stairs', props: { facing, half: 'bottom', shape: 'straight', waterlogged: false } },
205
+ { x: stone.x, y: stone.y, z: stone.z, name: 'stone' }
206
+ ]
207
+ }
208
+
133
209
  function farmlandFieldBlocks(): BlockSpec[] {
134
210
  const blocks: BlockSpec[] = []
135
211
  for (let z = 0; z < 16; z++) {
@@ -331,3 +407,59 @@ test('shader cubes: dirt side not culled beside bottom slab', () => {
331
407
  expect(wasmShader).toBe(legacy)
332
408
  expect(wasmShader).toBeGreaterThan(5)
333
409
  })
410
+
411
+ test('culling regression: east-facing stair + stone to east — full silhouette, no inset riser drop (issue #81)', () => {
412
+ const world = buildWorld(stairWithStoneNeighbor('east', { x: 1, y: 0, z: 0 }))
413
+ assertMesherParity(world, 14)
414
+ })
415
+
416
+ test('culling regression: west-facing stair + stone to east — inset riser kept (issue #81)', () => {
417
+ const world = buildWorld(stairWithStoneNeighbor('west', { x: 1, y: 0, z: 0 }))
418
+ assertMesherParity(world, 16)
419
+ expect(hasInsetHorizontalRiser(renderWasmSection(world), 0, 0, 0, 1)).toBe(true)
420
+ })
421
+
422
+ test('culling regression: south-facing stair + stone to south — inset riser kept (issue #81)', () => {
423
+ const world = buildWorld(stairWithStoneNeighbor('south', { x: 0, y: 0, z: 1 }))
424
+ // Tall half faces south toward stone (east/west analog); stone's north face is culled.
425
+ assertMesherParity(world, 14)
426
+ })
427
+
428
+ test('culling regression: north-facing stair + stone to south — inset riser kept (issue #81)', () => {
429
+ const world = buildWorld([
430
+ { x: 0, y: 0, z: 0, name: 'cut_copper_stairs', props: { facing: 'north', half: 'bottom', shape: 'straight', waterlogged: false } },
431
+ { x: 0, y: 0, z: 1, name: 'stone' }
432
+ ])
433
+ assertMesherParity(world, 16)
434
+ })
435
+
436
+ test('culling regression: all horizontal facings beside occluding cube — parity table (issue #81)', () => {
437
+ const cases: Array<{ blocks: BlockSpec[]; quads: number }> = [
438
+ { blocks: stairWithStoneNeighbor('east', { x: 1, y: 0, z: 0 }), quads: 14 },
439
+ { blocks: stairWithStoneNeighbor('west', { x: 1, y: 0, z: 0 }), quads: 16 },
440
+ { blocks: stairWithStoneNeighbor('south', { x: 0, y: 0, z: 1 }), quads: 14 },
441
+ {
442
+ blocks: [
443
+ { x: 0, y: 0, z: 0, name: 'cut_copper_stairs', props: { facing: 'north', half: 'bottom', shape: 'straight', waterlogged: false } },
444
+ { x: 0, y: 0, z: 1, name: 'stone' }
445
+ ],
446
+ quads: 16
447
+ }
448
+ ]
449
+ for (const { blocks, quads } of cases) {
450
+ assertMesherParity(buildWorld(blocks), quads)
451
+ }
452
+ })
453
+
454
+ test('culling regression: sofa scene — acacia stairs risers beside stripped log (issue #81)', () => {
455
+ const world = buildWorld([
456
+ { x: 1, y: 0, z: 0, name: 'stripped_acacia_log' },
457
+ { x: 1, y: 1, z: 0, name: 'acacia_pressure_plate' },
458
+ { x: 0, y: 0, z: 0, name: 'acacia_stairs', props: { facing: 'west', half: 'bottom', shape: 'straight', waterlogged: false } },
459
+ { x: 2, y: 0, z: 0, name: 'acacia_stairs', props: { facing: 'east', half: 'bottom', shape: 'straight', waterlogged: false } }
460
+ ])
461
+ assertMesherParity(world, 31)
462
+ const section = renderWasmSection(world)
463
+ expect(hasInsetHorizontalRiser(section, 0, 0, 0, 1)).toBe(true)
464
+ expect(hasInsetHorizontalRiser(section, 2, 0, 0, -1)).toBe(true)
465
+ })