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,435 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { createCanvas } from '../lib/utils'
|
|
3
|
+
|
|
4
|
+
// Centralized visual configuration (in screen pixels)
|
|
5
|
+
export const WAYPOINT_CONFIG = {
|
|
6
|
+
// Target size in screen pixels (this controls the final sprite size)
|
|
7
|
+
TARGET_SCREEN_PX: 150,
|
|
8
|
+
// Canvas size for internal rendering (keep power of 2 for textures)
|
|
9
|
+
CANVAS_SIZE: 256,
|
|
10
|
+
// Relative positions in canvas (0-1)
|
|
11
|
+
LAYOUT: {
|
|
12
|
+
DOT_Y: 0.3,
|
|
13
|
+
NAME_Y: 0.45,
|
|
14
|
+
DISTANCE_Y: 0.55,
|
|
15
|
+
},
|
|
16
|
+
// Multiplier for canvas internal resolution to keep text crisp
|
|
17
|
+
CANVAS_SCALE: 2,
|
|
18
|
+
ARROW: {
|
|
19
|
+
enabledDefault: false,
|
|
20
|
+
pixelSize: 50,
|
|
21
|
+
paddingPx: 50,
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type WaypointSprite = {
|
|
26
|
+
group: THREE.Group
|
|
27
|
+
sprite: THREE.Sprite
|
|
28
|
+
// Offscreen arrow controls
|
|
29
|
+
enableOffscreenArrow: (enabled: boolean) => void
|
|
30
|
+
setArrowParent: (parent: THREE.Object3D | null) => void
|
|
31
|
+
// Convenience combined updater
|
|
32
|
+
updateForCamera: (
|
|
33
|
+
cameraPosition: THREE.Vector3,
|
|
34
|
+
camera: THREE.PerspectiveCamera,
|
|
35
|
+
viewportWidthPx: number,
|
|
36
|
+
viewportHeightPx: number
|
|
37
|
+
) => boolean
|
|
38
|
+
// Utilities
|
|
39
|
+
setColor: (color: number) => void
|
|
40
|
+
setLabel: (label?: string) => void
|
|
41
|
+
updateDistanceText: (label: string, distanceText: string) => void
|
|
42
|
+
setVisible: (visible: boolean) => void
|
|
43
|
+
setPosition: (x: number, y: number, z: number) => void
|
|
44
|
+
dispose: () => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createWaypointSprite (options: {
|
|
48
|
+
position: THREE.Vector3 | { x: number, y: number, z: number },
|
|
49
|
+
color?: number,
|
|
50
|
+
label?: string,
|
|
51
|
+
depthTest?: boolean,
|
|
52
|
+
// Y offset in world units used by updateScaleWorld only (screen-pixel API ignores this)
|
|
53
|
+
labelYOffset?: number,
|
|
54
|
+
metadata?: any,
|
|
55
|
+
}): WaypointSprite {
|
|
56
|
+
const color = options.color ?? 0xFF_00_00
|
|
57
|
+
const depthTest = options.depthTest ?? false
|
|
58
|
+
const labelYOffset = options.labelYOffset ?? 1.5
|
|
59
|
+
|
|
60
|
+
// Build combined sprite
|
|
61
|
+
const sprite = createCombinedSprite(color, options.label ?? '', '0m', depthTest)
|
|
62
|
+
sprite.renderOrder = 10
|
|
63
|
+
let currentLabel = options.label ?? ''
|
|
64
|
+
|
|
65
|
+
// Performance optimization: cache distance text to avoid unnecessary updates
|
|
66
|
+
let lastDistanceText = '0m'
|
|
67
|
+
let lastDistance = 0
|
|
68
|
+
|
|
69
|
+
// Offscreen arrow (detached by default)
|
|
70
|
+
let arrowSprite: THREE.Sprite | undefined
|
|
71
|
+
let arrowParent: THREE.Object3D | null = null
|
|
72
|
+
let arrowEnabled = WAYPOINT_CONFIG.ARROW.enabledDefault
|
|
73
|
+
|
|
74
|
+
// Group for easy add/remove
|
|
75
|
+
const group = new THREE.Group()
|
|
76
|
+
group.add(sprite)
|
|
77
|
+
|
|
78
|
+
// Initial position
|
|
79
|
+
const { x, y, z } = options.position
|
|
80
|
+
group.position.set(x, y, z)
|
|
81
|
+
|
|
82
|
+
function setColor (newColor: number) {
|
|
83
|
+
const canvas = drawCombinedCanvas(newColor, currentLabel, '0m')
|
|
84
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
85
|
+
const mat = sprite.material
|
|
86
|
+
mat.map?.dispose()
|
|
87
|
+
mat.map = texture
|
|
88
|
+
mat.needsUpdate = true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function setLabel (newLabel?: string) {
|
|
92
|
+
currentLabel = newLabel ?? ''
|
|
93
|
+
const canvas = drawCombinedCanvas(color, currentLabel, '0m')
|
|
94
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
95
|
+
const mat = sprite.material
|
|
96
|
+
mat.map?.dispose()
|
|
97
|
+
mat.map = texture
|
|
98
|
+
mat.needsUpdate = true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function updateDistanceText (label: string, distanceText: string) {
|
|
102
|
+
// Performance optimization: only update if distance text actually changed
|
|
103
|
+
if (distanceText === lastDistanceText) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
lastDistanceText = distanceText
|
|
107
|
+
|
|
108
|
+
const canvas = drawCombinedCanvas(color, label, distanceText)
|
|
109
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
110
|
+
const mat = sprite.material
|
|
111
|
+
mat.map?.dispose()
|
|
112
|
+
mat.map = texture
|
|
113
|
+
mat.needsUpdate = true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function setVisible (visible: boolean) {
|
|
117
|
+
sprite.visible = visible
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function setPosition (nx: number, ny: number, nz: number) {
|
|
121
|
+
group.position.set(nx, ny, nz)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Keep constant pixel size on screen using global config
|
|
125
|
+
function updateScaleScreenPixels (
|
|
126
|
+
cameraPosition: THREE.Vector3,
|
|
127
|
+
cameraFov: number,
|
|
128
|
+
distance: number,
|
|
129
|
+
viewportHeightPx: number
|
|
130
|
+
) {
|
|
131
|
+
const vFovRad = cameraFov * Math.PI / 180
|
|
132
|
+
const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * distance
|
|
133
|
+
// Use configured target screen size
|
|
134
|
+
const scale = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.TARGET_SCREEN_PX / viewportHeightPx)
|
|
135
|
+
sprite.scale.set(scale, scale, 1)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function ensureArrow () {
|
|
139
|
+
if (arrowSprite) return
|
|
140
|
+
const size = 128
|
|
141
|
+
const canvas = createCanvas(size, size)
|
|
142
|
+
const ctx = canvas.getContext('2d')!
|
|
143
|
+
ctx.clearRect(0, 0, size, size)
|
|
144
|
+
|
|
145
|
+
// Draw arrow shape
|
|
146
|
+
ctx.beginPath()
|
|
147
|
+
ctx.moveTo(size * 0.15, size * 0.5)
|
|
148
|
+
ctx.lineTo(size * 0.85, size * 0.5)
|
|
149
|
+
ctx.lineTo(size * 0.5, size * 0.15)
|
|
150
|
+
ctx.closePath()
|
|
151
|
+
|
|
152
|
+
// Use waypoint color for arrow
|
|
153
|
+
const colorHex = `#${color.toString(16).padStart(6, '0')}`
|
|
154
|
+
ctx.lineWidth = 6
|
|
155
|
+
ctx.strokeStyle = 'black'
|
|
156
|
+
ctx.stroke()
|
|
157
|
+
ctx.fillStyle = colorHex
|
|
158
|
+
ctx.fill()
|
|
159
|
+
|
|
160
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
161
|
+
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false, depthWrite: false })
|
|
162
|
+
arrowSprite = new THREE.Sprite(material)
|
|
163
|
+
arrowSprite.renderOrder = 12
|
|
164
|
+
arrowSprite.visible = false
|
|
165
|
+
if (arrowParent) arrowParent.add(arrowSprite)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function enableOffscreenArrow (enabled: boolean) {
|
|
169
|
+
arrowEnabled = enabled
|
|
170
|
+
if (!enabled && arrowSprite) arrowSprite.visible = false
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function setArrowParent (parent: THREE.Object3D | null) {
|
|
174
|
+
if (arrowSprite?.parent) arrowSprite.parent.remove(arrowSprite)
|
|
175
|
+
arrowParent = parent
|
|
176
|
+
if (arrowSprite && parent) parent.add(arrowSprite)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function updateOffscreenArrow (
|
|
180
|
+
camera: THREE.PerspectiveCamera,
|
|
181
|
+
viewportWidthPx: number,
|
|
182
|
+
viewportHeightPx: number
|
|
183
|
+
): boolean {
|
|
184
|
+
if (!arrowEnabled) return true
|
|
185
|
+
ensureArrow()
|
|
186
|
+
if (!arrowSprite) return true
|
|
187
|
+
|
|
188
|
+
// Check if onlyLeftRight is enabled in metadata
|
|
189
|
+
const onlyLeftRight = options.metadata?.onlyLeftRight === true
|
|
190
|
+
|
|
191
|
+
// Build camera basis using camera.up to respect custom orientations
|
|
192
|
+
const forward = new THREE.Vector3()
|
|
193
|
+
camera.getWorldDirection(forward) // camera look direction
|
|
194
|
+
const upWorld = camera.up.clone().normalize()
|
|
195
|
+
const right = new THREE.Vector3().copy(forward).cross(upWorld).normalize()
|
|
196
|
+
const upCam = new THREE.Vector3().copy(right).cross(forward).normalize()
|
|
197
|
+
|
|
198
|
+
// Vector from camera to waypoint
|
|
199
|
+
const camPos = new THREE.Vector3().setFromMatrixPosition(camera.matrixWorld)
|
|
200
|
+
const toWp = new THREE.Vector3(group.position.x, group.position.y, group.position.z).sub(camPos)
|
|
201
|
+
|
|
202
|
+
// Components in camera basis
|
|
203
|
+
const z = toWp.dot(forward)
|
|
204
|
+
const x = toWp.dot(right)
|
|
205
|
+
const y = toWp.dot(upCam)
|
|
206
|
+
|
|
207
|
+
const aspect = viewportWidthPx / viewportHeightPx
|
|
208
|
+
const vFovRad = camera.fov * Math.PI / 180
|
|
209
|
+
const hFovRad = 2 * Math.atan(Math.tan(vFovRad / 2) * aspect)
|
|
210
|
+
|
|
211
|
+
// Determine if waypoint is inside view frustum using angular checks
|
|
212
|
+
const thetaX = Math.atan2(x, z)
|
|
213
|
+
const thetaY = Math.atan2(y, z)
|
|
214
|
+
const visible = z > 0 && Math.abs(thetaX) <= hFovRad / 2 && Math.abs(thetaY) <= vFovRad / 2
|
|
215
|
+
if (visible) {
|
|
216
|
+
arrowSprite.visible = false
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Direction on screen in normalized frustum units
|
|
221
|
+
let rx = thetaX / (hFovRad / 2)
|
|
222
|
+
let ry = thetaY / (vFovRad / 2)
|
|
223
|
+
|
|
224
|
+
// If behind the camera, snap to dominant axis to avoid confusing directions
|
|
225
|
+
if (z <= 0) {
|
|
226
|
+
if (Math.abs(rx) > Math.abs(ry)) {
|
|
227
|
+
rx = Math.sign(rx)
|
|
228
|
+
ry = 0
|
|
229
|
+
} else {
|
|
230
|
+
rx = 0
|
|
231
|
+
ry = Math.sign(ry)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Apply onlyLeftRight logic - restrict arrows to left/right edges only
|
|
236
|
+
if (onlyLeftRight) {
|
|
237
|
+
// Force the arrow to appear only on left or right edges
|
|
238
|
+
if (Math.abs(rx) > Math.abs(ry)) {
|
|
239
|
+
// Horizontal direction is dominant, keep it
|
|
240
|
+
ry = 0
|
|
241
|
+
} else {
|
|
242
|
+
// Vertical direction is dominant, but we want only left/right
|
|
243
|
+
// So choose left or right based on the sign of rx
|
|
244
|
+
rx = rx >= 0 ? 1 : -1
|
|
245
|
+
ry = 0
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Place on the rectangle border [-1,1]x[-1,1]
|
|
250
|
+
const s = Math.max(Math.abs(rx), Math.abs(ry)) || 1
|
|
251
|
+
let ndcX = rx / s
|
|
252
|
+
let ndcY = ry / s
|
|
253
|
+
|
|
254
|
+
// Apply padding in pixel space by clamping
|
|
255
|
+
const padding = WAYPOINT_CONFIG.ARROW.paddingPx
|
|
256
|
+
const pxX = ((ndcX + 1) * 0.5) * viewportWidthPx
|
|
257
|
+
const pxY = ((1 - ndcY) * 0.5) * viewportHeightPx
|
|
258
|
+
const clampedPxX = Math.min(Math.max(pxX, padding), viewportWidthPx - padding)
|
|
259
|
+
const clampedPxY = Math.min(Math.max(pxY, padding), viewportHeightPx - padding)
|
|
260
|
+
ndcX = (clampedPxX / viewportWidthPx) * 2 - 1
|
|
261
|
+
ndcY = -(clampedPxY / viewportHeightPx) * 2 + 1
|
|
262
|
+
|
|
263
|
+
// Compute world position at a fixed distance in front of the camera using camera basis
|
|
264
|
+
const placeDist = Math.max(2, camera.near * 4)
|
|
265
|
+
const halfPlaneHeight = Math.tan(vFovRad / 2) * placeDist
|
|
266
|
+
const halfPlaneWidth = halfPlaneHeight * aspect
|
|
267
|
+
const pos = camPos.clone()
|
|
268
|
+
.add(forward.clone().multiplyScalar(placeDist))
|
|
269
|
+
.add(right.clone().multiplyScalar(ndcX * halfPlaneWidth))
|
|
270
|
+
.add(upCam.clone().multiplyScalar(ndcY * halfPlaneHeight))
|
|
271
|
+
|
|
272
|
+
// Update arrow sprite
|
|
273
|
+
arrowSprite.visible = true
|
|
274
|
+
arrowSprite.position.copy(pos)
|
|
275
|
+
|
|
276
|
+
// Angle for rotation relative to screen right/up (derived from camera up vector)
|
|
277
|
+
const angle = Math.atan2(ry, rx)
|
|
278
|
+
arrowSprite.material.rotation = angle - Math.PI / 2
|
|
279
|
+
|
|
280
|
+
// Constant pixel size for arrow (use fixed placement distance)
|
|
281
|
+
const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * placeDist
|
|
282
|
+
const sPx = worldUnitsPerScreenHeightAtDist * (WAYPOINT_CONFIG.ARROW.pixelSize / viewportHeightPx)
|
|
283
|
+
arrowSprite.scale.set(sPx, sPx, 1)
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function computeDistance (cameraPosition: THREE.Vector3): number {
|
|
288
|
+
return cameraPosition.distanceTo(group.position)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function updateForCamera (
|
|
292
|
+
cameraPosition: THREE.Vector3,
|
|
293
|
+
camera: THREE.PerspectiveCamera,
|
|
294
|
+
viewportWidthPx: number,
|
|
295
|
+
viewportHeightPx: number
|
|
296
|
+
): boolean {
|
|
297
|
+
const distance = computeDistance(cameraPosition)
|
|
298
|
+
// Keep constant pixel size
|
|
299
|
+
updateScaleScreenPixels(cameraPosition, camera.fov, distance, viewportHeightPx)
|
|
300
|
+
|
|
301
|
+
// Performance optimization: only update distance text if distance changed significantly
|
|
302
|
+
const roundedDistance = Math.round(distance)
|
|
303
|
+
if (Math.abs(roundedDistance - lastDistance) >= 1) {
|
|
304
|
+
lastDistance = roundedDistance
|
|
305
|
+
updateDistanceText(currentLabel, `${roundedDistance}m`)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Update arrow and visibility
|
|
309
|
+
const onScreen = updateOffscreenArrow(camera, viewportWidthPx, viewportHeightPx)
|
|
310
|
+
setVisible(onScreen)
|
|
311
|
+
return onScreen
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function dispose () {
|
|
315
|
+
const mat = sprite.material
|
|
316
|
+
mat.map?.dispose()
|
|
317
|
+
mat.dispose()
|
|
318
|
+
if (arrowSprite) {
|
|
319
|
+
// Remove arrow from parent before disposing
|
|
320
|
+
if (arrowSprite.parent) {
|
|
321
|
+
arrowSprite.parent.remove(arrowSprite)
|
|
322
|
+
}
|
|
323
|
+
const am = arrowSprite.material
|
|
324
|
+
am.map?.dispose()
|
|
325
|
+
am.dispose()
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
group,
|
|
331
|
+
sprite,
|
|
332
|
+
enableOffscreenArrow,
|
|
333
|
+
setArrowParent,
|
|
334
|
+
updateForCamera,
|
|
335
|
+
setColor,
|
|
336
|
+
setLabel,
|
|
337
|
+
updateDistanceText,
|
|
338
|
+
setVisible,
|
|
339
|
+
setPosition,
|
|
340
|
+
dispose,
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Internal helpers
|
|
345
|
+
function drawCombinedCanvas (color: number, id: string, distance: string): OffscreenCanvas {
|
|
346
|
+
const scale = WAYPOINT_CONFIG.CANVAS_SCALE * (globalThis.devicePixelRatio || 1)
|
|
347
|
+
const size = WAYPOINT_CONFIG.CANVAS_SIZE * scale
|
|
348
|
+
const canvas = createCanvas(size, size)
|
|
349
|
+
const ctx = canvas.getContext('2d')!
|
|
350
|
+
|
|
351
|
+
// Clear canvas
|
|
352
|
+
ctx.clearRect(0, 0, size, size)
|
|
353
|
+
|
|
354
|
+
// Draw dot
|
|
355
|
+
const centerX = size / 2
|
|
356
|
+
const dotY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DOT_Y)
|
|
357
|
+
const radius = Math.round(size * 0.05) // Dot takes up ~12% of canvas height
|
|
358
|
+
const borderWidth = Math.max(2, Math.round(4 * scale))
|
|
359
|
+
|
|
360
|
+
// Outer border (black)
|
|
361
|
+
ctx.beginPath()
|
|
362
|
+
ctx.arc(centerX, dotY, radius + borderWidth, 0, Math.PI * 2)
|
|
363
|
+
ctx.fillStyle = 'black'
|
|
364
|
+
ctx.fill()
|
|
365
|
+
|
|
366
|
+
// Inner circle (colored)
|
|
367
|
+
ctx.beginPath()
|
|
368
|
+
ctx.arc(centerX, dotY, radius, 0, Math.PI * 2)
|
|
369
|
+
ctx.fillStyle = `#${color.toString(16).padStart(6, '0')}`
|
|
370
|
+
ctx.fill()
|
|
371
|
+
|
|
372
|
+
// Text properties
|
|
373
|
+
ctx.textAlign = 'center'
|
|
374
|
+
ctx.textBaseline = 'middle'
|
|
375
|
+
|
|
376
|
+
// Title
|
|
377
|
+
const nameFontPx = Math.round(size * 0.08) // ~8% of canvas height
|
|
378
|
+
const distanceFontPx = Math.round(size * 0.06) // ~6% of canvas height
|
|
379
|
+
ctx.font = `bold ${nameFontPx}px mojangles`
|
|
380
|
+
ctx.lineWidth = Math.max(2, Math.round(3 * scale))
|
|
381
|
+
const nameY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.NAME_Y)
|
|
382
|
+
|
|
383
|
+
ctx.strokeStyle = 'black'
|
|
384
|
+
ctx.strokeText(id, centerX, nameY)
|
|
385
|
+
ctx.fillStyle = 'white'
|
|
386
|
+
ctx.fillText(id, centerX, nameY)
|
|
387
|
+
|
|
388
|
+
// Distance
|
|
389
|
+
ctx.font = `bold ${distanceFontPx}px mojangles`
|
|
390
|
+
ctx.lineWidth = Math.max(2, Math.round(2 * scale))
|
|
391
|
+
const distanceY = Math.round(size * WAYPOINT_CONFIG.LAYOUT.DISTANCE_Y)
|
|
392
|
+
|
|
393
|
+
ctx.strokeStyle = 'black'
|
|
394
|
+
ctx.strokeText(distance, centerX, distanceY)
|
|
395
|
+
ctx.fillStyle = '#CCCCCC'
|
|
396
|
+
ctx.fillText(distance, centerX, distanceY)
|
|
397
|
+
|
|
398
|
+
return canvas
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function createCombinedSprite (color: number, id: string, distance: string, depthTest: boolean): THREE.Sprite {
|
|
402
|
+
const canvas = drawCombinedCanvas(color, id, distance)
|
|
403
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
404
|
+
texture.anisotropy = 1
|
|
405
|
+
texture.magFilter = THREE.LinearFilter
|
|
406
|
+
texture.minFilter = THREE.LinearFilter
|
|
407
|
+
const material = new THREE.SpriteMaterial({
|
|
408
|
+
map: texture,
|
|
409
|
+
transparent: true,
|
|
410
|
+
opacity: 1,
|
|
411
|
+
depthTest,
|
|
412
|
+
depthWrite: false,
|
|
413
|
+
})
|
|
414
|
+
const sprite = new THREE.Sprite(material)
|
|
415
|
+
sprite.position.set(0, 0, 0)
|
|
416
|
+
return sprite
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export const WaypointHelpers = {
|
|
420
|
+
// World-scale constant size helper
|
|
421
|
+
computeWorldScale (distance: number, fixedReference = 10) {
|
|
422
|
+
return Math.max(0.0001, distance / fixedReference)
|
|
423
|
+
},
|
|
424
|
+
// Screen-pixel constant size helper
|
|
425
|
+
computeScreenPixelScale (
|
|
426
|
+
camera: THREE.PerspectiveCamera,
|
|
427
|
+
distance: number,
|
|
428
|
+
pixelSize: number,
|
|
429
|
+
viewportHeightPx: number
|
|
430
|
+
) {
|
|
431
|
+
const vFovRad = camera.fov * Math.PI / 180
|
|
432
|
+
const worldUnitsPerScreenHeightAtDist = Math.tan(vFovRad / 2) * 2 * distance
|
|
433
|
+
return worldUnitsPerScreenHeightAtDist * (pixelSize / viewportHeightPx)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { WorldRendererThree } from './worldRendererThree'
|
|
3
|
+
import { createWaypointSprite, type WaypointSprite } from './waypointSprite'
|
|
4
|
+
|
|
5
|
+
interface Waypoint {
|
|
6
|
+
id: string
|
|
7
|
+
x: number
|
|
8
|
+
y: number
|
|
9
|
+
z: number
|
|
10
|
+
minDistance: number
|
|
11
|
+
color: number
|
|
12
|
+
label?: string
|
|
13
|
+
sprite: WaypointSprite
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface WaypointOptions {
|
|
17
|
+
color?: number
|
|
18
|
+
label?: string
|
|
19
|
+
minDistance?: number
|
|
20
|
+
metadata?: any
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class WaypointsRenderer {
|
|
24
|
+
private readonly waypoints = new Map<string, Waypoint>()
|
|
25
|
+
private readonly waypointScene = new THREE.Scene()
|
|
26
|
+
|
|
27
|
+
// Performance optimization: cache camera position to reduce update frequency
|
|
28
|
+
private readonly lastCameraPosition = new THREE.Vector3()
|
|
29
|
+
private lastUpdateTime = 0
|
|
30
|
+
private readonly UPDATE_THROTTLE_MS = 16 // ~60fps max update rate
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly worldRenderer: WorldRendererThree
|
|
34
|
+
) {
|
|
35
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
36
|
+
// this.addWaypoint('spawn', 0, 0, 0, { })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private updateWaypoints() {
|
|
41
|
+
const currentTime = performance.now()
|
|
42
|
+
const playerPos = this.worldRenderer.cameraObject.position
|
|
43
|
+
|
|
44
|
+
// Performance optimization: throttle updates and check for significant camera movement
|
|
45
|
+
const cameraMovedSignificantly = this.lastCameraPosition.distanceTo(playerPos) > 0.5
|
|
46
|
+
const timeToUpdate = currentTime - this.lastUpdateTime > this.UPDATE_THROTTLE_MS
|
|
47
|
+
|
|
48
|
+
if (!cameraMovedSignificantly && !timeToUpdate) {
|
|
49
|
+
return // Skip update if camera hasn't moved much and not enough time passed
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.lastCameraPosition.copy(playerPos)
|
|
53
|
+
this.lastUpdateTime = currentTime
|
|
54
|
+
|
|
55
|
+
const sizeVec = this.worldRenderer.renderer.getSize(new THREE.Vector2())
|
|
56
|
+
|
|
57
|
+
for (const waypoint of this.waypoints.values()) {
|
|
58
|
+
const waypointPos = new THREE.Vector3(waypoint.x, waypoint.y, waypoint.z)
|
|
59
|
+
const distance = playerPos.distanceTo(waypointPos)
|
|
60
|
+
const visible = !waypoint.minDistance || distance >= waypoint.minDistance
|
|
61
|
+
|
|
62
|
+
waypoint.sprite.setVisible(visible)
|
|
63
|
+
|
|
64
|
+
if (visible) {
|
|
65
|
+
// Update position
|
|
66
|
+
waypoint.sprite.setPosition(waypoint.x, waypoint.y, waypoint.z)
|
|
67
|
+
// Ensure camera-based update each frame
|
|
68
|
+
waypoint.sprite.updateForCamera(this.worldRenderer.getCameraPosition(), this.worldRenderer.camera, sizeVec.width, sizeVec.height)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
render() {
|
|
74
|
+
if (this.waypoints.size === 0) return
|
|
75
|
+
|
|
76
|
+
// Update waypoint scaling
|
|
77
|
+
this.updateWaypoints()
|
|
78
|
+
|
|
79
|
+
// Render waypoints scene with the world camera
|
|
80
|
+
this.worldRenderer.renderer.render(this.waypointScene, this.worldRenderer.camera)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Removed sprite/label texture creation. Use utils/waypointSprite.ts
|
|
84
|
+
|
|
85
|
+
addWaypoint(
|
|
86
|
+
id: string,
|
|
87
|
+
x: number,
|
|
88
|
+
y: number,
|
|
89
|
+
z: number,
|
|
90
|
+
options: WaypointOptions = {}
|
|
91
|
+
) {
|
|
92
|
+
// Remove existing waypoint if it exists
|
|
93
|
+
this.removeWaypoint(id)
|
|
94
|
+
|
|
95
|
+
const color = options.color ?? 0xFF_00_00
|
|
96
|
+
const { label, metadata } = options
|
|
97
|
+
const minDistance = options.minDistance ?? 0
|
|
98
|
+
|
|
99
|
+
const sprite = createWaypointSprite({
|
|
100
|
+
position: new THREE.Vector3(x, y, z),
|
|
101
|
+
color,
|
|
102
|
+
label: (label || id),
|
|
103
|
+
metadata,
|
|
104
|
+
})
|
|
105
|
+
sprite.enableOffscreenArrow(true)
|
|
106
|
+
sprite.setArrowParent(this.waypointScene)
|
|
107
|
+
|
|
108
|
+
this.waypointScene.add(sprite.group)
|
|
109
|
+
|
|
110
|
+
this.waypoints.set(id, {
|
|
111
|
+
id, x: x + 0.5, y: y + 0.5, z: z + 0.5, minDistance,
|
|
112
|
+
color, label,
|
|
113
|
+
sprite,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
removeWaypoint(id: string) {
|
|
118
|
+
const waypoint = this.waypoints.get(id)
|
|
119
|
+
if (waypoint) {
|
|
120
|
+
this.waypointScene.remove(waypoint.sprite.group)
|
|
121
|
+
waypoint.sprite.dispose()
|
|
122
|
+
this.waypoints.delete(id)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
clear() {
|
|
127
|
+
for (const id of this.waypoints.keys()) {
|
|
128
|
+
this.removeWaypoint(id)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
testWaypoint() {
|
|
133
|
+
this.addWaypoint('Test Point', 0, 70, 0, { color: 0x00_FF_00, label: 'Test Point' })
|
|
134
|
+
this.addWaypoint('Spawn', 0, 64, 0, { color: 0xFF_FF_00, label: 'Spawn' })
|
|
135
|
+
this.addWaypoint('Far Point', 100, 70, 100, { color: 0x00_00_FF, label: 'Far Point' })
|
|
136
|
+
this.addWaypoint('Far Point 2', 180, 170, 100, { color: 0x00_00_FF, label: 'Far Point 2' })
|
|
137
|
+
this.addWaypoint('Far Point 3', 1000, 100, 1000, { color: 0x00_00_FF, label: 'Far Point 3' })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getWaypoint(id: string): Waypoint | undefined {
|
|
141
|
+
return this.waypoints.get(id)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getAllWaypoints(): Waypoint[] {
|
|
145
|
+
return [...this.waypoints.values()]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setWaypointColor(id: string, color: number) {
|
|
149
|
+
const waypoint = this.waypoints.get(id)
|
|
150
|
+
if (waypoint) {
|
|
151
|
+
waypoint.sprite.setColor(color)
|
|
152
|
+
waypoint.color = color
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
setWaypointLabel(id: string, label?: string) {
|
|
157
|
+
const waypoint = this.waypoints.get(id)
|
|
158
|
+
if (waypoint) {
|
|
159
|
+
waypoint.label = label
|
|
160
|
+
waypoint.sprite.setLabel(label)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|