minecraft-renderer 0.1.46 → 0.1.48

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,6 +28,7 @@ import { PlayerStateReactive } from '../playerState/playerState'
28
28
  import { ResourcesManager, ResourcesManagerTransferred } from '../resourcesManager'
29
29
  import { preloadMesherWorkerScript } from './preloadWorkers'
30
30
  import type { MenuBackgroundOptions } from '../three/menuBackground/types'
31
+ import type { RendererStorageOptions } from '../three/menuBackground/defaultOptions'
31
32
 
32
33
  export interface AppViewerOptions {
33
34
  config?: Partial<GraphicsBackendConfig>
@@ -81,6 +82,9 @@ export class AppViewer {
81
82
  // Timing
82
83
  lastCamUpdate = 0
83
84
 
85
+ /** Bound by `subscribeRendererOptions` / `bindRendererOptions` — source of truth for renderer-owned settings. */
86
+ private getRendererOptions?: () => RendererStorageOptions
87
+
84
88
  constructor(options: AppViewerOptions = {}, public resourcesManager: ResourcesManager = new ResourcesManager()) {
85
89
  this.config = {
86
90
  ...defaultGraphicsBackendConfig,
@@ -117,6 +121,11 @@ export class AppViewer {
117
121
  return preloadMesherWorkerScript({ script })
118
122
  }
119
123
 
124
+ /** Wire app options storage (valtio proxy) for backend init (WebGL gpuPreference, etc.). */
125
+ bindRendererOptions(getOptions: () => RendererStorageOptions): void {
126
+ this.getRendererOptions = getOptions
127
+ }
128
+
120
129
  /**
121
130
  * Load a graphics backend.
122
131
  */
@@ -132,6 +141,7 @@ export class AppViewer {
132
141
 
133
142
  const loaderOptions: GraphicsInitOptions = {
134
143
  config: this.config,
144
+ getRendererOptions: this.getRendererOptions,
135
145
  callbacks: {
136
146
  displayCriticalError: (error) => {
137
147
  console.error('[AppViewer] Critical error:', error)
@@ -89,7 +89,6 @@ export type WorldRendererConfig = typeof defaultWorldRendererConfig
89
89
  */
90
90
  export const defaultGraphicsBackendConfig: GraphicsBackendConfig = {
91
91
  fpsLimit: undefined,
92
- powerPreference: undefined,
93
92
  sceneBackground: 'lightblue',
94
93
  timeoutRendering: false
95
94
  }
@@ -9,4 +9,5 @@ export * from './types'
9
9
  export * from './config'
10
10
  export * from './playerState'
11
11
  export * from './appViewer'
12
+ export * from './rendererOptionsSync'
12
13
  export * from '../performanceMonitor'
@@ -0,0 +1,244 @@
1
+ //@ts-nocheck
2
+ /**
3
+ * Maps app options storage → AppViewer runtime (in-world config, graphics config, menu background).
4
+ * Call `subscribeRendererOptions` once after viewer init; keep volume sync in the app.
5
+ */
6
+
7
+ import { subscribe } from 'valtio/vanilla'
8
+ import type { AppViewer } from './appViewer'
9
+ import type { RendererStorageOptions } from '../three/menuBackground/defaultOptions'
10
+ import type { MenuBackgroundOptions } from '../three/menuBackground/types'
11
+ import type { MenuBackgroundRenderer } from '../three/menuBackground/renderer'
12
+ import { menuBackgroundSpeedToMultiplier } from '../three/menuBackground/config'
13
+ import type { FuturisticCameraId, FuturisticSceneId, MinecraftBlockGroupId } from '../three/menuBackground/futuristic'
14
+ import { setSkinsConfig } from '../lib/utils/skins'
15
+
16
+ export type { RendererStorageOptions } from '../three/menuBackground/defaultOptions'
17
+
18
+ export interface ApplyRendererOptionsContext {
19
+ isSafari?: boolean
20
+ isCypress?: boolean
21
+ windowFocused?: boolean
22
+ }
23
+
24
+ export interface RendererOptionsSubscribeHooks {
25
+ isSafari?: boolean
26
+ isCypress?: boolean
27
+ getWindowFocused?: () => boolean
28
+ onRegisterFocusHandlers?: (handlers: { onFocus: () => void, onBlur: () => void }) => void
29
+ }
30
+
31
+ export interface RendererWorldViewLike {
32
+ keepChunksDistance: number
33
+ }
34
+
35
+ export function menuBackgroundOptionsFromStorage(o: Pick<
36
+ RendererStorageOptions,
37
+ | 'menuBackgroundMode'
38
+ | 'menuBackgroundMinecraftTextures'
39
+ | 'menuBackgroundFuturisticScene'
40
+ | 'menuBackgroundFuturisticCamera'
41
+ | 'menuBackgroundFuturisticBlockGroup'
42
+ | 'menuBackgroundFuturisticCameraSpeed'
43
+ | 'menuBackgroundFuturisticBlockSpeed'
44
+ >): MenuBackgroundOptions {
45
+ return {
46
+ mode: o.menuBackgroundMode as MenuBackgroundOptions['mode'],
47
+ useMinecraftTextures: o.menuBackgroundMinecraftTextures,
48
+ futuristicScene: o.menuBackgroundFuturisticScene as FuturisticSceneId,
49
+ futuristicCamera: o.menuBackgroundFuturisticCamera as FuturisticCameraId,
50
+ futuristicBlockGroup: o.menuBackgroundFuturisticBlockGroup as MinecraftBlockGroupId,
51
+ futuristicCameraSpeed: menuBackgroundSpeedToMultiplier(o.menuBackgroundFuturisticCameraSpeed),
52
+ futuristicBlockSpeed: menuBackgroundSpeedToMultiplier(o.menuBackgroundFuturisticBlockSpeed),
53
+ }
54
+ }
55
+
56
+ export function applyMenuBackgroundLiveOptions(
57
+ menu: MenuBackgroundRenderer,
58
+ o: Pick<
59
+ RendererStorageOptions,
60
+ | 'menuBackgroundFuturisticScene'
61
+ | 'menuBackgroundFuturisticCamera'
62
+ | 'menuBackgroundFuturisticBlockGroup'
63
+ | 'menuBackgroundFuturisticCameraSpeed'
64
+ | 'menuBackgroundFuturisticBlockSpeed'
65
+ >
66
+ ): void {
67
+ const futuristic = menu.futuristic
68
+ if (!futuristic) return
69
+ futuristic.setScene?.(o.menuBackgroundFuturisticScene)
70
+ futuristic.setCamera?.(o.menuBackgroundFuturisticCamera)
71
+ void futuristic.setBlockGroup?.(o.menuBackgroundFuturisticBlockGroup)
72
+ futuristic.setCameraSpeed?.(menuBackgroundSpeedToMultiplier(o.menuBackgroundFuturisticCameraSpeed))
73
+ futuristic.setBlockSpeed?.(menuBackgroundSpeedToMultiplier(o.menuBackgroundFuturisticBlockSpeed))
74
+ }
75
+
76
+ function resolveWasmMesherActive(o: RendererStorageOptions): boolean {
77
+ return o.rendererMesher !== 'legacy-js'
78
+ }
79
+
80
+ function applyMesherWorkersPreset(
81
+ appViewer: AppViewer,
82
+ o: RendererStorageOptions,
83
+ wasmActive: boolean
84
+ ): void {
85
+ const cfg = appViewer.inWorldRenderingConfig
86
+ const override = o.rendererMeshersCountOverride
87
+ const applyMesherWorkers = (workers: number) => {
88
+ cfg.mesherWorkers = override ?? workers
89
+ }
90
+ switch (o.rendererWorldPerformance) {
91
+ case 'low-energy':
92
+ applyMesherWorkers(1)
93
+ cfg.dedicatedChangeWorker = false
94
+ break
95
+ case 'normal':
96
+ applyMesherWorkers(2)
97
+ cfg.dedicatedChangeWorker = !wasmActive
98
+ break
99
+ case 'maximum':
100
+ applyMesherWorkers(Math.max(3, Math.min(navigator.hardwareConcurrency ?? 0, 8)))
101
+ cfg.dedicatedChangeWorker = !wasmActive
102
+ break
103
+ }
104
+ }
105
+
106
+ function applyFpsLimit(
107
+ appViewer: AppViewer,
108
+ o: RendererStorageOptions,
109
+ windowFocused: boolean
110
+ ): void {
111
+ const backgroundFpsLimit = o.backgroundRendering
112
+ const normalFpsLimit = o.frameLimit
113
+
114
+ if (windowFocused) {
115
+ appViewer.config.fpsLimit = normalFpsLimit || undefined
116
+ } else if (backgroundFpsLimit === '5fps') {
117
+ appViewer.config.fpsLimit = 5
118
+ } else if (backgroundFpsLimit === '20fps') {
119
+ appViewer.config.fpsLimit = 20
120
+ } else {
121
+ appViewer.config.fpsLimit = undefined
122
+ }
123
+ }
124
+
125
+ function applyStatsVisible(
126
+ appViewer: AppViewer,
127
+ o: RendererStorageOptions,
128
+ ctx: ApplyRendererOptionsContext
129
+ ): void {
130
+ const { renderDebug } = o
131
+ if (renderDebug === 'none' || ctx.isCypress) {
132
+ appViewer.config.statsVisible = 0
133
+ } else if (renderDebug === 'basic') {
134
+ appViewer.config.statsVisible = 1
135
+ } else if (renderDebug === 'advanced') {
136
+ appViewer.config.statsVisible = 2
137
+ }
138
+ }
139
+
140
+ // ensure no object assigns to the config
141
+ export function applyRendererOptions(
142
+ appViewer: AppViewer,
143
+ o: RendererStorageOptions,
144
+ ctx: ApplyRendererOptionsContext = {}
145
+ ): void {
146
+ const cfg = appViewer.inWorldRenderingConfig
147
+ const wasmActive = resolveWasmMesherActive(o)
148
+
149
+ cfg.showChunkBorders = o.showChunkBorders
150
+ cfg.futuristicReveal = o.rendererFuturisticReveal
151
+ applyMesherWorkersPreset(appViewer, o, wasmActive)
152
+ cfg.renderEntities = o.renderEntities
153
+ applyStatsVisible(appViewer, o, ctx)
154
+ applyFpsLimit(appViewer, o, ctx.windowFocused !== false)
155
+
156
+ cfg.vrSupport = o.vrSupport
157
+ cfg.vrPageGameRendering = o.vrPageGameRendering
158
+ cfg.enableDebugOverlay = o.rendererPerfDebugOverlay
159
+
160
+ cfg.clipWorldBelowY = o.clipWorldBelowY
161
+ cfg.extraBlockRenderers = !o.disableBlockEntityTextures
162
+ cfg.fetchPlayerSkins = o.loadPlayerSkins
163
+ cfg.highlightBlockColor = o.highlightBlockColor
164
+ cfg.wasmMesher = wasmActive
165
+ cfg.disableMesherConversionCache = !!ctx.isSafari
166
+
167
+ setSkinsConfig({ apiEnabled: o.loadPlayerSkins })
168
+
169
+ cfg.smoothLighting = o.smoothLighting
170
+ cfg.shadingTheme = o.vanillaLook ? 'vanilla' : 'high-contrast'
171
+ cfg.starfield = o.starfieldRendering
172
+ cfg.defaultSkybox = o.defaultSkybox
173
+ cfg.fov = o.fov
174
+ }
175
+
176
+ /** World-view + hand/camera options (call when WorldView is ready). */
177
+ export function applyRendererWorldViewOptions(
178
+ appViewer: AppViewer,
179
+ worldView: RendererWorldViewLike,
180
+ o: Pick<
181
+ RendererStorageOptions,
182
+ 'keepChunksDistance' | 'renderEars' | 'showHand' | 'viewBobbing' | 'dayCycleAndLighting'
183
+ >
184
+ ): void {
185
+ worldView.keepChunksDistance = o.keepChunksDistance
186
+ const cfg = appViewer.inWorldRenderingConfig
187
+ cfg.renderEars = o.renderEars
188
+ cfg.showHand = o.showHand
189
+ cfg.viewBobbing = o.viewBobbing
190
+ cfg.dayCycle = o.dayCycleAndLighting
191
+ }
192
+
193
+ /**
194
+ * Subscribe to options changes and sync renderer runtime.
195
+ * Returns unsubscribe. Volume is intentionally excluded — wire it in the app.
196
+ */
197
+ export function subscribeRendererOptions<T extends RendererStorageOptions>(
198
+ appViewer: AppViewer,
199
+ optionsProxy: T,
200
+ hooks: RendererOptionsSubscribeHooks = {}
201
+ ): () => void {
202
+ appViewer.bindRendererOptions(() => optionsProxy as RendererStorageOptions)
203
+
204
+ let windowFocused = hooks.getWindowFocused?.() ?? true
205
+
206
+ const run = () => {
207
+ const snapshot = optionsProxy as RendererStorageOptions
208
+ applyRendererOptions(appViewer, snapshot, {
209
+ isSafari: hooks.isSafari,
210
+ isCypress: hooks.isCypress,
211
+ windowFocused,
212
+ })
213
+
214
+ if (appViewer.currentDisplay === 'menu') {
215
+ const menu = appViewer.backend?.getMenuBackground?.()
216
+ if (menu) applyMenuBackgroundLiveOptions(menu, snapshot)
217
+ }
218
+ }
219
+
220
+ run()
221
+
222
+ hooks.onRegisterFocusHandlers?.({
223
+ onFocus: () => {
224
+ windowFocused = true
225
+ run()
226
+ },
227
+ onBlur: () => {
228
+ windowFocused = false
229
+ run()
230
+ },
231
+ })
232
+
233
+ return subscribe(optionsProxy, run)
234
+ }
235
+
236
+ /** Call when mineflayer bot is created (lighting depends on protocol features). */
237
+ export function applyRendererEnableLighting(
238
+ appViewer: AppViewer,
239
+ newVersionsLighting: boolean,
240
+ blockStateIdSupported: boolean
241
+ ): void {
242
+ appViewer.inWorldRenderingConfig.enableLighting =
243
+ !blockStateIdSupported || newVersionsLighting
244
+ }
@@ -23,12 +23,13 @@ export interface SoundSystem {
23
23
  }
24
24
 
25
25
  import type { MenuBackgroundOptions } from '../three/menuBackground/types'
26
+ import type { RendererStorageOptions } from '../three/menuBackground/defaultOptions'
27
+ import type { MenuBackgroundRenderer } from '../three/menuBackground/renderer'
26
28
  import type { PerformanceInstabilityFactors } from '../performanceMonitor'
27
29
 
28
30
  /** Graphics backend configuration */
29
31
  export interface GraphicsBackendConfig {
30
32
  fpsLimit?: number
31
- powerPreference?: 'high-performance' | 'low-power'
32
33
  statsVisible?: number
33
34
  sceneBackground: string
34
35
  timeoutRendering?: boolean
@@ -96,6 +97,8 @@ export interface RendererReactiveState {
96
97
  /** Graphics initialization options */
97
98
  export interface GraphicsInitOptions<S = any> {
98
99
  config: GraphicsBackendConfig
100
+ /** Live app options (e.g. valtio proxy); used for WebGL `gpuPreference` at context creation. */
101
+ getRendererOptions?: () => RendererStorageOptions
99
102
  rendererSpecificSettings: S
100
103
  callbacks: {
101
104
  displayCriticalError: (error: Error) => void
@@ -127,6 +130,8 @@ export interface GraphicsBackend {
127
130
  soundSystem?: any
128
131
  backendMethods?: any
129
132
  getDebugOverlay?(): { entitiesString?: string, left?: Record<string, string>, right?: Record<string, string> }
133
+ /** Active main-menu background, when `currentDisplay === 'menu'`. */
134
+ getMenuBackground?(): MenuBackgroundRenderer | undefined
130
135
  }
131
136
 
132
137
  /** Graphics backend loader function type */
@@ -0,0 +1,79 @@
1
+ //@ts-nocheck
2
+ import { Vec3 } from 'vec3'
3
+ import type { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
4
+ import legacyJson from '../lib/preflatMap.json'
5
+ import type { World, WorldBlock as Block } from './world'
6
+
7
+ const calculatedBlocksEntries = Object.entries(legacyJson.clientCalculatedBlocks)
8
+
9
+ /**
10
+ * Block name + properties for model lookup. Only runs neighbor/preflat work when
11
+ * `world.preflat` (legacy); modern block-state worlds use `fromStateId` only.
12
+ */
13
+ export function resolveBlockPropertiesForMeshing(
14
+ world: World | undefined,
15
+ cursor: Vec3,
16
+ blockProvider: WorldBlockProvider,
17
+ blockStateId: number,
18
+ PrismarineBlockCtor: { fromStateId: (id: number, biome: number) => Block }
19
+ ): { name: string, properties: Record<string, unknown> } {
20
+ if (world?.preflat) {
21
+ const block = world.getBlock(cursor, blockProvider, {})
22
+ if (block) {
23
+ let properties: Record<string, unknown> = { ...block.getProperties() }
24
+ const patch = preflatBlockCalculation(block, world, cursor)
25
+ if (patch) properties = { ...properties, ...patch }
26
+ return { name: block.name, properties }
27
+ }
28
+ }
29
+ const fromState = PrismarineBlockCtor.fromStateId(blockStateId, 1)
30
+ return { name: fromState.name, properties: fromState.getProperties() }
31
+ }
32
+
33
+ export function preflatBlockCalculation(block: Block, world: World, position: Vec3) {
34
+ const type = calculatedBlocksEntries.find(([name, blocks]) => blocks.includes(block.name))?.[0]
35
+ if (!type) return
36
+ switch (type) {
37
+ case 'directional': {
38
+ const isSolidConnection = !block.name.includes('redstone') && !block.name.includes('tripwire')
39
+ const neighbors = [
40
+ world.getBlock(position.offset(0, 0, 1)),
41
+ world.getBlock(position.offset(0, 0, -1)),
42
+ world.getBlock(position.offset(1, 0, 0)),
43
+ world.getBlock(position.offset(-1, 0, 0))
44
+ ]
45
+ const props = {}
46
+ let changed = false
47
+ for (const [i, neighbor] of neighbors.entries()) {
48
+ const isConnectedToSolid = isSolidConnection ? (neighbor && !neighbor.transparent) : false
49
+ if (isConnectedToSolid || neighbor?.name === block.name) {
50
+ props[['south', 'north', 'east', 'west'][i]] = 'true'
51
+ changed = true
52
+ }
53
+ }
54
+ return changed ? props : undefined
55
+ }
56
+ case 'block_snowy': {
57
+ const aboveIsSnow = world.getBlock(position.offset(0, 1, 0))?.name === 'snow'
58
+ if (aboveIsSnow) {
59
+ return {
60
+ snowy: `${aboveIsSnow}`
61
+ }
62
+ } else {
63
+ return
64
+ }
65
+ }
66
+ case 'door': {
67
+ const { half } = block.getProperties()
68
+ if (half === 'upper') {
69
+ const lower = world.getBlock(position.offset(0, -1, 0))
70
+ if (lower?.name === block.name) {
71
+ return {
72
+ ...lower.getProperties(),
73
+ half: 'upper'
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,26 @@
1
+ //@ts-nocheck
2
+ /** Shared geometry export shapes (worker bridge + main-thread viewer). */
3
+
4
+ export interface ExportedSection {
5
+ key: string
6
+ position: { x: number, y: number, z: number }
7
+ geometry: {
8
+ positions: number[]
9
+ normals: number[]
10
+ colors: number[]
11
+ uvs: number[]
12
+ indices: number[]
13
+ }
14
+ shaderCubes?: unknown
15
+ }
16
+
17
+ export interface ExportedWorldGeometry {
18
+ version: string
19
+ exportedAt: string
20
+ camera: {
21
+ position: { x: number, y: number, z: number }
22
+ rotation: { pitch: number, yaw: number }
23
+ }
24
+ sections: ExportedSection[]
25
+ textureAtlasDataUrl?: string
26
+ }
@@ -2,7 +2,6 @@
2
2
  import { Vec3 } from 'vec3'
3
3
  import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
4
4
  import moreBlockDataGeneratedJson from '../lib/moreBlockDataGenerated.json'
5
- import legacyJson from '../lib/preflatMap.json'
6
5
  import { BlockType } from '../playground/shared'
7
6
  import { World, BlockModelPartsResolved, WorldBlock as Block, WorldBlock, worldColumnKey } from './world'
8
7
  import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
@@ -10,6 +9,9 @@ import { getSideShading, vertexLightFromAo } from './vertexShading'
10
9
  import { INVISIBLE_BLOCKS } from './worldConstants'
11
10
  import { MesherGeometryOutput, HighestBlockInfo } from './shared'
12
11
  import { collectBlockEntityMetadata } from './blockEntityMetadata'
12
+ import { preflatBlockCalculation, resolveBlockPropertiesForMeshing } from './blockPropertiesForMeshing'
13
+
14
+ export { preflatBlockCalculation, resolveBlockPropertiesForMeshing } from './blockPropertiesForMeshing'
13
15
 
14
16
  // Log function disabled by default for zero overhead in production hot loops
15
17
  const ENABLE_TS_LOGS = false
@@ -51,84 +53,6 @@ function prepareTints(tints) {
51
53
  })
52
54
  }
53
55
 
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
-
80
- export function preflatBlockCalculation(block: Block, world: World, position: Vec3) {
81
- const type = calculatedBlocksEntries.find(([name, blocks]) => blocks.includes(block.name))?.[0]
82
- if (!type) return
83
- switch (type) {
84
- case 'directional': {
85
- const isSolidConnection = !block.name.includes('redstone') && !block.name.includes('tripwire')
86
- const neighbors = [
87
- world.getBlock(position.offset(0, 0, 1)),
88
- world.getBlock(position.offset(0, 0, -1)),
89
- world.getBlock(position.offset(1, 0, 0)),
90
- world.getBlock(position.offset(-1, 0, 0))
91
- ]
92
- // set needed props to true: east:'false',north:'false',south:'false',west:'false'
93
- const props = {}
94
- let changed = false
95
- for (const [i, neighbor] of neighbors.entries()) {
96
- const isConnectedToSolid = isSolidConnection ? (neighbor && !neighbor.transparent) : false
97
- if (isConnectedToSolid || neighbor?.name === block.name) {
98
- props[['south', 'north', 'east', 'west'][i]] = 'true'
99
- changed = true
100
- }
101
- }
102
- return changed ? props : undefined
103
- }
104
- // case 'gate_in_wall': {}
105
- case 'block_snowy': {
106
- const aboveIsSnow = world.getBlock(position.offset(0, 1, 0))?.name === 'snow'
107
- if (aboveIsSnow) {
108
- return {
109
- snowy: `${aboveIsSnow}`
110
- }
111
- } else {
112
- return
113
- }
114
- }
115
- case 'door': {
116
- // upper half matches lower in
117
- const { half } = block.getProperties()
118
- if (half === 'upper') {
119
- // copy other properties
120
- const lower = world.getBlock(position.offset(0, -1, 0))
121
- if (lower?.name === block.name) {
122
- return {
123
- ...lower.getProperties(),
124
- half: 'upper'
125
- }
126
- }
127
- }
128
- }
129
- }
130
- }
131
-
132
56
  function tintToGl(tint) {
133
57
  const r = (tint >> 16) & 0xff
134
58
  const g = (tint >> 8) & 0xff
@@ -47,6 +47,8 @@ export const getInitialPlayerState = () => proxy({
47
47
  heldItemOff: undefined as HandItemBlock | undefined,
48
48
  perspective: 'first_person' as CameraPerspective,
49
49
  onFire: false,
50
+ /** Gameplay FOV scale (sprint, bow, zoom, etc.); base FOV comes from renderer options. */
51
+ fovMultiplier: 1,
50
52
 
51
53
  cameraSpectatingEntity: undefined as number | undefined,
52
54
 
@@ -13,20 +13,15 @@ import * as THREE from 'three'
13
13
  import Stats from 'stats.js'
14
14
  import StatsGl from 'stats-gl'
15
15
  import * as tween from '@tweenjs/tween.js'
16
- import type { GraphicsInitOptions } from '../graphicsBackend/types'
16
+ import type { GraphicsBackendConfig, GraphicsInitOptions } from '../graphicsBackend/types'
17
+ import { gpuPreferenceToWebGLPowerPreference } from '../three/menuBackground/defaultOptions'
17
18
  import { WorldRendererConfig } from '../graphicsBackend'
18
19
 
19
20
  // ============================================================================
20
21
  // Types (co-located with implementation)
21
22
  // ============================================================================
22
23
 
23
- export interface GraphicsBackendConfig {
24
- fpsLimit?: number
25
- powerPreference?: 'high-performance' | 'low-power'
26
- statsVisible?: number
27
- sceneBackground: string
28
- timeoutRendering?: boolean
29
- }
24
+ export type { GraphicsBackendConfig }
30
25
 
31
26
  export interface FrameTimingEvent {
32
27
  type: 'frameStart' | 'frameEnd' | 'cameraUpdate' | 'frameDisplay'
@@ -207,11 +202,12 @@ export class DocumentRenderer {
207
202
  }
208
203
 
209
204
  try {
205
+ const gpuPreference = initOptions.getRendererOptions?.()?.gpuPreference ?? 'default'
210
206
  this.renderer = new THREE.WebGLRenderer({
211
207
  canvas: this.canvas as HTMLCanvasElement,
212
208
  preserveDrawingBuffer: true,
213
209
  logarithmicDepthBuffer: true,
214
- powerPreference: this.config.powerPreference
210
+ powerPreference: gpuPreferenceToWebGLPowerPreference(gpuPreference)
215
211
  })
216
212
  } catch (err: any) {
217
213
  initOptions.callbacks.displayCriticalError(
@@ -162,8 +162,6 @@ export const createGraphicsBackendBase = () => {
162
162
  mergedOptions,
163
163
  !!process.env.SINGLE_FILE_BUILD_MODE
164
164
  )
165
- ; (globalThis as any).menuBackgroundRenderer = menuBackgroundRenderer
166
-
167
165
  callModsMethod('menuBackgroundCreated', menuBackgroundRenderer)
168
166
  await menuBackgroundRenderer.start(mergedOptions)
169
167
  callModsMethod('menuBackgroundReady', menuBackgroundRenderer)
@@ -240,6 +238,7 @@ export const createGraphicsBackendBase = () => {
240
238
  documentRenderer!.setPaused(!rendering)
241
239
  if (worldRenderer) worldRenderer.renderingActive = rendering
242
240
  },
241
+ getMenuBackground: () => menuBackgroundRenderer ?? undefined,
243
242
  getDebugOverlay: () => ({
244
243
  get entitiesString() {
245
244
  return worldRenderer?.entities.getDebugString()