minecraft-renderer 0.1.106 → 0.1.108

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.
Files changed (98) hide show
  1. package/dist/lightOwnerWorker.js +9 -0
  2. package/dist/lightOwnerWorker.js.map +7 -0
  3. package/dist/mesher.js +1 -1
  4. package/dist/mesher.js.map +2 -2
  5. package/dist/mesherWasm.js +222 -222
  6. package/dist/minecraft-renderer.js +105 -76
  7. package/dist/minecraft-renderer.js.meta.json +1 -1
  8. package/dist/threeWorker.js +537 -508
  9. package/package.json +1 -1
  10. package/src/bundler/bundlePrepare.test.ts +25 -0
  11. package/src/bundler/bundlePrepare.ts +1 -1
  12. package/src/graphicsBackend/appViewer.ts +15 -0
  13. package/src/graphicsBackend/config.ts +8 -0
  14. package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
  15. package/src/graphicsBackend/rendererDefaultOptions.ts +18 -1
  16. package/src/graphicsBackend/rendererOptionsSync.ts +4 -3
  17. package/src/lib/blockEntityLightRegistry.ts +4 -0
  18. package/src/lib/skyLight.ts +17 -0
  19. package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
  20. package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
  21. package/src/lib/worldrendererCommon.ts +340 -67
  22. package/src/lighting/cache/rendererLightCache.test.ts +158 -0
  23. package/src/lighting/cache/rendererLightCache.ts +248 -0
  24. package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
  25. package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
  26. package/src/lighting/debug/clientLightProbe.test.ts +24 -0
  27. package/src/lighting/debug/clientLightProbe.ts +20 -0
  28. package/src/lighting/debug/clientLightTrace.test.ts +271 -0
  29. package/src/lighting/debug/clientLightTrace.ts +335 -0
  30. package/src/lighting/entity/entityLightController.test.ts +216 -0
  31. package/src/lighting/entity/entityLightController.ts +322 -0
  32. package/src/lighting/owner/clientLightOwner.test.ts +467 -0
  33. package/src/lighting/owner/clientLightOwner.ts +523 -0
  34. package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
  35. package/src/lighting/owner/lightOwnerHost.ts +202 -0
  36. package/src/lighting/owner/lightOwnerWorker.ts +154 -0
  37. package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
  38. package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
  39. package/src/lighting/tables/lightTables1171.generated.json +10 -0
  40. package/src/lighting/tables/lightTables1171.test.ts +168 -0
  41. package/src/lighting/tables/lightTables1171.ts +59 -0
  42. package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
  43. package/src/lighting/versioning/clientLightVersions.ts +203 -0
  44. package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
  45. package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
  46. package/src/mesher-shared/lightNibblePack.ts +26 -0
  47. package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
  48. package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
  49. package/src/mesher-shared/shared.ts +48 -1
  50. package/src/three/chunkMeshManager.ts +26 -0
  51. package/src/three/entities.ts +97 -8
  52. package/src/three/entity/EntityMesh.ts +1 -0
  53. package/src/three/entity/legWalkAnimation.test.ts +154 -0
  54. package/src/three/entity/legWalkAnimation.ts +193 -0
  55. package/src/three/globalBlockBuffer.ts +94 -2
  56. package/src/three/globalLegacyBuffer.ts +94 -2
  57. package/src/three/graphicsBackendBase.ts +2 -9
  58. package/src/three/modules/clouds.ts +191 -0
  59. package/src/three/modules/index.ts +4 -0
  60. package/src/three/modules/sunMoon.ts +151 -0
  61. package/src/three/skyboxRenderer.ts +15 -8
  62. package/src/three/tests/clouds.test.ts +59 -0
  63. package/src/three/tests/globalBlockBuffer.test.ts +98 -0
  64. package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
  65. package/src/three/tests/waterFog.test.ts +25 -0
  66. package/src/three/waterFog.ts +28 -0
  67. package/src/three/worldRendererThree.ts +84 -39
  68. package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
  69. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
  70. package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
  71. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  72. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
  73. package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
  74. package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
  75. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
  76. package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
  77. package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
  78. package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
  79. package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
  80. package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
  81. package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
  82. package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
  83. package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
  84. package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
  85. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
  86. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
  87. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
  88. package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
  89. package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
  90. package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
  91. package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
  92. package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
  93. package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
  94. package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
  95. package/src/worldView/types.ts +3 -0
  96. package/src/worldView/worldView.geometryReload.test.ts +68 -0
  97. package/src/worldView/worldView.spiral.test.ts +1 -1
  98. package/src/worldView/worldView.ts +49 -1
