minecraft-renderer 0.1.36 → 0.1.38
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/dist/minecraft-renderer.js +58 -58
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +408 -408
- package/package.json +1 -1
- package/src/graphicsBackend/config.ts +1 -1
- package/src/three/chunkMeshManager.ts +167 -6
- package/src/three/holdingBlock.ts +39 -35
- package/src/three/holdingBlockItemIdentity.test.ts +43 -0
- package/src/three/holdingBlockItemIdentity.ts +30 -0
- package/src/three/holdingBlockLegacy.ts +22 -25
- package/src/three/modules/sciFiWorldReveal.ts +2 -2
- package/src/three/sceneOrigin.ts +2 -2
- package/src/three/worldRendererThree.ts +139 -24
package/package.json
CHANGED
|
@@ -28,17 +28,46 @@ export interface SectionObject extends THREE.Group {
|
|
|
28
28
|
headsContainer?: THREE.Group
|
|
29
29
|
bannersContainer?: THREE.Group
|
|
30
30
|
boxHelper?: THREE.BoxHelper
|
|
31
|
+
/**
|
|
32
|
+
* World-space coordinates of the section origin. Cached so that
|
|
33
|
+
* {@link ChunkMeshManager.updateBoxHelper} can position lazily-created
|
|
34
|
+
* border helpers correctly under camera-relative rendering, where
|
|
35
|
+
* `mesh.position` is proxied to (world - sceneOrigin) and cannot be
|
|
36
|
+
* reused directly for objects that are tracked separately.
|
|
37
|
+
*/
|
|
38
|
+
worldX?: number
|
|
39
|
+
worldY?: number
|
|
40
|
+
worldZ?: number
|
|
31
41
|
foutain?: boolean
|
|
42
|
+
/**
|
|
43
|
+
* True while the section is held invisible by the "Batch Chunks Display"
|
|
44
|
+
* (`_renderByChunks`) feature, waiting for the parent chunk to finish meshing
|
|
45
|
+
* before being shown together with the rest of the chunk.
|
|
46
|
+
*/
|
|
47
|
+
_waitingForChunkDisplay?: boolean
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
export class ChunkMeshManager {
|
|
35
51
|
private readonly meshPool: ChunkMeshPool[] = []
|
|
36
52
|
private readonly activeSections = new Map<string, ChunkMeshPool>()
|
|
37
53
|
readonly sectionObjects: Record<string, SectionObject> = {}
|
|
54
|
+
/**
|
|
55
|
+
* Sections kept invisible because the "Batch Chunks Display" option is on
|
|
56
|
+
* and their parent chunk hasn't finished meshing yet. Keyed by chunk key
|
|
57
|
+
* (`x,z`); flushed by `WorldRendererThree.finishChunk(chunkKey)`.
|
|
58
|
+
*/
|
|
59
|
+
readonly waitingChunksToDisplay: Record<string, string[]> = {}
|
|
38
60
|
private poolSize!: number
|
|
39
61
|
private maxPoolSize!: number
|
|
40
62
|
private minPoolSize!: number
|
|
41
63
|
private readonly signHeadsRenderer: SignHeadsRenderer
|
|
64
|
+
/**
|
|
65
|
+
* Shared transparent material used as the basis for the wireframe chunk
|
|
66
|
+
* border `BoxHelper` created lazily in {@link updateBoxHelper}. Kept on the
|
|
67
|
+
* manager so the BoxHelper machinery doesn't allocate a new material per
|
|
68
|
+
* section.
|
|
69
|
+
*/
|
|
70
|
+
private readonly chunkBoxMaterial = new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 })
|
|
42
71
|
|
|
43
72
|
// Performance tracking
|
|
44
73
|
private hits = 0
|
|
@@ -64,7 +93,7 @@ export class ChunkMeshManager {
|
|
|
64
93
|
|
|
65
94
|
constructor (
|
|
66
95
|
public worldRenderer: WorldRendererThree,
|
|
67
|
-
public scene: THREE.
|
|
96
|
+
public scene: THREE.Object3D,
|
|
68
97
|
public material: THREE.Material,
|
|
69
98
|
public worldHeight: number,
|
|
70
99
|
viewDistance = 3,
|
|
@@ -156,6 +185,17 @@ export class ChunkMeshManager {
|
|
|
156
185
|
// Store metadata
|
|
157
186
|
sectionObject.tilesCount = geometryData.positions.length / 3 / 4
|
|
158
187
|
sectionObject.blocksCount = geometryData.blocksCount
|
|
188
|
+
sectionObject.worldX = geometryData.sx
|
|
189
|
+
sectionObject.worldY = geometryData.sy
|
|
190
|
+
sectionObject.worldZ = geometryData.sz
|
|
191
|
+
// Stamp the section key so modules (e.g. sciFiWorldReveal) can resolve
|
|
192
|
+
// mesh -> section without falling back to sceneOrigin world-position math.
|
|
193
|
+
;(sectionObject as any).sectionKey = sectionKey
|
|
194
|
+
// Tag the group so `WorldRendererThree.getThirdPersonCamera` raycast can
|
|
195
|
+
// still find chunk meshes — the old `WorldBlockGeometry` set this name
|
|
196
|
+
// unconditionally; the pooling port lost that and only the border-helper
|
|
197
|
+
// path used to restore it.
|
|
198
|
+
sectionObject.name = 'chunk'
|
|
159
199
|
|
|
160
200
|
try {
|
|
161
201
|
// Add signs container
|
|
@@ -218,13 +258,64 @@ export class ChunkMeshManager {
|
|
|
218
258
|
this.scene.add(sectionObject)
|
|
219
259
|
sectionObject.matrixAutoUpdate = false
|
|
220
260
|
|
|
261
|
+
// Create chunk border helper eagerly when the option is on so freshly
|
|
262
|
+
// streamed sections immediately get the F3+G yellow wireframe instead of
|
|
263
|
+
// appearing only on the next toggle.
|
|
264
|
+
if (this.worldRenderer.displayOptions?.inWorldRenderingConfig?.showChunkBorders) {
|
|
265
|
+
this.updateBoxHelper(sectionKey, true)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Honor "Batch Chunks Display" (`_renderByChunks`): keep this section's
|
|
269
|
+
// mesh hidden until the whole chunk has finished meshing, so users see a
|
|
270
|
+
// chunk appear as a single 16xHx16 tile instead of streaming per-section.
|
|
271
|
+
// Updates to chunks that are already finished bypass batching to avoid
|
|
272
|
+
// flickering on block changes / lighting updates.
|
|
273
|
+
const chunkCoords = sectionKey.split(',')
|
|
274
|
+
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
|
|
275
|
+
const renderByChunks = !!this.worldRenderer.displayOptions
|
|
276
|
+
?.inWorldRenderingConfig?._renderByChunks
|
|
277
|
+
if (renderByChunks && !this.worldRenderer.finishedChunks[chunkKey]) {
|
|
278
|
+
sectionObject.visible = false
|
|
279
|
+
sectionObject._waitingForChunkDisplay = true
|
|
280
|
+
const list = this.waitingChunksToDisplay[chunkKey] ?? (this.waitingChunksToDisplay[chunkKey] = [])
|
|
281
|
+
if (!list.includes(sectionKey)) list.push(sectionKey)
|
|
282
|
+
}
|
|
283
|
+
|
|
221
284
|
return sectionObject
|
|
222
285
|
}
|
|
223
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Reveal all sections of a chunk that were held invisible by the
|
|
289
|
+
* "Batch Chunks Display" option. Called from `WorldRendererThree.finishChunk`.
|
|
290
|
+
*/
|
|
291
|
+
finishChunkDisplay (chunkKey: string): void {
|
|
292
|
+
const sectionKeys = this.waitingChunksToDisplay[chunkKey]
|
|
293
|
+
if (!sectionKeys) return
|
|
294
|
+
for (const sectionKey of sectionKeys) {
|
|
295
|
+
const sectionObject = this.sectionObjects[sectionKey]
|
|
296
|
+
if (!sectionObject) continue
|
|
297
|
+
sectionObject._waitingForChunkDisplay = false
|
|
298
|
+
sectionObject.visible = true
|
|
299
|
+
}
|
|
300
|
+
delete this.waitingChunksToDisplay[chunkKey]
|
|
301
|
+
}
|
|
302
|
+
|
|
224
303
|
cleanupSection (sectionKey: string) {
|
|
225
304
|
// Remove section object from scene
|
|
226
305
|
const sectionObject = this.sectionObjects[sectionKey]
|
|
227
306
|
if (sectionObject) {
|
|
307
|
+
// Drop from any pending "batch display" queue so we don't try to flip
|
|
308
|
+
// visibility on a stale (released) object later.
|
|
309
|
+
if (sectionObject._waitingForChunkDisplay) {
|
|
310
|
+
const chunkCoords = sectionKey.split(',')
|
|
311
|
+
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
|
|
312
|
+
const list = this.waitingChunksToDisplay[chunkKey]
|
|
313
|
+
if (list) {
|
|
314
|
+
const idx = list.indexOf(sectionKey)
|
|
315
|
+
if (idx !== -1) list.splice(idx, 1)
|
|
316
|
+
if (list.length === 0) delete this.waitingChunksToDisplay[chunkKey]
|
|
317
|
+
}
|
|
318
|
+
}
|
|
228
319
|
// Cleanup banner textures before disposing
|
|
229
320
|
if (sectionObject.bannersContainer) {
|
|
230
321
|
sectionObject.bannersContainer.traverse((child) => {
|
|
@@ -243,6 +334,21 @@ export class ChunkMeshManager {
|
|
|
243
334
|
}
|
|
244
335
|
this.worldRenderer.sceneOrigin.removeAndUntrackAll(sectionObject)
|
|
245
336
|
this.scene.remove(sectionObject)
|
|
337
|
+
// boxHelper lives directly on the scene (so it stays world-anchored
|
|
338
|
+
// under camera-relative rendering), so it must be cleaned up explicitly
|
|
339
|
+
// — `removeAndUntrackAll` above only walks `sectionObject` descendants.
|
|
340
|
+
if (sectionObject.boxHelper) {
|
|
341
|
+
this.worldRenderer.sceneOrigin.removeAndUntrack(sectionObject.boxHelper)
|
|
342
|
+
this.scene.remove(sectionObject.boxHelper)
|
|
343
|
+
sectionObject.boxHelper.geometry.dispose()
|
|
344
|
+
const helperMat = sectionObject.boxHelper.material as THREE.Material | THREE.Material[]
|
|
345
|
+
if (Array.isArray(helperMat)) {
|
|
346
|
+
for (const m of helperMat) m.dispose()
|
|
347
|
+
} else {
|
|
348
|
+
helperMat.dispose()
|
|
349
|
+
}
|
|
350
|
+
sectionObject.boxHelper = undefined
|
|
351
|
+
}
|
|
246
352
|
delete this.sectionObjects[sectionKey]
|
|
247
353
|
}
|
|
248
354
|
}
|
|
@@ -285,19 +391,28 @@ export class ChunkMeshManager {
|
|
|
285
391
|
/**
|
|
286
392
|
* Update box helper for a section
|
|
287
393
|
*/
|
|
288
|
-
updateBoxHelper (sectionKey: string, showChunkBorders: boolean, chunkBoxMaterial: THREE.Material) {
|
|
394
|
+
updateBoxHelper (sectionKey: string, showChunkBorders: boolean, chunkBoxMaterial: THREE.Material = this.chunkBoxMaterial) {
|
|
289
395
|
const sectionObject = this.sectionObjects[sectionKey]
|
|
290
396
|
if (!sectionObject?.mesh) return
|
|
291
397
|
|
|
292
398
|
if (showChunkBorders) {
|
|
293
399
|
if (!sectionObject.boxHelper) {
|
|
294
|
-
// mesh
|
|
400
|
+
// Build a 16x16x16 reference mesh in world coordinates so BoxHelper's
|
|
401
|
+
// `setFromObject` produces the correct geometry. The reference mesh is
|
|
402
|
+
// not added to the scene; only the resulting BoxHelper is.
|
|
295
403
|
const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), chunkBoxMaterial)
|
|
296
|
-
staticChunkMesh.position.copy(sectionObject.mesh.position)
|
|
297
404
|
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
|
|
298
405
|
boxHelper.name = 'helper'
|
|
299
|
-
|
|
300
|
-
|
|
406
|
+
// Add directly to the scene and track it through sceneOrigin so that
|
|
407
|
+
// camera-relative rendering (floating origin) keeps the helper pinned
|
|
408
|
+
// to its world coordinates instead of following the camera.
|
|
409
|
+
const sx = sectionObject.worldX ?? 0
|
|
410
|
+
const sy = sectionObject.worldY ?? 0
|
|
411
|
+
const sz = sectionObject.worldZ ?? 0
|
|
412
|
+
this.worldRenderer.sceneOrigin.track(boxHelper, { updateMatrix: true })
|
|
413
|
+
boxHelper.position.set(sx, sy, sz)
|
|
414
|
+
boxHelper.updateMatrix()
|
|
415
|
+
this.scene.add(boxHelper)
|
|
301
416
|
sectionObject.boxHelper = boxHelper
|
|
302
417
|
}
|
|
303
418
|
sectionObject.boxHelper.visible = true
|
|
@@ -306,6 +421,28 @@ export class ChunkMeshManager {
|
|
|
306
421
|
}
|
|
307
422
|
}
|
|
308
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Create / toggle chunk border helpers for every active section. Used by
|
|
426
|
+
* `WorldRendererThree.updateShowChunksBorder` so the F3+G hotkey works
|
|
427
|
+
* after the move from `WorldBlockGeometry` (which created the helpers
|
|
428
|
+
* eagerly per section) to the pooled `ChunkMeshManager`.
|
|
429
|
+
*/
|
|
430
|
+
updateAllBoxHelpers (showChunkBorders: boolean) {
|
|
431
|
+
for (const sectionKey of Object.keys(this.sectionObjects)) {
|
|
432
|
+
this.updateBoxHelper(sectionKey, showChunkBorders)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Forward to {@link SignHeadsRenderer.cleanChunkTextures} so callers in
|
|
438
|
+
* `WorldRendererThree` (which historically owned the sign-texture cache)
|
|
439
|
+
* can invalidate cached sign textures when a section is marked dirty,
|
|
440
|
+
* without reaching into the manager's private members.
|
|
441
|
+
*/
|
|
442
|
+
cleanSignChunkTextures (x: number, z: number) {
|
|
443
|
+
this.signHeadsRenderer.cleanChunkTextures(x, z)
|
|
444
|
+
}
|
|
445
|
+
|
|
309
446
|
/**
|
|
310
447
|
* Get mesh for section if it exists
|
|
311
448
|
*/
|
|
@@ -459,6 +596,7 @@ export class ChunkMeshManager {
|
|
|
459
596
|
|
|
460
597
|
this.meshPool.length = 0
|
|
461
598
|
this.activeSections.clear()
|
|
599
|
+
this.chunkBoxMaterial.dispose()
|
|
462
600
|
}
|
|
463
601
|
|
|
464
602
|
// Private helper methods
|
|
@@ -667,6 +805,12 @@ export class ChunkMeshManager {
|
|
|
667
805
|
updateSectionsVisibility (): void {
|
|
668
806
|
const cameraPos = this.worldRenderer.cameraSectionPos
|
|
669
807
|
for (const [sectionKey, sectionObject] of Object.entries(this.sectionObjects)) {
|
|
808
|
+
// Don't override "Batch Chunks Display" hiding — those sections must
|
|
809
|
+
// stay invisible until their chunk finishes meshing.
|
|
810
|
+
if (sectionObject._waitingForChunkDisplay) {
|
|
811
|
+
sectionObject.visible = false
|
|
812
|
+
continue
|
|
813
|
+
}
|
|
670
814
|
if (!this.performanceOverrideDistance) {
|
|
671
815
|
sectionObject.visible = true
|
|
672
816
|
continue
|
|
@@ -805,4 +949,21 @@ class SignHeadsRenderer {
|
|
|
805
949
|
textures[texturekey] = tex
|
|
806
950
|
return tex
|
|
807
951
|
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Dispose all cached sign textures for the chunk containing world coords
|
|
955
|
+
* (x, z). Called from `WorldRendererThree.cleanChunkTextures` so that
|
|
956
|
+
* re-meshes triggered by `setSectionDirty` (e.g. a player edits a sign)
|
|
957
|
+
* pick up fresh block-entity NBT instead of returning the stale cached
|
|
958
|
+
* texture from {@link SignHeadsRenderer.getSignTexture}.
|
|
959
|
+
*/
|
|
960
|
+
cleanChunkTextures (x: number, z: number) {
|
|
961
|
+
const key = `${Math.floor(x / 16)},${Math.floor(z / 16)}`
|
|
962
|
+
const textures = this.chunkTextures.get(key)
|
|
963
|
+
if (!textures) return
|
|
964
|
+
for (const k of Object.keys(textures)) {
|
|
965
|
+
textures[k].dispose()
|
|
966
|
+
delete textures[k]
|
|
967
|
+
}
|
|
968
|
+
}
|
|
808
969
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
2
|
import * as THREE from 'three'
|
|
3
3
|
import * as tweenJs from '@tweenjs/tween.js'
|
|
4
|
-
import PrismarineItem from 'prismarine-item'
|
|
5
4
|
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
|
6
5
|
import { BlockModel } from 'mc-assets'
|
|
7
6
|
import { DebugGui } from '../lib/DebugGui'
|
|
@@ -17,8 +16,14 @@ import { getThreeBlockModelGroup } from '../mesher/standaloneRenderer'
|
|
|
17
16
|
import { IndexedData } from 'minecraft-data'
|
|
18
17
|
import { WorldRendererConfig } from '../graphicsBackend'
|
|
19
18
|
import { computeCameraBob, type CameraBobInput } from '../lib/cameraBobbing'
|
|
19
|
+
import { getFirstPersonItemSpecificProps, getHandItemRenderKey } from './holdingBlockItemIdentity'
|
|
20
20
|
|
|
21
21
|
const _tempMat = new THREE.Matrix4()
|
|
22
|
+
const wrapPi = (a: number) => {
|
|
23
|
+
a = (a + Math.PI) % (Math.PI * 2)
|
|
24
|
+
if (a < 0) a += Math.PI * 2
|
|
25
|
+
return a - Math.PI
|
|
26
|
+
}
|
|
22
27
|
|
|
23
28
|
// Vanilla renderPlayerArm transform chain
|
|
24
29
|
function buildBareHandMatrix(swingProgress: number, equipProgress: number): THREE.Matrix4 {
|
|
@@ -94,6 +99,7 @@ export default class HoldingBlock implements IHoldingBlock {
|
|
|
94
99
|
equipProgress = 0 // 0 = fully visible, 1 = hidden
|
|
95
100
|
stopUpdate = false
|
|
96
101
|
lastHeldItem: HandItemBlock | undefined
|
|
102
|
+
lastHeldItemRenderKey: string | undefined
|
|
97
103
|
currentDisplayType: 'hand' | 'item' | 'block' = 'hand'
|
|
98
104
|
isSwinging = false
|
|
99
105
|
nextIterStopCallbacks: Array<() => void> | undefined
|
|
@@ -116,6 +122,7 @@ export default class HoldingBlock implements IHoldingBlock {
|
|
|
116
122
|
|
|
117
123
|
constructor(public worldRenderer: WorldRendererThree, public offHand = false) {
|
|
118
124
|
this.initCameraGroup()
|
|
125
|
+
this.swingAnimator = new HandSwingAnimator()
|
|
119
126
|
this.unsubs.push(
|
|
120
127
|
this.worldRenderer.onReactivePlayerStateUpdated('heldItemMain', () => {
|
|
121
128
|
if (!this.offHand) {
|
|
@@ -332,44 +339,32 @@ export default class HoldingBlock implements IHoldingBlock {
|
|
|
332
339
|
}
|
|
333
340
|
|
|
334
341
|
isDifferentItem(block: HandItemBlock | undefined) {
|
|
335
|
-
|
|
336
|
-
if (!this.lastHeldItem) {
|
|
337
|
-
return true
|
|
338
|
-
}
|
|
339
|
-
if (this.lastHeldItem.name !== block?.name) {
|
|
340
|
-
return true
|
|
341
|
-
}
|
|
342
|
-
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
|
343
|
-
if (!Item.equal(this.lastHeldItem.fullItem, block?.fullItem ?? {}) || JSON.stringify(this.lastHeldItem.fullItem.components) !== JSON.stringify(block?.fullItem?.components)) {
|
|
344
|
-
return true
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return false
|
|
342
|
+
return this.lastHeldItemRenderKey !== getHandItemRenderKey(this.worldRenderer, block)
|
|
348
343
|
}
|
|
349
344
|
|
|
350
345
|
updateCameraGroup() {
|
|
351
346
|
if (this.stopUpdate) return
|
|
352
347
|
const { camera } = this
|
|
353
348
|
|
|
354
|
-
// Hand rotation momentum (xBob/yBob) — vanilla Minecraft inertia effect
|
|
355
|
-
// Use base rotation from CameraShake (actual player view angles)
|
|
356
349
|
const now = performance.now()
|
|
357
350
|
const baseRotation = this.worldRenderer.cameraShake.getBaseRotation()
|
|
358
351
|
const actualPitch = baseRotation.pitch
|
|
359
352
|
const actualYaw = baseRotation.yaw
|
|
353
|
+
|
|
360
354
|
if (this.lastBobUpdateTime === 0) {
|
|
361
355
|
this.xBob = actualPitch
|
|
362
356
|
this.yBob = actualYaw
|
|
363
|
-
this.lastBobUpdateTime = now
|
|
364
357
|
} else {
|
|
365
358
|
const dt = Math.min((now - this.lastBobUpdateTime) / 1000, 0.1)
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
this.xBob += (actualPitch - this.xBob) *
|
|
369
|
-
this.yBob += (actualYaw - this.yBob) *
|
|
359
|
+
const pitchFactor = 1 - Math.pow(0.5, dt * 28)
|
|
360
|
+
const yawFactor = 1 - Math.pow(0.5, dt * 36)
|
|
361
|
+
this.xBob += (actualPitch - this.xBob) * pitchFactor
|
|
362
|
+
this.yBob += wrapPi(actualYaw - this.yBob) * yawFactor
|
|
370
363
|
}
|
|
371
|
-
|
|
372
|
-
|
|
364
|
+
|
|
365
|
+
this.lastBobUpdateTime = now
|
|
366
|
+
const pitchOffset = (actualPitch - this.xBob) * -0.05
|
|
367
|
+
const yawOffset = wrapPi(actualYaw - this.yBob) * -0.035
|
|
373
368
|
|
|
374
369
|
this.cameraGroup.position.copy(camera.position)
|
|
375
370
|
this.cameraGroup.rotation.copy(camera.rotation)
|
|
@@ -431,11 +426,7 @@ export default class HoldingBlock implements IHoldingBlock {
|
|
|
431
426
|
const result = this.worldRenderer.entities.getItemMesh({
|
|
432
427
|
...handItem.fullItem,
|
|
433
428
|
itemId: handItem.id,
|
|
434
|
-
},
|
|
435
|
-
'minecraft:display_context': 'firstperson',
|
|
436
|
-
'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks,
|
|
437
|
-
'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks,
|
|
438
|
-
}, false, this.lastItemModelName)
|
|
429
|
+
}, getFirstPersonItemSpecificProps(this.worldRenderer), false, this.lastItemModelName)
|
|
439
430
|
if (result) {
|
|
440
431
|
const { mesh: itemMesh, isBlock, modelName } = result
|
|
441
432
|
if (isBlock) {
|
|
@@ -487,8 +478,11 @@ export default class HoldingBlock implements IHoldingBlock {
|
|
|
487
478
|
this.holdingBlock?.removeFromParent()
|
|
488
479
|
this.holdingBlock = undefined
|
|
489
480
|
this.currentDisplayType = 'hand'
|
|
490
|
-
this.swingAnimator
|
|
491
|
-
|
|
481
|
+
const swingAnimator = this.swingAnimator
|
|
482
|
+
swingAnimator?.stopSwing()
|
|
483
|
+
if (swingAnimator) {
|
|
484
|
+
swingAnimator.type = 'hand'
|
|
485
|
+
}
|
|
492
486
|
this.idleAnimator = undefined
|
|
493
487
|
return
|
|
494
488
|
}
|
|
@@ -516,10 +510,14 @@ export default class HoldingBlock implements IHoldingBlock {
|
|
|
516
510
|
|
|
517
511
|
switchRequest = 0
|
|
518
512
|
async setNewItem(handItem?: HandItemBlock) {
|
|
519
|
-
|
|
513
|
+
const nextRenderKey = getHandItemRenderKey(this.worldRenderer, handItem)
|
|
514
|
+
const itemChanged = this.lastHeldItemRenderKey !== nextRenderKey
|
|
515
|
+
this.lastHeldItem = handItem
|
|
516
|
+
if (!itemChanged) return
|
|
517
|
+
|
|
518
|
+
this.lastHeldItemRenderKey = nextRenderKey
|
|
520
519
|
this.lastItemModelName = undefined
|
|
521
520
|
const switchRequest = ++this.switchRequest
|
|
522
|
-
this.lastHeldItem = handItem
|
|
523
521
|
|
|
524
522
|
let playAppearAnimation = false
|
|
525
523
|
if (this.holdingBlock) {
|
|
@@ -535,7 +533,11 @@ export default class HoldingBlock implements IHoldingBlock {
|
|
|
535
533
|
|
|
536
534
|
if (!handItem) {
|
|
537
535
|
this.currentDisplayType = 'hand'
|
|
538
|
-
|
|
536
|
+
const swingAnimator = this.swingAnimator
|
|
537
|
+
swingAnimator?.stopSwing()
|
|
538
|
+
if (swingAnimator) {
|
|
539
|
+
swingAnimator.type = 'hand'
|
|
540
|
+
}
|
|
539
541
|
this.idleAnimator = undefined
|
|
540
542
|
this.blockSwapAnimation = undefined
|
|
541
543
|
return
|
|
@@ -553,8 +555,10 @@ export default class HoldingBlock implements IHoldingBlock {
|
|
|
553
555
|
await this.playBlockSwapAnimation('appeared')
|
|
554
556
|
}
|
|
555
557
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
+
const swingAnimator = this.swingAnimator
|
|
559
|
+
if (swingAnimator) {
|
|
560
|
+
swingAnimator.type = result.type
|
|
561
|
+
}
|
|
558
562
|
// Idle animation disabled — walking bob is handled by vanilla bobView applied to cameraGroup
|
|
559
563
|
this.idleAnimator = undefined
|
|
560
564
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { expect, test, vi } from 'vitest'
|
|
3
|
+
import { getHandItemRenderKey } from './holdingBlockItemIdentity'
|
|
4
|
+
|
|
5
|
+
test('hand item render key ignores live use state but keeps first-person display context', () => {
|
|
6
|
+
const getItemRenderData = vi.fn(() => ({ modelName: 'item/bow' }))
|
|
7
|
+
const worldRenderer = {
|
|
8
|
+
playerStateReactive: {
|
|
9
|
+
itemUsageTicks: 20,
|
|
10
|
+
},
|
|
11
|
+
resourcesManager: {
|
|
12
|
+
currentResources: {},
|
|
13
|
+
},
|
|
14
|
+
getItemRenderData,
|
|
15
|
+
} as any
|
|
16
|
+
|
|
17
|
+
const handItem = {
|
|
18
|
+
type: 'item',
|
|
19
|
+
id: 261,
|
|
20
|
+
name: 'minecraft:bow',
|
|
21
|
+
fullItem: {
|
|
22
|
+
count: 1,
|
|
23
|
+
},
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
const activeKey = getHandItemRenderKey(worldRenderer, handItem)
|
|
27
|
+
worldRenderer.playerStateReactive.itemUsageTicks = 0
|
|
28
|
+
const idleKey = getHandItemRenderKey(worldRenderer, handItem)
|
|
29
|
+
|
|
30
|
+
expect(activeKey).toBe(idleKey)
|
|
31
|
+
expect(getItemRenderData).toHaveBeenNthCalledWith(1, {
|
|
32
|
+
...handItem.fullItem,
|
|
33
|
+
itemId: handItem.id,
|
|
34
|
+
}, {
|
|
35
|
+
'minecraft:display_context': 'firstperson',
|
|
36
|
+
})
|
|
37
|
+
expect(getItemRenderData).toHaveBeenNthCalledWith(2, {
|
|
38
|
+
...handItem.fullItem,
|
|
39
|
+
itemId: handItem.id,
|
|
40
|
+
}, {
|
|
41
|
+
'minecraft:display_context': 'firstperson',
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import type { HandItemBlock, ItemSpecificContextProperties } from '../playerState/types'
|
|
3
|
+
import type { WorldRendererThree } from './worldRendererThree'
|
|
4
|
+
|
|
5
|
+
export const getFirstPersonItemSpecificProps = (worldRenderer: WorldRendererThree): ItemSpecificContextProperties => ({
|
|
6
|
+
'minecraft:display_context': 'firstperson',
|
|
7
|
+
'minecraft:use_duration': worldRenderer.playerStateReactive.itemUsageTicks,
|
|
8
|
+
'minecraft:using_item': !!worldRenderer.playerStateReactive.itemUsageTicks,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const getFirstPersonItemIdentityProps = (): ItemSpecificContextProperties => ({
|
|
12
|
+
'minecraft:display_context': 'firstperson',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const getHandItemRenderKey = (worldRenderer: WorldRendererThree, handItem?: HandItemBlock) => {
|
|
16
|
+
if (!handItem) return 'empty'
|
|
17
|
+
if (handItem.type === 'hand') return 'hand'
|
|
18
|
+
|
|
19
|
+
const itemIdentifier = handItem.name ?? (handItem.id !== undefined ? `#${handItem.id}` : 'unknown')
|
|
20
|
+
if (!worldRenderer.resourcesManager.currentResources) {
|
|
21
|
+
return `${handItem.type}:${itemIdentifier}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const renderData = worldRenderer.getItemRenderData({
|
|
25
|
+
...handItem.fullItem,
|
|
26
|
+
itemId: handItem.id,
|
|
27
|
+
}, getFirstPersonItemIdentityProps())
|
|
28
|
+
|
|
29
|
+
return `${handItem.type}:${itemIdentifier}:${renderData.modelName}`
|
|
30
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
2
|
import * as THREE from 'three'
|
|
3
3
|
import * as tweenJs from '@tweenjs/tween.js'
|
|
4
|
-
import PrismarineItem from 'prismarine-item'
|
|
5
4
|
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
|
6
5
|
import { BlockModel } from 'mc-assets'
|
|
7
6
|
import { DebugGui } from '../lib/DebugGui'
|
|
@@ -17,6 +16,7 @@ import { getThreeBlockModelGroup } from '../mesher/standaloneRenderer'
|
|
|
17
16
|
import { IndexedData } from 'minecraft-data'
|
|
18
17
|
import { WorldRendererConfig } from '../graphicsBackend'
|
|
19
18
|
import { IHoldingBlock } from './holdingBlockTypes'
|
|
19
|
+
import { getFirstPersonItemSpecificProps, getHandItemRenderKey } from './holdingBlockItemIdentity'
|
|
20
20
|
|
|
21
21
|
const rotationPositionData = {
|
|
22
22
|
itemRight: {
|
|
@@ -103,6 +103,7 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
|
|
|
103
103
|
camera = new THREE.PerspectiveCamera(75, 1, 0.1, 100)
|
|
104
104
|
stopUpdate = false
|
|
105
105
|
lastHeldItem: HandItemBlock | undefined
|
|
106
|
+
lastHeldItemRenderKey: string | undefined
|
|
106
107
|
isSwinging = false
|
|
107
108
|
nextIterStopCallbacks: Array<() => void> | undefined
|
|
108
109
|
idleAnimator: HandIdleAnimator | undefined
|
|
@@ -304,19 +305,7 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
|
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
isDifferentItem(block: HandItemBlock | undefined) {
|
|
307
|
-
|
|
308
|
-
if (!this.lastHeldItem) {
|
|
309
|
-
return true
|
|
310
|
-
}
|
|
311
|
-
if (this.lastHeldItem.name !== block?.name) {
|
|
312
|
-
return true
|
|
313
|
-
}
|
|
314
|
-
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
|
315
|
-
if (!Item.equal(this.lastHeldItem.fullItem, block?.fullItem ?? {}) || JSON.stringify(this.lastHeldItem.fullItem.components) !== JSON.stringify(block?.fullItem?.components)) {
|
|
316
|
-
return true
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return false
|
|
308
|
+
return this.lastHeldItemRenderKey !== getHandItemRenderKey(this.worldRenderer, block)
|
|
320
309
|
}
|
|
321
310
|
|
|
322
311
|
updateCameraGroup() {
|
|
@@ -355,11 +344,7 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
|
|
|
355
344
|
const result = this.worldRenderer.entities.getItemMesh({
|
|
356
345
|
...handItem.fullItem,
|
|
357
346
|
itemId: handItem.id,
|
|
358
|
-
},
|
|
359
|
-
'minecraft:display_context': 'firstperson',
|
|
360
|
-
'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks,
|
|
361
|
-
'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks,
|
|
362
|
-
}, false, this.lastItemModelName)
|
|
347
|
+
}, getFirstPersonItemSpecificProps(this.worldRenderer), false, this.lastItemModelName)
|
|
363
348
|
if (result) {
|
|
364
349
|
const { mesh: itemMesh, isBlock, modelName } = result
|
|
365
350
|
if (isBlock) {
|
|
@@ -421,10 +406,14 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
|
|
|
421
406
|
|
|
422
407
|
switchRequest = 0
|
|
423
408
|
async setNewItem(handItem?: HandItemBlock) {
|
|
424
|
-
|
|
409
|
+
const nextRenderKey = getHandItemRenderKey(this.worldRenderer, handItem)
|
|
410
|
+
const itemChanged = this.lastHeldItemRenderKey !== nextRenderKey
|
|
411
|
+
this.lastHeldItem = handItem
|
|
412
|
+
if (!itemChanged) return
|
|
413
|
+
|
|
414
|
+
this.lastHeldItemRenderKey = nextRenderKey
|
|
425
415
|
this.lastItemModelName = undefined
|
|
426
416
|
const switchRequest = ++this.switchRequest
|
|
427
|
-
this.lastHeldItem = handItem
|
|
428
417
|
let playAppearAnimation = false
|
|
429
418
|
if (this.holdingBlock) {
|
|
430
419
|
// play disappear animation
|
|
@@ -472,7 +461,8 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
|
|
|
472
461
|
await this.playBlockSwapAnimation('appeared')
|
|
473
462
|
}
|
|
474
463
|
|
|
475
|
-
this.swingAnimator
|
|
464
|
+
this.swingAnimator ??= new HandSwingAnimator(this.holdingBlockInnerGroup)
|
|
465
|
+
this.swingAnimator.setHandMesh(this.holdingBlockInnerGroup)
|
|
476
466
|
this.swingAnimator.type = result.type
|
|
477
467
|
if (this.config.viewBobbing) {
|
|
478
468
|
this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.worldRenderer.playerStateReactive)
|
|
@@ -783,9 +773,9 @@ class HandSwingAnimator {
|
|
|
783
773
|
private lastTime = 0
|
|
784
774
|
private isAnimating = false
|
|
785
775
|
private stopRequested = false
|
|
786
|
-
private
|
|
787
|
-
private
|
|
788
|
-
private
|
|
776
|
+
private originalRotation: THREE.Euler
|
|
777
|
+
private originalPosition: THREE.Vector3
|
|
778
|
+
private originalScale: THREE.Vector3
|
|
789
779
|
|
|
790
780
|
readonly debugParams = {
|
|
791
781
|
// Animation timing
|
|
@@ -849,6 +839,13 @@ class HandSwingAnimator {
|
|
|
849
839
|
// this.debugGui.activate()
|
|
850
840
|
}
|
|
851
841
|
|
|
842
|
+
setHandMesh(handMesh: THREE.Object3D) {
|
|
843
|
+
this.handMesh = handMesh
|
|
844
|
+
this.originalRotation.copy(handMesh.rotation)
|
|
845
|
+
this.originalPosition.copy(handMesh.position)
|
|
846
|
+
this.originalScale.copy(handMesh.scale)
|
|
847
|
+
}
|
|
848
|
+
|
|
852
849
|
update() {
|
|
853
850
|
if (!this.isAnimating && !this.debugParams.animationStage) {
|
|
854
851
|
// If not animating, ensure we're at original position
|
|
@@ -58,7 +58,7 @@ export class SciFiWorldRevealModule implements RendererModuleController {
|
|
|
58
58
|
// Store original methods for patching
|
|
59
59
|
private originalFinishChunk: ((chunkKey: string) => void) | null = null
|
|
60
60
|
private originalDestroy: (() => void) | null = null
|
|
61
|
-
private originalSceneAdd: ((...object: THREE.Object3D[]) => THREE.
|
|
61
|
+
private originalSceneAdd: ((...object: THREE.Object3D[]) => THREE.Scene) | null = null
|
|
62
62
|
private originalHandleWorkerMessage: ((data: { geometry: MesherGeometryOutput; key: string; type: string }) => void) | null = null
|
|
63
63
|
|
|
64
64
|
private configEnabled = true
|
|
@@ -163,7 +163,7 @@ export class SciFiWorldRevealModule implements RendererModuleController {
|
|
|
163
163
|
|
|
164
164
|
// Patch scene.add to intercept mesh additions
|
|
165
165
|
this.originalSceneAdd = wr.scene.add.bind(wr.scene)
|
|
166
|
-
wr.scene.add = (...objects: THREE.Object3D[]): THREE.
|
|
166
|
+
wr.scene.add = (...objects: THREE.Object3D[]): THREE.Scene => {
|
|
167
167
|
// Call original add first
|
|
168
168
|
const result = this.originalSceneAdd!(...objects)
|
|
169
169
|
|
package/src/three/sceneOrigin.ts
CHANGED
|
@@ -174,7 +174,7 @@ export class SceneOrigin {
|
|
|
174
174
|
/** Untrack an Object3D and remove it from the scene */
|
|
175
175
|
removeAndUntrack(obj: Object3D): void {
|
|
176
176
|
this.untrack(obj)
|
|
177
|
-
|
|
177
|
+
obj.removeFromParent()
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
/** Untrack an Object3D and all its descendants, then remove from the scene */
|
|
@@ -182,7 +182,7 @@ export class SceneOrigin {
|
|
|
182
182
|
obj.traverse((child) => {
|
|
183
183
|
this.untrack(child)
|
|
184
184
|
})
|
|
185
|
-
|
|
185
|
+
obj.removeFromParent()
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
/** Get stored world position for a tracked object */
|