minecraft-renderer 0.1.44 → 0.1.46

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.44",
3
+ "version": "0.1.46",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { proxy } from 'valtio'
7
+ import { defaultPerformanceInstabilityFactors } from '../performanceMonitor'
7
8
  import type {
8
9
  GraphicsBackendConfig,
9
10
  RendererReactiveState,
@@ -117,6 +118,7 @@ export const getDefaultRendererState = (): {
117
118
  heightmaps: new Map<string, Int16Array>(),
118
119
  allChunksLoaded: false,
119
120
  mesherWork: false,
121
+ instabilityFactors: defaultPerformanceInstabilityFactors(),
120
122
  intersectMedia: null
121
123
  },
122
124
  renderer: '...',
@@ -9,3 +9,4 @@ export * from './types'
9
9
  export * from './config'
10
10
  export * from './playerState'
11
11
  export * from './appViewer'
12
+ export * from '../performanceMonitor'
@@ -23,6 +23,7 @@ export interface SoundSystem {
23
23
  }
24
24
 
25
25
  import type { MenuBackgroundOptions } from '../three/menuBackground/types'
26
+ import type { PerformanceInstabilityFactors } from '../performanceMonitor'
26
27
 
27
28
  /** Graphics backend configuration */
28
29
  export interface GraphicsBackendConfig {
@@ -76,6 +77,8 @@ export interface RendererReactiveState {
76
77
  heightmaps: Map<string, Int16Array>
77
78
  allChunksLoaded: boolean
78
79
  mesherWork: boolean
80
+ /** Low-FPS / render instability factors (see `performanceMonitor`). */
81
+ instabilityFactors: PerformanceInstabilityFactors
79
82
  intersectMedia: any | null
80
83
  }
81
84
  renderer: string
@@ -6,6 +6,7 @@ import legacyJson from '../lib/preflatMap.json'
6
6
  import { BlockType } from '../playground/shared'
7
7
  import { World, BlockModelPartsResolved, WorldBlock as Block, WorldBlock, worldColumnKey } from './world'
8
8
  import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
9
+ import { getSideShading, vertexLightFromAo } from './vertexShading'
9
10
  import { INVISIBLE_BLOCKS } from './worldConstants'
10
11
  import { MesherGeometryOutput, HighestBlockInfo } from './shared'
11
12
  import { collectBlockEntityMetadata } from './blockEntityMetadata'
@@ -51,6 +52,31 @@ function prepareTints(tints) {
51
52
  }
52
53
 
53
54
  const calculatedBlocksEntries = Object.entries(legacyJson.clientCalculatedBlocks)
55
+
56
+ /**
57
+ * Block name + properties for model lookup. Only runs neighbor/preflat work when
58
+ * `world.preflat` (legacy); modern block-state worlds use `fromStateId` only.
59
+ */
60
+ export function resolveBlockPropertiesForMeshing(
61
+ world: World | undefined,
62
+ cursor: Vec3,
63
+ blockProvider: WorldBlockProvider,
64
+ blockStateId: number,
65
+ PrismarineBlockCtor: { fromStateId: (id: number, biome: number) => Block }
66
+ ): { name: string, properties: Record<string, unknown> } {
67
+ if (world?.preflat) {
68
+ const block = world.getBlock(cursor, blockProvider, {})
69
+ if (block) {
70
+ let properties: Record<string, unknown> = { ...block.getProperties() }
71
+ const patch = preflatBlockCalculation(block, world, cursor)
72
+ if (patch) properties = { ...properties, ...patch }
73
+ return { name: block.name, properties }
74
+ }
75
+ }
76
+ const fromState = PrismarineBlockCtor.fromStateId(blockStateId, 1)
77
+ return { name: fromState.name, properties: fromState.getProperties() }
78
+ }
79
+
54
80
  export function preflatBlockCalculation(block: Block, world: World, position: Vec3) {
55
81
  const type = calculatedBlocksEntries.find(([name, blocks]) => blocks.includes(block.name))?.[0]
56
82
  if (!type) return
@@ -398,13 +424,7 @@ function renderElement(world: World, cursor: Vec3, element: BlockElement, doAO:
398
424
  // 10%
399
425
  const { smoothLighting, shadingTheme, cardinalLight } = world.config
400
426
  const faceLight = world.getLight(neighborPos, undefined, undefined, block.name)
401
- const sideShading = (shadingTheme === 'high-contrast')
402
- ? (0.8 + 0.5 * Math.max(0, 0.66 * dir[0] + 0.66 * dir[1] + 0.33 * dir[2])) // old directional light behavior
403
- : (
404
- cardinalLight === 'nether'
405
- ? (0.5 + Math.abs(0.1 * dir[0] + 0.4 * dir[1] + 0.3 * dir[2]))
406
- : (0.75 + 0.25 * dir[1] + 0.05 * (Math.abs(dir[2]) - 3 * Math.abs(dir[0])))
407
- )
427
+ const sideShading = getSideShading(dir, shadingTheme, cardinalLight)
408
428
  const baseLight = sideShading * faceLight / 15
409
429
  for (const pos of corners) {
410
430
  let vertex = [
@@ -478,10 +498,7 @@ function renderElement(world: World, cursor: Vec3, element: BlockElement, doAO:
478
498
  // TODO: correctly interpolate ao light based on pos (evaluate once for each corner of the block)
479
499
 
480
500
  const ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock))
481
- const ao_bias = (shadingTheme === 'high-contrast') ? 0.25 : 0.4
482
- const ao_scale = (shadingTheme === 'high-contrast') ? 0.25 : 0.2
483
- // todo light should go upper on lower blocks
484
- light = sideShading * (ao * ao_scale + ao_bias) * (cornerLightResult / 15)
501
+ light = vertexLightFromAo(ao, cornerLightResult, sideShading, shadingTheme)
485
502
  aos.push(ao)
486
503
 
487
504
  // Log AO and light for this corner (corner index is aos.length - 1)
@@ -0,0 +1,35 @@
1
+ //@ts-nocheck
2
+ import type { MesherConfig } from './shared'
3
+
4
+ export type FaceDirection = readonly [number, number, number]
5
+
6
+ /** Directional face darkening (matches legacy `renderElement` in models.ts). */
7
+ export function getSideShading(
8
+ dir: FaceDirection,
9
+ shadingTheme: MesherConfig['shadingTheme'],
10
+ cardinalLight: MesherConfig['cardinalLight']
11
+ ): number {
12
+ if (shadingTheme === 'high-contrast') {
13
+ return 0.8 + 0.5 * Math.max(0, 0.66 * dir[0] + 0.66 * dir[1] + 0.33 * dir[2])
14
+ }
15
+ if (cardinalLight === 'nether') {
16
+ return 0.5 + Math.abs(0.1 * dir[0] + 0.4 * dir[1] + 0.3 * dir[2])
17
+ }
18
+ return 0.75 + 0.25 * dir[1] + 0.05 * (Math.abs(dir[2]) - 3 * Math.abs(dir[0]))
19
+ }
20
+
21
+ /** Per-vertex brightness from AO (0–3) and corner light (0–15). */
22
+ export function vertexLightFromAo(
23
+ ao: number,
24
+ cornerLight15: number,
25
+ sideShading: number,
26
+ shadingTheme: MesherConfig['shadingTheme']
27
+ ): number {
28
+ const lightNorm = cornerLight15 / 15
29
+ if (shadingTheme === 'high-contrast') {
30
+ return sideShading * ((ao + 1) / 4) * lightNorm
31
+ }
32
+ const aoBias = 0.4
33
+ const aoScale = 0.2
34
+ return sideShading * (ao * aoScale + aoBias) * lightNorm
35
+ }
@@ -0,0 +1,77 @@
1
+ //@ts-nocheck
2
+ import {
3
+ CONSTANT_LONG_RENDER_FRACTION,
4
+ CONSTANT_LONG_RENDER_MIN_SAMPLES,
5
+ FAST_SCENE_WITHOUT_ENTITIES_MS,
6
+ HIGH_TEXTURE_COUNT,
7
+ LONG_RENDER_TIME_MS,
8
+ LOW_FPS_THRESHOLD,
9
+ RENDER_TIME_HISTORY_SIZE,
10
+ SLOW_ENTITIES_RENDER_MS,
11
+ } from './constants'
12
+ import type { FramePerformanceSample, PerformanceInstabilityFactors } from './types'
13
+
14
+ /**
15
+ * Tracks render/FPS signals and writes instability factors into reactive state
16
+ * (alongside `mesherWork`).
17
+ */
18
+ export class PerformanceMonitor {
19
+ private readonly renderTimeHistory: number[] = []
20
+
21
+ constructor(private readonly factors: PerformanceInstabilityFactors) {}
22
+
23
+ onFrame(sample: FramePerformanceSample): void {
24
+ this.pushRenderTime(sample.totalMs)
25
+ this.recompute(sample)
26
+ }
27
+
28
+ private pushRenderTime(ms: number): void {
29
+ this.renderTimeHistory.push(ms)
30
+ if (this.renderTimeHistory.length > RENDER_TIME_HISTORY_SIZE) {
31
+ this.renderTimeHistory.shift()
32
+ }
33
+ }
34
+
35
+ private recompute(sample: FramePerformanceSample): void {
36
+ const lowFps = sample.fps > 0 && sample.fps <= LOW_FPS_THRESHOLD
37
+ const sceneWithoutEntitiesMs = Math.max(0, sample.totalMs - sample.entitiesMs)
38
+
39
+ const longRenderTime = sample.totalMs >= LONG_RENDER_TIME_MS
40
+
41
+ const historyLen = this.renderTimeHistory.length
42
+ const longFrames = this.renderTimeHistory.filter(t => t >= LONG_RENDER_TIME_MS).length
43
+ const constantLongRenderTime =
44
+ historyLen >= CONSTANT_LONG_RENDER_MIN_SAMPLES &&
45
+ longFrames / historyLen >= CONSTANT_LONG_RENDER_FRACTION
46
+
47
+ const tooManyTextures = sample.loadedTextureCount >= HIGH_TEXTURE_COUNT
48
+
49
+ const tooManyEntities =
50
+ lowFps &&
51
+ sample.entitiesMs >= SLOW_ENTITIES_RENDER_MS &&
52
+ sceneWithoutEntitiesMs <= FAST_SCENE_WITHOUT_ENTITIES_MS
53
+
54
+ const hasKnownCause =
55
+ longRenderTime ||
56
+ constantLongRenderTime ||
57
+ tooManyEntities ||
58
+ tooManyTextures
59
+
60
+ const unknownReason = lowFps && !hasKnownCause
61
+
62
+ this.factors.longRenderTime = longRenderTime
63
+ this.factors.constantLongRenderTime = constantLongRenderTime
64
+ this.factors.tooManyEntities = tooManyEntities
65
+ this.factors.tooManyTextures = tooManyTextures
66
+ this.factors.unknownReason = unknownReason
67
+ }
68
+
69
+ reset(): void {
70
+ this.renderTimeHistory.length = 0
71
+ this.factors.longRenderTime = false
72
+ this.factors.constantLongRenderTime = false
73
+ this.factors.tooManyEntities = false
74
+ this.factors.tooManyTextures = false
75
+ this.factors.unknownReason = false
76
+ }
77
+ }
@@ -0,0 +1,24 @@
1
+ //@ts-nocheck
2
+ /** Recent frame exceeded this → `longRenderTime`. */
3
+ export const LONG_RENDER_TIME_MS = 30
4
+
5
+ /** Scene pass without entities faster than this → candidate for entity bottleneck. */
6
+ export const FAST_SCENE_WITHOUT_ENTITIES_MS = 20
7
+
8
+ /** Entity pass slower than this (with low FPS) → `tooManyEntities`. */
9
+ export const SLOW_ENTITIES_RENDER_MS = 8
10
+
11
+ /** FPS at or below this is treated as low performance. */
12
+ export const LOW_FPS_THRESHOLD = 45
13
+
14
+ /** Loaded WebGL textures at or above this → `tooManyTextures` (labels, signs, iOS). */
15
+ export const HIGH_TEXTURE_COUNT = 100
16
+
17
+ /** Ring buffer length for sustained render-time analysis. */
18
+ export const RENDER_TIME_HISTORY_SIZE = 24
19
+
20
+ /** Fraction of recent frames over `LONG_RENDER_TIME_MS` → `constantLongRenderTime`. */
21
+ export const CONSTANT_LONG_RENDER_FRACTION = 0.65
22
+
23
+ /** Minimum frames in history before `constantLongRenderTime` can trigger. */
24
+ export const CONSTANT_LONG_RENDER_MIN_SAMPLES = 8
@@ -0,0 +1,16 @@
1
+ //@ts-nocheck
2
+ import type { PerformanceInstabilityFactors } from './types'
3
+
4
+ const FACTOR_CODES: Array<{ key: keyof PerformanceInstabilityFactors, code: string }> = [
5
+ { key: 'longRenderTime', code: 'LR' },
6
+ { key: 'constantLongRenderTime', code: 'CLR' },
7
+ { key: 'tooManyEntities', code: 'ENT' },
8
+ { key: 'tooManyTextures', code: 'TEX' },
9
+ { key: 'unknownReason', code: 'UNK' },
10
+ ]
11
+
12
+ /** Compact debug overlay fragment, e.g. `LR+ENT` or empty string. */
13
+ export function formatPerformanceFactorsDebug(factors: PerformanceInstabilityFactors): string {
14
+ const active = FACTOR_CODES.filter(({ key }) => factors[key]).map(({ code }) => code)
15
+ return active.length > 0 ? active.join('+') : ''
16
+ }
@@ -0,0 +1,10 @@
1
+ //@ts-nocheck
2
+ export type { FramePerformanceSample, PerformanceInstabilityFactors } from './types'
3
+ export { defaultPerformanceInstabilityFactors } from './types'
4
+ export {
5
+ LONG_RENDER_TIME_MS,
6
+ LOW_FPS_THRESHOLD,
7
+ HIGH_TEXTURE_COUNT,
8
+ } from './constants'
9
+ export { PerformanceMonitor } from './PerformanceMonitor'
10
+ export { formatPerformanceFactorsDebug } from './formatPerformanceFactorsDebug'
@@ -0,0 +1,27 @@
1
+ //@ts-nocheck
2
+ /** Low-FPS / instability factors written to reactive renderer state. */
3
+ export interface PerformanceInstabilityFactors {
4
+ longRenderTime: boolean
5
+ constantLongRenderTime: boolean
6
+ tooManyEntities: boolean
7
+ tooManyTextures: boolean
8
+ unknownReason: boolean
9
+ }
10
+
11
+ export const defaultPerformanceInstabilityFactors = (): PerformanceInstabilityFactors => ({
12
+ longRenderTime: false,
13
+ constantLongRenderTime: false,
14
+ tooManyEntities: false,
15
+ tooManyTextures: false,
16
+ unknownReason: false,
17
+ })
18
+
19
+ export interface FramePerformanceSample {
20
+ /** Full `WorldRendererThree.render()` duration in ms. */
21
+ totalMs: number
22
+ /** Time spent in `entities.render()` this frame (0 if skipped). */
23
+ entitiesMs: number
24
+ loadedTextureCount: number
25
+ /** FPS from the last completed 1s window (0 before first sample). */
26
+ fps: number
27
+ }
@@ -58,7 +58,9 @@ export class RainModule implements RendererModuleController {
58
58
  }
59
59
 
60
60
  render?: (deltaTime: number) => void = (deltaTime) => {
61
- if (!this.enabled || !this.instancedMesh) return
61
+ if (!this.enabled || !this.instancedMesh || !this.material) return
62
+
63
+ this.syncMaterialToSceneFog()
62
64
 
63
65
  const cameraPos = this.worldRenderer.getCameraPosition()
64
66
  this.instancedMesh.position.set(0, 0, 0)
@@ -135,16 +137,32 @@ export class RainModule implements RendererModuleController {
135
137
  this.particles = []
136
138
  }
137
139
 
140
+ /** Match scene fog so rain fades with distance instead of a flat blue sheet. */
141
+ private syncMaterialToSceneFog(): void {
142
+ if (!this.material) return
143
+ const fog = this.worldRenderer.scene.fog
144
+ if (fog instanceof THREE.Fog || fog instanceof THREE.FogExp2) {
145
+ this.material.color.copy(fog.color)
146
+ } else {
147
+ this.material.color.set(0xcc_dd_ee)
148
+ }
149
+ this.material.fog = true
150
+ }
151
+
138
152
  private createRain(): void {
139
153
  this.geometry = new THREE.BoxGeometry(0.03, 0.3, 0.03)
140
154
  this.material = new THREE.MeshBasicMaterial({
141
- color: 0x44_66_99,
155
+ color: 0xcc_dd_ee,
142
156
  transparent: true,
143
- opacity: 0.6,
157
+ opacity: 0.35,
158
+ depthWrite: false,
159
+ fog: true,
144
160
  })
145
161
 
146
162
  this.instancedMesh = new THREE.InstancedMesh(this.geometry, this.material, PARTICLE_COUNT)
147
163
  this.instancedMesh.name = 'rain-particles'
164
+ this.instancedMesh.frustumCulled = false
165
+ this.syncMaterialToSceneFog()
148
166
 
149
167
  const dummy = new THREE.Matrix4()
150
168
  const position = new THREE.Vector3()
@@ -36,6 +36,7 @@ import { downloadWorldGeometry } from './worldGeometryExport'
36
36
  import { ChunkMeshManager } from './chunkMeshManager'
37
37
  import type { RendererModuleManifest, RegisteredModule, RendererModuleController } from './rendererModuleSystem'
38
38
  import { BUILTIN_MODULES } from './modules/index'
39
+ import { formatPerformanceFactorsDebug, PerformanceMonitor } from '../performanceMonitor'
39
40
 
40
41
  type SectionKey = string
41
42
 
@@ -57,6 +58,7 @@ export class WorldRendererThree extends WorldRendererCommon {
57
58
  ambientLight = new THREE.AmbientLight(0xcc_cc_cc)
58
59
  directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
59
60
  entities = new Entities(this, (globalThis as any).mcData)
61
+ performanceMonitor!: PerformanceMonitor
60
62
  cameraGroupVr?: THREE.Object3D
61
63
  material = new THREE.MeshBasicMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
62
64
  itemsTexture!: THREE.Texture
@@ -154,6 +156,8 @@ export class WorldRendererThree extends WorldRendererCommon {
154
156
  if (!displayOptions.resourcesManager) throw new Error('resourcesManager is required in displayOptions')
155
157
  super(displayOptions.resourcesManager, displayOptions, initOptions)
156
158
 
159
+ this.performanceMonitor = new PerformanceMonitor(this.reactiveState.world.instabilityFactors)
160
+
157
161
  this.renderer = renderer
158
162
  displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...'
159
163
 
@@ -713,7 +717,10 @@ export class WorldRendererThree extends WorldRendererCommon {
713
717
  text += `B: ${formatCompact(this.blocksRendered)} `
714
718
  text += `MEM: ${this.chunkMeshManager.getEstimatedMemoryUsage().total} `
715
719
  const poolStats = this.chunkMeshManager.getStats()
716
- text += `POOL: ${poolStats.activeCount}/${poolStats.poolSize}`
720
+ text += `POOL: ${poolStats.activeCount}/${poolStats.poolSize} `
721
+ const pf = formatPerformanceFactorsDebug(this.reactiveState.world.instabilityFactors)
722
+ if (pf) text += `PF: ${pf} `
723
+ // entities can be seen in F3
717
724
  pane.updateText(text)
718
725
  this.backendInfoReport = text
719
726
  }
@@ -1201,8 +1208,11 @@ export class WorldRendererThree extends WorldRendererCommon {
1201
1208
  this.camera.updateProjectionMatrix()
1202
1209
  }
1203
1210
 
1211
+ let entitiesRenderMs = 0
1204
1212
  if (!this.reactiveDebugParams.disableEntities) {
1213
+ const entitiesStart = performance.now()
1205
1214
  this.entities.render()
1215
+ entitiesRenderMs = performance.now() - entitiesStart
1206
1216
  }
1207
1217
 
1208
1218
  // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
@@ -1243,6 +1253,13 @@ export class WorldRendererThree extends WorldRendererCommon {
1243
1253
  this.renderTimeAvgCount++
1244
1254
  this.renderTimeAvg = ((this.renderTimeAvg * (this.renderTimeAvgCount - 1)) + totalTime) / this.renderTimeAvgCount
1245
1255
  this.renderTimeMax = Math.max(this.renderTimeMax, totalTime)
1256
+
1257
+ this.performanceMonitor.onFrame({
1258
+ totalMs: totalTime,
1259
+ entitiesMs: entitiesRenderMs,
1260
+ loadedTextureCount: this.renderer.info.memory.textures,
1261
+ fps: this.lastFps,
1262
+ })
1246
1263
  }
1247
1264
 
1248
1265
  renderHead(position: Vec3, rotation: number, isWall: boolean, blockEntity) {
@@ -1450,6 +1467,7 @@ export class WorldRendererThree extends WorldRendererCommon {
1450
1467
  }
1451
1468
 
1452
1469
  destroy(): void {
1470
+ this.performanceMonitor?.reset()
1453
1471
  this.pendingSectionUpdates.clear()
1454
1472
  this.pendingSectionBufferStartTimes.clear()
1455
1473
  this.chunkMeshManager.dispose()
@@ -13,6 +13,8 @@ import { elemFaces, buildRotationMatrix, matmul3, matmulmat3, vecadd3, vecsub3 }
13
13
  import type { ExportedWorldGeometry, ExportedSection } from '../../three/worldGeometryExport'
14
14
  import type { MesherGeometryOutput } from '../../mesher-shared/shared'
15
15
  import type { World } from '../../mesher-shared/world'
16
+ import { resolveBlockPropertiesForMeshing } from '../../mesher-shared/models'
17
+ import { getSideShading, vertexLightFromAo } from '../../mesher-shared/vertexShading'
16
18
 
17
19
  // Handle both default and named export
18
20
  const worldBlockProvider = (worldBlockProviderModule as any).default || worldBlockProviderModule
@@ -134,6 +136,18 @@ export function extractColumnHeightmap(
134
136
  return out
135
137
  }
136
138
 
139
+ function computeMesherVertexLight(
140
+ world: World | undefined,
141
+ ao: number,
142
+ cornerLight15: number,
143
+ faceDir: [number, number, number]
144
+ ): number {
145
+ const shadingTheme = world?.config.shadingTheme ?? 'high-contrast'
146
+ const cardinalLight = world?.config.cardinalLight ?? 'default'
147
+ const sideShading = getSideShading(faceDir, shadingTheme, cardinalLight)
148
+ return vertexLightFromAo(ao, cornerLight15, sideShading, shadingTheme)
149
+ }
150
+
137
151
  /**
138
152
  * Get or create cached block model with precomputed matrices
139
153
  */
@@ -141,10 +155,32 @@ function getCachedBlockModel(
141
155
  blockStateId: number,
142
156
  version: string,
143
157
  blockProvider: WorldBlockProvider,
144
- PrismarineBlock: any
158
+ PrismarineBlock: any,
159
+ world?: World,
160
+ blockPos?: { x: number, y: number, z: number }
145
161
  ): CachedBlockModel | null {
146
- // Use a module-level cache
147
- const cacheKey = `${version}:${blockStateId}`
162
+ const usePreflat = !!(world?.preflat && blockPos)
163
+ let blockName: string
164
+ let blockProps: Record<string, unknown>
165
+ if (usePreflat) {
166
+ const resolved = resolveBlockPropertiesForMeshing(
167
+ world,
168
+ new Vec3(blockPos!.x, blockPos!.y, blockPos!.z),
169
+ blockProvider,
170
+ blockStateId,
171
+ PrismarineBlock
172
+ )
173
+ blockName = resolved.name
174
+ blockProps = resolved.properties
175
+ } else {
176
+ const blockObj = PrismarineBlock.fromStateId(blockStateId, 1)
177
+ blockName = blockObj.name
178
+ blockProps = blockObj.getProperties()
179
+ }
180
+
181
+ const cacheKey = usePreflat
182
+ ? `${version}:${blockStateId}:${blockName}:${JSON.stringify(blockProps)}`
183
+ : `${version}:${blockStateId}`
148
184
  if (!(globalThis as any).__wasmBlockModelCache) {
149
185
  (globalThis as any).__wasmBlockModelCache = new Map()
150
186
  }
@@ -156,11 +192,13 @@ function getCachedBlockModel(
156
192
 
157
193
  try {
158
194
  const blockObj = PrismarineBlock.fromStateId(blockStateId, 1)
159
- const blockName = blockObj.name
160
- const blockProps = blockObj.getProperties()
195
+ if (!usePreflat) {
196
+ blockName = blockObj.name
197
+ blockProps = blockObj.getProperties()
198
+ }
161
199
 
162
200
  const models = blockProvider.getAllResolvedModels0_1(
163
- { name: blockName, properties: blockProps },
201
+ { name: blockName, properties: blockProps as Record<string, string | number | boolean> },
164
202
  false
165
203
  )
166
204
 
@@ -505,7 +543,14 @@ export function renderWasmOutputToGeometry(
505
543
  }
506
544
  }
507
545
 
508
- const cachedModel = getCachedBlockModel(blockStateId, version, blockProvider, PrismarineBlock)
546
+ const cachedModel = getCachedBlockModel(
547
+ blockStateId,
548
+ version,
549
+ blockProvider,
550
+ PrismarineBlock,
551
+ world,
552
+ { x: bx, y: by, z: bz }
553
+ )
509
554
  if (!cachedModel) continue
510
555
 
511
556
  if (false) {
@@ -654,10 +699,9 @@ export function renderWasmOutputToGeometry(
654
699
  // But WASM light calculation seems to return 0.0, so we need to handle that
655
700
  // In the test case, TypeScript gets baseLight = 1.0 (full brightness)
656
701
  // So we should use 1.0 as the base light value when WASM returns 0
657
- const baseLight = lightValues[cornerIdx]
658
- const cornerLightResult = baseLight * 15
659
-
660
- const light = (ao + 1) / 4 * (cornerLightResult / 15)
702
+ const cornerLight15 = (lightValues[cornerIdx] ?? 1) * 15
703
+ const faceDir = transformedDir as [number, number, number]
704
+ const light = computeMesherVertexLight(world, ao, cornerLight15, faceDir)
661
705
 
662
706
  colors.push(tint[0] * light, tint[1] * light, tint[2] * light)
663
707
 
@@ -907,7 +951,8 @@ export function renderWasmOutputToGeometry(
907
951
  }
908
952
 
909
953
  if (doAO) {
910
- light = (ao + 1) / 4 * (cornerLightResult / 15)
954
+ const faceDir = transformedDirI as [number, number, number]
955
+ light = computeMesherVertexLight(world, ao, cornerLightResult, faceDir)
911
956
  }
912
957
 
913
958
  colors.push(tint[0] * light!, tint[1] * light!, tint[2] * light!)