minecraft-renderer 0.1.58 → 0.1.59

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.58",
3
+ "version": "0.1.59",
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,14 +4,16 @@ import * as THREE from 'three'
4
4
  import * as nbt from 'prismarine-nbt'
5
5
  import { Vec3 } from 'vec3'
6
6
  import { MesherGeometryOutput } from '../mesher-shared/shared'
7
- import { getShaderCubeResources } from '../wasm-mesher/bridge/shaderCubeBridge'
7
+ import { getShaderCubeResources, SHADER_CUBES_WORDS_PER_FACE } from '../wasm-mesher/bridge/shaderCubeBridge'
8
8
  import { createCubeBlockMaterial } from './shaders/cubeBlockShader'
9
9
  import { createShaderCubeMesh, disposeShaderCubeMesh } from './shaderCubeMesh'
10
10
  import { GlobalBlockBuffer } from './globalBlockBuffer'
11
11
  import {
12
12
  computeShaderSectionRaycastAabb,
13
+ isPointInsideAabb,
13
14
  raycastAabb,
14
- type ShaderSectionRaycastBox,
15
+ raycastShaderBlocksAabb,
16
+ type ShaderSectionRaycastEntry,
15
17
  } from './sectionRaycastAabb'
16
18
  import { chunkPos } from '../lib/simpleUtils'
17
19
  import { renderSign } from '../sign-renderer'