@@ -0,0 +1,154 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+ import { expect, test } from 'vitest'
4
+ import { applyLegRig, assignLegPhases, calculateLegSwing, createLegRig } from './legWalkAnimation'
5
+
6
+ const close = (a: THREE.Vector3, b: THREE.Vector3) => a.distanceTo(b) < 1e-4
7
+
8
+ /** Mirrors `getMesh`: bones carry absolute pivots even when parented, vertices are authored in mesh space. */
9
+ // bound before scaling like `getMesh`, so skinned vertices come back in 1/16 block units
10
+ function makeBedrockQuadruped() {
11
+ const legs = [
12
+ { name: 'leg0', pivot: [-4, 12, 7] },
13
+ { name: 'leg1', pivot: [4, 12, 7] },
14
+ { name: 'leg2', pivot: [-4, 12, -6] },
15
+ { name: 'leg3', pivot: [4, 12, -6] }
16
+ ]
17
+ const body = new THREE.Bone()
18
+ body.name = 'bone_body'
19
+ body.position.set(0, 5, 2)
20
+ const bones = [body]
21
+ const positions: number[] = []
22
+ const skinIndices: number[] = []
23
+ const skinWeights: number[] = []
24
+ for (const [i, leg] of legs.entries()) {
25
+ const bone = new THREE.Bone()
26
+ bone.name = `bone_${leg.name}`
27
+ bone.position.set(leg.pivot[0], leg.pivot[1], leg.pivot[2])
28
+ body.add(bone)
29
+ bones.push(bone)
30
+ // single foot vertex 12px under the pivot
31
+ positions.push(leg.pivot[0], leg.pivot[1] - 12, leg.pivot[2])
32
+ skinIndices.push(i + 1, 0, 0, 0)
33
+ skinWeights.push(1, 0, 0, 0)
34
+ }
35
+ const geometry = new THREE.BufferGeometry()
36
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
37
+ geometry.setAttribute('skinIndex', new THREE.Uint16BufferAttribute(skinIndices, 4))
38
+ geometry.setAttribute('skinWeight', new THREE.Float32BufferAttribute(skinWeights, 4))
39
+ const mesh = new THREE.SkinnedMesh(geometry, new THREE.MeshBasicMaterial())
40
+ mesh.add(body)
41
+ mesh.bind(new THREE.Skeleton(bones))
42
+ mesh.scale.set(1 / 16, 1 / 16, 1 / 16)
43
+ const root = new THREE.Object3D()
44
+ root.add(mesh)
45
+ return { root, mesh, legs }
46
+ }
47
+
48
+ test('calculateLegSwing follows vanilla gait and is zero at rest', () => {
49
+ expect(calculateLegSwing(5, 0, 0)).toBe(0)
50
+ expect(calculateLegSwing(0, 1, 0)).toBeCloseTo(1.4)
51
+ expect(calculateLegSwing(0, 0.5, Math.PI)).toBeCloseTo(-0.7)
52
+ expect(calculateLegSwing(Number.NaN, 1, 0)).toBe(0)
53
+ })
54
+
55
+ test('assignLegPhases alternates bipeds and pairs quadruped diagonals', () => {
56
+ expect(
57
+ assignLegPhases([
58
+ { x: -1.9, z: 0 },
59
+ { x: 1.9, z: 0 }
60
+ ])
61
+ ).toEqual([Math.PI, 0])
62
+ // leg0..leg3 as hind-right, hind-left, front-right, front-left
63
+ const quad = assignLegPhases([
64
+ { x: -4, z: 7 },
65
+ { x: 4, z: 7 },
66
+ { x: -4, z: -6 },
67
+ { x: 4, z: -6 }
68
+ ])!
69
+ expect(quad[0]).toBe(quad[3])
70
+ expect(quad[1]).toBe(quad[2])
71
+ expect(quad[0]).not.toBe(quad[1])
72
+ expect(assignLegPhases([{ x: 0, z: 0 }])).toBeUndefined()
73
+ expect(assignLegPhases(Array.from({ length: 8 }, (_, i) => ({ x: i, z: i % 2 })))).toBeUndefined()
74
+ })
75
+
76
+ test('bedrock legs rotate about their own pivot even when parented to an offset body bone', () => {
77
+ const { root, mesh, legs } = makeBedrockQuadruped()
78
+ const rig = createLegRig(root, 'cow')!
79
+ expect(rig.legs).toHaveLength(4)
80
+
81
+ applyLegRig(rig, 0.4 * Math.PI, 1)
82
+ mesh.updateWorldMatrix(true, true)
83
+ mesh.skeleton.update()
84
+
85
+ const vertex = new THREE.Vector3()
86
+ for (const [i, leg] of legs.entries()) {
87
+ const pivot = new THREE.Vector3(...(leg.pivot as [number, number, number]))
88
+ const expectedAngle = calculateLegSwing(0.4 * Math.PI, 1, rig.legs[i].phase)
89
+ const foot = new THREE.Vector3(pivot.x, pivot.y - 12, pivot.z)
90
+ const expected = foot
91
+ .clone()
92
+ .sub(pivot)
93
+ .applyAxisAngle(new THREE.Vector3(1, 0, 0), expectedAngle)
94
+ .add(pivot)
95
+ mesh.getVertexPosition(i, vertex).multiplyScalar(16)
96
+ expect(close(vertex, expected), `leg ${i}`).toBe(true)
97
+ // the pivot itself never moves
98
+ expect(Math.abs(vertex.distanceTo(pivot) - 12)).toBeLessThan(1e-4)
99
+ }
100
+ })
101
+
102
+ test('legs settle back to the exact rest pose once the mob stops', () => {
103
+ const { root, mesh, legs } = makeBedrockQuadruped()
104
+ const rig = createLegRig(root, 'pig')!
105
+ applyLegRig(rig, 3, 0.8)
106
+ applyLegRig(rig, 3, 0)
107
+ mesh.updateWorldMatrix(true, true)
108
+ mesh.skeleton.update()
109
+ const vertex = new THREE.Vector3()
110
+ for (const [i, leg] of legs.entries()) {
111
+ mesh.getVertexPosition(i, vertex).multiplyScalar(16)
112
+ expect(close(vertex, new THREE.Vector3(leg.pivot[0], leg.pivot[1] - 12, leg.pivot[2]))).toBe(true)
113
+ }
114
+ })
115
+
116
+ function makeObjBiped() {
117
+ const root = new THREE.Object3D()
118
+ const makeLeg = (name: string, x: number) => {
119
+ const geometry = new THREE.BoxGeometry(0.25, 0.75, 0.25)
120
+ geometry.translate(x, 0.375, 0)
121
+ const mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial())
122
+ mesh.name = name
123
+ return mesh
124
+ }
125
+ const left = makeLeg('LeftLeg', 0.125)
126
+ const right = makeLeg('RightLeg', -0.125)
127
+ root.add(left, right)
128
+ return { root, left, right }
129
+ }
130
+
131
+ test('OBJ legs pivot at the top of their box without moving at rest', () => {
132
+ const { root, left, right } = makeObjBiped()
133
+ const rig = createLegRig(root, 'enderman')!
134
+ expect(rig.legs).toHaveLength(2)
135
+
136
+ left.updateMatrix()
137
+ left.geometry.computeBoundingBox()
138
+ const restBox = left.geometry.boundingBox!.clone().applyMatrix4(left.matrix)
139
+ expect(restBox.min.y).toBeCloseTo(0)
140
+ expect(restBox.max.y).toBeCloseTo(0.75)
141
+
142
+ applyLegRig(rig, 0, 1)
143
+ expect(left.rotation.x).toBeCloseTo(calculateLegSwing(0, 1, rig.legs.find(l => l.kind === 'mesh' && l.meshes[0] === left)!.phase))
144
+ expect(left.rotation.x).toBeCloseTo(-right.rotation.x)
145
+ // the hinge (original top centre of the leg) stays put while the leg rotates
146
+ left.updateMatrix()
147
+ const hinge = new THREE.Vector3(0, 0, 0).applyMatrix4(left.matrix)
148
+ expect(close(hinge, new THREE.Vector3(0.125, 0.75, 0))).toBe(true)
149
+ })
150
+
151
+ test('excluded and unrecognised models get no rig', () => {
152
+ expect(createLegRig(makeObjBiped().root, 'parrot')).toBeUndefined()
153
+ expect(createLegRig(new THREE.Object3D(), 'cow')).toBeUndefined()
154
+ })
@@ -0,0 +1,193 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+
4
+ export const LEG_RIG_USER_DATA_KEY = '_legRig'
5
+ export const LEG_ANIMATION_USER_DATA_KEY = '_legAnimation'
6
+
7
+ /** Vanilla ModelBiped / ModelQuadruped constants. */
8
+ export const LEG_SWING_FREQUENCY = 0.6662
9
+ export const LEG_SWING_AMPLITUDE = 1.4
10
+
11
+ /** Entities whose legs are not driven by ground walking (fly, hop, swim or use bespoke animations). */
12
+ const LEG_ANIMATION_EXCLUDED = new Set(['armor_stand', 'axolotl', 'ender_dragon', 'frog', 'parrot', 'rabbit', 'turtle'])
13
+
14
+ const LEG_NAME_PATTERN = /leg/i
15
+
16
+ type BoneLeg = {
17
+ kind: 'bone'
18
+ bone: THREE.Bone
19
+ /** Pivot in mesh space, the point the vertices were authored around. */
20
+ pivot: THREE.Vector3
21
+ /** Parent bone matrix relative to the skinned mesh at bind time. */
22
+ parentMatrix: THREE.Matrix4
23
+ parentMatrixInverse: THREE.Matrix4
24
+ /** Local bone matrix at bind time. */
25
+ restMatrix: THREE.Matrix4
26
+ }
27
+
28
+ type MeshLeg = {
29
+ kind: 'mesh'
30
+ meshes: THREE.Mesh[]
31
+ restRotationX: number[]
32
+ }
33
+
34
+ export type LegRigLeg = (BoneLeg | MeshLeg) & { phase: number }
35
+
36
+ export type LegRig = {
37
+ legs: LegRigLeg[]
38
+ /** True while legs are rotated away from rest, so settling back is written exactly once. */
39
+ posed: boolean
40
+ }
41
+
42
+ type LegCandidate = { name: string; pivot: THREE.Vector3; build: (phase: number) => LegRigLeg }
43
+
44
+ /** Vanilla leg swing angle (radians around the model X axis). */
45
+ export function calculateLegSwing(limbSwing: number, limbSwingAmount: number, phase: number): number {
46
+ const amount = Number.isFinite(limbSwingAmount) ? Math.max(0, limbSwingAmount) : 0
47
+ if (amount === 0 || !Number.isFinite(limbSwing)) return 0
48
+ return Math.cos(limbSwing * LEG_SWING_FREQUENCY + phase) * LEG_SWING_AMPLITUDE * amount
49
+ }
50
+
51
+ /**
52
+ * Diagonal pairs move together on four legged mobs and opposite legs alternate on bipeds.
53
+ * Derived from pivot positions so it works for any bone naming (leg0, rightLeg, left_hind_leg...).
54
+ * Returns the phase offset per candidate, or undefined if the leg layout isn't recognised.
55
+ */
56
+ export function assignLegPhases(pivots: Array<{ x: number; z: number }>): number[] | undefined {
57
+ if (pivots.length !== 2 && pivots.length !== 4) return undefined
58
+
59
+ const meanX = pivots.reduce((sum, p) => sum + p.x, 0) / pivots.length
60
+ const meanZ = pivots.reduce((sum, p) => sum + p.z, 0) / pivots.length
61
+ const xs = pivots.map(p => p.x - meanX)
62
+ const zs = pivots.map(p => p.z - meanZ)
63
+ const xSpread = Math.max(...xs) - Math.min(...xs)
64
+ const zSpread = Math.max(...zs) - Math.min(...zs)
65
+
66
+ // Legs side by side: alternate left/right.
67
+ if (pivots.length === 2 || zSpread < xSpread * 0.5) {
68
+ if (xSpread === 0) return undefined
69
+ return xs.map(x => (x > 0 ? 0 : Math.PI))
70
+ }
71
+
72
+ if (xSpread === 0) return undefined
73
+ return xs.map((x, i) => (x > 0 === zs[i] > 0 ? 0 : Math.PI))
74
+ }
75
+
76
+ function collectBoneCandidates(skinned: THREE.SkinnedMesh): LegCandidate[] {
77
+ skinned.updateWorldMatrix(true, true)
78
+ const meshInverse = skinned.matrixWorld.clone().invert()
79
+ const candidates: LegCandidate[] = []
80
+ for (const bone of skinned.skeleton.bones) {
81
+ const name = bone.name.replace(/^bone_/, '')
82
+ if (!LEG_NAME_PATTERN.test(name)) continue
83
+ // Sub-bones such as `Leg1B` hang off the same pivot and would be double counted.
84
+ if (/^leg\d[b-z]$/i.test(name)) continue
85
+ const pivot = bone.position.clone()
86
+ const parentMatrix = bone.parent instanceof THREE.Bone ? meshInverse.clone().multiply(bone.parent.matrixWorld) : new THREE.Matrix4()
87
+ candidates.push({
88
+ name,
89
+ pivot,
90
+ build: phase => ({
91
+ kind: 'bone',
92
+ phase,
93
+ bone,
94
+ pivot,
95
+ parentMatrix,
96
+ parentMatrixInverse: parentMatrix.clone().invert(),
97
+ restMatrix: bone.matrix.clone()
98
+ })
99
+ })
100
+ }
101
+ return candidates
102
+ }
103
+
104
+ function collectMeshCandidates(root: THREE.Object3D): LegCandidate[] {
105
+ const groups = new Map<string, THREE.Mesh[]>()
106
+ for (const child of root.children) {
107
+ if (!(child instanceof THREE.Mesh) || child instanceof THREE.SkinnedMesh) continue
108
+ if (!LEG_NAME_PATTERN.test(child.name)) continue
109
+ const group = groups.get(child.name)
110
+ if (group) group.push(child)
111
+ else groups.set(child.name, [child])
112
+ }
113
+
114
+ const candidates: LegCandidate[] = []
115
+ for (const [name, meshes] of groups) {
116
+ // Overlay layers share a name, so the pivot is the union of them.
117
+ const box = new THREE.Box3()
118
+ for (const mesh of meshes) {
119
+ mesh.updateMatrix()
120
+ mesh.geometry.computeBoundingBox()
121
+ box.union(mesh.geometry.boundingBox!.clone().applyMatrix4(mesh.matrix))
122
+ }
123
+ // Legs hang from the top of their box and swing around it.
124
+ const pivot = new THREE.Vector3((box.min.x + box.max.x) / 2, box.max.y, (box.min.z + box.max.z) / 2)
125
+ candidates.push({
126
+ name,
127
+ pivot,
128
+ build: phase => {
129
+ for (const mesh of meshes) {
130
+ mesh.geometry.translate(-pivot.x, -pivot.y, -pivot.z)
131
+ mesh.position.add(pivot)
132
+ }
133
+ return { kind: 'mesh', phase, meshes, restRotationX: meshes.map(mesh => mesh.rotation.x) }
134
+ }
135
+ })
136
+ }
137
+ return candidates
138
+ }
139
+
140
+ function buildLegs(candidates: LegCandidate[]): LegRigLeg[] {
141
+ const phases = assignLegPhases(candidates.map(c => c.pivot))
142
+ if (!phases) return []
143
+ return candidates.map((candidate, i) => candidate.build(phases[i]))
144
+ }
145
+
146
+ /**
147
+ * Finds the legs of a mob model (bedrock bones or OBJ parts) and prepares them to be swung
148
+ * around their pivots. Returns undefined when the model has no recognisable 2 or 4 leg layout.
149
+ */
150
+ export function createLegRig(root: THREE.Object3D, entityName?: string): LegRig | undefined {
151
+ if (entityName && LEG_ANIMATION_EXCLUDED.has(entityName)) return undefined
152
+
153
+ const legs: LegRigLeg[] = []
154
+ root.traverse(child => {
155
+ if (child instanceof THREE.SkinnedMesh) legs.push(...buildLegs(collectBoneCandidates(child)))
156
+ })
157
+ legs.push(...buildLegs(collectMeshCandidates(root)))
158
+
159
+ if (legs.length === 0) return undefined
160
+ return { legs, posed: false }
161
+ }
162
+
163
+ const tmpPivotMatrix = new THREE.Matrix4()
164
+ const tmpRotation = new THREE.Matrix4()
165
+ const tmpDeform = new THREE.Matrix4()
166
+ const tmpLocal = new THREE.Matrix4()
167
+
168
+ function applyBoneLeg(leg: BoneLeg, angle: number): void {
169
+ // Skinning deforms vertices by D = T(pivot) * Rx * T(-pivot) in mesh space. Bones are parented with absolute
170
+ // pivots, so the local rotation is conjugated by the parent bind matrix to produce exactly that deformation.
171
+ tmpDeform.makeTranslation(leg.pivot.x, leg.pivot.y, leg.pivot.z)
172
+ tmpDeform.multiply(tmpRotation.makeRotationX(angle))
173
+ tmpDeform.multiply(tmpPivotMatrix.makeTranslation(-leg.pivot.x, -leg.pivot.y, -leg.pivot.z))
174
+
175
+ tmpLocal.copy(leg.parentMatrixInverse).multiply(tmpDeform).multiply(leg.parentMatrix).multiply(leg.restMatrix)
176
+ tmpLocal.decompose(leg.bone.position, leg.bone.quaternion, leg.bone.scale)
177
+ }
178
+
179
+ /** Poses all legs of the rig. Cheap no-op while the mob stays still. */
180
+ export function applyLegRig(rig: LegRig, limbSwing: number, limbSwingAmount: number): void {
181
+ const moving = Number.isFinite(limbSwingAmount) && limbSwingAmount >= 1e-4
182
+ if (!moving && !rig.posed) return
183
+ rig.posed = moving
184
+
185
+ for (const leg of rig.legs) {
186
+ const angle = moving ? calculateLegSwing(limbSwing, limbSwingAmount, leg.phase) : 0
187
+ if (leg.kind === 'bone') {
188
+ applyBoneLeg(leg, angle)
189
+ } else {
190
+ for (const [i, mesh] of leg.meshes.entries()) mesh.rotation.x = leg.restRotationX[i] + angle
191
+ }
192
+ }
193
+ }
@@ -12,6 +12,7 @@ import {
12
12
  import { VERTICES_PER_FACE, computeSectionOriginRel } from './shaders/cubeBlockShader'
13
13
  import { computeCameraRelativeUniforms, type RenderOrigin } from './shaders/legacyBlockShader'
14
14
  import { packWord2Empty } from '../wasm-mesher/bridge/shaderCubeBridge'
15
+ import { isClientLightTraceEnabled, recordClientLightTrace, recordClientLightTraceGpuSample } from '../lighting/debug/clientLightTrace'
15
16
 
16
17
  type WebGLRendererInternals = THREE.WebGLRenderer & {
17
18
  properties: {
@@ -162,9 +163,9 @@ export class GlobalBlockBuffer {
162
163
  getSectionDrawStart(sectionKey: string): number | undefined {
163
164
  const slot = this.sectionSlots.get(sectionKey)
164
165
  if (!slot) return undefined
165
- if (this.pendingMove?.key === sectionKey) return this.pendingMove.oldStart
166
166
  const replace = this.pendingReplace.get(sectionKey)
167
167
  if (replace) return replace.oldStart
168
+ if (this.pendingMove?.key === sectionKey) return this.pendingMove.oldStart
168
169
  if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return undefined
169
170
  return slot.start
170
171
  }
@@ -172,13 +173,37 @@ export class GlobalBlockBuffer {
172
173
  getSectionDrawCount(sectionKey: string): number | undefined {
173
174
  const slot = this.sectionSlots.get(sectionKey)
174
175
  if (!slot) return undefined
175
- if (this.pendingMove?.key === sectionKey) return this.pendingMove.count
176
176
  const replace = this.pendingReplace.get(sectionKey)
177
177
  if (replace) return replace.oldCount
178
+ if (this.pendingMove?.key === sectionKey) return this.pendingMove.count
178
179
  if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return undefined
179
180
  return slot.count
180
181
  }
181
182
 
183
+ getGpuSlotTrace(sectionKey: string):
184
+ | {
185
+ displayedStart?: number
186
+ displayedCount?: number
187
+ candidateStart: number
188
+ candidateCount: number
189
+ pendingReplace: boolean
190
+ pendingMove: boolean
191
+ unuploadedRanges: number
192
+ }
193
+ | undefined {
194
+ const slot = this.sectionSlots.get(sectionKey)
195
+ if (!slot) return undefined
196
+ return {
197
+ displayedStart: this.getSectionDrawStart(sectionKey),
198
+ displayedCount: this.getSectionDrawCount(sectionKey),
199
+ candidateStart: slot.start,
200
+ candidateCount: slot.count,
201
+ pendingReplace: this.pendingReplace.has(sectionKey),
202
+ pendingMove: this.pendingMove?.key === sectionKey,
203
+ unuploadedRanges: this.pendingRanges.length
204
+ }
205
+ }
206
+
182
207
  getUploadEpoch(): number {
183
208
  return this.uploadEpoch
184
209
  }
@@ -282,6 +307,20 @@ export class GlobalBlockBuffer {
282
307
  this.markDirty(slot.start, slot.start + faceCount - 1)
283
308
  this.mesh.geometry.instanceCount = this.highWatermark
284
309
  this.layoutVersion++
310
+ if (isClientLightTraceEnabled()) {
311
+ const gpu = this.getGpuSlotTrace(sectionKey)
312
+ recordClientLightTrace({
313
+ phase: 'gpuStaged',
314
+ sectionKey,
315
+ displayedStart: gpu?.displayedStart,
316
+ displayedCount: gpu?.displayedCount,
317
+ candidateStart: gpu?.candidateStart,
318
+ candidateCount: gpu?.candidateCount,
319
+ pendingReplace: gpu?.pendingReplace,
320
+ pendingMove: gpu?.pendingMove,
321
+ unuploadedRanges: gpu?.unuploadedRanges
322
+ })
323
+ }
285
324
  }
286
325
 
287
326
  getLayoutVersion(): number {
@@ -388,6 +427,20 @@ export class GlobalBlockBuffer {
388
427
  this.markDirty(newStart, newStart + section.count - 1)
389
428
  this.pendingMove = { key: section.key, oldStart, newStart, count: section.count }
390
429
  this.layoutVersion++
430
+ if (isClientLightTraceEnabled()) {
431
+ const gpu = this.getGpuSlotTrace(section.key)
432
+ recordClientLightTrace({
433
+ phase: 'gpuStaged',
434
+ sectionKey: section.key,
435
+ displayedStart: gpu?.displayedStart,
436
+ displayedCount: gpu?.displayedCount,
437
+ candidateStart: gpu?.candidateStart,
438
+ candidateCount: gpu?.candidateCount,
439
+ pendingReplace: gpu?.pendingReplace,
440
+ pendingMove: true,
441
+ unuploadedRanges: gpu?.unuploadedRanges
442
+ })
443
+ }
391
444
  }
392
445
 
393
446
  uploadDirtyRange(): void {
@@ -411,6 +464,13 @@ export class GlobalBlockBuffer {
411
464
  r.start = offset + count
412
465
  }
413
466
  this.uploadEpoch++
467
+ if (isClientLightTraceEnabled()) {
468
+ recordClientLightTraceGpuSample('gpuUploaded', this.highWatermark, () => ({
469
+ phase: 'gpuUploaded',
470
+ unuploadedRanges: this.pendingRanges.length,
471
+ drawableFaces: this.highWatermark
472
+ }))
473
+ }
414
474
  }
415
475
 
416
476
  setCameraOrigin(renderOrigin: RenderOrigin, x: number, y: number, z: number): void {
@@ -608,6 +668,9 @@ export class GlobalBlockBuffer {
608
668
  private findMovableSection(maxCount: number): { key: string; start: number; count: number } | undefined {
609
669
  const sections: Array<{ key: string; start: number; count: number }> = []
610
670
  for (const [key, slot] of this.sectionSlots) {
671
+ if (this.pendingReplace.has(key)) continue
672
+ if (this.pendingMove?.key === key) continue
673
+ if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) continue
611
674
  sections.push({ key, start: slot.start, count: slot.count })
612
675
  }
613
676
  if (sections.length === 0) return undefined
@@ -684,11 +747,26 @@ export class GlobalBlockBuffer {
684
747
  this.mesh.geometry.instanceCount = this.highWatermark
685
748
  this.layoutVersion++
686
749
  this.uploadEpoch++
750
+ if (isClientLightTraceEnabled()) {
751
+ const gpu = this.getGpuSlotTrace(key)
752
+ recordClientLightTrace({
753
+ phase: 'gpuCommitted',
754
+ sectionKey: key,
755
+ displayedStart: gpu?.displayedStart,
756
+ displayedCount: gpu?.displayedCount,
757
+ candidateStart: gpu?.candidateStart,
758
+ candidateCount: gpu?.candidateCount,
759
+ pendingReplace: false,
760
+ pendingMove: gpu?.pendingMove,
761
+ unuploadedRanges: gpu?.unuploadedRanges
762
+ })
763
+ }
687
764
  }
688
765
 
689
766
  private finalizePendingMove(): void {
690
767
  const move = this.pendingMove
691
768
  if (!move) return
769
+ const movedKey = move.key
692
770
 
693
771
  const { oldStart, count } = move
694
772
  this.zeroAndFreeSlot(oldStart, count)
@@ -697,6 +775,20 @@ export class GlobalBlockBuffer {
697
775
  this.pendingMove = null
698
776
  this.layoutVersion++
699
777
  this.uploadEpoch++
778
+ if (isClientLightTraceEnabled()) {
779
+ const gpu = this.getGpuSlotTrace(movedKey)
780
+ recordClientLightTrace({
781
+ phase: 'gpuCommitted',
782
+ sectionKey: movedKey,
783
+ displayedStart: gpu?.displayedStart,
784
+ displayedCount: gpu?.displayedCount,
785
+ candidateStart: gpu?.candidateStart,
786
+ candidateCount: gpu?.candidateCount,
787
+ pendingReplace: gpu?.pendingReplace,
788
+ pendingMove: false,
789
+ unuploadedRanges: gpu?.unuploadedRanges
790
+ })
791
+ }
700
792
  }
701
793
 
702
794
  private shrinkHighWatermark(): void {
@@ -10,6 +10,7 @@ import {
10
10
  type LegacyMultiDrawScratch
11
11
  } from './legacyMultiDraw'
12
12
  import { computeCameraRelativeUniforms, type RenderOrigin } from './shaders/legacyBlockShader'
13
+ import { isClientLightTraceEnabled, recordClientLightTrace, recordClientLightTraceGpuSample } from '../lighting/debug/clientLightTrace'
13
14
 
14
15
  const VERTS_PER_QUAD = 4
15
16
  const INDICES_PER_QUAD = 6
@@ -337,6 +338,20 @@ export class GlobalLegacyBuffer {
337
338
  this.markDirty(slot.start, slot.start + quadCount - 1)
338
339
  this.syncDefaultDrawGroups()
339
340
  this.layoutVersion++
341
+ if (isClientLightTraceEnabled()) {
342
+ const gpu = this.getGpuSlotTrace(sectionKey)
343
+ recordClientLightTrace({
344
+ phase: 'gpuStaged',
345
+ sectionKey,
346
+ displayedStart: gpu?.displayedStart,
347
+ displayedCount: gpu?.displayedCount,
348
+ candidateStart: gpu?.candidateStart,
349
+ candidateCount: gpu?.candidateCount,
350
+ pendingReplace: gpu?.pendingReplace,
351
+ pendingMove: gpu?.pendingMove,
352
+ unuploadedRanges: gpu?.unuploadedRanges
353
+ })
354
+ }
340
355
  return true
341
356
  }
342
357
 
@@ -367,9 +382,9 @@ export class GlobalLegacyBuffer {
367
382
  getSectionDrawStart(sectionKey: string): number | undefined {
368
383
  const slot = this.sectionSlots.get(sectionKey)
369
384
  if (!slot) return undefined
370
- if (this.pendingMove?.key === sectionKey) return this.pendingMove.oldStart
371
385
  const replace = this.pendingReplace.get(sectionKey)
372
386
  if (replace) return replace.oldStart
387
+ if (this.pendingMove?.key === sectionKey) return this.pendingMove.oldStart
373
388
  if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return undefined
374
389
  return slot.start
375
390
  }
@@ -377,13 +392,37 @@ export class GlobalLegacyBuffer {
377
392
  getSectionDrawCount(sectionKey: string): number | undefined {
378
393
  const slot = this.sectionSlots.get(sectionKey)
379
394
  if (!slot) return undefined
380
- if (this.pendingMove?.key === sectionKey) return this.pendingMove.count
381
395
  const replace = this.pendingReplace.get(sectionKey)
382
396
  if (replace) return replace.oldCount
397
+ if (this.pendingMove?.key === sectionKey) return this.pendingMove.count
383
398
  if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return undefined
384
399
  return slot.count
385
400
  }
386
401
 
402
+ getGpuSlotTrace(sectionKey: string):
403
+ | {
404
+ displayedStart?: number
405
+ displayedCount?: number
406
+ candidateStart: number
407
+ candidateCount: number
408
+ pendingReplace: boolean
409
+ pendingMove: boolean
410
+ unuploadedRanges: number
411
+ }
412
+ | undefined {
413
+ const slot = this.sectionSlots.get(sectionKey)
414
+ if (!slot) return undefined
415
+ return {
416
+ displayedStart: this.getSectionDrawStart(sectionKey),
417
+ displayedCount: this.getSectionDrawCount(sectionKey),
418
+ candidateStart: slot.start,
419
+ candidateCount: slot.count,
420
+ pendingReplace: this.pendingReplace.has(sectionKey),
421
+ pendingMove: this.pendingMove?.key === sectionKey,
422
+ unuploadedRanges: this.pendingRanges.length
423
+ }
424
+ }
425
+
387
426
  getPendingMove(): PendingMove | null {
388
427
  return this.pendingMove
389
428
  }
@@ -425,6 +464,20 @@ export class GlobalLegacyBuffer {
425
464
  this.markDirty(newStart, newStart + section.count - 1)
426
465
  this.pendingMove = { key: section.key, oldStart, newStart, count: section.count }
427
466
  this.layoutVersion++
467
+ if (isClientLightTraceEnabled()) {
468
+ const gpu = this.getGpuSlotTrace(section.key)
469
+ recordClientLightTrace({
470
+ phase: 'gpuStaged',
471
+ sectionKey: section.key,
472
+ displayedStart: gpu?.displayedStart,
473
+ displayedCount: gpu?.displayedCount,
474
+ candidateStart: gpu?.candidateStart,
475
+ candidateCount: gpu?.candidateCount,
476
+ pendingReplace: gpu?.pendingReplace,
477
+ pendingMove: true,
478
+ unuploadedRanges: gpu?.unuploadedRanges
479
+ })
480
+ }
428
481
  }
429
482
 
430
483
  updateDrawSpans(visible: VisibleSectionSpan[], mode: 'opaque' | 'sortedBlend'): void {
@@ -696,6 +749,13 @@ export class GlobalLegacyBuffer {
696
749
  r.start = quadOffset + quadCount
697
750
  }
698
751
  this.uploadEpoch++
752
+ if (isClientLightTraceEnabled()) {
753
+ recordClientLightTraceGpuSample('gpuUploaded', this.highWatermark, () => ({
754
+ phase: 'gpuUploaded',
755
+ unuploadedRanges: this.pendingRanges.length,
756
+ drawableFaces: this.highWatermark
757
+ }))
758
+ }
699
759
  }
700
760
 
701
761
  uploadDirtyIndexRange(): void {
@@ -898,6 +958,9 @@ export class GlobalLegacyBuffer {
898
958
  private findMovableSection(maxCount: number): { key: string; start: number; count: number } | undefined {
899
959
  const sections: Array<{ key: string; start: number; count: number }> = []
900
960
  for (const [key, slot] of this.sectionSlots) {
961
+ if (this.pendingReplace.has(key)) continue
962
+ if (this.pendingMove?.key === key) continue
963
+ if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) continue
901
964
  sections.push({ key, start: slot.start, count: slot.count })
902
965
  }
903
966
  if (sections.length === 0) return undefined
@@ -1006,11 +1069,26 @@ export class GlobalLegacyBuffer {
1006
1069
  this.syncDefaultDrawGroups()
1007
1070
  this.layoutVersion++
1008
1071
  this.uploadEpoch++
1072
+ if (isClientLightTraceEnabled()) {
1073
+ const gpu = this.getGpuSlotTrace(key)
1074
+ recordClientLightTrace({
1075
+ phase: 'gpuCommitted',
1076
+ sectionKey: key,
1077
+ displayedStart: gpu?.displayedStart,
1078
+ displayedCount: gpu?.displayedCount,
1079
+ candidateStart: gpu?.candidateStart,
1080
+ candidateCount: gpu?.candidateCount,
1081
+ pendingReplace: false,
1082
+ pendingMove: gpu?.pendingMove,
1083
+ unuploadedRanges: gpu?.unuploadedRanges
1084
+ })
1085
+ }
1009
1086
  }
1010
1087
 
1011
1088
  private finalizePendingMove(): void {
1012
1089
  const move = this.pendingMove
1013
1090
  if (!move) return
1091
+ const movedKey = move.key
1014
1092
 
1015
1093
  const { oldStart, count } = move
1016
1094
  this.zeroAndFreeSlot(oldStart, count)
@@ -1019,6 +1097,20 @@ export class GlobalLegacyBuffer {
1019
1097
  this.pendingMove = null
1020
1098
  this.layoutVersion++
1021
1099
  this.uploadEpoch++
1100
+ if (isClientLightTraceEnabled()) {
1101
+ const gpu = this.getGpuSlotTrace(movedKey)
1102
+ recordClientLightTrace({
1103
+ phase: 'gpuCommitted',
1104
+ sectionKey: movedKey,
1105
+ displayedStart: gpu?.displayedStart,
1106
+ displayedCount: gpu?.displayedCount,
1107
+ candidateStart: gpu?.candidateStart,
1108
+ candidateCount: gpu?.candidateCount,
1109
+ pendingReplace: gpu?.pendingReplace,
1110
+ pendingMove: false,
1111
+ unuploadedRanges: gpu?.unuploadedRanges
1112
+ })
1113
+ }
1022
1114
  }
1023
1115
 
1024
1116
  private growCapacity(minQuads: number): void {