minecraft-renderer 0.1.43 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/mesher.js +35 -35
  2. package/dist/mesher.js.map +4 -4
  3. package/dist/mesherWasm.js +61 -61
  4. package/dist/minecraft-renderer.js +59 -59
  5. package/dist/minecraft-renderer.js.meta.json +1 -1
  6. package/dist/threeWorker.js +458 -458
  7. package/package.json +1 -1
  8. package/src/graphicsBackend/appViewer.ts +19 -7
  9. package/src/graphicsBackend/types.ts +5 -1
  10. package/src/index.ts +33 -0
  11. package/src/lib/ui/newStats.ts +16 -2
  12. package/src/lib/worldrendererCommon.ts +2 -2
  13. package/src/mesher-shared/models.ts +28 -11
  14. package/src/mesher-shared/vertexShading.ts +35 -0
  15. package/src/three/entities.ts +22 -6
  16. package/src/three/graphicsBackendBase.ts +28 -20
  17. package/src/three/graphicsBackendOffThread.ts +1 -2
  18. package/src/three/menuBackground/activeView.ts +19 -0
  19. package/src/three/menuBackground/classic.ts +148 -0
  20. package/src/three/menuBackground/config.ts +23 -0
  21. package/src/three/menuBackground/defaultOptions.ts +141 -0
  22. package/src/three/menuBackground/futuristic.ts +859 -0
  23. package/src/three/menuBackground/index.ts +36 -0
  24. package/src/three/menuBackground/renderer.ts +97 -0
  25. package/src/three/menuBackground/shared.ts +3 -0
  26. package/src/three/menuBackground/types.ts +37 -0
  27. package/src/three/menuBackground/worldBlocks.ts +144 -0
  28. package/src/three/modules/rain.ts +21 -3
  29. package/src/three/waypointSprite.ts +108 -106
  30. package/src/three/worldRendererThree.ts +9 -12
  31. package/src/wasm-mesher/bridge/render-from-wasm.ts +57 -12
  32. package/src/three/panorama.ts +0 -312
  33. package/src/three/panoramaShared.ts +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -27,10 +27,12 @@ import { defaultWorldRendererConfig, defaultGraphicsBackendConfig, getDefaultRen
27
27
  import { PlayerStateReactive } from '../playerState/playerState'
28
28
  import { ResourcesManager, ResourcesManagerTransferred } from '../resourcesManager'
29
29
  import { preloadMesherWorkerScript } from './preloadWorkers'
30
+ import type { MenuBackgroundOptions } from '../three/menuBackground/types'
30
31
 
31
32
  export interface AppViewerOptions {
32
33
  config?: Partial<GraphicsBackendConfig>
33
34
  rendererConfig?: Partial<WorldRendererConfig>
35
+ menuBackground?: MenuBackgroundOptions
34
36
  }
35
37
 
36
38
  /**
@@ -49,6 +51,7 @@ export class AppViewer {
49
51
 
50
52
  // Configuration
51
53
  readonly config: GraphicsBackendConfig
54
+ readonly menuBackgroundOptions: MenuBackgroundOptions
52
55
  readonly inWorldRenderingConfig: WorldRendererConfig
53
56
 
54
57
  // Backend
@@ -83,7 +86,10 @@ export class AppViewer {
83
86
  ...defaultGraphicsBackendConfig,
84
87
  ...options.config
85
88
  }
86
-
89
+ this.menuBackgroundOptions = {
90
+ ...options.config?.menuBackground,
91
+ ...options.menuBackground
92
+ }
87
93
  this.inWorldRenderingConfig = proxy({
88
94
  ...defaultWorldRendererConfig,
89
95
  ...options.rendererConfig
@@ -145,8 +151,8 @@ export class AppViewer {
145
151
 
146
152
  // Execute queued action if exists
147
153
  if (this.currentState) {
148
- if (this.currentState.method === 'startPanorama') {
149
- this.startPanorama()
154
+ if (this.currentState.method === 'startMenuBackground') {
155
+ this.startMenuBackground(...this.currentState.args)
150
156
  } else {
151
157
  const { method, args } = this.currentState
152
158
  ; (this.backend as any)[method](...args)
@@ -202,17 +208,23 @@ export class AppViewer {
202
208
  }
203
209
 
204
210
  /**
205
- * Start panorama display (menu background).
211
+ * Start the main-menu background (3D scene behind UI).
206
212
  */
207
- startPanorama(): void {
213
+ startMenuBackground(menuBackgroundOptions?: MenuBackgroundOptions): void {
208
214
  if (this.currentDisplay === 'menu') return
209
215
 
216
+ const merged: MenuBackgroundOptions = {
217
+ ...this.menuBackgroundOptions,
218
+ ...menuBackgroundOptions,
219
+ resourcesManager: menuBackgroundOptions?.resourcesManager ?? this.resourcesManager
220
+ }
221
+
210
222
  if (this.backend) {
211
223
  this.currentDisplay = 'menu'
212
- this.backend.startPanorama()
224
+ this.backend.startMenuBackground(merged)
213
225
  }
214
226
 
215
- this.currentState = { method: 'startPanorama', args: [] }
227
+ this.currentState = { method: 'startMenuBackground', args: [merged] }
216
228
  }
217
229
 
218
230
  /**
@@ -22,6 +22,8 @@ export interface SoundSystem {
22
22
  destroy: () => void
23
23
  }
24
24
 
25
+ import type { MenuBackgroundOptions } from '../three/menuBackground/types'
26
+
25
27
  /** Graphics backend configuration */
26
28
  export interface GraphicsBackendConfig {
27
29
  fpsLimit?: number
@@ -29,6 +31,8 @@ export interface GraphicsBackendConfig {
29
31
  statsVisible?: number
30
32
  sceneBackground: string
31
33
  timeoutRendering?: boolean
34
+ /** Default options when `startMenuBackground()` is called without arguments */
35
+ menuBackground?: MenuBackgroundOptions
32
36
  }
33
37
 
34
38
  // ============================================================================
@@ -112,7 +116,7 @@ export interface DisplayWorldOptions {
112
116
  export interface GraphicsBackend {
113
117
  id: string
114
118
  displayName: string
115
- startPanorama(): Promise<void>
119
+ startMenuBackground(options?: MenuBackgroundOptions): Promise<void>
116
120
  startWorld(options: DisplayWorldOptions): Promise<void>
117
121
  disconnect(): void
118
122
  setRendering(rendering: boolean): void
package/src/index.ts CHANGED
@@ -78,3 +78,36 @@ export {
78
78
  addCanvasForWorker,
79
79
  isWebWorker
80
80
  } from './three/documentRenderer'
81
+ export { MC_RENDERER_DEBUG_OVERLAY_CLASS } from './lib/ui/newStats'
82
+
83
+ // Main-menu background (title screen backdrop)
84
+ export type {
85
+ MenuBackgroundMode,
86
+ MenuBackgroundOptions,
87
+ MenuBackgroundView,
88
+ FuturisticSceneId,
89
+ FuturisticCameraId,
90
+ FuturisticMenuBackgroundOptions,
91
+ MinecraftBlockGroupId
92
+ } from './three/menuBackground'
93
+ export {
94
+ MenuBackgroundRenderer,
95
+ ClassicMenuBackground,
96
+ FuturisticMenuBackground,
97
+ WorldBlocksMenuBackground,
98
+ MENU_BACKGROUND_MC_VERSION,
99
+ FUTURISTIC_SCENE_IDS,
100
+ FUTURISTIC_CAMERA_IDS,
101
+ FUTURISTIC_SCENE_LABELS,
102
+ FUTURISTIC_CAMERA_LABELS,
103
+ MINECRAFT_BLOCK_GROUPS,
104
+ MINECRAFT_BLOCK_GROUP_IDS,
105
+ MINECRAFT_BLOCK_GROUP_LABELS,
106
+ RENDERER_DEFAULT_OPTIONS,
107
+ RENDERER_OPTIONS_META,
108
+ RENDERER_RENDER_GUI_SECTIONS,
109
+ MENU_BACKGROUND_OPTION_DEFAULTS,
110
+ MENU_BACKGROUND_MOTION_DEFAULTS,
111
+ menuBackgroundSpeedToMultiplier
112
+ } from './three/menuBackground'
113
+ export type { RendererDefaultOptionKey, RendererOptionMeta } from './three/menuBackground'
@@ -7,7 +7,17 @@ const rightOffset = 0
7
7
  const stats = {}
8
8
 
9
9
  let lastY = 40
10
- export const addNewStat = (id: string, width = 80, x = rightOffset, y = lastY) => {
10
+
11
+ /** Class for advanced stats pane; host app should set z-index (see integrating app global CSS). */
12
+ export const MC_RENDERER_DEBUG_OVERLAY_CLASS = 'mc-renderer-debug-overlay'
13
+
14
+ export const addNewStat = (
15
+ id: string,
16
+ width = 80,
17
+ x = rightOffset,
18
+ y = lastY,
19
+ opts?: { className?: string },
20
+ ) => {
11
21
  if (isWebWorker) return { updateText() { }, setVisibility() { } }
12
22
 
13
23
  const pane = document.createElement('div')
@@ -20,7 +30,11 @@ export const addNewStat = (id: string, width = 80, x = rightOffset, y = lastY) =
20
30
  pane.style.padding = '2px'
21
31
  pane.style.fontFamily = 'monospace'
22
32
  pane.style.fontSize = '12px'
23
- pane.style.zIndex = '100'
33
+ if (opts?.className) {
34
+ pane.className = opts.className
35
+ } else {
36
+ pane.style.zIndex = '100'
37
+ }
24
38
  pane.style.pointerEvents = 'none'
25
39
  document.body.appendChild(pane)
26
40
  stats[id] = pane
@@ -11,7 +11,7 @@ import { dynamicMcDataFiles } from './buildSharedConfig.mjs'
11
11
  import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState, SoundSystem } from '../graphicsBackend/types'
12
12
  import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent, SECTION_HEIGHT } from '../mesher-shared/shared'
13
13
  import { chunkPos } from './simpleUtils'
14
- import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
14
+ import { addNewStat, MC_RENDERER_DEBUG_OVERLAY_CLASS, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
15
15
  import { getPlayerStateUtils } from '../graphicsBackend/playerState'
16
16
  // TODO: Fix PlayerStateRenderer and PlayerStateUtils imports
17
17
  type PlayerStateUtils = ReturnType<typeof getPlayerStateUtils>
@@ -194,7 +194,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
194
194
  updateStatText('loaded-chunks', `${loadedChunks}/${this.chunksLength} chunks (${this.lastChunkDistance}/${this.viewDistance})`)
195
195
  })
196
196
 
197
- addNewStat('downloaded-chunks', 100, 140, 20)
197
+ addNewStat('downloaded-chunks', 100, 140, 20, { className: MC_RENDERER_DEBUG_OVERLAY_CLASS })
198
198
 
199
199
  this.connect(this.displayOptions.worldView as any)
200
200
 
@@ -6,6 +6,7 @@ import legacyJson from '../lib/preflatMap.json'
6
6
  import { BlockType } from '../playground/shared'
7
7
  import { World, BlockModelPartsResolved, WorldBlock as Block, WorldBlock, worldColumnKey } from './world'
8
8
  import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
9
+ import { getSideShading, vertexLightFromAo } from './vertexShading'
9
10
  import { INVISIBLE_BLOCKS } from './worldConstants'
10
11
  import { MesherGeometryOutput, HighestBlockInfo } from './shared'
11
12
  import { collectBlockEntityMetadata } from './blockEntityMetadata'
@@ -51,6 +52,31 @@ function prepareTints(tints) {
51
52
  }
52
53
 
53
54
  const calculatedBlocksEntries = Object.entries(legacyJson.clientCalculatedBlocks)
55
+
56
+ /**
57
+ * Block name + properties for model lookup. Only runs neighbor/preflat work when
58
+ * `world.preflat` (legacy); modern block-state worlds use `fromStateId` only.
59
+ */
60
+ export function resolveBlockPropertiesForMeshing(
61
+ world: World | undefined,
62
+ cursor: Vec3,
63
+ blockProvider: WorldBlockProvider,
64
+ blockStateId: number,
65
+ PrismarineBlockCtor: { fromStateId: (id: number, biome: number) => Block }
66
+ ): { name: string, properties: Record<string, unknown> } {
67
+ if (world?.preflat) {
68
+ const block = world.getBlock(cursor, blockProvider, {})
69
+ if (block) {
70
+ let properties: Record<string, unknown> = { ...block.getProperties() }
71
+ const patch = preflatBlockCalculation(block, world, cursor)
72
+ if (patch) properties = { ...properties, ...patch }
73
+ return { name: block.name, properties }
74
+ }
75
+ }
76
+ const fromState = PrismarineBlockCtor.fromStateId(blockStateId, 1)
77
+ return { name: fromState.name, properties: fromState.getProperties() }
78
+ }
79
+
54
80
  export function preflatBlockCalculation(block: Block, world: World, position: Vec3) {
55
81
  const type = calculatedBlocksEntries.find(([name, blocks]) => blocks.includes(block.name))?.[0]
56
82
  if (!type) return
@@ -398,13 +424,7 @@ function renderElement(world: World, cursor: Vec3, element: BlockElement, doAO:
398
424
  // 10%
399
425
  const { smoothLighting, shadingTheme, cardinalLight } = world.config
400
426
  const faceLight = world.getLight(neighborPos, undefined, undefined, block.name)
401
- const sideShading = (shadingTheme === 'high-contrast')
402
- ? (0.8 + 0.5 * Math.max(0, 0.66 * dir[0] + 0.66 * dir[1] + 0.33 * dir[2])) // old directional light behavior
403
- : (
404
- cardinalLight === 'nether'
405
- ? (0.5 + Math.abs(0.1 * dir[0] + 0.4 * dir[1] + 0.3 * dir[2]))
406
- : (0.75 + 0.25 * dir[1] + 0.05 * (Math.abs(dir[2]) - 3 * Math.abs(dir[0])))
407
- )
427
+ const sideShading = getSideShading(dir, shadingTheme, cardinalLight)
408
428
  const baseLight = sideShading * faceLight / 15
409
429
  for (const pos of corners) {
410
430
  let vertex = [
@@ -478,10 +498,7 @@ function renderElement(world: World, cursor: Vec3, element: BlockElement, doAO:
478
498
  // TODO: correctly interpolate ao light based on pos (evaluate once for each corner of the block)
479
499
 
480
500
  const ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock))
481
- const ao_bias = (shadingTheme === 'high-contrast') ? 0.25 : 0.4
482
- const ao_scale = (shadingTheme === 'high-contrast') ? 0.25 : 0.2
483
- // todo light should go upper on lower blocks
484
- light = sideShading * (ao * ao_scale + ao_bias) * (cornerLightResult / 15)
501
+ light = vertexLightFromAo(ao, cornerLightResult, sideShading, shadingTheme)
485
502
  aos.push(ao)
486
503
 
487
504
  // Log AO and light for this corner (corner index is aos.length - 1)
@@ -0,0 +1,35 @@
1
+ //@ts-nocheck
2
+ import type { MesherConfig } from './shared'
3
+
4
+ export type FaceDirection = readonly [number, number, number]
5
+
6
+ /** Directional face darkening (matches legacy `renderElement` in models.ts). */
7
+ export function getSideShading(
8
+ dir: FaceDirection,
9
+ shadingTheme: MesherConfig['shadingTheme'],
10
+ cardinalLight: MesherConfig['cardinalLight']
11
+ ): number {
12
+ if (shadingTheme === 'high-contrast') {
13
+ return 0.8 + 0.5 * Math.max(0, 0.66 * dir[0] + 0.66 * dir[1] + 0.33 * dir[2])
14
+ }
15
+ if (cardinalLight === 'nether') {
16
+ return 0.5 + Math.abs(0.1 * dir[0] + 0.4 * dir[1] + 0.3 * dir[2])
17
+ }
18
+ return 0.75 + 0.25 * dir[1] + 0.05 * (Math.abs(dir[2]) - 3 * Math.abs(dir[0]))
19
+ }
20
+
21
+ /** Per-vertex brightness from AO (0–3) and corner light (0–15). */
22
+ export function vertexLightFromAo(
23
+ ao: number,
24
+ cornerLight15: number,
25
+ sideShading: number,
26
+ shadingTheme: MesherConfig['shadingTheme']
27
+ ): number {
28
+ const lightNorm = cornerLight15 / 15
29
+ if (shadingTheme === 'high-contrast') {
30
+ return sideShading * ((ao + 1) / 4) * lightNorm
31
+ }
32
+ const aoBias = 0.4
33
+ const aoScale = 0.2
34
+ return sideShading * (ao * aoScale + aoBias) * lightNorm
35
+ }
@@ -108,6 +108,14 @@ function poseToEuler(pose: any, defaultValue?: THREE.Euler) {
108
108
  return defaultValue ?? new THREE.Euler()
109
109
  }
110
110
 
111
+ const TAU_YAW = Math.PI * 2
112
+
113
+ /** Prismarine yaw in radians → shortest delta from→to in (-π, π]. */
114
+ function shortestYawRadians(fromYawRad: number, toYawRad: number): number {
115
+ const norm = ((toYawRad - fromYawRad) % TAU_YAW + TAU_YAW) % TAU_YAW
116
+ return norm > Math.PI ? norm - TAU_YAW : norm
117
+ }
118
+
111
119
  function getUsernameTexture({
112
120
  username,
113
121
  nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)',
@@ -1366,17 +1374,25 @@ export class Entities {
1366
1374
  })
1367
1375
  .start()
1368
1376
  }
1369
- if (entity.yaw) {
1370
- const da = (entity.yaw - e.rotation.y) % (Math.PI * 2)
1371
- const dy = 2 * da % (Math.PI * 2) - da
1377
+ if (typeof entity.yaw === 'number' && Number.isFinite(entity.yaw)) {
1378
+ const dy = shortestYawRadians(e.rotation.y, entity.yaw)
1372
1379
  new TWEEN.Tween(e.rotation).to({ y: e.rotation.y + dy }, ANIMATION_DURATION).start()
1373
1380
  }
1374
1381
 
1375
1382
  if (e?.playerObject && overrides?.rotation?.head) {
1376
1383
  const { playerObject } = e
1377
- const headRotationDiff = overrides.rotation.head.y ? overrides.rotation.head.y - entity.yaw : 0
1378
- playerObject.skin.head.rotation.y = -headRotationDiff
1379
- playerObject.skin.head.rotation.x = overrides.rotation.head.x ? - overrides.rotation.head.x : 0
1384
+ const hy = overrides.rotation.head.y
1385
+ const headYawWorld =
1386
+ typeof hy === 'number' && Number.isFinite(hy) ? hy : entity.yaw
1387
+ const headYawOffset =
1388
+ typeof headYawWorld === 'number' && typeof entity.yaw === 'number' && Number.isFinite(headYawWorld) && Number.isFinite(entity.yaw)
1389
+ ? shortestYawRadians(entity.yaw, headYawWorld)
1390
+ : 0
1391
+ playerObject.skin.head.rotation.y = headYawOffset
1392
+
1393
+ const hp = overrides.rotation.head.x
1394
+ playerObject.skin.head.rotation.x =
1395
+ typeof hp === 'number' && Number.isFinite(hp) ? -hp : 0
1380
1396
  }
1381
1397
  }
1382
1398
 
@@ -14,7 +14,8 @@ import { ResourcesManager } from '../resourcesManager'
14
14
  import { FrameTimingCollector } from '../lib/frameTimingCollector'
15
15
  import { WorldRendererThree } from './worldRendererThree'
16
16
  import { DocumentRenderer, isWebWorker, ThreeRendererMainData } from './documentRenderer'
17
- import { PanoramaRenderer } from './panorama'
17
+ import { MenuBackgroundRenderer } from './menuBackground'
18
+ import type { MenuBackgroundOptions } from './menuBackground/types'
18
19
  import { WorldViewWorker } from '../worldView'
19
20
  import type { FeedChunkPacketPayload } from '../worldView/types'
20
21
 
@@ -127,7 +128,7 @@ export const createGraphicsBackendBase = () => {
127
128
  // Private state
128
129
  let initOptions!: GraphicsInitOptions
129
130
  let documentRenderer: DocumentRenderer | null = null
130
- let panoramaRenderer: PanoramaRenderer | null = null
131
+ let menuBackgroundRenderer: MenuBackgroundRenderer | null = null
131
132
  let worldRenderer: WorldRendererThree | null = null
132
133
  let frameTimingCollector: FrameTimingCollector | null = null
133
134
 
@@ -146,19 +147,26 @@ export const createGraphicsBackendBase = () => {
146
147
  callModsMethod('default', backend)
147
148
  }
148
149
 
149
- const startPanorama = async () => {
150
+ const startMenuBackground = async (menuBackgroundStartOptions?: MenuBackgroundOptions) => {
150
151
  if (!documentRenderer) throw new Error('Document renderer not initialized')
151
152
  if (worldRenderer) return
152
153
 
153
- if (!panoramaRenderer) {
154
- // Create panorama-specific init options with resourcesManager
155
- const panoramaInitOptions = { ...initOptions }
156
- panoramaRenderer = new PanoramaRenderer(documentRenderer, panoramaInitOptions, !!process.env.SINGLE_FILE_BUILD_MODE)
157
- ; (globalThis as any).panoramaRenderer = panoramaRenderer
158
-
159
- callModsMethod('panoramaCreated', panoramaRenderer)
160
- await panoramaRenderer.start()
161
- callModsMethod('panoramaReady', panoramaRenderer)
154
+ if (!menuBackgroundRenderer) {
155
+ const mergedOptions: MenuBackgroundOptions = {
156
+ ...initOptions.config.menuBackground,
157
+ ...menuBackgroundStartOptions
158
+ }
159
+ menuBackgroundRenderer = new MenuBackgroundRenderer(
160
+ documentRenderer,
161
+ { ...initOptions },
162
+ mergedOptions,
163
+ !!process.env.SINGLE_FILE_BUILD_MODE
164
+ )
165
+ ; (globalThis as any).menuBackgroundRenderer = menuBackgroundRenderer
166
+
167
+ callModsMethod('menuBackgroundCreated', menuBackgroundRenderer)
168
+ await menuBackgroundRenderer.start(mergedOptions)
169
+ callModsMethod('menuBackgroundReady', menuBackgroundRenderer)
162
170
  }
163
171
  }
164
172
 
@@ -172,9 +180,9 @@ export const createGraphicsBackendBase = () => {
172
180
  // Set resourcesManager globally for world rendering
173
181
  ; (globalThis as any).resourcesManager = displayOptions.resourcesManager
174
182
 
175
- if (panoramaRenderer) {
176
- panoramaRenderer.dispose()
177
- panoramaRenderer = null
183
+ if (menuBackgroundRenderer) {
184
+ menuBackgroundRenderer.dispose()
185
+ menuBackgroundRenderer = null
178
186
  }
179
187
 
180
188
  worldRenderer = new WorldRendererThree(documentRenderer.renderer, initOptions, displayOptions)
@@ -206,9 +214,9 @@ export const createGraphicsBackendBase = () => {
206
214
  }
207
215
 
208
216
  const disconnect = () => {
209
- if (panoramaRenderer) {
210
- panoramaRenderer.dispose()
211
- panoramaRenderer = null
217
+ if (menuBackgroundRenderer) {
218
+ menuBackgroundRenderer.dispose()
219
+ menuBackgroundRenderer = null
212
220
  }
213
221
 
214
222
  if (documentRenderer) {
@@ -225,7 +233,7 @@ export const createGraphicsBackendBase = () => {
225
233
  const backend: GraphicsBackend = {
226
234
  id: 'threejs',
227
235
  displayName: `three.js ${THREE.REVISION}`,
228
- startPanorama,
236
+ startMenuBackground,
229
237
  startWorld,
230
238
  disconnect,
231
239
  setRendering(rendering) {
@@ -269,7 +277,7 @@ export const createGraphicsBackendBase = () => {
269
277
  updateSizeExternal(width: number, height: number, pixelRatio: number) {
270
278
  documentRenderer?.updateSizeExternal(width, height, pixelRatio)
271
279
  },
272
- startPanorama,
280
+ startMenuBackground,
273
281
  startWorld,
274
282
  disconnect,
275
283
  setRendering: backend.setRendering,
@@ -58,8 +58,7 @@ export const createGraphicsBackendOffThread: GraphicsBackendLoader = async (init
58
58
  const backend: GraphicsBackend = {
59
59
  id: 'threejs',
60
60
  displayName: `three.js ${THREE.REVISION}`,
61
- // startPanorama: proxy.startPanorama,
62
- async startPanorama() { },
61
+ async startMenuBackground() { },
63
62
  async startWorld(options) {
64
63
  const workerThreeSendData = {
65
64
  ...dynamicMcDataFiles,
@@ -0,0 +1,19 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+
4
+ /** Contract for a main-menu background implementation (classic cubemap, futuristic scene, etc.). */
5
+ export interface MenuBackgroundView {
6
+ readonly scene: THREE.Scene
7
+ readonly camera: THREE.PerspectiveCamera
8
+ init(): Promise<void>
9
+ update(dt: number, sizeChanged: boolean): void
10
+ dispose(): void
11
+ }
12
+
13
+ export function resizeMenuBackgroundCamera(
14
+ camera: THREE.PerspectiveCamera,
15
+ canvas: { width: number, height: number }
16
+ ) {
17
+ camera.aspect = canvas.width / canvas.height
18
+ camera.updateProjectionMatrix()
19
+ }
@@ -0,0 +1,148 @@
1
+ //@ts-nocheck
2
+ import { join } from 'path'
3
+ import * as THREE from 'three'
4
+ import { EntityMesh } from '../entity/EntityMesh'
5
+ import type { DocumentRenderer } from '../documentRenderer'
6
+ import { loadThreeJsTextureFromUrl, loadThreeJsTextureFromUrlSync } from '../threeJsUtils'
7
+ import type { MenuBackgroundView } from './activeView'
8
+ import { resizeMenuBackgroundCamera } from './activeView'
9
+
10
+ const date = new Date()
11
+ const isChristmas = date.getMonth() === 11 && date.getDate() >= 24 && date.getDate() <= 26
12
+
13
+ const panoramaFiles = [
14
+ 'panorama_3.webp', // right (+x)
15
+ 'panorama_1.webp', // left (-x)
16
+ 'panorama_4.webp', // top (+y)
17
+ 'panorama_5.webp', // bottom (-y)
18
+ 'panorama_0.webp', // front (+z)
19
+ 'panorama_2.webp', // back (-z)
20
+ ]
21
+
22
+ const FADE_IN_DURATION_MS = 200
23
+
24
+ /**
25
+ * Vanilla-style rotating cubemap (Minecraft title-screen style) with optional squids.
26
+ */
27
+ export class ClassicMenuBackground implements MenuBackgroundView {
28
+ readonly scene: THREE.Scene
29
+ readonly camera: THREE.PerspectiveCamera
30
+
31
+ private readonly startTimes = new Map<THREE.MeshBasicMaterial, number>()
32
+ private time = 0
33
+ private panoramaGroup: THREE.Object3D | null = null
34
+
35
+ constructor(private readonly documentRenderer: DocumentRenderer) {
36
+ this.scene = new THREE.Scene()
37
+ this.scene.background = new THREE.Color(0x32_45_68)
38
+
39
+ const ambient = new THREE.AmbientLight(0xcc_cc_cc)
40
+ this.scene.add(ambient)
41
+ const directional = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
42
+ directional.position.set(1, 1, 0.5).normalize()
43
+ directional.castShadow = true
44
+ this.scene.add(directional)
45
+
46
+ this.camera = new THREE.PerspectiveCamera(
47
+ 85,
48
+ documentRenderer.canvas.width / documentRenderer.canvas.height,
49
+ 0.05,
50
+ 1000
51
+ )
52
+ this.camera.position.set(0, 0, 0)
53
+ this.camera.rotation.set(0, 0, 0)
54
+ }
55
+
56
+ async init() {
57
+ this.buildCubemap()
58
+ }
59
+
60
+ update(_dt: number, sizeChanged: boolean) {
61
+ if (sizeChanged) {
62
+ resizeMenuBackgroundCamera(this.camera, this.documentRenderer.canvas)
63
+ }
64
+ }
65
+
66
+ dispose() {
67
+ this.scene.clear()
68
+ this.panoramaGroup = null
69
+ this.startTimes.clear()
70
+ }
71
+
72
+ private buildCubemap() {
73
+ const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000)
74
+ const panorMaterials: THREE.MeshBasicMaterial[] = []
75
+
76
+ for (const file of panoramaFiles) {
77
+ const load = async () => {
78
+ const { texture } = loadThreeJsTextureFromUrlSync(join('background', isChristmas ? 'christmas' : '', file))
79
+
80
+ texture.matrixAutoUpdate = false
81
+ texture.matrix.set(-1, 0, 1, 0, 1, 0, 0, 0, 1)
82
+ texture.wrapS = THREE.ClampToEdgeWrapping
83
+ texture.wrapT = THREE.ClampToEdgeWrapping
84
+ texture.minFilter = THREE.LinearFilter
85
+ texture.magFilter = THREE.LinearFilter
86
+
87
+ const material = new THREE.MeshBasicMaterial({
88
+ map: texture,
89
+ transparent: true,
90
+ side: THREE.DoubleSide,
91
+ depthWrite: false,
92
+ opacity: 0
93
+ })
94
+
95
+ this.startTimes.set(material, Date.now())
96
+ panorMaterials.push(material)
97
+ }
98
+
99
+ void load()
100
+ }
101
+
102
+ const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials)
103
+ panoramaBox.onBeforeRender = () => {
104
+ this.time += 0.01
105
+ panoramaBox.rotation.y = Math.PI + this.time * 0.01
106
+ panoramaBox.rotation.z = Math.sin(-this.time * 0.001) * 0.001
107
+
108
+ for (const material of panorMaterials) {
109
+ const startTime = this.startTimes.get(material)
110
+ if (startTime) {
111
+ const elapsed = Date.now() - startTime
112
+ material.opacity = Math.min(1, elapsed / FADE_IN_DURATION_MS)
113
+ }
114
+ }
115
+ }
116
+
117
+ const group = new THREE.Object3D()
118
+ group.add(panoramaBox)
119
+
120
+ if (!isChristmas) {
121
+ for (let i = 0; i < 20; i++) {
122
+ const m = new EntityMesh('1.16.4', 'squid').mesh
123
+ m.position.set(Math.random() * 30 - 15, Math.random() * 20 - 10, Math.random() * 10 - 17)
124
+ m.rotation.set(0, Math.PI + Math.random(), -Math.PI / 4, 'ZYX')
125
+ const v = Math.random() * 0.01
126
+ m.children[0].onBeforeRender = () => {
127
+ m.rotation.y += v
128
+ m.rotation.z = Math.cos(panoramaBox.rotation.y * 3) * Math.PI / 4 - Math.PI / 2
129
+ }
130
+ group.add(m)
131
+ }
132
+ }
133
+
134
+ this.scene.add(group)
135
+ this.panoramaGroup = group
136
+ }
137
+
138
+ /** Debug helper: flat cubemap face in front of the camera. */
139
+ async debugImageInFrontOfCamera() {
140
+ const image = await loadThreeJsTextureFromUrl(join('background', 'panorama_0.webp'))
141
+ const mesh = new THREE.Mesh(
142
+ new THREE.PlaneGeometry(1000, 1000),
143
+ new THREE.MeshBasicMaterial({ map: image })
144
+ )
145
+ mesh.position.set(0, 0, -500)
146
+ this.scene.add(mesh)
147
+ }
148
+ }
@@ -0,0 +1,23 @@
1
+ //@ts-nocheck
2
+ import type { MenuBackgroundMode } from './types'
3
+ import type { FuturisticCameraId, FuturisticSceneId, MinecraftBlockGroupId } from './futuristic'
4
+
5
+ /** Single source of truth for menu-background defaults (settings + runtime fallbacks). */
6
+ export const MENU_BACKGROUND_OPTION_DEFAULTS = {
7
+ mode: 'futuristic' as MenuBackgroundMode,
8
+ minecraftTextures: true,
9
+ futuristicScene: 'light' as FuturisticSceneId,
10
+ futuristicCamera: 'dive' as FuturisticCameraId,
11
+ futuristicBlockGroup: 'stainedGlass' as MinecraftBlockGroupId,
12
+ /** 0–200 (%). 100 = 1× motion. */
13
+ futuristicCameraSpeedPercent: 80,
14
+ futuristicBlockSpeedPercent: 40
15
+ } as const
16
+
17
+ export const menuBackgroundSpeedToMultiplier = (percent: number) => percent / 100
18
+
19
+ /** Default camera / block motion multipliers (1 = 100%). */
20
+ export const MENU_BACKGROUND_MOTION_DEFAULTS = {
21
+ camera: menuBackgroundSpeedToMultiplier(MENU_BACKGROUND_OPTION_DEFAULTS.futuristicCameraSpeedPercent),
22
+ block: menuBackgroundSpeedToMultiplier(MENU_BACKGROUND_OPTION_DEFAULTS.futuristicBlockSpeedPercent)
23
+ } as const