Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/graphicsBackend/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export const defaultWorldRendererConfig = {
dayCycle: true,
smoothLighting: true,
enableLighting: true,
/** Entity shading is independently opt-in; block lighting remains controlled by enableLighting. */
enableEntityLighting: false,
starfield: true,
defaultSkybox: true,
renderEntities: true,
Expand Down
62 changes: 62 additions & 0 deletions src/graphicsBackend/rendererDefaultOptions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it } from 'vitest'
import { migrateRendererOptions, RENDERER_DEFAULT_OPTIONS, resolveEnableLighting } from './rendererDefaultOptions'

describe('migrateRendererOptions', () => {
it('drops the leaked migration flag so it is not a user option', () => {
const saved: Record<string, unknown> = {
newVersionsLighting: false,
migratedNewVersionsLightingDefault: true
}
migrateRendererOptions(saved)
expect(saved.newVersionsLighting).toBe(false)
expect(saved).not.toHaveProperty('migratedNewVersionsLightingDefault')
})
})

describe('newer-version lighting defaults', () => {
it('keeps lighting opt-in on 1.13+', () => {
expect(RENDERER_DEFAULT_OPTIONS.newVersionsLighting).toBe(false)
expect(resolveEnableLighting(RENDERER_DEFAULT_OPTIONS.newVersionsLighting, true)).toBe(false)
})

it('defaults entity lighting to off independently of world lighting', () => {
expect(RENDERER_DEFAULT_OPTIONS.entityLighting).toBe(false)
})

it.each([false, true])('preserves a saved lighting preference (%s)', value => {
const saved: Record<string, unknown> = { newVersionsLighting: value }
migrateRendererOptions(saved)
expect(saved.newVersionsLighting).toBe(value)
})

it('does not migrate old world-lighting settings into entity lighting', () => {
const saved: Record<string, unknown> = { newVersionsLighting: true }
migrateRendererOptions(saved)
expect(saved.newVersionsLighting).toBe(true)
expect(saved).not.toHaveProperty('entityLighting')
})

it('does not invent a stored value when the key was never saved', () => {
const saved: Record<string, unknown> = {}
migrateRendererOptions(saved)
expect(saved).not.toHaveProperty('newVersionsLighting')
expect(saved).not.toHaveProperty('entityLighting')
})
})

describe('resolveEnableLighting', () => {
it('follows the option on 1.13+ (blockStateId)', () => {
expect(resolveEnableLighting(true, true)).toBe(true)
expect(resolveEnableLighting(false, true)).toBe(false)
})

it('always enables lighting on pre-1.13', () => {
expect(resolveEnableLighting(false, false)).toBe(true)
expect(resolveEnableLighting(true, false)).toBe(true)
})

it('treats a missing protocol probe as pre-1.13 so a menu toggle cannot throw', () => {
expect(resolveEnableLighting(false, undefined)).toBe(true)
expect(resolveEnableLighting(true, undefined)).toBe(true)
})
})
20 changes: 19 additions & 1 deletion src/graphicsBackend/rendererDefaultOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const RENDERER_DEFAULT_OPTIONS = {
vanillaLook: false as boolean,
smoothLighting: true as boolean,
newVersionsLighting: false as boolean,
entityLighting: false as boolean,
vrSupport: true as boolean,
vrPageGameRendering: false as boolean,
clipWorldBelowY: undefined as number | undefined,
Expand Down Expand Up @@ -121,6 +122,17 @@ export function migrateRendererOptions(saved: Record<string, unknown>): void {
}
delete saved[oldKey]
}

// Internal one-shot flag from an earlier lighting-default migration. Never a user option.
delete saved.migratedNewVersionsLightingDefault
}

/**
* Runtime `enableLighting` from the stored option and protocol.
* Pre-1.13 (no blockStateId) always lights. Missing protocol (menu, no bot) must not throw.
*/
export function resolveEnableLighting(newVersionsLighting: boolean, blockStateIdSupported: boolean | undefined): boolean {
return blockStateIdSupported !== true || newVersionsLighting
}