@@ -99,8 +101,11 @@ export class ChunkMeshManager {
99
101
  private cubeShaderMaterial: THREE.ShaderMaterial | null = null
100
102
  /** One instanced mesh for all shader-cube faces (single draw call). */
101
103
  globalBlockBuffer: GlobalBlockBuffer | null = null
102
- /** Tight world AABBs for third-person raycast; no per-section Object3D. */
103
- private readonly shaderSectionRaycastBoxes = new Map<string, ShaderSectionRaycastBox>()
104
+ /** Tight world AABBs for third-person raycast; block word0 read from GlobalBlockBuffer or deferred. */
105
+ private readonly shaderSectionRaycastBoxes = new Map<string, ShaderSectionRaycastEntry>()
106
+ /** Per-raycast block dedup; safe while the eye is inside at most one section aggregate AABB per call. */
107
+ private readonly blockRaycastVisitGen = new Uint16Array(4096)
108
+ private blockRaycastVisitStamp = 1
104
109
 
105
110
  // Performance tracking
106
111
  private hits = 0
@@ -256,7 +261,12 @@ export class ChunkMeshManager {
256
261
  ): void {
257
262
  const box = computeShaderSectionRaycastAabb(words, faceCount, sectionCenterX, sectionCenterY, sectionCenterZ)
258
263
  if (box) {
259
- this.shaderSectionRaycastBoxes.set(sectionKey, box)
264
+ this.shaderSectionRaycastBoxes.set(sectionKey, {
265
+ box,
266
+ sectionCenterX,
267
+ sectionCenterY,
268
+ sectionCenterZ,
269
+ })
260
270
  } else {
261
271
  this.shaderSectionRaycastBoxes.delete(sectionKey)
262
272
  }
@@ -284,20 +294,63 @@ export class ChunkMeshManager {
284
294
  let closest = maxDist
285
295
  let found = false
286
296
 
287
- for (const [key, box] of this.shaderSectionRaycastBoxes) {
297
+ this.blockRaycastVisitStamp++
298
+ if (this.blockRaycastVisitStamp >= 65535) {
299
+ this.blockRaycastVisitGen.fill(0)
300
+ this.blockRaycastVisitStamp = 1
301
+ }
302
+
303
+ for (const [key, entry] of this.shaderSectionRaycastBoxes) {
288
304
  const section = this.sectionObjects[key]
289
305
  if (section && !section.visible) continue
290
306
 
307
+ const { box } = entry
291
308
  const dcx = box.cx - ox
292
309
  const dcy = box.cy - oy
293
310
  const dcz = box.cz - oz
294
311
  if (dcx * dcx + dcy * dcy + dcz * dcz > maxCenterDistSq) continue
295
312
 
296
- const t = raycastAabb(
313
+ let t = raycastAabb(
297
314
  ox, oy, oz, dx, dy, dz,
298
315
  box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ,
299
316
  closest,
300
317
  )
318
+ if (t === undefined && isPointInsideAabb(ox, oy, oz, box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) {
319
+ const gb = this.globalBlockBuffer
320
+ const slot = gb?.getSectionSlot(key)
321
+ if (gb && slot) {
322
+ t = raycastShaderBlocksAabb(
323
+ gb.getW0(),
324
+ slot.start,
325
+ slot.count,
326
+ 1,
327
+ entry.sectionCenterX,
328
+ entry.sectionCenterY,
329
+ entry.sectionCenterZ,
330
+ ox, oy, oz, dx, dy, dz,
331
+ closest,
332
+ this.blockRaycastVisitGen,
333
+ this.blockRaycastVisitStamp,
334
+ )
335
+ } else {
336
+ const def = this.sectionObjects[key]?.deferredShaderCubes
337
+ if (def) {
338
+ t = raycastShaderBlocksAabb(
339
+ def.words,
340
+ 0,
341
+ def.count,
342
+ SHADER_CUBES_WORDS_PER_FACE,
343
+ entry.sectionCenterX,
344
+ entry.sectionCenterY,
345
+ entry.sectionCenterZ,
346
+ ox, oy, oz, dx, dy, dz,
347
+ closest,
348
+ this.blockRaycastVisitGen,
349
+ this.blockRaycastVisitStamp,
350
+ )
351
+ }
352
+ }
353
+ }
301
354
  if (t !== undefined && t < closest) {
302
355
  closest = t
303
356
  found = true
@@ -106,6 +106,15 @@ export class GlobalBlockBuffer {
106
106
  return this.sectionSlots.has(sectionKey)
107
107
  }
108
108
 
109
+ getSectionSlot (sectionKey: string): { start: number, count: number } | undefined {
110
+ return this.sectionSlots.get(sectionKey)
111
+ }
112
+
113
+ /** Fetch fresh each raycast — growCapacity reallocates the backing array. */
114
+ getW0 (): Uint32Array {
115
+ return this.w0
116
+ }
117
+
109
118
  /** Copy live GPU words and remove the section (sci-fi reveal hide / restore). */
110
119
  takeSectionData (sectionKey: string): GlobalBlockBufferShaderData | undefined {
111
120
  const slot = this.sectionSlots.get(sectionKey)
@@ -14,6 +14,13 @@ export type ShaderSectionRaycastBox = {
14
14
  cz: number
15
15
  }
16
16
 
17
+ export type ShaderSectionRaycastEntry = {
18
+ box: ShaderSectionRaycastBox
19
+ sectionCenterX: number
20
+ sectionCenterY: number
21
+ sectionCenterZ: number
22
+ }
23
+
17
24
  /**
18
25
  * Tight world-space AABB covering occupied shader-cube blocks in a section.
19
26
  * `sectionCenter*` is geometryData.sx/sy/sz (section base + 8).
@@ -74,7 +81,7 @@ export function computeShaderSectionRaycastAabb (
74
81
  }
75
82
  }
76
83
 
77
- function isPointInsideAabb (
84
+ export function isPointInsideAabb (
78
85
  ox: number,
79
86
  oy: number,
80
87
  oz: number,
@@ -150,6 +157,99 @@ export function raycastAabb (
150
157
  return tmin <= tmax && tmin >= 0 ? tmin : undefined
151
158
  }
152
159
 
160
+ /** Ray origin inside AABB: distance to exit face along the ray. */
161
+ export function raycastAabbFromInside (
162
+ ox: number,
163
+ oy: number,
164
+ oz: number,
165
+ dx: number,
166
+ dy: number,
167
+ dz: number,
168
+ minX: number,
169
+ minY: number,
170
+ minZ: number,
171
+ maxX: number,
172
+ maxY: number,
173
+ maxZ: number,
174
+ maxDist: number,
175
+ ): number | undefined {
176
+ let tExit = maxDist
177
+
178
+ if (Math.abs(dx) >= 1e-8) {
179
+ const t = dx > 0 ? (maxX - ox) / dx : (minX - ox) / dx
180
+ if (t > 1e-6) tExit = Math.min(tExit, t)
181
+ } else if (ox < minX || ox > maxX) return undefined
182
+
183
+ if (Math.abs(dy) >= 1e-8) {
184
+ const t = dy > 0 ? (maxY - oy) / dy : (minY - oy) / dy
185
+ if (t > 1e-6) tExit = Math.min(tExit, t)
186
+ } else if (oy < minY || oy > maxY) return undefined
187
+
188
+ if (Math.abs(dz) >= 1e-8) {
189
+ const t = dz > 0 ? (maxZ - oz) / dz : (minZ - oz) / dz
190
+ if (t > 1e-6) tExit = Math.min(tExit, t)
191
+ } else if (oz < minZ || oz > maxZ) return undefined
192
+
193
+ return tExit <= maxDist ? tExit : undefined
194
+ }
195
+
196
+ /** Per-block raycast; `word0Stride` 1 = GlobalBlockBuffer SoA, 4 = deferred AoS. */
197
+ export function raycastShaderBlocksAabb (
198
+ w0Source: Uint32Array,
199
+ start: number,
200
+ faceCount: number,
201
+ word0Stride: number,
202
+ sectionCenterX: number,
203
+ sectionCenterY: number,
204
+ sectionCenterZ: number,
205
+ ox: number,
206
+ oy: number,
207
+ oz: number,
208
+ dx: number,
209
+ dy: number,
210
+ dz: number,
211
+ maxDist: number,
212
+ visitGen: Uint16Array,
213
+ visitStamp: number,
214
+ ): number | undefined {
215
+ const baseX = sectionCenterX - 8
216
+ const baseY = sectionCenterY - 8
217
+ const baseZ = sectionCenterZ - 8
218
+
219
+ let closest = maxDist
220
+ let found = false
221
+
222
+ for (let i = 0; i < faceCount; i++) {
223
+ const w0 = w0Source[start + i * word0Stride]!
224
+ const lx = w0 & ((1 << WORD0.LX_BITS) - 1)
225
+ const ly = (w0 >> WORD0.LY_SHIFT) & ((1 << WORD0.LY_BITS) - 1)
226
+ const lz = (w0 >> WORD0.LZ_SHIFT) & ((1 << WORD0.LZ_BITS) - 1)
227
+ const visitIdx = lx + (ly << 4) + (lz << 8)
228
+ if (visitGen[visitIdx] === visitStamp) continue
229
+ visitGen[visitIdx] = visitStamp
230
+
231
+ const minX = baseX + lx
232
+ const minY = baseY + ly
233
+ const minZ = baseZ + lz
234
+ const maxX = minX + 1
235
+ const maxY = minY + 1
236
+ const maxZ = minZ + 1
237
+
238
+ let t: number | undefined
239
+ if (isPointInsideAabb(ox, oy, oz, minX, minY, minZ, maxX, maxY, maxZ)) {
240
+ t = raycastAabbFromInside(ox, oy, oz, dx, dy, dz, minX, minY, minZ, maxX, maxY, maxZ, closest)
241
+ } else {
242
+ t = raycastAabb(ox, oy, oz, dx, dy, dz, minX, minY, minZ, maxX, maxY, maxZ, closest)
243
+ }
244
+ if (t !== undefined && t < closest) {
245
+ closest = t
246
+ found = true
247
+ }
248
+ }
249
+
250
+ return found ? closest : undefined
251
+ }
252
+
153
253
  /** 16³ section box centered at (cx, cy, cz) — tests / legacy helper. */
154
254
  export function raycastSectionAabb (
155
255
  ox: number,
@@ -3,6 +3,7 @@ import { test, expect } from 'vitest'
3
3
  import {
4
4
  computeShaderSectionRaycastAabb,
5
5
  raycastAabb,
6
+ raycastShaderBlocksAabb,
6
7
  raycastSectionAabb,
7
8
  } from '../../three/sectionRaycastAabb'
8
9
  import { SHADER_CUBES_WORDS_PER_FACE } from '../bridge/shaderCubeBridge'
@@ -46,6 +47,62 @@ test('raycastAabb: origin inside box is ignored', () => {
46
47
  expect(t).toBeUndefined()
47
48
  })
48
49
 
50
+ test('raycastShaderBlocksAabb: hits wall block when origin is inside section aggregate AABB', () => {
51
+ const words = new Uint32Array(SHADER_CUBES_WORDS_PER_FACE * 3)
52
+ words[0] = 8 | (4 << WORD0.LY_SHIFT) | (8 << WORD0.LZ_SHIFT)
53
+ words[4] = 8 | (5 << WORD0.LY_SHIFT) | (8 << WORD0.LZ_SHIFT)
54
+ words[8] = 8 | (6 << WORD0.LY_SHIFT) | (8 << WORD0.LZ_SHIFT)
55
+ const visitGen = new Uint16Array(4096)
56
+ const visitStamp = 1
57
+ const t = raycastShaderBlocksAabb(words, 0, 3, SHADER_CUBES_WORDS_PER_FACE, 8, 8, 8, 4.5, 5.5, 8.5, 1, 0, 0, 10, visitGen, visitStamp)!
58
+ expect(t).toBeGreaterThan(0)
59
+ expect(t).toBeLessThan(4)
60
+ })
61
+
62
+ test('raycastShaderBlocksAabb: ray into empty space does not hit', () => {
63
+ const words = new Uint32Array(SHADER_CUBES_WORDS_PER_FACE)
64
+ words[0] = 8 | (4 << WORD0.LY_SHIFT) | (8 << WORD0.LZ_SHIFT)
65
+ const visitGen = new Uint16Array(4096)
66
+ const visitStamp = 1
67
+ const t = raycastShaderBlocksAabb(words, 0, 1, SHADER_CUBES_WORDS_PER_FACE, 8, 8, 8, 4.5, 5.5, 8.5, -1, 0, 0, 10, visitGen, visitStamp)
68
+ expect(t).toBeUndefined()
69
+ })
70
+
71
+ test('raycastShaderBlocksAabb: eye inside solid block uses exit distance', () => {
72
+ const lx = 5
73
+ const ly = 3
74
+ const lz = 7
75
+ const sectionCenterX = 8
76
+ const sectionCenterY = 8
77
+ const sectionCenterZ = 8
78
+ const words = new Uint32Array(SHADER_CUBES_WORDS_PER_FACE)
79
+ words[0] = lx | (ly << WORD0.LY_SHIFT) | (lz << WORD0.LZ_SHIFT)
80
+ const visitGen = new Uint16Array(4096)
81
+ const visitStamp = 1
82
+ const ox = sectionCenterX - 8 + lx + 0.5
83
+ const oy = sectionCenterY - 8 + ly + 0.5
84
+ const oz = sectionCenterZ - 8 + lz + 0.5
85
+ const t = raycastShaderBlocksAabb(
86
+ words, 0, 1, SHADER_CUBES_WORDS_PER_FACE,
87
+ sectionCenterX, sectionCenterY, sectionCenterZ,
88
+ ox, oy, oz, 0, 0, 1, 10, visitGen, visitStamp,
89
+ )!
90
+ expect(t).toBeGreaterThan(0)
91
+ expect(t).toBeLessThanOrEqual(10)
92
+ expect(t).toBeCloseTo(0.5, 5)
93
+ })
94
+
95
+ test('raycastShaderBlocksAabb: SoA stride-1 layout (GlobalBlockBuffer style)', () => {
96
+ const w0 = new Uint32Array(2)
97
+ w0[0] = 8 | (4 << WORD0.LY_SHIFT) | (8 << WORD0.LZ_SHIFT)
98
+ w0[1] = 8 | (5 << WORD0.LY_SHIFT) | (8 << WORD0.LZ_SHIFT)
99
+ const visitGen = new Uint16Array(4096)
100
+ const visitStamp = 1
101
+ const t = raycastShaderBlocksAabb(w0, 0, 2, 1, 8, 8, 8, 4.5, 5.5, 8.5, 1, 0, 0, 10, visitGen, visitStamp)!
102
+ expect(t).toBeGreaterThan(0)
103
+ expect(t).toBeLessThan(4)
104
+ })
105
+
49
106
  test('raycastAabb: narrow floor slab blocks downward ray', () => {
50
107
  const words = new Uint32Array(SHADER_CUBES_WORDS_PER_FACE * 2)
51
108
  words[0] = 4 | (0 << WORD0.LY_SHIFT) | (4 << WORD0.LZ_SHIFT)