minecraft-renderer 0.1.104 → 0.1.106

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.104",
3
+ "version": "0.1.106",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -80,7 +80,7 @@
80
80
  "lil-gui": "^0.18.2",
81
81
  "lodash": "^4.17.21",
82
82
  "mc-assets": "^0.2.72",
83
- "minecraft-data": "3.98.0",
83
+ "minecraft-data": "3.117.0",
84
84
  "mineflayer": "^4.33.0",
85
85
  "npm-run-all": "^4.1.5",
86
86
  "prettier": "^3.8.4",
@@ -1,10 +1,14 @@
1
1
  //@ts-nocheck
2
2
  import { test, expect } from 'vitest'
3
- import { versions } from 'minecraft-data'
3
+ import minecraftData, { versions } from 'minecraft-data'
4
4
  import { INVISIBLE_BLOCKS } from '../../mesher-shared/worldConstants'
5
5
  import { setup } from './mesherTester'
6
6
 
7
- const lastVersion = versions.pc.map(x => x.minecraftVersion).filter(version => !version.includes('w'))[0]
7
+ // newest release that minecraft-data has data for (its protocol versions list can run ahead of the data, e.g. 26.2 / 26.3)
8
+ const lastVersion = versions.pc
9
+ .map(x => x.minecraftVersion)
10
+ .filter(version => !/w|-pre|-rc|-snapshot/.test(version))
11
+ .find(version => minecraftData(version))!
8
12
 
