minecraft-renderer 0.1.0
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/README.md +297 -0
- package/dist/index.html +83 -0
- package/dist/static/image/arrow.6f27b59f.png +0 -0
- package/dist/static/image/blocksAtlasLatest.7850afa3.png +0 -0
- package/dist/static/image/blocksAtlasLegacy.5c76823d.png +0 -0
- package/dist/static/image/itemsAtlasLatest.36036f95.png +0 -0
- package/dist/static/image/itemsAtlasLegacy.dcb1b58d.png +0 -0
- package/dist/static/image/tipped_arrow.6f27b59f.png +0 -0
- package/dist/static/js/365.f05233ab.js +8462 -0
- package/dist/static/js/365.f05233ab.js.LICENSE.txt +52 -0
- package/dist/static/js/async/738.efa27644.js +1 -0
- package/dist/static/js/index.092ec5be.js +56 -0
- package/dist/static/js/lib-polyfill.98986ac5.js +1 -0
- package/dist/static/js/lib-react.5c9129e0.js +2 -0
- package/dist/static/js/lib-react.5c9129e0.js.LICENSE.txt +39 -0
- package/package.json +104 -0
- package/src/assets/destroy_stage_0.png +0 -0
- package/src/assets/destroy_stage_1.png +0 -0
- package/src/assets/destroy_stage_2.png +0 -0
- package/src/assets/destroy_stage_3.png +0 -0
- package/src/assets/destroy_stage_4.png +0 -0
- package/src/assets/destroy_stage_5.png +0 -0
- package/src/assets/destroy_stage_6.png +0 -0
- package/src/assets/destroy_stage_7.png +0 -0
- package/src/assets/destroy_stage_8.png +0 -0
- package/src/assets/destroy_stage_9.png +0 -0
- package/src/examples/README.md +146 -0
- package/src/examples/appViewerExample.ts +205 -0
- package/src/examples/initialMenuStart.ts +161 -0
- package/src/graphicsBackend/appViewer.ts +297 -0
- package/src/graphicsBackend/config.ts +119 -0
- package/src/graphicsBackend/index.ts +10 -0
- package/src/graphicsBackend/playerState.ts +61 -0
- package/src/graphicsBackend/types.ts +143 -0
- package/src/index.ts +97 -0
- package/src/lib/DebugGui.ts +190 -0
- package/src/lib/animationController.ts +85 -0
- package/src/lib/buildSharedConfig.mjs +1 -0
- package/src/lib/cameraBobbing.ts +94 -0
- package/src/lib/canvas2DOverlay.example.ts +361 -0
- package/src/lib/canvas2DOverlay.quickstart.ts +242 -0
- package/src/lib/canvas2DOverlay.ts +381 -0
- package/src/lib/cleanupDecorator.ts +29 -0
- package/src/lib/createPlayerObject.ts +55 -0
- package/src/lib/frameTimingCollector.ts +164 -0
- package/src/lib/guiRenderer.ts +283 -0
- package/src/lib/items.ts +140 -0
- package/src/lib/mesherlogReader.ts +131 -0
- package/src/lib/moreBlockDataGenerated.json +714 -0
- package/src/lib/preflatMap.json +1741 -0
- package/src/lib/simpleUtils.ts +40 -0
- package/src/lib/smoothSwitcher.ts +168 -0
- package/src/lib/spiral.ts +29 -0
- package/src/lib/ui/newStats.ts +120 -0
- package/src/lib/utils/proxy.ts +23 -0
- package/src/lib/utils/skins.ts +63 -0
- package/src/lib/utils.ts +76 -0
- package/src/lib/workerProxy.ts +342 -0
- package/src/lib/worldrendererCommon.ts +1088 -0
- package/src/mesher/mesher.ts +253 -0
- package/src/mesher/models.ts +769 -0
- package/src/mesher/modelsGeometryCommon.ts +142 -0
- package/src/mesher/shared.ts +80 -0
- package/src/mesher/standaloneRenderer.ts +270 -0
- package/src/mesher/test/a.ts +3 -0
- package/src/mesher/test/mesherTester.ts +76 -0
- package/src/mesher/test/playground.ts +19 -0
- package/src/mesher/test/test-perf.ts +74 -0
- package/src/mesher/test/tests.test.ts +56 -0
- package/src/mesher/world.ts +294 -0
- package/src/mesher/worldConstants.ts +1 -0
- package/src/modules/index.ts +11 -0
- package/src/modules/starfield.ts +313 -0
- package/src/modules/types.ts +110 -0
- package/src/playerState/playerState.ts +78 -0
- package/src/playerState/types.ts +36 -0
- package/src/playground/allEntitiesDebug.ts +170 -0
- package/src/playground/baseScene.ts +587 -0
- package/src/playground/mobileControls.tsx +268 -0
- package/src/playground/playground.html +83 -0
- package/src/playground/playground.ts +11 -0
- package/src/playground/playgroundUi.tsx +140 -0
- package/src/playground/reactUtils.ts +71 -0
- package/src/playground/scenes/allEntities.ts +13 -0
- package/src/playground/scenes/entities.ts +37 -0
- package/src/playground/scenes/floorRandom.ts +33 -0
- package/src/playground/scenes/frequentUpdates.ts +148 -0
- package/src/playground/scenes/geometryExport.ts +142 -0
- package/src/playground/scenes/index.ts +12 -0
- package/src/playground/scenes/lightingStarfield.ts +40 -0
- package/src/playground/scenes/main.ts +313 -0
- package/src/playground/scenes/railsCobweb.ts +14 -0
- package/src/playground/scenes/rotationIssue.ts +7 -0
- package/src/playground/scenes/slabsOptimization.ts +16 -0
- package/src/playground/scenes/transparencyIssue.ts +11 -0
- package/src/playground/shared.ts +79 -0
- package/src/resourcesManager/index.ts +5 -0
- package/src/resourcesManager/resourcesManager.ts +314 -0
- package/src/shims/minecraftData.ts +41 -0
- package/src/sign-renderer/index.html +21 -0
- package/src/sign-renderer/index.ts +216 -0
- package/src/sign-renderer/noop.js +1 -0
- package/src/sign-renderer/playground.ts +38 -0
- package/src/sign-renderer/tests.test.ts +69 -0
- package/src/sign-renderer/vite.config.ts +10 -0
- package/src/three/appShared.ts +75 -0
- package/src/three/bannerRenderer.ts +275 -0
- package/src/three/cameraShake.ts +120 -0
- package/src/three/cinimaticScript.ts +350 -0
- package/src/three/documentRenderer.ts +491 -0
- package/src/three/entities.ts +1580 -0
- package/src/three/entity/EntityMesh.ts +707 -0
- package/src/three/entity/animations.js +171 -0
- package/src/three/entity/armorModels.json +204 -0
- package/src/three/entity/armorModels.ts +36 -0
- package/src/three/entity/entities.json +6230 -0
- package/src/three/entity/exportedModels.js +38 -0
- package/src/three/entity/externalTextures.json +1 -0
- package/src/three/entity/models/allay.obj +325 -0
- package/src/three/entity/models/arrow.obj +60 -0
- package/src/three/entity/models/axolotl.obj +509 -0
- package/src/three/entity/models/blaze.obj +601 -0
- package/src/three/entity/models/boat.obj +417 -0
- package/src/three/entity/models/camel.obj +1061 -0
- package/src/three/entity/models/cat.obj +509 -0
- package/src/three/entity/models/chicken.obj +371 -0
- package/src/three/entity/models/cod.obj +371 -0
- package/src/three/entity/models/creeper.obj +279 -0
- package/src/three/entity/models/dolphin.obj +371 -0
- package/src/three/entity/models/ender_dragon.obj +2993 -0
- package/src/three/entity/models/enderman.obj +325 -0
- package/src/three/entity/models/endermite.obj +187 -0
- package/src/three/entity/models/fox.obj +463 -0
- package/src/three/entity/models/frog.obj +739 -0
- package/src/three/entity/models/ghast.obj +463 -0
- package/src/three/entity/models/goat.obj +601 -0
- package/src/three/entity/models/guardian.obj +1015 -0
- package/src/three/entity/models/horse.obj +1061 -0
- package/src/three/entity/models/llama.obj +509 -0
- package/src/three/entity/models/minecart.obj +233 -0
- package/src/three/entity/models/parrot.obj +509 -0
- package/src/three/entity/models/piglin.obj +739 -0
- package/src/three/entity/models/pillager.obj +371 -0
- package/src/three/entity/models/rabbit.obj +555 -0
- package/src/three/entity/models/sheep.obj +555 -0
- package/src/three/entity/models/shulker.obj +141 -0
- package/src/three/entity/models/sniffer.obj +693 -0
- package/src/three/entity/models/spider.obj +509 -0
- package/src/three/entity/models/tadpole.obj +95 -0
- package/src/three/entity/models/turtle.obj +371 -0
- package/src/three/entity/models/vex.obj +325 -0
- package/src/three/entity/models/villager.obj +509 -0
- package/src/three/entity/models/warden.obj +463 -0
- package/src/three/entity/models/witch.obj +647 -0
- package/src/three/entity/models/wolf.obj +509 -0
- package/src/three/entity/models/zombie_villager.obj +463 -0
- package/src/three/entity/objModels.js +1 -0
- package/src/three/fireworks.ts +661 -0
- package/src/three/fireworksRenderer.ts +434 -0
- package/src/three/globals.d.ts +7 -0
- package/src/three/graphicsBackend.ts +274 -0
- package/src/three/graphicsBackendOffThread.ts +107 -0
- package/src/three/hand.ts +89 -0
- package/src/three/holdingBlock.ts +926 -0
- package/src/three/index.ts +20 -0
- package/src/three/itemMesh.ts +427 -0
- package/src/three/modules.d.ts +14 -0
- package/src/three/panorama.ts +308 -0
- package/src/three/panoramaShared.ts +1 -0
- package/src/three/renderSlot.ts +82 -0
- package/src/three/skyboxRenderer.ts +406 -0
- package/src/three/starField.ts +13 -0
- package/src/three/threeJsMedia.ts +731 -0
- package/src/three/threeJsMethods.ts +15 -0
- package/src/three/threeJsParticles.ts +160 -0
- package/src/three/threeJsSound.ts +95 -0
- package/src/three/threeJsUtils.ts +90 -0
- package/src/three/waypointSprite.ts +435 -0
- package/src/three/waypoints.ts +163 -0
- package/src/three/world/cursorBlock.ts +172 -0
- package/src/three/world/vr.ts +257 -0
- package/src/three/worldGeometryExport.ts +259 -0
- package/src/three/worldGeometryHandler.ts +279 -0
- package/src/three/worldRendererThree.ts +1381 -0
- package/src/worldView/index.ts +6 -0
- package/src/worldView/types.ts +66 -0
- package/src/worldView/worldView.ts +424 -0
|
@@ -0,0 +1,1381 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { Vec3 } from 'vec3'
|
|
3
|
+
import nbt from 'prismarine-nbt'
|
|
4
|
+
import PrismarineChatLoader from 'prismarine-chat'
|
|
5
|
+
import * as tweenJs from '@tweenjs/tween.js'
|
|
6
|
+
import { Biome } from 'minecraft-data'
|
|
7
|
+
import { renderSign } from '../sign-renderer'
|
|
8
|
+
import { DisplayWorldOptions, GraphicsInitOptions } from '@/graphicsBackend/types'
|
|
9
|
+
import { chunkPos, sectionPos } from '../lib/simpleUtils'
|
|
10
|
+
import { WorldRendererCommon } from '../lib/worldrendererCommon'
|
|
11
|
+
import { addNewStat } from '../lib/ui/newStats'
|
|
12
|
+
import { MesherGeometryOutput } from '../mesher/shared'
|
|
13
|
+
import { ItemSpecificContextProperties } from '@/playerState/types'
|
|
14
|
+
import { setBlockPosition } from '../mesher/standaloneRenderer'
|
|
15
|
+
import { getBannerTexture, createBannerMesh, releaseBannerTexture } from './bannerRenderer'
|
|
16
|
+
import { getMyHand } from './hand'
|
|
17
|
+
import HoldingBlock from './holdingBlock'
|
|
18
|
+
import { getMesh } from './entity/EntityMesh'
|
|
19
|
+
import { armorModel } from './entity/armorModels'
|
|
20
|
+
import { disposeObject, loadThreeJsTextureFromBitmap } from './threeJsUtils'
|
|
21
|
+
import { CursorBlock } from './world/cursorBlock'
|
|
22
|
+
import { getItemUv } from './appShared'
|
|
23
|
+
import { Entities } from './entities'
|
|
24
|
+
import { ThreeJsSound } from './threeJsSound'
|
|
25
|
+
import { CameraShake } from './cameraShake'
|
|
26
|
+
import { ThreeJsMedia } from './threeJsMedia'
|
|
27
|
+
import { Fountain } from './threeJsParticles'
|
|
28
|
+
import { WaypointsRenderer } from './waypoints'
|
|
29
|
+
import { FireworksRenderer } from './fireworksRenderer'
|
|
30
|
+
import { CinimaticScriptRunner, CinimaticScript } from './cinimaticScript'
|
|
31
|
+
import { DEFAULT_TEMPERATURE, SkyboxRenderer } from './skyboxRenderer'
|
|
32
|
+
import { FireworksManager } from './fireworks'
|
|
33
|
+
import { downloadWorldGeometry } from './worldGeometryExport'
|
|
34
|
+
|
|
35
|
+
type SectionKey = string
|
|
36
|
+
|
|
37
|
+
export class WorldRendererThree extends WorldRendererCommon {
|
|
38
|
+
outputFormat = 'threeJs' as const
|
|
39
|
+
sectionObjects: Record<string, THREE.Object3D & { foutain?: boolean }> = {}
|
|
40
|
+
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
|
|
41
|
+
signsCache = new Map<string, any>()
|
|
42
|
+
starField: StarField
|
|
43
|
+
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
|
|
44
|
+
holdingBlock: HoldingBlock
|
|
45
|
+
holdingBlockLeft: HoldingBlock
|
|
46
|
+
scene = new THREE.Scene()
|
|
47
|
+
ambientLight = new THREE.AmbientLight(0xcc_cc_cc)
|
|
48
|
+
directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
|
|
49
|
+
entities = new Entities(this, (globalThis as any).mcData)
|
|
50
|
+
cameraGroupVr?: THREE.Object3D
|
|
51
|
+
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
|
|
52
|
+
itemsTexture!: THREE.Texture
|
|
53
|
+
cursorBlock: CursorBlock
|
|
54
|
+
onRender: Array<() => void> = []
|
|
55
|
+
cameraShake: CameraShake
|
|
56
|
+
cameraContainer!: THREE.Object3D
|
|
57
|
+
media: ThreeJsMedia
|
|
58
|
+
waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] }
|
|
59
|
+
waypoints: WaypointsRenderer
|
|
60
|
+
cinimaticScript: CinimaticScriptRunner
|
|
61
|
+
camera!: THREE.PerspectiveCamera
|
|
62
|
+
renderTimeAvg = 0
|
|
63
|
+
// Memory usage tracking (in bytes)
|
|
64
|
+
estimatedMemoryUsage = 0
|
|
65
|
+
sectionsOffsetsAnimations = {} as {
|
|
66
|
+
[chunkKey: string]: {
|
|
67
|
+
time: number,
|
|
68
|
+
// also specifies direction
|
|
69
|
+
speedX: number,
|
|
70
|
+
speedY: number,
|
|
71
|
+
speedZ: number,
|
|
72
|
+
|
|
73
|
+
currentOffsetX: number,
|
|
74
|
+
currentOffsetY: number,
|
|
75
|
+
currentOffsetZ: number,
|
|
76
|
+
|
|
77
|
+
limitX?: number,
|
|
78
|
+
limitY?: number,
|
|
79
|
+
limitZ?: number,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
fountains: Fountain[] = []
|
|
83
|
+
fireworksLegacy: FireworksRenderer
|
|
84
|
+
DEBUG_RAYCAST = false
|
|
85
|
+
skyboxRenderer: SkyboxRenderer
|
|
86
|
+
fireworks: FireworksManager
|
|
87
|
+
|
|
88
|
+
private currentPosTween?: tweenJs.Tween<THREE.Vector3>
|
|
89
|
+
private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }>
|
|
90
|
+
|
|
91
|
+
get tilesRendered() {
|
|
92
|
+
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get blocksRendered() {
|
|
96
|
+
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).blocksCount, 0)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
constructor(public renderer: THREE.WebGLRenderer, public initOptions: GraphicsInitOptions, public displayOptions: DisplayWorldOptions) {
|
|
100
|
+
if (!initOptions.resourcesManager) throw new Error('resourcesManager is required')
|
|
101
|
+
super(initOptions.resourcesManager, displayOptions, initOptions)
|
|
102
|
+
|
|
103
|
+
this.renderer = renderer
|
|
104
|
+
displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...'
|
|
105
|
+
this.starField = new StarField(this)
|
|
106
|
+
this.cursorBlock = new CursorBlock(this)
|
|
107
|
+
this.holdingBlock = new HoldingBlock(this)
|
|
108
|
+
this.holdingBlockLeft = new HoldingBlock(this, true)
|
|
109
|
+
|
|
110
|
+
// Initialize skybox renderer
|
|
111
|
+
this.skyboxRenderer = new SkyboxRenderer(this.scene, false, null)
|
|
112
|
+
void this.skyboxRenderer.init()
|
|
113
|
+
|
|
114
|
+
this.addDebugOverlay()
|
|
115
|
+
this.resetScene()
|
|
116
|
+
void this.init()
|
|
117
|
+
|
|
118
|
+
this.soundSystem = new ThreeJsSound(this)
|
|
119
|
+
this.cameraShake = new CameraShake(this, this.onRender)
|
|
120
|
+
this.media = new ThreeJsMedia(this)
|
|
121
|
+
this.fireworksLegacy = new FireworksRenderer(this)
|
|
122
|
+
this.waypoints = new WaypointsRenderer(this)
|
|
123
|
+
this.cinimaticScript = new CinimaticScriptRunner(
|
|
124
|
+
this,
|
|
125
|
+
(pos, yaw, pitch) => this.setCinimaticCamera(pos, yaw, pitch),
|
|
126
|
+
(fov) => this.setCinimaticFov(fov),
|
|
127
|
+
() => ({
|
|
128
|
+
position: new Vec3(this.cameraObject.position.x, this.cameraObject.position.y, this.cameraObject.position.z),
|
|
129
|
+
yaw: this.cameraShake.getBaseRotation().yaw,
|
|
130
|
+
pitch: this.cameraShake.getBaseRotation().pitch,
|
|
131
|
+
fov: this.camera.fov
|
|
132
|
+
})
|
|
133
|
+
)
|
|
134
|
+
this.fireworks = new FireworksManager(this.scene)
|
|
135
|
+
|
|
136
|
+
// this.fountain = new Fountain(this.scene, this.scene, {
|
|
137
|
+
// position: new THREE.Vector3(0, 10, 0),
|
|
138
|
+
// })
|
|
139
|
+
|
|
140
|
+
this.renderUpdateEmitter.on('chunkFinished', (chunkKey: string) => {
|
|
141
|
+
this.finishChunk(chunkKey)
|
|
142
|
+
})
|
|
143
|
+
this.worldSwitchActions()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get cameraObject() {
|
|
147
|
+
return this.cameraGroupVr ?? this.cameraContainer
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
worldSwitchActions() {
|
|
151
|
+
this.onWorldSwitched.push(() => {
|
|
152
|
+
// clear custom blocks
|
|
153
|
+
this.protocolCustomBlocks.clear()
|
|
154
|
+
// Reset section animations
|
|
155
|
+
this.sectionsOffsetsAnimations = {}
|
|
156
|
+
// Clear waypoints
|
|
157
|
+
this.waypoints.clear()
|
|
158
|
+
// Stop any running cinematic scripts
|
|
159
|
+
this.cinimaticScript.stopScript()
|
|
160
|
+
// Clear fireworks
|
|
161
|
+
this.fireworks.clear()
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
downloadWorldGeometry() {
|
|
166
|
+
downloadWorldGeometry(this, this.cameraObject.position, this.cameraShake.getBaseRotation(), 'world-geometry.json')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
updateEntity(e, isPosUpdate = false) {
|
|
170
|
+
const overrides = {
|
|
171
|
+
rotation: {
|
|
172
|
+
head: {
|
|
173
|
+
x: e.headPitch ?? e.pitch,
|
|
174
|
+
y: e.headYaw,
|
|
175
|
+
z: 0
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (isPosUpdate) {
|
|
180
|
+
this.entities.updateEntityPosition(e, false, overrides)
|
|
181
|
+
} else {
|
|
182
|
+
this.entities.update(e, overrides)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
updatePlayerEntity(e: any) {
|
|
187
|
+
this.entities.handlePlayerEntity(e)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
resetScene() {
|
|
191
|
+
this.scene.matrixAutoUpdate = false // for perf
|
|
192
|
+
this.scene.background = new THREE.Color(this.initOptions.config.sceneBackground)
|
|
193
|
+
this.scene.add(this.ambientLight)
|
|
194
|
+
this.directionalLight.position.set(1, 1, 0.5).normalize()
|
|
195
|
+
this.directionalLight.castShadow = true
|
|
196
|
+
this.scene.add(this.directionalLight)
|
|
197
|
+
|
|
198
|
+
const size = this.renderer.getSize(new THREE.Vector2())
|
|
199
|
+
this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
|
|
200
|
+
this.cameraContainer = new THREE.Object3D()
|
|
201
|
+
this.cameraContainer.add(this.camera)
|
|
202
|
+
this.scene.add(this.cameraContainer)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
override watchReactivePlayerState() {
|
|
206
|
+
super.watchReactivePlayerState()
|
|
207
|
+
this.onReactivePlayerStateUpdated('inWater', (value) => {
|
|
208
|
+
this.skyboxRenderer.updateWaterState(value, this.playerStateReactive.waterBreathing)
|
|
209
|
+
})
|
|
210
|
+
this.onReactivePlayerStateUpdated('waterBreathing', (value) => {
|
|
211
|
+
this.skyboxRenderer.updateWaterState(this.playerStateReactive.inWater, value)
|
|
212
|
+
})
|
|
213
|
+
this.onReactivePlayerStateUpdated('ambientLight', (value) => {
|
|
214
|
+
if (!value) return
|
|
215
|
+
this.ambientLight.intensity = value
|
|
216
|
+
})
|
|
217
|
+
this.onReactivePlayerStateUpdated('directionalLight', (value) => {
|
|
218
|
+
if (!value) return
|
|
219
|
+
this.directionalLight.intensity = value
|
|
220
|
+
})
|
|
221
|
+
this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => {
|
|
222
|
+
this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes)
|
|
223
|
+
})
|
|
224
|
+
this.onReactivePlayerStateUpdated('diggingBlock', (value) => {
|
|
225
|
+
this.cursorBlock.updateBreakAnimation(value ? { x: value.x, y: value.y, z: value.z } : undefined, value?.stage ?? null, value?.mergedShape)
|
|
226
|
+
})
|
|
227
|
+
this.onReactivePlayerStateUpdated('perspective', (value) => {
|
|
228
|
+
// Update camera perspective when it changes
|
|
229
|
+
const vecPos = new Vec3(this.cameraObject.position.x, this.cameraObject.position.y, this.cameraObject.position.z)
|
|
230
|
+
this.updateCamera(vecPos, this.cameraShake.getBaseRotation().yaw, this.cameraShake.getBaseRotation().pitch)
|
|
231
|
+
// todo also update camera when block within camera was changed
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
override watchReactiveConfig() {
|
|
236
|
+
super.watchReactiveConfig()
|
|
237
|
+
this.onReactiveConfigUpdated('showChunkBorders', (value) => {
|
|
238
|
+
this.updateShowChunksBorder(value)
|
|
239
|
+
})
|
|
240
|
+
this.onReactiveConfigUpdated('defaultSkybox', (value) => {
|
|
241
|
+
this.skyboxRenderer.updateDefaultSkybox(value)
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
changeHandSwingingState(isAnimationPlaying: boolean, isLeft = false) {
|
|
246
|
+
const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock
|
|
247
|
+
if (isAnimationPlaying) {
|
|
248
|
+
holdingBlock.startSwing()
|
|
249
|
+
} else {
|
|
250
|
+
holdingBlock.stopSwing()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async updateAssetsData(): Promise<void> {
|
|
255
|
+
const resources = this.resourcesManager.currentResources
|
|
256
|
+
|
|
257
|
+
const oldTexture = this.material.map
|
|
258
|
+
const oldItemsTexture = this.itemsTexture
|
|
259
|
+
|
|
260
|
+
const texture = loadThreeJsTextureFromBitmap(resources.blocksAtlasImage!)
|
|
261
|
+
texture.needsUpdate = true
|
|
262
|
+
texture.flipY = false
|
|
263
|
+
this.material.map = texture
|
|
264
|
+
|
|
265
|
+
const itemsTexture = loadThreeJsTextureFromBitmap(resources.itemsAtlasImage!)
|
|
266
|
+
itemsTexture.needsUpdate = true
|
|
267
|
+
itemsTexture.flipY = false
|
|
268
|
+
this.itemsTexture = itemsTexture
|
|
269
|
+
|
|
270
|
+
if (oldTexture) {
|
|
271
|
+
oldTexture.dispose()
|
|
272
|
+
}
|
|
273
|
+
if (oldItemsTexture) {
|
|
274
|
+
oldItemsTexture.dispose()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await super.updateAssetsData()
|
|
278
|
+
this.onAllTexturesLoaded()
|
|
279
|
+
if (Object.keys(this.loadedChunks).length > 0) {
|
|
280
|
+
console.log('rerendering chunks because of texture update')
|
|
281
|
+
this.rerenderAllChunks()
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
onAllTexturesLoaded() {
|
|
286
|
+
this.holdingBlock.ready = true
|
|
287
|
+
this.holdingBlock.updateItem()
|
|
288
|
+
this.holdingBlockLeft.ready = true
|
|
289
|
+
this.holdingBlockLeft.updateItem()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
changeBackgroundColor(color: [number, number, number]): void {
|
|
293
|
+
this.scene.background = new THREE.Color(color[0], color[1], color[2])
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
timeUpdated(newTime: number): void {
|
|
297
|
+
const nightTime = 13_500
|
|
298
|
+
const morningStart = 23_000
|
|
299
|
+
const displayStars = newTime > nightTime && newTime < morningStart
|
|
300
|
+
if (displayStars) {
|
|
301
|
+
this.starField.addToScene()
|
|
302
|
+
} else {
|
|
303
|
+
this.starField.remove()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.skyboxRenderer.updateTime(newTime)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
biomeUpdated(biome: Biome): void {
|
|
310
|
+
if (biome?.temperature !== undefined) {
|
|
311
|
+
this.skyboxRenderer.updateTemperature(biome.temperature)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
biomeReset(): void {
|
|
316
|
+
// Reset to default temperature when biome is unknown
|
|
317
|
+
this.skyboxRenderer.updateTemperature(DEFAULT_TEMPERATURE)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
getItemRenderData(item: Record<string, any>, specificProps: ItemSpecificContextProperties) {
|
|
321
|
+
return getItemUv(item, specificProps, this.resourcesManager, this.playerStateReactive)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async demoModel() {
|
|
325
|
+
//@ts-expect-error
|
|
326
|
+
const pos = cursorBlockRel(0, 1, 0).position
|
|
327
|
+
|
|
328
|
+
const mesh = (await getMyHand())!
|
|
329
|
+
// mesh.rotation.y = THREE.MathUtils.degToRad(90)
|
|
330
|
+
setBlockPosition(mesh, pos)
|
|
331
|
+
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
|
332
|
+
mesh.add(helper)
|
|
333
|
+
this.scene.add(mesh)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
demoItem() {
|
|
337
|
+
//@ts-expect-error
|
|
338
|
+
const pos = cursorBlockRel(0, 1, 0).position
|
|
339
|
+
const { mesh } = this.entities.getItemMesh({
|
|
340
|
+
itemId: 541,
|
|
341
|
+
}, {})!
|
|
342
|
+
mesh.position.set(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5)
|
|
343
|
+
// mesh.scale.set(0.5, 0.5, 0.5)
|
|
344
|
+
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
|
345
|
+
mesh.add(helper)
|
|
346
|
+
this.scene.add(mesh)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
debugOverlayAdded = false
|
|
350
|
+
addDebugOverlay() {
|
|
351
|
+
if (this.debugOverlayAdded) return
|
|
352
|
+
this.debugOverlayAdded = true
|
|
353
|
+
const pane = addNewStat('debug-overlay')
|
|
354
|
+
setInterval(() => {
|
|
355
|
+
pane.setVisibility(this.displayAdvancedStats)
|
|
356
|
+
if (this.displayAdvancedStats) {
|
|
357
|
+
const formatBigNumber = (num: number) => {
|
|
358
|
+
return new Intl.NumberFormat('en-US', {}).format(num)
|
|
359
|
+
}
|
|
360
|
+
let text = ''
|
|
361
|
+
text += `C: ${formatBigNumber(this.renderer.info.render.calls)} `
|
|
362
|
+
text += `TR: ${formatBigNumber(this.renderer.info.render.triangles)} `
|
|
363
|
+
text += `TE: ${formatBigNumber(this.renderer.info.memory.textures)} `
|
|
364
|
+
text += `F: ${formatBigNumber(this.tilesRendered)} `
|
|
365
|
+
text += `B: ${formatBigNumber(this.blocksRendered)} `
|
|
366
|
+
text += `MEM: ${this.getEstimatedMemoryUsage().readable}`
|
|
367
|
+
pane.updateText(text)
|
|
368
|
+
this.backendInfoReport = text
|
|
369
|
+
}
|
|
370
|
+
}, 200)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Optionally update data that are depedendent on the viewer position
|
|
375
|
+
*/
|
|
376
|
+
updatePosDataChunk(key: string) {
|
|
377
|
+
const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16))
|
|
378
|
+
// sum of distances: x + y + z
|
|
379
|
+
const chunkDistance = Math.abs(x - this.cameraSectionPos.x) + Math.abs(y - this.cameraSectionPos.y) + Math.abs(z - this.cameraSectionPos.z)
|
|
380
|
+
const section = this.sectionObjects[key].children.find(child => child.name === 'mesh')!
|
|
381
|
+
section.renderOrder = 500 - chunkDistance
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
override updateViewerPosition(pos: Vec3): void {
|
|
385
|
+
this.viewerChunkPosition = pos
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
cameraSectionPositionUpdate() {
|
|
389
|
+
// eslint-disable-next-line guard-for-in
|
|
390
|
+
for (const key in this.sectionObjects) {
|
|
391
|
+
const value = this.sectionObjects[key]
|
|
392
|
+
if (!value) continue
|
|
393
|
+
this.updatePosDataChunk(key)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
getDir(current: number, origin: number) {
|
|
398
|
+
if (current === origin) return 0
|
|
399
|
+
return current < origin ? 1 : -1
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
finishChunk(chunkKey: string) {
|
|
403
|
+
for (const sectionKey of this.waitingChunksToDisplay[chunkKey] ?? []) {
|
|
404
|
+
this.sectionObjects[sectionKey].visible = true
|
|
405
|
+
}
|
|
406
|
+
delete this.waitingChunksToDisplay[chunkKey]
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// debugRecomputedDeletedObjects = 0
|
|
410
|
+
handleWorkerMessage(data: { geometry: MesherGeometryOutput, key, type }): void {
|
|
411
|
+
if (data.type !== 'geometry') return
|
|
412
|
+
let object: THREE.Object3D = this.sectionObjects[data.key]
|
|
413
|
+
if (object) {
|
|
414
|
+
// Track memory usage removal for existing section
|
|
415
|
+
this.removeSectionMemoryUsage(object)
|
|
416
|
+
// Cleanup banner textures before disposing
|
|
417
|
+
object.traverse((child) => {
|
|
418
|
+
if ((child as any).bannerTexture) {
|
|
419
|
+
releaseBannerTexture((child as any).bannerTexture)
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
this.scene.remove(object)
|
|
423
|
+
disposeObject(object)
|
|
424
|
+
delete this.sectionObjects[data.key]
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const chunkCoords = data.key.split(',')
|
|
428
|
+
if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return
|
|
429
|
+
|
|
430
|
+
// if (object) {
|
|
431
|
+
// this.debugRecomputedDeletedObjects++
|
|
432
|
+
// }
|
|
433
|
+
|
|
434
|
+
const geometry = new THREE.BufferGeometry()
|
|
435
|
+
const positionAttr = new THREE.BufferAttribute(data.geometry.positions, 3)
|
|
436
|
+
const normalAttr = new THREE.BufferAttribute(data.geometry.normals, 3)
|
|
437
|
+
const colorAttr = new THREE.BufferAttribute(data.geometry.colors, 3)
|
|
438
|
+
const uvAttr = new THREE.BufferAttribute(data.geometry.uvs, 2)
|
|
439
|
+
const indexAttr = new THREE.BufferAttribute(data.geometry.indices as Uint32Array | Uint16Array, 1)
|
|
440
|
+
|
|
441
|
+
geometry.setAttribute('position', positionAttr)
|
|
442
|
+
geometry.setAttribute('normal', normalAttr)
|
|
443
|
+
geometry.setAttribute('color', colorAttr)
|
|
444
|
+
geometry.setAttribute('uv', uvAttr)
|
|
445
|
+
geometry.index = indexAttr
|
|
446
|
+
|
|
447
|
+
// Track memory usage for this section (before disposing CPU arrays)
|
|
448
|
+
this.addSectionMemoryUsage(geometry)
|
|
449
|
+
|
|
450
|
+
// Force GPU upload and then dispose CPU arrays to free RAM
|
|
451
|
+
this.disposeCpuArraysAfterGpuUpload(geometry)
|
|
452
|
+
|
|
453
|
+
const mesh = new THREE.Mesh(geometry, this.material)
|
|
454
|
+
mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
|
455
|
+
mesh.name = 'mesh'
|
|
456
|
+
object = new THREE.Group()
|
|
457
|
+
object.add(mesh)
|
|
458
|
+
// mesh with static dimensions: 16x16x16
|
|
459
|
+
const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 }))
|
|
460
|
+
staticChunkMesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
|
461
|
+
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
|
|
462
|
+
boxHelper.name = 'helper'
|
|
463
|
+
object.add(boxHelper)
|
|
464
|
+
object.name = 'chunk';
|
|
465
|
+
(object as any).tilesCount = data.geometry.positions.length / 3 / 4;
|
|
466
|
+
(object as any).blocksCount = data.geometry.blocksCount
|
|
467
|
+
if (!this.displayOptions.inWorldRenderingConfig.showChunkBorders) {
|
|
468
|
+
boxHelper.visible = false
|
|
469
|
+
}
|
|
470
|
+
// should not compute it once
|
|
471
|
+
if (Object.keys(data.geometry.signs).length) {
|
|
472
|
+
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(data.geometry.signs)) {
|
|
473
|
+
const signBlockEntity = this.blockEntities[posKey]
|
|
474
|
+
if (!signBlockEntity) continue
|
|
475
|
+
const [x, y, z] = posKey.split(',')
|
|
476
|
+
const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, isHanging, nbt.simplify(signBlockEntity))
|
|
477
|
+
if (!sign) continue
|
|
478
|
+
object.add(sign)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (Object.keys(data.geometry.heads).length) {
|
|
482
|
+
for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.heads)) {
|
|
483
|
+
const headBlockEntity = this.blockEntities[posKey]
|
|
484
|
+
if (!headBlockEntity) continue
|
|
485
|
+
const [x, y, z] = posKey.split(',')
|
|
486
|
+
const head = this.renderHead(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(headBlockEntity))
|
|
487
|
+
if (!head) continue
|
|
488
|
+
object.add(head)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (Object.keys(data.geometry.banners).length) {
|
|
492
|
+
for (const [posKey, { isWall, rotation, blockName }] of Object.entries(data.geometry.banners)) {
|
|
493
|
+
const bannerBlockEntity = this.blockEntities[posKey]
|
|
494
|
+
if (!bannerBlockEntity) continue
|
|
495
|
+
const [x, y, z] = posKey.split(',')
|
|
496
|
+
const bannerTexture = getBannerTexture(this, blockName, nbt.simplify(bannerBlockEntity))
|
|
497
|
+
if (!bannerTexture) continue
|
|
498
|
+
const banner = createBannerMesh(new Vec3(+x, +y, +z), rotation, isWall, bannerTexture)
|
|
499
|
+
object.add(banner)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
this.sectionObjects[data.key] = object
|
|
503
|
+
if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
|
|
504
|
+
object.visible = false
|
|
505
|
+
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
|
|
506
|
+
this.waitingChunksToDisplay[chunkKey] ??= []
|
|
507
|
+
this.waitingChunksToDisplay[chunkKey].push(data.key)
|
|
508
|
+
if (this.finishedChunks[chunkKey]) {
|
|
509
|
+
// todo it might happen even when it was not an update
|
|
510
|
+
this.finishChunk(chunkKey)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
this.updatePosDataChunk(data.key)
|
|
515
|
+
object.matrixAutoUpdate = false
|
|
516
|
+
mesh.onAfterRender = (renderer, scene, camera, geometry, material, group) => {
|
|
517
|
+
// mesh.matrixAutoUpdate = false
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
this.scene.add(object)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Dispose CPU arrays after GPU upload to reduce RAM usage
|
|
525
|
+
*/
|
|
526
|
+
private disposeCpuArraysAfterGpuUpload(geometry: THREE.BufferGeometry): void {
|
|
527
|
+
// Set up callbacks to dispose CPU arrays after GPU upload
|
|
528
|
+
// Three.js will automatically upload to GPU on first render
|
|
529
|
+
const { attributes } = geometry
|
|
530
|
+
for (const attributeName of Object.keys(attributes)) {
|
|
531
|
+
const attribute = attributes[attributeName]
|
|
532
|
+
if (attribute instanceof THREE.InterleavedBufferAttribute) continue
|
|
533
|
+
if (attribute.onUploadCallback) {
|
|
534
|
+
// If there's already a callback, chain it
|
|
535
|
+
const existingCallback = attribute.onUploadCallback
|
|
536
|
+
attribute.onUploadCallback = () => {
|
|
537
|
+
existingCallback()
|
|
538
|
+
this.disposeCpuArray(attribute)
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
attribute.onUploadCallback = () => this.disposeCpuArray(attribute)
|
|
542
|
+
}
|
|
543
|
+
// Force the upload callback by marking as needing update
|
|
544
|
+
attribute.needsUpdate = true
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Handle index attribute
|
|
548
|
+
if (geometry.index) {
|
|
549
|
+
if (geometry.index.onUploadCallback) {
|
|
550
|
+
const existingCallback = geometry.index.onUploadCallback
|
|
551
|
+
geometry.index.onUploadCallback = () => {
|
|
552
|
+
existingCallback()
|
|
553
|
+
this.disposeCpuArray(geometry.index!)
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
geometry.index.onUploadCallback = () => this.disposeCpuArray(geometry.index!)
|
|
557
|
+
}
|
|
558
|
+
geometry.index.needsUpdate = true
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Dispose CPU array data from buffer attribute
|
|
564
|
+
*/
|
|
565
|
+
private disposeCpuArray(attribute: THREE.BufferAttribute): void {
|
|
566
|
+
// Clear the CPU array reference to free memory
|
|
567
|
+
// Note: This makes the attribute read-only from CPU side
|
|
568
|
+
if (attribute.array) {
|
|
569
|
+
// Store original array type for potential recreation
|
|
570
|
+
// attribute.userData = attribute.userData || {}
|
|
571
|
+
// attribute.userData.originalArrayType = attribute.array.constructor.name
|
|
572
|
+
// attribute.userData.originalLength = attribute.array.length
|
|
573
|
+
// attribute.userData.originalItemSize = attribute.itemSize
|
|
574
|
+
|
|
575
|
+
// Clear the array reference
|
|
576
|
+
; (attribute as any).array = null
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
getSignTexture(position: Vec3, blockEntity, isHanging, backSide = false) {
|
|
581
|
+
const chunk = chunkPos(position)
|
|
582
|
+
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
|
|
583
|
+
if (!textures) {
|
|
584
|
+
textures = {}
|
|
585
|
+
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
|
|
586
|
+
}
|
|
587
|
+
const texturekey = `${position.x},${position.y},${position.z}`
|
|
588
|
+
// todo investigate bug and remove this so don't need to clean in section dirty
|
|
589
|
+
if (textures[texturekey]) return textures[texturekey]
|
|
590
|
+
|
|
591
|
+
const PrismarineChat = PrismarineChatLoader(this.version)
|
|
592
|
+
const canvas = renderSign(blockEntity, isHanging, PrismarineChat)
|
|
593
|
+
if (!canvas) return
|
|
594
|
+
const tex = new THREE.Texture(canvas)
|
|
595
|
+
tex.magFilter = THREE.NearestFilter
|
|
596
|
+
tex.minFilter = THREE.NearestFilter
|
|
597
|
+
tex.needsUpdate = true
|
|
598
|
+
textures[texturekey] = tex
|
|
599
|
+
return tex
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
getCameraPosition() {
|
|
603
|
+
const worldPos = new THREE.Vector3()
|
|
604
|
+
this.camera.getWorldPosition(worldPos)
|
|
605
|
+
return worldPos
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
getSectionCameraPosition() {
|
|
609
|
+
const pos = this.getCameraPosition()
|
|
610
|
+
return new Vec3(
|
|
611
|
+
Math.floor(pos.x / 16),
|
|
612
|
+
Math.floor(pos.y / 16),
|
|
613
|
+
Math.floor(pos.z / 16)
|
|
614
|
+
)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
updateCameraSectionPos() {
|
|
618
|
+
const newSectionPos = this.getSectionCameraPosition()
|
|
619
|
+
if (!this.cameraSectionPos.equals(newSectionPos)) {
|
|
620
|
+
this.cameraSectionPos = newSectionPos
|
|
621
|
+
this.cameraSectionPositionUpdate()
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
setFirstPersonCamera(pos: Vec3 | null, yaw: number, pitch: number) {
|
|
626
|
+
const yOffset = this.playerStateReactive.eyeHeight
|
|
627
|
+
|
|
628
|
+
this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
|
629
|
+
this.media.tryIntersectMedia()
|
|
630
|
+
this.updateCameraSectionPos()
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
getThirdPersonCamera(pos: THREE.Vector3 | null, yaw: number, pitch: number) {
|
|
634
|
+
pos ??= this.cameraObject.position
|
|
635
|
+
|
|
636
|
+
// Calculate camera offset based on perspective
|
|
637
|
+
const isBack = this.playerStateReactive.perspective === 'third_person_back'
|
|
638
|
+
const distance = 4 // Default third person distance
|
|
639
|
+
|
|
640
|
+
// Calculate direction vector using proper world orientation
|
|
641
|
+
// We need to get the camera's current look direction and use that for positioning
|
|
642
|
+
|
|
643
|
+
// Create a direction vector that represents where the camera is looking
|
|
644
|
+
// This matches the Three.js camera coordinate system
|
|
645
|
+
const direction = new THREE.Vector3(0, 0, -1) // Forward direction in camera space
|
|
646
|
+
|
|
647
|
+
// Apply the same rotation that's applied to the camera container
|
|
648
|
+
const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitch)
|
|
649
|
+
const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yaw)
|
|
650
|
+
const finalQuat = new THREE.Quaternion().multiplyQuaternions(yawQuat, pitchQuat)
|
|
651
|
+
|
|
652
|
+
// Transform the direction vector by the camera's rotation
|
|
653
|
+
direction.applyQuaternion(finalQuat)
|
|
654
|
+
|
|
655
|
+
// For back view, we want the camera behind the player (opposite to view direction)
|
|
656
|
+
// For front view, we want the camera in front of the player (same as view direction)
|
|
657
|
+
if (isBack) {
|
|
658
|
+
direction.multiplyScalar(-1)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Create debug visualization if advanced stats are enabled
|
|
662
|
+
if (this.DEBUG_RAYCAST) {
|
|
663
|
+
this.debugRaycast(pos, direction, distance)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Perform raycast to avoid camera going through blocks
|
|
667
|
+
const raycaster = new THREE.Raycaster()
|
|
668
|
+
raycaster.set(pos, direction)
|
|
669
|
+
raycaster.far = distance // Limit raycast distance
|
|
670
|
+
|
|
671
|
+
// Filter to only nearby chunks for performance
|
|
672
|
+
const nearbyChunks = Object.values(this.sectionObjects)
|
|
673
|
+
.filter(obj => obj.name === 'chunk' && obj.visible)
|
|
674
|
+
.filter(obj => {
|
|
675
|
+
// Get the mesh child which has the actual geometry
|
|
676
|
+
const mesh = obj.children.find(child => child.name === 'mesh')
|
|
677
|
+
if (!mesh) return false
|
|
678
|
+
|
|
679
|
+
// Check distance from player position to chunk
|
|
680
|
+
const chunkWorldPos = new THREE.Vector3()
|
|
681
|
+
mesh.getWorldPosition(chunkWorldPos)
|
|
682
|
+
const distance = pos.distanceTo(chunkWorldPos)
|
|
683
|
+
return distance < 80 // Only check chunks within 80 blocks
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
// Get all mesh children for raycasting
|
|
687
|
+
const meshes: THREE.Object3D[] = []
|
|
688
|
+
for (const chunk of nearbyChunks) {
|
|
689
|
+
const mesh = chunk.children.find(child => child.name === 'mesh')
|
|
690
|
+
if (mesh) meshes.push(mesh)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const intersects = raycaster.intersectObjects(meshes, false)
|
|
694
|
+
|
|
695
|
+
let finalDistance = distance
|
|
696
|
+
if (intersects.length > 0) {
|
|
697
|
+
// Use intersection distance minus a small offset to prevent clipping
|
|
698
|
+
finalDistance = Math.max(0.5, intersects[0].distance - 0.2)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const finalPos = new Vec3(
|
|
702
|
+
pos.x + direction.x * finalDistance,
|
|
703
|
+
pos.y + direction.y * finalDistance,
|
|
704
|
+
pos.z + direction.z * finalDistance
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
return finalPos
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
private debugRaycastHelper?: THREE.ArrowHelper
|
|
711
|
+
private debugHitPoint?: THREE.Mesh
|
|
712
|
+
|
|
713
|
+
private debugRaycast(pos: THREE.Vector3, direction: THREE.Vector3, distance: number) {
|
|
714
|
+
// Remove existing debug objects
|
|
715
|
+
if (this.debugRaycastHelper) {
|
|
716
|
+
this.scene.remove(this.debugRaycastHelper)
|
|
717
|
+
this.debugRaycastHelper = undefined
|
|
718
|
+
}
|
|
719
|
+
if (this.debugHitPoint) {
|
|
720
|
+
this.scene.remove(this.debugHitPoint)
|
|
721
|
+
this.debugHitPoint = undefined
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Create raycast arrow
|
|
725
|
+
this.debugRaycastHelper = new THREE.ArrowHelper(
|
|
726
|
+
direction.clone().normalize(),
|
|
727
|
+
pos,
|
|
728
|
+
distance,
|
|
729
|
+
0xff_00_00, // Red color
|
|
730
|
+
distance * 0.1,
|
|
731
|
+
distance * 0.05
|
|
732
|
+
)
|
|
733
|
+
this.scene.add(this.debugRaycastHelper)
|
|
734
|
+
|
|
735
|
+
// Create hit point indicator
|
|
736
|
+
const hitGeometry = new THREE.SphereGeometry(0.2, 8, 8)
|
|
737
|
+
const hitMaterial = new THREE.MeshBasicMaterial({ color: 0x00_ff_00 })
|
|
738
|
+
this.debugHitPoint = new THREE.Mesh(hitGeometry, hitMaterial)
|
|
739
|
+
this.debugHitPoint.position.copy(pos).add(direction.clone().multiplyScalar(distance))
|
|
740
|
+
this.scene.add(this.debugHitPoint)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
prevFramePerspective = null as string | null
|
|
744
|
+
|
|
745
|
+
setCinimaticCamera(pos: Vec3, yaw: number, pitch: number): void {
|
|
746
|
+
// Directly set camera position and rotation for cinematic mode
|
|
747
|
+
this.cameraObject.position.set(pos.x, pos.y, pos.z)
|
|
748
|
+
this.cameraShake.setBaseRotation(pitch, yaw)
|
|
749
|
+
this.updateCameraSectionPos()
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
setCinimaticFov(fov: number): void {
|
|
753
|
+
this.camera.fov = fov
|
|
754
|
+
this.camera.updateProjectionMatrix()
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
updateCamera(pos: Vec3 | null, yaw: number, pitch: number): void {
|
|
758
|
+
// Skip position/rotation updates if cinematic script is running
|
|
759
|
+
if (this.cinimaticScript.running) {
|
|
760
|
+
return
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// if (this.freeFlyMode) {
|
|
764
|
+
// pos = this.freeFlyState.position
|
|
765
|
+
// pitch = this.freeFlyState.pitch
|
|
766
|
+
// yaw = this.freeFlyState.yaw
|
|
767
|
+
// }
|
|
768
|
+
|
|
769
|
+
if (pos) {
|
|
770
|
+
if (this.renderer.xr.isPresenting) {
|
|
771
|
+
pos.y -= this.camera.position.y // Fix Y position of camera in world
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
this.currentPosTween?.stop()
|
|
775
|
+
// Use instant camera updates (0 delay) in playground mode when camera controls are enabled
|
|
776
|
+
const tweenDelay = this.displayOptions.inWorldRenderingConfig.instantCameraUpdate
|
|
777
|
+
? 0
|
|
778
|
+
: (this.playerStateUtils.isSpectatingEntity() ? 150 : 50)
|
|
779
|
+
this.currentPosTween = new tweenJs.Tween(this.cameraObject.position).to({ x: pos.x, y: pos.y, z: pos.z }, tweenDelay).start()
|
|
780
|
+
// this.freeFlyState.position = pos
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (this.playerStateUtils.isSpectatingEntity()) {
|
|
784
|
+
const rotation = this.cameraShake.getBaseRotation()
|
|
785
|
+
// wrap in the correct direction
|
|
786
|
+
let yawOffset = 0
|
|
787
|
+
const halfPi = Math.PI / 2
|
|
788
|
+
if (rotation.yaw < halfPi && yaw > Math.PI + halfPi) {
|
|
789
|
+
yawOffset = -Math.PI * 2
|
|
790
|
+
} else if (yaw < halfPi && rotation.yaw > Math.PI + halfPi) {
|
|
791
|
+
yawOffset = Math.PI * 2
|
|
792
|
+
}
|
|
793
|
+
this.currentRotTween?.stop()
|
|
794
|
+
this.currentRotTween = new tweenJs.Tween(rotation).to({ pitch, yaw: yaw + yawOffset }, 100)
|
|
795
|
+
.onUpdate(params => this.cameraShake.setBaseRotation(params.pitch, params.yaw - yawOffset)).start()
|
|
796
|
+
} else {
|
|
797
|
+
this.currentRotTween?.stop()
|
|
798
|
+
this.cameraShake.setBaseRotation(pitch, yaw)
|
|
799
|
+
|
|
800
|
+
const { perspective } = this.playerStateReactive
|
|
801
|
+
if (perspective === 'third_person_back' || perspective === 'third_person_front') {
|
|
802
|
+
// Use getThirdPersonCamera for proper raycasting with max distance of 4
|
|
803
|
+
const currentCameraPos = this.cameraObject.position
|
|
804
|
+
const thirdPersonPos = this.getThirdPersonCamera(
|
|
805
|
+
new THREE.Vector3(currentCameraPos.x, currentCameraPos.y, currentCameraPos.z),
|
|
806
|
+
yaw,
|
|
807
|
+
pitch
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
const distance = currentCameraPos.distanceTo(new THREE.Vector3(thirdPersonPos.x, thirdPersonPos.y, thirdPersonPos.z))
|
|
811
|
+
// Apply Z offset based on perspective and calculated distance
|
|
812
|
+
const zOffset = perspective === 'third_person_back' ? distance : -distance
|
|
813
|
+
this.camera.position.set(0, 0, zOffset)
|
|
814
|
+
|
|
815
|
+
if (perspective === 'third_person_front') {
|
|
816
|
+
// Flip camera view 180 degrees around Y axis for front view
|
|
817
|
+
this.camera.rotation.set(0, Math.PI, 0)
|
|
818
|
+
} else {
|
|
819
|
+
this.camera.rotation.set(0, 0, 0)
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
this.camera.position.set(0, 0, 0)
|
|
823
|
+
this.camera.rotation.set(0, 0, 0)
|
|
824
|
+
|
|
825
|
+
// remove any debug raycasting
|
|
826
|
+
if (this.debugRaycastHelper) {
|
|
827
|
+
this.scene.remove(this.debugRaycastHelper)
|
|
828
|
+
this.debugRaycastHelper = undefined
|
|
829
|
+
}
|
|
830
|
+
if (this.debugHitPoint) {
|
|
831
|
+
this.scene.remove(this.debugHitPoint)
|
|
832
|
+
this.debugHitPoint = undefined
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
this.updateCameraSectionPos()
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
debugChunksVisibilityOverride() {
|
|
841
|
+
const { chunksRenderAboveOverride, chunksRenderBelowOverride, chunksRenderDistanceOverride, chunksRenderAboveEnabled, chunksRenderBelowEnabled, chunksRenderDistanceEnabled } = this.reactiveDebugParams
|
|
842
|
+
|
|
843
|
+
const baseY = this.cameraSectionPos.y * 16
|
|
844
|
+
|
|
845
|
+
if (
|
|
846
|
+
this.displayOptions.inWorldRenderingConfig.enableDebugOverlay &&
|
|
847
|
+
chunksRenderAboveOverride !== undefined ||
|
|
848
|
+
chunksRenderBelowOverride !== undefined ||
|
|
849
|
+
chunksRenderDistanceOverride !== undefined
|
|
850
|
+
) {
|
|
851
|
+
for (const [key, object] of Object.entries(this.sectionObjects)) {
|
|
852
|
+
const [x, y, z] = key.split(',').map(Number)
|
|
853
|
+
const isVisible =
|
|
854
|
+
// eslint-disable-next-line no-constant-binary-expression, sonarjs/no-redundant-boolean
|
|
855
|
+
(chunksRenderAboveEnabled && chunksRenderAboveOverride !== undefined) ? y <= (baseY + chunksRenderAboveOverride) : true &&
|
|
856
|
+
// eslint-disable-next-line @stylistic/indent-binary-ops, no-constant-binary-expression, sonarjs/no-redundant-boolean
|
|
857
|
+
(chunksRenderBelowEnabled && chunksRenderBelowOverride !== undefined) ? y >= (baseY - chunksRenderBelowOverride) : true &&
|
|
858
|
+
// eslint-disable-next-line @stylistic/indent-binary-ops
|
|
859
|
+
(chunksRenderDistanceEnabled && chunksRenderDistanceOverride !== undefined) ? Math.abs(y - baseY) <= chunksRenderDistanceOverride : true
|
|
860
|
+
|
|
861
|
+
object.visible = isVisible
|
|
862
|
+
}
|
|
863
|
+
} else {
|
|
864
|
+
for (const object of Object.values(this.sectionObjects)) {
|
|
865
|
+
object.visible = true
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
render(sizeChanged = false) {
|
|
871
|
+
this.currentRenderedFrames++
|
|
872
|
+
if (this.reactiveDebugParams.stopRendering) return
|
|
873
|
+
this.debugChunksVisibilityOverride()
|
|
874
|
+
const start = performance.now()
|
|
875
|
+
this.lastRendered = performance.now()
|
|
876
|
+
this.cursorBlock.render()
|
|
877
|
+
this.updateSectionOffsets()
|
|
878
|
+
|
|
879
|
+
// Update skybox position to follow camera
|
|
880
|
+
const cameraPos = this.getCameraPosition()
|
|
881
|
+
this.skyboxRenderer.update(cameraPos, this.viewDistance)
|
|
882
|
+
|
|
883
|
+
const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov
|
|
884
|
+
if (sizeOrFovChanged) {
|
|
885
|
+
const size = this.renderer.getSize(new THREE.Vector2())
|
|
886
|
+
this.camera.aspect = size.width / size.height
|
|
887
|
+
this.camera.fov = this.displayOptions.inWorldRenderingConfig.fov
|
|
888
|
+
this.camera.updateProjectionMatrix()
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (!this.reactiveDebugParams.disableEntities) {
|
|
892
|
+
this.entities.render()
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
|
|
896
|
+
const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
|
|
897
|
+
this.renderer.render(this.scene, cam)
|
|
898
|
+
|
|
899
|
+
if (
|
|
900
|
+
this.displayOptions.inWorldRenderingConfig.showHand &&
|
|
901
|
+
this.playerStateReactive.gameMode !== 'spectator' &&
|
|
902
|
+
this.playerStateReactive.perspective === 'first_person' &&
|
|
903
|
+
// !this.freeFlyMode &&
|
|
904
|
+
!this.renderer.xr.isPresenting
|
|
905
|
+
) {
|
|
906
|
+
this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
|
|
907
|
+
this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
for (const fountain of this.fountains) {
|
|
911
|
+
if (this.sectionObjects[fountain.sectionId] && !this.sectionObjects[fountain.sectionId].foutain) {
|
|
912
|
+
fountain.createParticles(this.sectionObjects[fountain.sectionId])
|
|
913
|
+
this.sectionObjects[fountain.sectionId].foutain = true
|
|
914
|
+
}
|
|
915
|
+
fountain.render()
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
this.waypoints.render()
|
|
919
|
+
this.fireworks.update()
|
|
920
|
+
|
|
921
|
+
for (const onRender of this.onRender) {
|
|
922
|
+
onRender()
|
|
923
|
+
}
|
|
924
|
+
const end = performance.now()
|
|
925
|
+
const totalTime = end - start
|
|
926
|
+
this.renderTimeAvgCount++
|
|
927
|
+
this.renderTimeAvg = ((this.renderTimeAvg * (this.renderTimeAvgCount - 1)) + totalTime) / this.renderTimeAvgCount
|
|
928
|
+
this.renderTimeMax = Math.max(this.renderTimeMax, totalTime)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
renderHead(position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
|
932
|
+
let textureData: string
|
|
933
|
+
if (blockEntity.SkullOwner) {
|
|
934
|
+
textureData = blockEntity.SkullOwner.Properties?.textures?.[0]?.Value
|
|
935
|
+
} else {
|
|
936
|
+
textureData = blockEntity.profile?.properties?.find(p => p.name === 'textures')?.value
|
|
937
|
+
}
|
|
938
|
+
if (!textureData) return
|
|
939
|
+
|
|
940
|
+
try {
|
|
941
|
+
const decodedData = JSON.parse(Buffer.from(textureData, 'base64').toString())
|
|
942
|
+
let skinUrl = decodedData.textures?.SKIN?.url
|
|
943
|
+
const { skinTexturesProxy } = this.worldRendererConfig
|
|
944
|
+
if (skinTexturesProxy) {
|
|
945
|
+
skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)
|
|
946
|
+
.replace('https://textures.minecraft.net/', skinTexturesProxy)
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const mesh = getMesh(this, skinUrl, armorModel.head as any)
|
|
950
|
+
const group = new THREE.Group()
|
|
951
|
+
if (isWall) {
|
|
952
|
+
mesh.position.set(0, 0.3125, 0.3125)
|
|
953
|
+
}
|
|
954
|
+
// move head model down as armor have a different offset than blocks
|
|
955
|
+
mesh.position.y -= 23 / 16
|
|
956
|
+
group.add(mesh)
|
|
957
|
+
group.position.set(position.x + 0.5, position.y + 0.045, position.z + 0.5)
|
|
958
|
+
group.rotation.set(
|
|
959
|
+
0,
|
|
960
|
+
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
|
961
|
+
0
|
|
962
|
+
)
|
|
963
|
+
group.scale.set(0.8, 0.8, 0.8)
|
|
964
|
+
return group
|
|
965
|
+
} catch (err) {
|
|
966
|
+
console.error('Error decoding player texture:', err)
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
renderSign(position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
|
|
971
|
+
const tex = this.getSignTexture(position, blockEntity, isHanging)
|
|
972
|
+
|
|
973
|
+
if (!tex) return
|
|
974
|
+
|
|
975
|
+
// todo implement
|
|
976
|
+
// const key = JSON.stringify({ position, rotation, isWall })
|
|
977
|
+
// if (this.signsCache.has(key)) {
|
|
978
|
+
// console.log('cached', key)
|
|
979
|
+
// } else {
|
|
980
|
+
// this.signsCache.set(key, tex)
|
|
981
|
+
// }
|
|
982
|
+
|
|
983
|
+
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
|
|
984
|
+
mesh.renderOrder = 999
|
|
985
|
+
|
|
986
|
+
const lineHeight = 7 / 16
|
|
987
|
+
const scaleFactor = isHanging ? 1.3 : 1
|
|
988
|
+
mesh.scale.set(1 * scaleFactor, lineHeight * scaleFactor, 1 * scaleFactor)
|
|
989
|
+
|
|
990
|
+
const thickness = (isHanging ? 2 : 1.5) / 16
|
|
991
|
+
const wallSpacing = 0.25 / 16
|
|
992
|
+
if (isWall && !isHanging) {
|
|
993
|
+
mesh.position.set(0, 0, -0.5 + thickness + wallSpacing + 0.0001)
|
|
994
|
+
} else {
|
|
995
|
+
mesh.position.set(0, 0, thickness / 2 + 0.0001)
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const group = new THREE.Group()
|
|
999
|
+
group.rotation.set(
|
|
1000
|
+
0,
|
|
1001
|
+
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
|
1002
|
+
0
|
|
1003
|
+
)
|
|
1004
|
+
group.add(mesh)
|
|
1005
|
+
const height = (isHanging ? 10 : 8) / 16
|
|
1006
|
+
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
|
|
1007
|
+
const textPosition = height / 2 + heightOffset
|
|
1008
|
+
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
|
|
1009
|
+
return group
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
lightUpdate(chunkX: number, chunkZ: number) {
|
|
1013
|
+
// set all sections in the chunk dirty
|
|
1014
|
+
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
|
|
1015
|
+
this.setSectionDirty(new Vec3(chunkX, y, chunkZ))
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
rerenderAllChunks() { // todo not clear what to do with loading chunks
|
|
1020
|
+
for (const key of Object.keys(this.sectionObjects)) {
|
|
1021
|
+
const [x, y, z] = key.split(',').map(Number)
|
|
1022
|
+
this.setSectionDirty(new Vec3(x, y, z))
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
updateShowChunksBorder(value: boolean) {
|
|
1027
|
+
for (const object of Object.values(this.sectionObjects)) {
|
|
1028
|
+
for (const child of object.children) {
|
|
1029
|
+
if (child.name === 'helper') {
|
|
1030
|
+
child.visible = value
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
resetWorld() {
|
|
1037
|
+
super.resetWorld()
|
|
1038
|
+
|
|
1039
|
+
for (const mesh of Object.values(this.sectionObjects)) {
|
|
1040
|
+
// Track memory usage removal for all sections
|
|
1041
|
+
this.removeSectionMemoryUsage(mesh)
|
|
1042
|
+
this.scene.remove(mesh)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Reset memory tracking since all sections are cleared
|
|
1046
|
+
this.estimatedMemoryUsage = 0
|
|
1047
|
+
|
|
1048
|
+
// Clean up debug objects
|
|
1049
|
+
if (this.debugRaycastHelper) {
|
|
1050
|
+
this.scene.remove(this.debugRaycastHelper)
|
|
1051
|
+
this.debugRaycastHelper = undefined
|
|
1052
|
+
}
|
|
1053
|
+
if (this.debugHitPoint) {
|
|
1054
|
+
this.scene.remove(this.debugHitPoint)
|
|
1055
|
+
this.debugHitPoint = undefined
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
getLoadedChunksRelative(pos: Vec3, includeY = false) {
|
|
1060
|
+
const [currentX, currentY, currentZ] = sectionPos(pos)
|
|
1061
|
+
return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => {
|
|
1062
|
+
const [xRaw, yRaw, zRaw] = key.split(',').map(Number)
|
|
1063
|
+
const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw })
|
|
1064
|
+
const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}`
|
|
1065
|
+
return [setKey, o]
|
|
1066
|
+
}))
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
cleanChunkTextures(x, z) {
|
|
1070
|
+
const textures = this.chunkTextures.get(`${Math.floor(x / 16)},${Math.floor(z / 16)}`) ?? {}
|
|
1071
|
+
for (const key of Object.keys(textures)) {
|
|
1072
|
+
textures[key].dispose()
|
|
1073
|
+
delete textures[key]
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
readdChunks() {
|
|
1078
|
+
for (const key of Object.keys(this.sectionObjects)) {
|
|
1079
|
+
this.scene.remove(this.sectionObjects[key])
|
|
1080
|
+
}
|
|
1081
|
+
setTimeout(() => {
|
|
1082
|
+
for (const key of Object.keys(this.sectionObjects)) {
|
|
1083
|
+
this.scene.add(this.sectionObjects[key])
|
|
1084
|
+
}
|
|
1085
|
+
}, 500)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
disableUpdates(children = this.scene.children) {
|
|
1089
|
+
for (const child of children) {
|
|
1090
|
+
child.matrixWorldNeedsUpdate = false
|
|
1091
|
+
this.disableUpdates(child.children ?? [])
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
removeColumn(x, z) {
|
|
1096
|
+
super.removeColumn(x, z)
|
|
1097
|
+
|
|
1098
|
+
this.cleanChunkTextures(x, z)
|
|
1099
|
+
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
|
|
1100
|
+
this.setSectionDirty(new Vec3(x, y, z), false)
|
|
1101
|
+
const key = `${x},${y},${z}`
|
|
1102
|
+
const mesh = this.sectionObjects[key]
|
|
1103
|
+
if (mesh) {
|
|
1104
|
+
// Track memory usage removal
|
|
1105
|
+
this.removeSectionMemoryUsage(mesh)
|
|
1106
|
+
// Cleanup banner textures before disposing
|
|
1107
|
+
mesh.traverse((child) => {
|
|
1108
|
+
if ((child as any).bannerTexture) {
|
|
1109
|
+
releaseBannerTexture((child as any).bannerTexture)
|
|
1110
|
+
}
|
|
1111
|
+
})
|
|
1112
|
+
this.scene.remove(mesh)
|
|
1113
|
+
disposeObject(mesh)
|
|
1114
|
+
}
|
|
1115
|
+
delete this.sectionObjects[key]
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
setSectionDirty(...args: Parameters<WorldRendererCommon['setSectionDirty']>) {
|
|
1120
|
+
const [pos] = args
|
|
1121
|
+
this.cleanChunkTextures(pos.x, pos.z) // todo don't do this!
|
|
1122
|
+
super.setSectionDirty(...args)
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
static getRendererInfo(renderer: THREE.WebGLRenderer) {
|
|
1126
|
+
try {
|
|
1127
|
+
const gl = renderer.getContext()
|
|
1128
|
+
return `${gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info')!.UNMASKED_RENDERER_WEBGL)}`
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
console.warn('Failed to get renderer info', err)
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
worldStop() {
|
|
1135
|
+
this.media.onWorldStop()
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
destroy(): void {
|
|
1139
|
+
this.fireworksLegacy.destroy()
|
|
1140
|
+
super.destroy()
|
|
1141
|
+
this.skyboxRenderer.dispose()
|
|
1142
|
+
this.fireworks.dispose()
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
shouldObjectVisible(object: THREE.Object3D) {
|
|
1146
|
+
// Get chunk coordinates
|
|
1147
|
+
const chunkX = Math.floor(object.position.x / 16) * 16
|
|
1148
|
+
const chunkZ = Math.floor(object.position.z / 16) * 16
|
|
1149
|
+
const sectionY = Math.floor(object.position.y / 16) * 16
|
|
1150
|
+
|
|
1151
|
+
const chunkKey = `${chunkX},${chunkZ}`
|
|
1152
|
+
const sectionKey = `${chunkX},${sectionY},${chunkZ}`
|
|
1153
|
+
|
|
1154
|
+
return !!this.finishedChunks[chunkKey] || !!this.sectionObjects[sectionKey]
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
handleUserClick(button: 'left' | 'right') {
|
|
1158
|
+
this.media.handleUserClick(button)
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
updateSectionOffsets() {
|
|
1162
|
+
const currentTime = performance.now()
|
|
1163
|
+
for (const [key, anim] of Object.entries(this.sectionsOffsetsAnimations)) {
|
|
1164
|
+
const timeDelta = (currentTime - anim.time) / 1000 // Convert to seconds
|
|
1165
|
+
anim.time = currentTime
|
|
1166
|
+
|
|
1167
|
+
// Update offsets based on speed and time delta
|
|
1168
|
+
anim.currentOffsetX += anim.speedX * timeDelta
|
|
1169
|
+
anim.currentOffsetY += anim.speedY * timeDelta
|
|
1170
|
+
anim.currentOffsetZ += anim.speedZ * timeDelta
|
|
1171
|
+
|
|
1172
|
+
// Apply limits if they exist
|
|
1173
|
+
if (anim.limitX !== undefined) {
|
|
1174
|
+
if (anim.speedX > 0) {
|
|
1175
|
+
anim.currentOffsetX = Math.min(anim.currentOffsetX, anim.limitX)
|
|
1176
|
+
} else {
|
|
1177
|
+
anim.currentOffsetX = Math.max(anim.currentOffsetX, anim.limitX)
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (anim.limitY !== undefined) {
|
|
1181
|
+
if (anim.speedY > 0) {
|
|
1182
|
+
anim.currentOffsetY = Math.min(anim.currentOffsetY, anim.limitY)
|
|
1183
|
+
} else {
|
|
1184
|
+
anim.currentOffsetY = Math.max(anim.currentOffsetY, anim.limitY)
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if (anim.limitZ !== undefined) {
|
|
1188
|
+
if (anim.speedZ > 0) {
|
|
1189
|
+
anim.currentOffsetZ = Math.min(anim.currentOffsetZ, anim.limitZ)
|
|
1190
|
+
} else {
|
|
1191
|
+
anim.currentOffsetZ = Math.max(anim.currentOffsetZ, anim.limitZ)
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Apply the offset to the section object
|
|
1196
|
+
const section = this.sectionObjects[key]
|
|
1197
|
+
if (section) {
|
|
1198
|
+
section.position.set(
|
|
1199
|
+
anim.currentOffsetX,
|
|
1200
|
+
anim.currentOffsetY,
|
|
1201
|
+
anim.currentOffsetZ
|
|
1202
|
+
)
|
|
1203
|
+
section.updateMatrix()
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
reloadWorld() {
|
|
1209
|
+
this.entities.reloadEntities()
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Estimate memory usage of BufferGeometry attributes
|
|
1214
|
+
*/
|
|
1215
|
+
private estimateGeometryMemoryUsage(geometry: THREE.BufferGeometry): number {
|
|
1216
|
+
let memoryBytes = 0
|
|
1217
|
+
|
|
1218
|
+
// Calculate memory for each attribute
|
|
1219
|
+
const { attributes } = geometry
|
|
1220
|
+
for (const [name, attribute] of Object.entries(attributes)) {
|
|
1221
|
+
if (attribute?.array) {
|
|
1222
|
+
// Each number in typed arrays takes different bytes:
|
|
1223
|
+
// Float32Array: 4 bytes per number
|
|
1224
|
+
// Uint32Array: 4 bytes per number
|
|
1225
|
+
// Uint16Array: 2 bytes per number
|
|
1226
|
+
const bytesPerElement = attribute.array.BYTES_PER_ELEMENT
|
|
1227
|
+
memoryBytes += attribute.array.length * bytesPerElement
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Calculate memory for indices
|
|
1232
|
+
if (geometry.index?.array) {
|
|
1233
|
+
const bytesPerElement = geometry.index.array.BYTES_PER_ELEMENT
|
|
1234
|
+
memoryBytes += geometry.index.array.length * bytesPerElement
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
return memoryBytes
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Update memory usage when section is added
|
|
1242
|
+
*/
|
|
1243
|
+
private addSectionMemoryUsage(geometry: THREE.BufferGeometry): void {
|
|
1244
|
+
const memoryUsage = this.estimateGeometryMemoryUsage(geometry)
|
|
1245
|
+
this.estimatedMemoryUsage += memoryUsage
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Update memory usage when section is removed
|
|
1250
|
+
*/
|
|
1251
|
+
private removeSectionMemoryUsage(object: THREE.Object3D): void {
|
|
1252
|
+
// Find mesh with geometry in the object
|
|
1253
|
+
const mesh = object.children.find(child => child.name === 'mesh') as THREE.Mesh
|
|
1254
|
+
if (mesh?.geometry) {
|
|
1255
|
+
const memoryUsage = this.estimateGeometryMemoryUsage(mesh.geometry)
|
|
1256
|
+
this.estimatedMemoryUsage -= memoryUsage
|
|
1257
|
+
this.estimatedMemoryUsage = Math.max(0, this.estimatedMemoryUsage) // Ensure non-negative
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Get estimated memory usage in a human-readable format
|
|
1263
|
+
*/
|
|
1264
|
+
getEstimatedMemoryUsage(): { bytes: number; readable: string } {
|
|
1265
|
+
const bytes = this.estimatedMemoryUsage
|
|
1266
|
+
const mb = bytes / (1024 * 1024)
|
|
1267
|
+
const readable = `${mb.toFixed(2)} MB`
|
|
1268
|
+
return { bytes, readable }
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
class StarField {
|
|
1273
|
+
points?: THREE.Points
|
|
1274
|
+
private _enabled = true
|
|
1275
|
+
get enabled() {
|
|
1276
|
+
return this._enabled
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
set enabled(value) {
|
|
1280
|
+
this._enabled = value
|
|
1281
|
+
if (this.points) {
|
|
1282
|
+
this.points.visible = value
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
constructor(
|
|
1287
|
+
private readonly worldRenderer: WorldRendererThree
|
|
1288
|
+
) {
|
|
1289
|
+
const clock = new THREE.Clock()
|
|
1290
|
+
const speed = 0.2
|
|
1291
|
+
this.worldRenderer.onRender.push(() => {
|
|
1292
|
+
if (!this.points) return
|
|
1293
|
+
this.points.position.copy(this.worldRenderer.getCameraPosition());
|
|
1294
|
+
(this.points.material as StarfieldMaterial).uniforms.time.value = clock.getElapsedTime() * speed
|
|
1295
|
+
})
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
addToScene() {
|
|
1299
|
+
if (this.points || !this.enabled) return
|
|
1300
|
+
|
|
1301
|
+
const radius = 80
|
|
1302
|
+
const depth = 50
|
|
1303
|
+
const count = 7000
|
|
1304
|
+
const factor = 7
|
|
1305
|
+
const saturation = 10
|
|
1306
|
+
|
|
1307
|
+
const geometry = new THREE.BufferGeometry()
|
|
1308
|
+
|
|
1309
|
+
const genStar = r => new THREE.Vector3().setFromSpherical(new THREE.Spherical(r, Math.acos(1 - Math.random() * 2), Math.random() * 2 * Math.PI))
|
|
1310
|
+
|
|
1311
|
+
const positions = [] as number[]
|
|
1312
|
+
const colors = [] as number[]
|
|
1313
|
+
const sizes = Array.from({ length: count }, () => (0.5 + 0.5 * Math.random()) * factor)
|
|
1314
|
+
const color = new THREE.Color()
|
|
1315
|
+
let r = radius + depth
|
|
1316
|
+
const increment = depth / count
|
|
1317
|
+
for (let i = 0; i < count; i++) {
|
|
1318
|
+
r -= increment * Math.random()
|
|
1319
|
+
positions.push(...genStar(r).toArray())
|
|
1320
|
+
color.setHSL(i / count, saturation, 0.9)
|
|
1321
|
+
colors.push(color.r, color.g, color.b)
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
1325
|
+
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3))
|
|
1326
|
+
geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1))
|
|
1327
|
+
|
|
1328
|
+
// Create a material
|
|
1329
|
+
const material = new StarfieldMaterial()
|
|
1330
|
+
material.blending = THREE.AdditiveBlending
|
|
1331
|
+
material.depthTest = false
|
|
1332
|
+
material.transparent = true
|
|
1333
|
+
|
|
1334
|
+
// Create points and add them to the scene
|
|
1335
|
+
this.points = new THREE.Points(geometry, material)
|
|
1336
|
+
this.worldRenderer.scene.add(this.points)
|
|
1337
|
+
|
|
1338
|
+
this.points.renderOrder = -1
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
remove() {
|
|
1342
|
+
if (this.points) {
|
|
1343
|
+
this.points.geometry.dispose();
|
|
1344
|
+
(this.points.material as THREE.Material).dispose()
|
|
1345
|
+
this.worldRenderer.scene.remove(this.points)
|
|
1346
|
+
|
|
1347
|
+
this.points = undefined
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
const version = parseInt(THREE.REVISION.replaceAll(/\D+/g, ''), 10)
|
|
1353
|
+
class StarfieldMaterial extends THREE.ShaderMaterial {
|
|
1354
|
+
constructor() {
|
|
1355
|
+
super({
|
|
1356
|
+
uniforms: { time: { value: 0 }, fade: { value: 1 } },
|
|
1357
|
+
vertexShader: /* glsl */ `
|
|
1358
|
+
uniform float time;
|
|
1359
|
+
attribute float size;
|
|
1360
|
+
varying vec3 vColor;
|
|
1361
|
+
attribute vec3 color;
|
|
1362
|
+
void main() {
|
|
1363
|
+
vColor = color;
|
|
1364
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 0.5);
|
|
1365
|
+
gl_PointSize = 0.7 * size * (30.0 / -mvPosition.z) * (3.0 + sin(time + 100.0));
|
|
1366
|
+
gl_Position = projectionMatrix * mvPosition;
|
|
1367
|
+
}`,
|
|
1368
|
+
fragmentShader: /* glsl */ `
|
|
1369
|
+
uniform sampler2D pointTexture;
|
|
1370
|
+
uniform float fade;
|
|
1371
|
+
varying vec3 vColor;
|
|
1372
|
+
void main() {
|
|
1373
|
+
float opacity = 1.0;
|
|
1374
|
+
gl_FragColor = vec4(vColor, 1.0);
|
|
1375
|
+
|
|
1376
|
+
#include <tonemapping_fragment>
|
|
1377
|
+
#include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}>
|
|
1378
|
+
}`,
|
|
1379
|
+
})
|
|
1380
|
+
}
|
|
1381
|
+
}
|