minecraft-renderer 0.1.80 → 0.1.81

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.80",
3
+ "version": "0.1.81",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,6 +32,7 @@ import { generateSpiralMatrix } from './spiral'
32
32
  import { PlayerStateReactive } from '../playerState/playerState'
33
33
  import { IndexedData } from 'minecraft-data'
34
34
  import { WorldRendererConfig } from '../graphicsBackend/config'
35
+ import { CameraCollisionBlockCache } from '../three/cameraCollisionBlockCache'
35
36
 
36
37
  function mod(x, n) {
37
38
  return ((x % n) + n) % n
@@ -43,6 +44,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
43
44
  timeOfTheDay = 0
44
45
  lastMesherSkyLight = 15
45
46
  worldSizeParams = { minY: 0, worldHeight: 256 }
47
+ /** Block columns for third-person voxel DDA (renderer thread). */
48
+ cameraCollisionBlockCache: CameraCollisionBlockCache
46
49
  reactiveDebugParams = proxy({
47
50
  stopRendering: false,
48
51
  chunksRenderAboveOverride: undefined as number | undefined,
@@ -216,6 +219,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
216
219
  public displayOptions: DisplayWorldOptions,
217
220
  public initOptions: GraphicsInitOptions
218
221
  ) {
222
+ this.cameraCollisionBlockCache = new CameraCollisionBlockCache(this.displayOptions.version)
219
223
  this.snapshotInitialValues()
220
224
  this.worldRendererConfig = displayOptions.inWorldRenderingConfig
221
225
  this.playerStateReactive = displayOptions.playerStateReactive!
@@ -892,6 +896,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
892
896
  `-> chunk ${JSON.stringify({ x, z, chunkLength: chunk.length, customBlockModelsLength: customBlockModels ? Object.keys(customBlockModels).length : 0 })}`
893
897
  )
894
898
  this.mesherLogReader?.chunkReceived(x, z, chunk.length)
899
+ this.cameraCollisionBlockCache.ingestColumn(x, z, chunk)
895
900
  const sectionHeight = this.getSectionHeight()
896
901
  const CHUNK_SIZE = 16
897
902
 
@@ -960,6 +965,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
960
965
  delete this.finishedSections[sectionKey]
961
966
  }
962
967
  this.highestBlocksByChunks.delete(`${x},${z}`)
968
+ this.cameraCollisionBlockCache.removeColumn(x, z)
963
969
  const heightmapKey = `${Math.floor(x / 16)},${Math.floor(z / 16)}`
964
970
  delete this.reactiveState.world.heightmaps[heightmapKey]
965
971
 
@@ -1036,6 +1042,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1036
1042
  'loadChunk',
1037
1043
  ({ x, z, chunk, worldConfig, isLightUpdate }) => {
1038
1044
  this.worldSizeParams = worldConfig
1045
+ this.cameraCollisionBlockCache.setWorldBounds(worldConfig.minY, worldConfig.worldHeight)
1039
1046
  this.queuedChunks.add(`${x},${z}`)
1040
1047
  const args = [x, z, chunk, isLightUpdate]
1041
1048
  if (!currentLoadChunkBatch) {
@@ -1139,6 +1146,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1139
1146
  worldEmitter,
1140
1147
  'onWorldSwitch',
1141
1148
  () => {
1149
+ this.cameraCollisionBlockCache.clear()
1142
1150
  for (const fn of this.onWorldSwitched) {
1143
1151
  try {
1144
1152
  fn()
@@ -1229,6 +1237,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1229
1237
  }
1230
1238
  }
1231
1239
  this.logWorkerWork(`-> blockUpdate ${JSON.stringify({ pos, stateId, customBlockModels })}`)
1240
+ if (stateId !== undefined) {
1241
+ this.cameraCollisionBlockCache.setBlockStateId(pos.x, pos.y, pos.z, stateId)
1242
+ }
1232
1243
  this.setSectionDirty(pos, true, true)
1233
1244
  if (this.neighborChunkUpdates) {
1234
1245
  const CHUNK_SIZE = 16
@@ -0,0 +1,156 @@
1
+ //@ts-nocheck
2
+ import Chunks from 'prismarine-chunk'
3
+ import { Vec3 } from 'vec3'
4
+ import { getCameraCollisionSolidityTable, isStateIdSolidForCameraCollision } from './cameraCollisionSolidity'
5
+
6
+ /** 16³ blocks per section; 1 bit per block. */
7
+ const WORDS_PER_SECTION = 128
8
+ export const CAMERA_COLLISION_BYTES_PER_SECTION = WORDS_PER_SECTION * 4
9
+
10
+ type SectionBitset = Uint32Array
11
+
12
+ const sectionKey = (sx: number, sy: number, sz: number) => `${sx},${sy},${sz}`
13
+
14
+ const alignSection = (coord: number) => Math.floor(coord / 16) * 16
15
+
16
+ const localCoord = (coord: number) => ((coord % 16) + 16) % 16
17
+
18
+ const blockBitIndex = (lx: number, ly: number, lz: number) => lx + lz * 16 + ly * 256
19
+
20
+ const getBit = (bits: SectionBitset, idx: number) => {
21
+ const word = idx >>> 5
22
+ return (bits[word]! & (1 << (idx & 31))) !== 0
23
+ }
24
+
25
+ const setBit = (bits: SectionBitset, idx: number, value: boolean) => {
26
+ const word = idx >>> 5
27
+ const mask = 1 << (idx & 31)
28
+ if (value) bits[word]! |= mask
29
+ else bits[word]! &= ~mask
30
+ }
31
+
32
+ const isBitsetEmpty = (bits: SectionBitset) => {
33
+ for (let i = 0; i < WORDS_PER_SECTION; i++) {
34
+ if (bits[i]!) return false
35
+ }
36
+ return true
37
+ }
38
+
39
+ /**
40
+ * Sparse 1-bit-per-block solidity cache for third-person voxel DDA.
41
+ * Allocates a 512-byte section bitset only when it contains ≥1 solid block.
42
+ */
43
+ export class CameraCollisionBlockCache {
44
+ private readonly sections = new Map<string, SectionBitset>()
45
+ private Chunk: ReturnType<typeof Chunks>
46
+ private solidityTable: Uint8Array
47
+ private worldMinY = 0
48
+ private worldMaxY = 256
49
+
50
+ constructor(version: string) {
51
+ this.Chunk = Chunks(version) as ReturnType<typeof Chunks>
52
+ this.solidityTable = getCameraCollisionSolidityTable(version)
53
+ }
54
+
55
+ setVersion(version: string) {
56
+ this.Chunk = Chunks(version) as ReturnType<typeof Chunks>
57
+ this.solidityTable = getCameraCollisionSolidityTable(version)
58
+ this.sections.clear()
59
+ }
60
+
61
+ setWorldBounds(minY: number, worldHeight: number) {
62
+ if (this.worldMinY === minY && this.worldMaxY === worldHeight) return
63
+ this.worldMinY = minY
64
+ this.worldMaxY = worldHeight
65
+ this.sections.clear()
66
+ }
67
+
68
+ clear() {
69
+ this.sections.clear()
70
+ }
71
+
72
+ /** Allocated 16³ section bitsets (512 bytes each). */
73
+ getAllocatedSectionCount(): number {
74
+ return this.sections.size
75
+ }
76
+
77
+ getAllocatedBytes(): number {
78
+ return this.sections.size * CAMERA_COLLISION_BYTES_PER_SECTION
79
+ }
80
+
81
+ removeColumn(chunkX: number, chunkZ: number) {
82
+ const syStart = alignSection(this.worldMinY)
83
+ for (let sy = syStart; sy < this.worldMaxY; sy += 16) {
84
+ this.sections.delete(sectionKey(chunkX, sy, chunkZ))
85
+ }
86
+ }
87
+
88
+ ingestColumn(chunkX: number, chunkZ: number, chunkJson: unknown) {
89
+ const chunk = this.Chunk.fromJson(chunkJson as Parameters<ReturnType<typeof Chunks>['fromJson']>[0]) as unknown as {
90
+ getBlockStateId: (pos: Vec3) => number
91
+ }
92
+ const pos = new Vec3(0, 0, 0)
93
+
94
+ for (let y = this.worldMinY; y < this.worldMaxY; y++) {
95
+ pos.y = y
96
+ const sy = alignSection(y)
97
+ const ly = y - sy
98
+ for (let lz = 0; lz < 16; lz++) {
99
+ pos.z = lz
100
+ const wz = chunkZ + lz
101
+ const sz = alignSection(wz)
102
+ const lzLocal = wz - sz
103
+ for (let lx = 0; lx < 16; lx++) {
104
+ pos.x = lx
105
+ const stateId = chunk.getBlockStateId(pos) || 0
106
+ if (!isStateIdSolidForCameraCollision(stateId, this.solidityTable)) continue
107
+
108
+ const wx = chunkX + lx
109
+ const sx = alignSection(wx)
110
+ const key = sectionKey(sx, sy, sz)
111
+ let bits = this.sections.get(key)
112
+ if (!bits) {
113
+ bits = new Uint32Array(WORDS_PER_SECTION)
114
+ this.sections.set(key, bits)
115
+ }
116
+ setBit(bits, blockBitIndex(wx - sx, ly, lzLocal), true)
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ setBlockStateId(x: number, y: number, z: number, stateId: number) {
123
+ if (y < this.worldMinY || y >= this.worldMaxY) return
124
+
125
+ const sx = alignSection(x)
126
+ const sy = alignSection(y)
127
+ const sz = alignSection(z)
128
+ const key = sectionKey(sx, sy, sz)
129
+ const idx = blockBitIndex(localCoord(x), y - sy, localCoord(z))
130
+ const solid = isStateIdSolidForCameraCollision(stateId, this.solidityTable)
131
+
132
+ if (solid) {
133
+ let bits = this.sections.get(key)
134
+ if (!bits) {
135
+ bits = new Uint32Array(WORDS_PER_SECTION)
136
+ this.sections.set(key, bits)
137
+ }
138
+ setBit(bits, idx, true)
139
+ return
140
+ }
141
+
142
+ const bits = this.sections.get(key)
143
+ if (!bits) return
144
+ setBit(bits, idx, false)
145
+ if (isBitsetEmpty(bits)) this.sections.delete(key)
146
+ }
147
+
148
+ isSolidBlock(x: number, y: number, z: number): boolean {
149
+ if (y < this.worldMinY || y >= this.worldMaxY) return false
150
+
151
+ const bits = this.sections.get(sectionKey(alignSection(x), alignSection(y), alignSection(z)))
152
+ if (!bits) return false
153
+
154
+ return getBit(bits, blockBitIndex(localCoord(x), y - alignSection(y), localCoord(z)))
155
+ }
156
+ }
@@ -0,0 +1,50 @@
1
+ //@ts-nocheck
2
+ import MinecraftData from 'minecraft-data'
3
+ import PrismarineBlockLoader from 'prismarine-block'
4
+ import moreBlockDataGeneratedJson from '../lib/moreBlockDataGenerated.json'
5
+
6
+ const solidityCache = new Map<string, Uint8Array>()
7
+
8
+ const PASS_THROUGH_NAMES = new Set(['water', 'flowing_water', 'lava', 'flowing_lava', 'grass', 'short_grass', 'tall_grass'])
9
+
10
+ /** 1 = blocks third-person camera DDA; 0 = pass-through (air, plants, fluids). */
11
+ export function getCameraCollisionSolidityTable(version: string): Uint8Array {
12
+ const cached = solidityCache.get(version)
13
+ if (cached) return cached
14
+
15
+ const mcData = MinecraftData(version)
16
+ const Block = PrismarineBlockLoader(version)
17
+ const noOcclusionsSet = new Set(Object.keys(moreBlockDataGeneratedJson.noOcclusions))
18
+ const invisibleNames = new Set(mcData.blocksArray.filter(b => moreBlockDataGeneratedJson.invisibleBlocks[b.name]).map(b => b.name))
19
+
20
+ let maxStateId = 0
21
+ for (const idStr of Object.keys((mcData as { blocksByStateId: Record<string, unknown> }).blocksByStateId)) {
22
+ maxStateId = Math.max(maxStateId, Number(idStr))
23
+ }
24
+
25
+ const table = new Uint8Array(maxStateId + 1)
26
+ for (const idStr of Object.keys((mcData as { blocksByStateId: Record<string, unknown> }).blocksByStateId)) {
27
+ const stateId = Number(idStr)
28
+ if (!stateId) continue
29
+
30
+ const block = (
31
+ Block as { fromStateId: (id: number, y: number) => { name: string; transparent: boolean; boundingBox: string; shapes?: number[][] } | null }
32
+ ).fromStateId(stateId, 0)
33
+ if (!block) continue
34
+ if (invisibleNames.has(block.name)) continue
35
+ if (noOcclusionsSet.has(block.name)) continue
36
+ if (PASS_THROUGH_NAMES.has(block.name)) continue
37
+ if (block.boundingBox === 'empty') continue
38
+ if (block.transparent && block.boundingBox !== 'block') continue
39
+ if (!block.shapes || block.shapes.length === 0) continue
40
+
41
+ table[stateId] = 1
42
+ }
43
+
44
+ solidityCache.set(version, table)
45
+ return table
46
+ }
47
+
48
+ export function isStateIdSolidForCameraCollision(stateId: number, solidityTable: Uint8Array): boolean {
49
+ return stateId > 0 && stateId < solidityTable.length && solidityTable[stateId] === 1
50
+ }
@@ -3,7 +3,7 @@ import * as THREE from 'three'
3
3
  import * as nbt from 'prismarine-nbt'
4
4
  import { Vec3 } from 'vec3'
5
5
  import { MesherGeometryOutput } from '../mesher-shared/shared'
6
- import { getShaderCubeResources, SHADER_CUBES_WORDS_PER_FACE } from '../wasm-mesher/bridge/shaderCubeBridge'
6
+ import { getShaderCubeResources } from '../wasm-mesher/bridge/shaderCubeBridge'
7
7
  import {
8
8
  createCubeBlockMaterial,
9
9
  computeSectionOriginRel,
@@ -22,19 +22,12 @@ import {
22
22
  setLegacyLightmapParams,
23
23
  type RenderOrigin
24
24
  } from './shaders/legacyBlockShader'
25
- import { LEGACY_SECTION_HALF_EXTENT, sectionIntersectsFrustum, setupLegacySectionMatrix, updateLegacySectionCullState } from './legacySectionCull'
25
+ import { sectionIntersectsFrustum, setupLegacySectionMatrix, updateLegacySectionCullState } from './legacySectionCull'
26
26
  import { createShaderCubeMesh, disposeShaderCubeMesh } from './shaderCubeMesh'
27
27
  import { GlobalBlockBuffer } from './globalBlockBuffer'
28
28
  import { buildVisibleCubeSpans } from './cubeDrawSpans'
29
29
  import { GlobalLegacyBuffer, type LegacySectionGeometry } from './globalLegacyBuffer'
30
- import {
31
- computeShaderSectionRaycastAabb,
32
- isPointInsideAabb,
33
- raycastAabb,
34
- raycastShaderBlocksAabb,
35
- sectionAabbIntersectsRay,
36
- type ShaderSectionRaycastEntry
37
- } from './sectionRaycastAabb'
30
+ import { computeShaderSectionRaycastAabb, type ShaderSectionRaycastEntry } from './sectionRaycastAabb'
38
31
  import { getMesh } from './entity/EntityMesh'
39
32
  import type { WorldRendererThree } from './worldRendererThree'
40
33
  import { armorModel } from './entity/armorModels'
@@ -170,11 +163,8 @@ export class ChunkMeshManager {
170
163
  globalBlockBuffer: GlobalBlockBuffer | null = null
171
164
  globalLegacyBuffer: GlobalLegacyBuffer | null = null
172
165
  globalLegacyBlendBuffer: GlobalLegacyBuffer | null = null
173
- /** Tight world AABBs for third-person raycast; block word0 read from GlobalBlockBuffer or deferred. */
166
+ /** Tight world AABBs for shader-cube frustum culling (section centers). */
174
167
  private readonly shaderSectionRaycastBoxes = new Map<string, ShaderSectionRaycastEntry>()
175
- /** Per-raycast block dedup; safe while the eye is inside at most one section aggregate AABB per call. */
176
- private readonly blockRaycastVisitGen = new Uint16Array(4096)
177
- private blockRaycastVisitStamp = 1
178
168
 
179
169
  // Performance tracking
180
170
  private hits = 0
@@ -738,35 +728,6 @@ export class ChunkMeshManager {
738
728
  this.markCullDirty()
739
729
  }
740
730
 
741
- raycastGlobalLegacySections(raycaster: THREE.Raycaster, origin: THREE.Vector3, maxCenterDistance: number): number | undefined {
742
- const maxDistSq = maxCenterDistance * maxCenterDistance
743
- const dirX = raycaster.ray.direction.x
744
- const dirY = raycaster.ray.direction.y
745
- const dirZ = raycaster.ray.direction.z
746
- const far = raycaster.far
747
- const halfExtent = LEGACY_SECTION_HALF_EXTENT + 0.01
748
- const candidates: string[] = []
749
- for (const [key] of this.legacyCullSections) {
750
- const section = this.sectionObjects[key]
751
- if (!section || section.worldX === undefined) continue
752
- const dx = section.worldX - origin.x
753
- const dy = (section.worldY ?? 0) - origin.y
754
- const dz = (section.worldZ ?? 0) - origin.z
755
- if (dx * dx + dy * dy + dz * dz > maxDistSq) continue
756
- if (!sectionAabbIntersectsRay(section.worldX, section.worldY ?? 0, section.worldZ ?? 0, origin.x, origin.y, origin.z, dirX, dirY, dirZ, far, halfExtent))
757
- continue
758
- candidates.push(key)
759
- }
760
-
761
- if (candidates.length === 0) return undefined
762
-
763
- const hits: THREE.Intersection[] = []
764
- this.globalLegacyBuffer?.raycastSections(raycaster, candidates, hits)
765
- this.globalLegacyBlendBuffer?.raycastSections(raycaster, candidates, hits)
766
-
767
- return hits[0]?.distance
768
- }
769
-
770
731
  registerShaderSectionRaycastBox(
771
732
  sectionKey: string,
772
733
  words: Uint32Array,
@@ -792,91 +753,6 @@ export class ChunkMeshManager {
792
753
  this.shaderSectionRaycastBoxes.delete(sectionKey)
793
754
  }
794
755
 
795
- /** Closest hit against registered shader-cube AABBs (world-space ray). */
796
- raycastShaderSectionAABBs(originWorld: THREE.Vector3, direction: THREE.Vector3, maxDist: number, maxCenterDistance = 80): number | undefined {
797
- const ox = originWorld.x
798
- const oy = originWorld.y
799
- const oz = originWorld.z
800
- const dx = direction.x
801
- const dy = direction.y
802
- const dz = direction.z
803
- const maxCenterDistSq = maxCenterDistance * maxCenterDistance
804
-
805
- let closest = maxDist
806
- let found = false
807
-
808
- this.blockRaycastVisitStamp++
809
- if (this.blockRaycastVisitStamp >= 65535) {
810
- this.blockRaycastVisitGen.fill(0)
811
- this.blockRaycastVisitStamp = 1
812
- }
813
-
814
- for (const [key, entry] of this.shaderSectionRaycastBoxes) {
815
- const section = this.sectionObjects[key]
816
- if (section && !section.visible) continue
817
-
818
- const { box } = entry
819
- const dcx = box.cx - ox
820
- const dcy = box.cy - oy
821
- const dcz = box.cz - oz
822
- if (dcx * dcx + dcy * dcy + dcz * dcz > maxCenterDistSq) continue
823
-
824
- let t = raycastAabb(ox, oy, oz, dx, dy, dz, box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, closest)
825
- if (t === undefined && isPointInsideAabb(ox, oy, oz, box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) {
826
- const gb = this.globalBlockBuffer
827
- const slot = gb?.getSectionSlot(key)
828
- if (gb && slot) {
829
- t = raycastShaderBlocksAabb(
830
- gb.getW0(),
831
- slot.start,
832
- slot.count,
833
- 1,
834
- entry.sectionCenterX,
835
- entry.sectionCenterY,
836
- entry.sectionCenterZ,
837
- ox,
838
- oy,
839
- oz,
840
- dx,
841
- dy,
842
- dz,
843
- closest,
844
- this.blockRaycastVisitGen,
845
- this.blockRaycastVisitStamp
846
- )
847
- } else {
848
- const def = this.sectionObjects[key]?.deferredShaderCubes
849
- if (def) {
850
- t = raycastShaderBlocksAabb(
851
- def.words,
852
- 0,
853
- def.count,
854
- SHADER_CUBES_WORDS_PER_FACE,
855
- entry.sectionCenterX,
856
- entry.sectionCenterY,
857
- entry.sectionCenterZ,
858
- ox,
859
- oy,
860
- oz,
861
- dx,
862
- dy,
863
- dz,
864
- closest,
865
- this.blockRaycastVisitGen,
866
- this.blockRaycastVisitStamp
867
- )
868
- }
869
- }
870
- }
871
- if (t !== undefined && t < closest) {
872
- closest = t
873
- found = true
874
- }
875
- }
876
-
877
- return found ? closest : undefined
878
- }
879
-
880
756
  /**
881
757
  * Update or create a section with new geometry data
882
758
  */
@@ -644,46 +644,6 @@ export class GlobalLegacyBuffer {
644
644
  if (uf?.value?.set) uf.value.set(cameraOriginFrac.x, cameraOriginFrac.y, cameraOriginFrac.z)
645
645
  }
646
646
 
647
- raycastSections(raycaster: THREE.Raycaster, sectionKeys: Iterable<string>, out: THREE.Intersection[]): THREE.Intersection[] {
648
- const ray = raycaster.ray
649
- const closest = raycaster.near
650
- const far = raycaster.far
651
- _raycastOrigin.copy(ray.origin).sub(_raycastRenderOrigin.set(this.renderOrigin.x, this.renderOrigin.y, this.renderOrigin.z))
652
- _raycastRay.origin.copy(_raycastOrigin)
653
- _raycastRay.direction.copy(ray.direction)
654
-
655
- for (const key of sectionKeys) {
656
- const slot = this.sectionSlots.get(key)
657
- if (!slot) continue
658
-
659
- const dstVertBase = slot.start * VERTS_PER_QUAD
660
- const dstFloatBase = dstVertBase * FLOATS_PER_VERT
661
- const dstIndexBase = slot.start * INDICES_PER_QUAD
662
- const indexLen = slot.count * INDICES_PER_QUAD
663
-
664
- for (let i = 0; i < indexLen; i += 3) {
665
- const i0 = this.indices[dstIndexBase + i]!
666
- const i1 = this.indices[dstIndexBase + i + 1]!
667
- const i2 = this.indices[dstIndexBase + i + 2]!
668
- if (i0 === i1 && i1 === i2) continue
669
-
670
- const hit = intersectTriangle(_raycastRay, this.positions, this.aOrigin, dstFloatBase, i0, i1, i2, closest, far)
671
- if (hit !== null) {
672
- out.push({
673
- distance: hit,
674
- point: ray.at(hit, new THREE.Vector3()),
675
- object: this.mesh,
676
- face: null,
677
- faceIndex: Math.floor(i / 3)
678
- })
679
- }
680
- }
681
- }
682
-
683
- out.sort((a, b) => a.distance - b.distance)
684
- return out
685
- }
686
-
687
647
  getHighWatermark(): number {
688
648
  return this.highWatermark
689
649
  }
@@ -980,70 +940,3 @@ export class GlobalLegacyBuffer {
980
940
  this.pendingRanges.length = 0
981
941
  }
982
942
  }
983
-
984
- const _vA = new THREE.Vector3()
985
- const _vB = new THREE.Vector3()
986
- const _vC = new THREE.Vector3()
987
- const _edge1 = new THREE.Vector3()
988
- const _edge2 = new THREE.Vector3()
989
- const _normal = new THREE.Vector3()
990
- const _raycastOrigin = new THREE.Vector3()
991
- const _raycastRenderOrigin = new THREE.Vector3()
992
- const _raycastRay = new THREE.Ray()
993
-
994
- function readWorldVertex(positions: Float32Array, aOrigin: Float32Array, floatBase: number, vertIndex: number, target: THREE.Vector3): void {
995
- const f = floatBase + vertIndex * FLOATS_PER_VERT
996
- target.set(aOrigin[f]! + positions[f]!, aOrigin[f + 1]! + positions[f + 1]!, aOrigin[f + 2]! + positions[f + 2]!)
997
- }
998
-
999
- function intersectTriangle(
1000
- ray: THREE.Ray,
1001
- positions: Float32Array,
1002
- aOrigin: Float32Array,
1003
- floatBase: number,
1004
- i0: number,
1005
- i1: number,
1006
- i2: number,
1007
- near: number,
1008
- far: number
1009
- ): number | null {
1010
- readWorldVertex(positions, aOrigin, floatBase, i0, _vA)
1011
- readWorldVertex(positions, aOrigin, floatBase, i1, _vB)
1012
- readWorldVertex(positions, aOrigin, floatBase, i2, _vC)
1013
-
1014
- _edge1.subVectors(_vB, _vA)
1015
- _edge2.subVectors(_vC, _vA)
1016
- _normal.crossVectors(_edge1, _edge2)
1017
-
1018
- const denom = _normal.dot(ray.direction)
1019
- if (Math.abs(denom) < 1e-8) return null
1020
-
1021
- const t = _vA.clone().sub(ray.origin).dot(_normal) / denom
1022
- if (t < near || t > far) return null
1023
-
1024
- const p = ray.at(t, new THREE.Vector3())
1025
- if (!pointInTriangle(p, _vA, _vB, _vC)) return null
1026
- return t
1027
- }
1028
-
1029
- function pointInTriangle(p: THREE.Vector3, a: THREE.Vector3, b: THREE.Vector3, c: THREE.Vector3): boolean {
1030
- _edge1.subVectors(b, a)
1031
- _edge2.subVectors(c, a)
1032
- const n = _normal.crossVectors(_edge1, _edge2).normalize()
1033
-
1034
- const ab = _edge1
1035
- const ac = _edge2
1036
- const ap = p.clone().sub(a)
1037
-
1038
- const d00 = ab.dot(ab)
1039
- const d01 = ab.dot(ac)
1040
- const d11 = ac.dot(ac)
1041
- const d20 = ap.dot(ab)
1042
- const d21 = ap.dot(ac)
1043
- const denom = d00 * d11 - d01 * d01
1044
- if (Math.abs(denom) < 1e-12) return false
1045
- const v = (d11 * d20 - d01 * d21) / denom
1046
- const w = (d00 * d21 - d01 * d20) / denom
1047
- const u = 1 - v - w
1048
- return u >= -1e-4 && v >= -1e-4 && w >= -1e-4
1049
- }
@@ -95,31 +95,6 @@ export function isPointInsideAabb(
95
95
  return ox >= minX && ox <= maxX && oy >= minY && oy <= maxY && oz >= minZ && oz <= maxZ
96
96
  }
97
97
 
98
- /** True if a `far`-bounded ray from (ox,oy,oz) dir (dx,dy,dz) crosses or starts inside
99
- * the cube-section AABB centered at (cx,cy,cz) with the given half-extent. */
100
- export function sectionAabbIntersectsRay(
101
- cx: number,
102
- cy: number,
103
- cz: number,
104
- ox: number,
105
- oy: number,
106
- oz: number,
107
- dx: number,
108
- dy: number,
109
- dz: number,
110
- far: number,
111
- halfExtent: number
112
- ): boolean {
113
- const minX = cx - halfExtent
114
- const minY = cy - halfExtent
115
- const minZ = cz - halfExtent
116
- const maxX = cx + halfExtent
117
- const maxY = cy + halfExtent
118
- const maxZ = cz + halfExtent
119
- if (isPointInsideAabb(ox, oy, oz, minX, minY, minZ, maxX, maxY, maxZ)) return true
120
- return raycastAabb(ox, oy, oz, dx, dy, dz, minX, minY, minZ, maxX, maxY, maxZ, far) !== undefined
121
- }
122
-
123
98
  /** Ray–AABB entry distance, or undefined. Ignores hits when origin is inside the box. */
124
99
  export function raycastAabb(
125
100
  ox: number,
@@ -229,76 +204,3 @@ export function raycastAabbFromInside(
229
204
 
230
205
  return tExit <= maxDist ? tExit : undefined
231
206
  }
232
-
233
- /** Per-block raycast; `word0Stride` 1 = GlobalBlockBuffer SoA, 4 = deferred AoS. */
234
- export function raycastShaderBlocksAabb(
235
- w0Source: Uint32Array,
236
- start: number,
237
- faceCount: number,
238
- word0Stride: number,
239
- sectionCenterX: number,
240
- sectionCenterY: number,
241
- sectionCenterZ: number,
242
- ox: number,
243
- oy: number,
244
- oz: number,
245
- dx: number,
246
- dy: number,
247
- dz: number,
248
- maxDist: number,
249
- visitGen: Uint16Array,
250
- visitStamp: number
251
- ): number | undefined {
252
- const baseX = sectionCenterX - 8
253
- const baseY = sectionCenterY - 8
254
- const baseZ = sectionCenterZ - 8
255
-
256
- let closest = maxDist
257
- let found = false
258
-
259
- for (let i = 0; i < faceCount; i++) {
260
- const w0 = w0Source[start + i * word0Stride]!
261
- const lx = w0 & ((1 << WORD0.LX_BITS) - 1)
262
- const ly = (w0 >> WORD0.LY_SHIFT) & ((1 << WORD0.LY_BITS) - 1)
263
- const lz = (w0 >> WORD0.LZ_SHIFT) & ((1 << WORD0.LZ_BITS) - 1)
264
- const visitIdx = lx + (ly << 4) + (lz << 8)
265
- if (visitGen[visitIdx] === visitStamp) continue
266
- visitGen[visitIdx] = visitStamp
267
-
268
- const minX = baseX + lx
269
- const minY = baseY + ly
270
- const minZ = baseZ + lz
271
- const maxX = minX + 1
272
- const maxY = minY + 1
273
- const maxZ = minZ + 1
274
-
275
- let t: number | undefined
276
- if (isPointInsideAabb(ox, oy, oz, minX, minY, minZ, maxX, maxY, maxZ)) {
277
- t = raycastAabbFromInside(ox, oy, oz, dx, dy, dz, minX, minY, minZ, maxX, maxY, maxZ, closest)
278
- } else {
279
- t = raycastAabb(ox, oy, oz, dx, dy, dz, minX, minY, minZ, maxX, maxY, maxZ, closest)
280
- }
281
- if (t !== undefined && t < closest) {
282
- closest = t
283
- found = true
284
- }
285
- }
286
-
287
- return found ? closest : undefined
288
- }
289
-
290
- /** 16³ section box centered at (cx, cy, cz) — tests / legacy helper. */
291
- export function raycastSectionAabb(
292
- ox: number,
293
- oy: number,
294
- oz: number,
295
- dx: number,
296
- dy: number,
297
- dz: number,
298
- cx: number,
299
- cy: number,
300
- cz: number,
301
- maxDist: number
302
- ): number | undefined {
303
- return raycastAabb(ox, oy, oz, dx, dy, dz, cx - 8, cy - 8, cz - 8, cx + 8, cy + 8, cz + 8, maxDist)
304
- }