9
13
  const addPositions = [
10
14
  // [[0, 0, 0], 'diamond_block'],
@@ -16,42 +20,41 @@ const addPositions = [
16
20
  // [[0, 0, -1], 'stone'],
17
21
  ] as const
18
22
 
19
- test('Known blocks are not rendered', () => {
20
- const { mesherWorld, getGeometry, pos, mcData } = setup(lastVersion, addPositions as any)
23
+ /** Meshes the default state of every block of the version and returns the blocks that gave no geometry / errored */
24
+ function meshAllBlocks(version: string) {
25
+ const { mesherWorld, getGeometry, pos, mcData } = setup(version, addPositions as any)
21
26
  const ignoreAsExpected = new Set([...INVISIBLE_BLOCKS, 'water', 'lava'])
22
27
 
23
- let time = 0
24
- let times = 0
25
- const missingBlocks = {} /* as {[number, number]} */
26
- const erroredBlocks = {} /* as {[number, number]} */
28
+ const missingBlocks: string[] = []
29
+ const erroredBlocks: string[] = []
27
30
  for (const block of mcData.blocksArray) {
28
31
  if (ignoreAsExpected.has(block.name)) continue
29
- // if (block.maxStateId! - block.minStateId! > 100) continue
30
- // for (let i = block.minStateId!; i <= block.maxStateId!; i++) {
31
- for (let i = block.defaultState; i <= block.defaultState; i++) {
32
- // if (block.transparent) continue
33
- mesherWorld.setBlockStateId(pos, i)
34
- const start = performance.now()
35
- const { centerFaces, totalTiles, centerTileNeighbors, attr } = getGeometry()
36
- time += performance.now() - start
37
- times++
38
- if (centerFaces === 0) {
39
- const objAdd = attr.hadErrors ? erroredBlocks : missingBlocks
40
- if (objAdd[block.name]) continue
41
- objAdd[block.name] = true
42
- // invalidBlocks[block.name] = [i - block.defaultState!, centerTileNeighbors]
43
- // console.log('INVALID', block.name, centerTileNeighbors, i - block.minStateId)
44
- }
45
- }
32
+ mesherWorld.setBlockStateId(pos, block.defaultState)
33
+ const { centerFaces, attr } = getGeometry()
34
+ if (centerFaces === 0) (attr.hadErrors ? erroredBlocks : missingBlocks).push(block.name)
46
35
  }
47
- console.log('Checking blocks of version', lastVersion)
48
- console.log('Average time', time / times)
49
- // should be fixed, but to avoid regressions & for visibility
50
- // TODO resolve creaking_heart issue (1.21.3)
51
- expect(missingBlocks).toMatchInlineSnapshot(`
52
- {
53
- "structure_void": true,
54
- }
55
- `)
56
- expect(erroredBlocks).toMatchInlineSnapshot('{}')
36
+ return { missingBlocks, erroredBlocks }
37
+ }
38
+
39
+ // These tests only guard against making the renderer worse. Blocks that a new minecraft-data / mc-assets version adds
40
+ // without a model must not fail them (versions are updated automatically), so the newest version is only checked for errors.
41
+
42
+ // TODO resolve creaking_heart issue (1.21.3)
43
+ const KNOWN_MISSING_BLOCKS = ['structure_void']
44
+ // pinned: the data of a released version doesn't grow new blocks, so what renders here must keep rendering
45
+ const pinnedVersion = '1.21.8'
46
+
47
+ test(`Blocks of ${pinnedVersion} that rendered keep rendering`, () => {
48
+ const { missingBlocks, erroredBlocks } = meshAllBlocks(pinnedVersion)
49
+ // fixing a block (it renders now) is fine, only blocks that stopped rendering fail
50
+ expect(missingBlocks.filter(name => !KNOWN_MISSING_BLOCKS.includes(name))).toEqual([])
51
+ expect(erroredBlocks).toEqual([])
52
+ })
53
+
54
+ test('Newest version: no block fails to mesh', () => {
55
+ const { missingBlocks, erroredBlocks } = meshAllBlocks(lastVersion)
56
+ console.log('Checked blocks of version', lastVersion)
57
+ // visibility only, e.g. new block entities that need a custom model in mc-assets
58
+ if (missingBlocks.length > 0) console.warn(`Blocks of ${lastVersion} that render nothing:`, missingBlocks.join(', '))
59
+ expect(erroredBlocks).toEqual([])
57
60
  })
@@ -58,7 +58,7 @@ describe('mesher worker minecraft-data shim', () => {
58
58
  const { mcData } = supplyFor('1.21.8')
59
59
  const newest = mcData.protocolVersions[0]
60
60
  // a version from the future: only the main thread knows it
61
- const future = { ...mcData.version, minecraftVersion: '99.9', majorVersion: '99', dataVersion: newest.dataVersion + 1000, version: 9999 }
61
+ const future = { ...mcData.version, minecraftVersion: '99.9', majorVersion: '99', dataVersion: (newest.dataVersion ?? 0) + 1000, version: 9999 }
62
62
  ;(globalThis as any).mcData = {
63
63
  ...mcData,
64
64
  version: future,
@@ -0,0 +1,50 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, it } from 'vitest'
3
+ import { buildElementRotation, buildRotationMatrix, buildXyzRotationMatrix, matmul3 } from './modelsGeometryCommon'
4
+
5
+ const origin: [number, number, number] = [8, 8, 8]
6
+ const close = (a: number[], b: number[]) => a.forEach((v, i) => expect(v).toBeCloseTo(b[i], 9))
7
+
8
+ describe('buildElementRotation', () => {
9
+ it('keeps the axis/angle form working', () => {
10
+ const { localMatrix } = buildElementRotation({ origin, axis: 'y', angle: 90 })
11
+ close(matmul3(localMatrix, [1, 0, 0]), [0, 0, -1])
12
+ close(matmul3(localMatrix, [0, 0, 1]), [1, 0, 0])
13
+ })
14
+
15
+ it.each(['x', 'y', 'z'] as const)('x/y/z form about a single axis (%s) equals the axis/angle form', axis => {
16
+ const viaAxis = buildElementRotation({ origin: [2, 5, 11], axis, angle: 37 })
17
+ const viaXyz = buildElementRotation({ origin: [2, 5, 11], [axis]: 37 })
18
+ viaAxis.localMatrix.forEach((row, i) => close(viaXyz.localMatrix[i], row))
19
+ close(viaXyz.localShift, viaAxis.localShift)
20
+ })
21
+
22
+ it('applies a combined x/y/z rotation as rotationZYX: z first, then y, then x', () => {
23
+ const rotation = { x: -40.06672, y: 3.06532, z: -2.57258 }
24
+ const vector = [0.3, -1.2, 0.8]
25
+ const stepwise = matmul3(
26
+ buildRotationMatrix('x', rotation.x),
27
+ matmul3(buildRotationMatrix('y', rotation.y), matmul3(buildRotationMatrix('z', rotation.z), vector))
28
+ )
29
+ close(matmul3(buildXyzRotationMatrix(rotation), vector), stepwise)
30
+ // order matters (guards against x, y, z being swapped)
31
+ const reversed = matmul3(
32
+ buildRotationMatrix('z', rotation.z),
33
+ matmul3(buildRotationMatrix('y', rotation.y), matmul3(buildRotationMatrix('x', rotation.x), vector))
34
+ )
35
+ expect(Math.abs(reversed[0] - stepwise[0]) + Math.abs(reversed[1] - stepwise[1])).toBeGreaterThan(1e-3)
36
+ })
37
+
38
+ it('rotates around the element origin (the origin stays in place)', () => {
39
+ const rotationOrigin: [number, number, number] = [2.53162, 8.56801, 5.10154]
40
+ const { localMatrix, localShift } = buildElementRotation({ origin: rotationOrigin, x: -40, y: 3, z: -2.5 })
41
+ const moved = matmul3(localMatrix, rotationOrigin).map((v, i) => v + localShift[i])
42
+ close(moved, rotationOrigin)
43
+ })
44
+
45
+ it('treats a rotation without any angle as no rotation', () => {
46
+ const { localMatrix, localShift } = buildElementRotation({ origin })
47
+ localMatrix.forEach((row, i) => close(row, [0, 1, 2].map(j => (i === j ? 1 : 0))))
48
+ close(localShift, [0, 0, 0])
49
+ })
50
+ })
@@ -88,11 +88,27 @@ export function matmulmat3(a, b) {
88
88
  return te
89
89
  }
90
90
 
91
+ /** Plain row-major product a * b (not the transposed convention of matmulmat3) */
92
+ function mulMat3(a: number[][], b: number[][]) {
93
+ return a.map((row, i) => row.map((_, j) => row[0] * b[0][j] + row[1] * b[1][j] + row[2] * b[2][j]))
94
+ }
95
+
96
+ /**
97
+ * 26.x element rotation: degrees about each axis, `{ origin, x, y, z }` instead of `{ origin, axis, angle }`.
98
+ * Applied like vanilla's `rotationZYX(z, y, x)`: a vector is rotated about z first, then y, then x.
99
+ */
100
+ export function buildXyzRotationMatrix({ x = 0, y = 0, z = 0 }: { x?: number; y?: number; z?: number }) {
101
+ return mulMat3(buildRotationMatrix('x', x), mulMat3(buildRotationMatrix('y', y), buildRotationMatrix('z', z)))
102
+ }
103
+
91
104
  export function buildElementRotation(rotation) {
92
- let localMatrix = buildRotationMatrix(rotation.axis, rotation.angle)
105
+ // elements without an axis use the 26.x x/y/z form (a missing x/y/z is 0, so no rotation at all is the identity)
106
+ const isAxisAngle = rotation.axis !== undefined
107
+ let localMatrix = isAxisAngle ? buildRotationMatrix(rotation.axis, rotation.angle) : buildXyzRotationMatrix(rotation)
93
108
  let localShift = vecsub3(rotation.origin, matmul3(localMatrix, rotation.origin))
94
109
 
95
- if (rotation.rescale) {
110
+ // rescale is only defined for a single axis rotation
111
+ if (rotation.rescale && isAxisAngle) {
96
112
  const FIT_TO_BLOCK_SCALE_MULTIPLIER = 2 - Math.sqrt(2)
97
113
  const angleRad = (rotation.angle * Math.PI) / 180
98
114
  const scale = Math.abs(Math.sin(angleRad)) * FIT_TO_BLOCK_SCALE_MULTIPLIER
@@ -19,6 +19,7 @@ import { GlobalBlockBuffer } from '../../three/globalBlockBuffer'
19
19
  import { buildVisibleCubeSpans } from '../../three/cubeDrawSpans'
20
20
  import { createCubeBlockMaterial, computeSectionOriginRel, setCubeShadingTheme } from '../../three/shaders/cubeBlockShader'
21
21
  import * as THREE from 'three'
22
+ import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
22
23
  import { renderWasmOutputToGeometry } from '../bridge/render-from-wasm'
23
24
 
24
25
  const VERSION = '1.16.5'
@@ -29,8 +30,8 @@ function requireShaderCubeResources() {
29
30
  if (!resources) throw new Error('shader cube resources unavailable in test')
30
31
  return resources
31
32
  }
32
- /** mc-assets blocksAtlases.json → stone */
33
- const STONE_ATLAS_TILE_INDEX = 552
33
+ /** mc-assets blocksAtlases.json → stone (looked up, tile positions change whenever the atlas gets new textures) */
34
+ const STONE_ATLAS_TILE_INDEX = blocksAtlasesJson.latest.textures.stone.tileIndex
34
35
 
35
36
  beforeEach(() => {
36
37
  resetShaderCubeResources()