/** Settings UI metadata for {@link RENDERER_DEFAULT_OPTIONS} keys. */
Expand Down Expand Up @@ -235,7 +247,12 @@ export const RENDERER_OPTIONS_META: Partial<Record<RendererDefaultOptionKey, Ren
},
smoothLighting: {},
newVersionsLighting: {
text: 'Lighting in newer versions'
text: 'Lighting in newer versions',
tooltip: 'Block and sky lighting for 1.13+. Off renders the world fullbright.'
},
entityLighting: {
text: 'Entity lighting',
tooltip: 'Light entities from block and sky light. Requires world lighting to be enabled; off keeps entities fullbright.'
},
vrSupport: {
text: 'VR support',
Expand Down Expand Up @@ -304,6 +321,7 @@ export const RENDERER_RENDER_GUI_SECTIONS: ReadonlyArray<{
'smoothLighting',
'vanillaLook',
'newVersionsLighting',
'entityLighting',
'dayCycleAndLighting',
'loadPlayerSkins',
'renderEars',
Expand Down
22 changes: 22 additions & 0 deletions src/graphicsBackend/rendererOptionsSync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it, vi } from 'vitest'

vi.mock('../lib/utils/skins', () => ({ setSkinsConfig: () => {} }))

import { createWorldRendererConfig } from './config'
import { RENDERER_DEFAULT_OPTIONS } from './rendererDefaultOptions'
import { applyRendererOptions } from './rendererOptionsSync'

describe('applyRendererOptions entity lighting', () => {
it.each([false, true])('syncs entityLighting=%s to the runtime config', entityLighting => {
const inWorldRenderingConfig = createWorldRendererConfig()
const appViewer = {
inWorldRenderingConfig,
config: {},
currentDisplay: null
} as any

applyRendererOptions(appViewer, { ...RENDERER_DEFAULT_OPTIONS, entityLighting })

expect(inWorldRenderingConfig.enableEntityLighting).toBe(entityLighting)
})
})
7 changes: 4 additions & 3 deletions src/graphicsBackend/rendererOptionsSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { subscribe } from 'valtio/vanilla'
import type { AppViewer } from './appViewer'
import type { RendererStorageOptions } from './rendererDefaultOptions'
import { rendererShaderCubeDebugModeToValue } from './rendererDefaultOptions'
import { rendererShaderCubeDebugModeToValue, resolveEnableLighting } from './rendererDefaultOptions'
import type { MenuBackgroundOptions } from '../three/menuBackground/types'
import type { MenuBackgroundRenderer } from '../three/menuBackground/renderer'
import { menuBackgroundSpeedToMultiplier } from '../three/menuBackground/config'
Expand Down Expand Up @@ -132,6 +132,7 @@ export function applyRendererOptions(appViewer: AppViewer, o: RendererStorageOpt
cfg.futuristicReveal = o.rendererFuturisticReveal
applyMesherWorkersPreset(appViewer, o, wasmActive)
cfg.renderEntities = o.renderEntities
cfg.enableEntityLighting = o.entityLighting
applyStatsVisible(appViewer, o, ctx)
applyFpsLimit(appViewer, o, ctx.windowFocused !== false)

Expand Down Expand Up @@ -215,6 +216,6 @@ export function subscribeRendererOptions<T extends RendererStorageOptions>(
}

/** Call when mineflayer bot is created (lighting depends on protocol features). */
export function applyRendererEnableLighting(appViewer: AppViewer, newVersionsLighting: boolean, blockStateIdSupported: boolean): void {
appViewer.inWorldRenderingConfig.enableLighting = !blockStateIdSupported || newVersionsLighting
export function applyRendererEnableLighting(appViewer: AppViewer, newVersionsLighting: boolean, blockStateIdSupported: boolean | undefined): void {
appViewer.inWorldRenderingConfig.enableLighting = resolveEnableLighting(newVersionsLighting, blockStateIdSupported)
}
4 changes: 4 additions & 0 deletions src/lib/blockEntityLightRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export class BlockEntityLightRegistry {
return this.skyLevel
}

getLightmapParams(): BlockLightmapParams {
return { ...this.lightmapParams }
}

private refreshAll(): void {
for (const entry of this.entries) {
this.applyBrightness(entry)
Expand Down
11 changes: 10 additions & 1 deletion src/lib/worldrendererCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { PlayerStateReactive } from '../playerState/playerState'
import { IndexedData } from 'minecraft-data'
import { WorldRendererConfig } from '../graphicsBackend/config'
import { CameraCollisionBlockCache } from '../three/cameraCollisionBlockCache'
import { RendererLightCache } from '../three/rendererLightCache'

function mod(x, n) {
return ((x % n) + n) % n
Expand All @@ -45,6 +46,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
worldSizeParams = { minY: 0, worldHeight: 256 }
/** Block columns for third-person voxel DDA (renderer thread). */
cameraCollisionBlockCache: CameraCollisionBlockCache
/** Packed block/sky light for entity sampling (renderer thread). */
rendererLightCache: RendererLightCache
reactiveDebugParams = proxy({
stopRendering: false,
chunksRenderAboveOverride: undefined as number | undefined,
Expand Down Expand Up @@ -222,6 +225,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
public initOptions: GraphicsInitOptions
) {
this.cameraCollisionBlockCache = new CameraCollisionBlockCache(this.displayOptions.version)
this.rendererLightCache = new RendererLightCache(this.displayOptions.version)
this.snapshotInitialValues()
this.worldRendererConfig = displayOptions.inWorldRenderingConfig
this.playerStateReactive = displayOptions.playerStateReactive!
Expand Down Expand Up @@ -885,7 +889,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
x,
z,
chunk,
customBlockModels: customBlockModels || undefined
customBlockModels: customBlockModels || undefined,
isLightUpdate
})
}
// WASM mesher pushes heightmaps from `processColumnTick` after each
Expand All @@ -906,6 +911,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
)
this.mesherLogReader?.chunkReceived(x, z, chunk.length)
this.cameraCollisionBlockCache.ingestColumn(x, z, chunk)
this.rendererLightCache.ingestColumn(x, z, chunk)
const sectionHeight = this.getSectionHeight()
const CHUNK_SIZE = 16

Expand Down Expand Up @@ -975,6 +981,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}
this.highestBlocksByChunks.delete(`${x},${z}`)
this.cameraCollisionBlockCache.removeColumn(x, z)
this.rendererLightCache.removeColumn(x, z)
const heightmapKey = `${Math.floor(x / 16)},${Math.floor(z / 16)}`
delete this.reactiveState.world.heightmaps[heightmapKey]

Expand Down Expand Up @@ -1052,6 +1059,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
({ x, z, chunk, worldConfig, isLightUpdate }) => {
this.worldSizeParams = worldConfig
this.cameraCollisionBlockCache.setWorldBounds(worldConfig.minY, worldConfig.worldHeight)
this.rendererLightCache.setWorldBounds(worldConfig.minY, worldConfig.worldHeight)
this.queuedChunks.add(`${x},${z}`)
const args = [x, z, chunk, isLightUpdate]
if (!currentLoadChunkBatch) {
Expand Down Expand Up @@ -1156,6 +1164,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
'onWorldSwitch',
() => {
this.cameraCollisionBlockCache.clear()
this.rendererLightCache.clear()
for (const fn of this.onWorldSwitched) {
try {
fn()
Expand Down
10 changes: 10 additions & 0 deletions src/three/chunkMeshManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,15 @@ export class ChunkMeshManager {
if (this.globalLegacyShaderMaterial) setLegacySkyLevel(this.globalLegacyShaderMaterial, value)
if (this.globalLegacyBlendShaderMaterial) setLegacySkyLevel(this.globalLegacyBlendShaderMaterial, value)
this.blockEntityLightRegistry.setSkyLevel(value)
this.worldRenderer.entityLightController?.setSkyLevel(value)
}

getSkyLevel(): number {
return this.blockEntityLightRegistry.getSkyLevel()
}

getLightmapParams(): BlockLightmapParams {
return this.blockEntityLightRegistry.getLightmapParams()
}

setShadingTheme(theme: 'vanilla' | 'high-contrast', cardinalLight: string): void {
Expand All @@ -299,6 +308,7 @@ export class ChunkMeshManager {
if (this.globalLegacyShaderMaterial) setLegacyLightmapParams(this.globalLegacyShaderMaterial, params)
if (this.globalLegacyBlendShaderMaterial) setLegacyLightmapParams(this.globalLegacyBlendShaderMaterial, params)
this.blockEntityLightRegistry.setLightmapParams(params)
this.worldRenderer.entityLightController?.setLightmapParams(params)
}

private getLegacyShaderMaterial(): THREE.ShaderMaterial {
Expand Down
70 changes: 69 additions & 1 deletion src/three/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { WalkingGeneralSwing } from './entity/animations'
import { disposeObject, loadNearestFilterTexture, loadTexture, loadThreeJsTextureFromUrl } from './threeJsUtils'
import { armorModel, armorTextures, elytraTexture } from './entity/armorModels'
import { WorldRendererThree } from './worldRendererThree'
import type { EntityLightMeta } from './entityLightController'
import { IndexedData } from 'minecraft-data'
import { ItemSpecificContextProperties } from '../playerState/types'

Expand Down Expand Up @@ -385,9 +386,12 @@ export class Entities {
}

this.updateEntityEquipment(this.playerEntity, playerData)
this.registerEntityLight(this.playerEntity, this.playerEntity.originalEntity)
this.invalidateEntityLight(this.playerEntity)
}

clear() {
this.worldRenderer.entityLightController?.clear()
for (const mesh of Object.values(this.entities)) {
this.worldRenderer.sceneOrigin.removeAndUntrack(mesh)
disposeObject(mesh)
Expand Down Expand Up @@ -557,6 +561,7 @@ export class Entities {
this.applyRemoteBoatPassengerRotations()
this.applyLocalThirdPersonPlayerRotation()
this.updateBoatPaddleAnimations(dt)
this.applyEntityLights()
}

private applyRemoteBoatPassengerRotations() {
Expand Down Expand Up @@ -1082,6 +1087,7 @@ export class Entities {
if (c['additionalCleanup']) c['additionalCleanup']()
})
this.onRemoveEntity(entity)
this.worldRenderer.entityLightController?.unregister(e)
this.worldRenderer.sceneOrigin.removeAndUntrack(e)
disposeObject(e)
// todo dispose textures as well ?
Expand Down Expand Up @@ -1403,6 +1409,12 @@ export class Entities {
this.updateNameTagVisibility(e)

this.updateEntityPosition(entity, justAdded, overrides)
if (justAdded) {
this.registerEntityLight(e, entity)
} else {
this.worldRenderer.entityLightController?.setMeta(e, this.entityLightMeta(entity))
this.invalidateEntityLight(e)
}
}

applyEntityRenderHints(e: SceneEntity, entity: SceneEntity['originalEntity']) {
Expand Down Expand Up @@ -1699,8 +1711,52 @@ export class Entities {
}
}

onEntityMeshReady(mesh: THREE.Object3D) {
this.worldRenderer.entityLightController?.invalidateFromDescendant(mesh)
}

private entityLightSharedMaterials() {
return new Set<THREE.Material>([this.worldRenderer.material])
}

private entityLightMeta(entity: SceneEntity['originalEntity']): EntityLightMeta {
const localPlayerId = this.playerEntity?.originalEntity.id
const useLocalEyeHeight = entity.name === 'player' && localPlayerId !== undefined && entity.id === localPlayerId
return {
name: entity.name,
height: entity.height,
eyeHeight: useLocalEyeHeight ? this.worldRenderer.playerStateReactive.eyeHeight : undefined
}
}

private registerEntityLight(root: SceneEntity, entity: SceneEntity['originalEntity']) {
this.worldRenderer.entityLightController?.register(root, this.entityLightMeta(entity), this.entityLightSharedMaterials())
}

private invalidateEntityLight(root: THREE.Object3D) {
this.worldRenderer.entityLightController?.invalidateMaterials(root)
}

private applyEntityLights() {
const controller = this.worldRenderer.entityLightController
if (!controller) return
const shared = this.entityLightSharedMaterials()
const apply = (root: SceneEntity | null) => {
if (!root) return
if (root === this.playerEntity) {
controller.setMeta(root, this.entityLightMeta(root.originalEntity))
}
const worldPos = this.worldRenderer.sceneOrigin.getWorldPosition(root)
if (!worldPos) return
controller.update(root, worldPos, shared)
}
for (const entity of Object.values(this.entities)) apply(entity)
apply(this.playerEntity)
}

handleDamageEvent(entityId, damageAmount) {
const entityMesh = this.entities[entityId]?.children.find(c => c.name === 'mesh')
const entity = this.entities[entityId]
const entityMesh = entity?.children.find(c => c.name === 'mesh')
if (entityMesh) {
entityMesh.traverse(child => {
if (child instanceof THREE.Mesh && child.material.clone) {
Expand All @@ -1713,6 +1769,18 @@ export class Entities {
}
})
}
if (entity) {
this.worldRenderer.entityLightController?.markFlash(entity)
this.invalidateEntityLight(entity)
setTimeout(() => {
if (!this.entities[entityId]) return
this.worldRenderer.entityLightController?.endFlash(entity)
const worldPos = this.worldRenderer.sceneOrigin.getWorldPosition(entity)
if (worldPos) {
this.worldRenderer.entityLightController?.update(entity, worldPos, this.entityLightSharedMaterials())
}
}, 500)
}
}

raycastSceneDebug() {
Expand Down
Loading
Loading