Skip to content

Commit

Permalink
feat: Add lighting support for < 1.12 with smooth lighting (#98)
Browse files Browse the repository at this point in the history
(newer versions are available experimentally under option)
  • Loading branch information
zardoy authored Apr 16, 2024
1 parent 2cc524a commit 53a6d78
Show file tree
Hide file tree
Showing 16 changed files with 172 additions and 33 deletions.
2 changes: 1 addition & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can u
- `viewer.world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group.
- `debugSceneChunks` - The same as above, but relative to current bot position (e.g. 0,0 is the current chunk).
- `debugChangedOptions` - See what options are changed. Don't change options here.
- `localServer` - Only for singleplayer mode/host. Flying Squid server instance, see it's documentation for more.
- `localServer`/`server` - Only for singleplayer mode/host. Flying Squid server instance, see it's documentation for more.
- `localServer.overworld.storageProvider.regions` - See ALL LOADED region files with all raw data.

- `nbt.simplify(someNbt)` - Simplifies nbt data, so it's easier to read.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"title-case": "3.x",
"ua-parser-js": "^1.0.37",
"valtio": "^1.11.1",
"vec3": "^0.1.7",
"workbox-build": "^7.0.0"
},
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions prismarine-viewer/viewer/lib/mesher/mesher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
//@ts-check
/* global postMessage self */

import { World } from './world'
import { Vec3 } from 'vec3'
import { getSectionGeometry, setRendererData } from './models'
Expand Down Expand Up @@ -53,6 +50,9 @@ self.onmessage = ({ data }) => {
blockStatesReady = true
} else if (data.type === 'dirty') {
const loc = new Vec3(data.x, data.y, data.z)
world.skyLight = data.skyLight
world.smoothLighting = data.smoothLighting
world.enableLighting = data.enableLighting
setSectionDirty(loc, data.value)
} else if (data.type === 'chunk') {
world.addColumn(data.x, data.z, data.chunk)
Expand Down
29 changes: 24 additions & 5 deletions prismarine-viewer/viewer/lib/mesher/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ const elemFaces = {
}
}

function getLiquidRenderHeight (world, block, type) {
function getLiquidRenderHeight (world, block, type, pos) {
if (!block || block.type !== type) return 1 / 9
if (block.metadata === 0) { // source block
const blockAbove = world.getBlock(block.position.offset(0, 1, 0))
const blockAbove = world.getBlock(pos.offset(0, 1, 0))
if (blockAbove && blockAbove.type === type) return 1
return 8 / 9
}
Expand All @@ -122,7 +122,8 @@ function renderLiquid (world, cursor, texture, type, biome, water, attr) {
const heights: number[] = []
for (let z = -1; z <= 1; z++) {
for (let x = -1; x <= 1; x++) {
heights.push(getLiquidRenderHeight(world, world.getBlock(cursor.offset(x, 0, z)), type))
const pos = cursor.offset(x, 0, z)
heights.push(getLiquidRenderHeight(world, world.getBlock(pos), type, pos))
}
}
const cornerHeights = [
Expand Down Expand Up @@ -238,6 +239,9 @@ function buildRotationMatrix (axis, degree) {
}

function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr, globalMatrix, globalShift, block: Block, biome) {
const position = cursor
// const key = `${position.x},${position.y},${position.z}`
// if (!globalThis.allowedBlocks.includes(key)) return
const cullIfIdentical = block.name.indexOf('glass') >= 0

for (const face in element.faces) {
Expand Down Expand Up @@ -310,6 +314,9 @@ function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr
}

const aos: number[] = []
const neighborPos = position.plus(new Vec3(...dir))
let baseLightLevel = world.getLight(neighborPos)
const baseLight = baseLightLevel / 15
for (const pos of corners) {
let vertex = [
(pos[0] ? maxx : minx),
Expand Down Expand Up @@ -345,18 +352,30 @@ function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr
const side2 = world.getBlock(cursor.offset(...side2Dir))
const corner = world.getBlock(cursor.offset(...cornerDir))

let cornerLightResult = 15
if (world.smoothLighting) {
const side1Light = world.getLight(cursor.plus(new Vec3(...side1Dir)), true)
const side2Light = world.getLight(cursor.plus(new Vec3(...side2Dir)), true)
const cornerLight = world.getLight(cursor.plus(new Vec3(...cornerDir)), true)
// interpolate
cornerLightResult = Math.min(
Math.min(side1Light, side2Light),
cornerLight
)
}

const side1Block = world.shouldMakeAo(side1) ? 1 : 0
const side2Block = world.shouldMakeAo(side2) ? 1 : 0
const cornerBlock = world.shouldMakeAo(corner) ? 1 : 0

// TODO: correctly interpolate ao light based on pos (evaluate once for each corner of the block)

const ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock))
light = (ao + 1) / 4
light = (ao + 1) / 4 * cornerLightResult / 15
aos.push(ao)
}

attr.colors.push(tint[0] * light, tint[1] * light, tint[2] * light)
attr.colors.push(baseLight * tint[0] * light, baseLight * tint[1] * light, baseLight * tint[2] * light)
}

if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) {
Expand Down
67 changes: 55 additions & 12 deletions prismarine-viewer/viewer/lib/mesher/world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,6 @@ function columnKey (x, z) {
return `${x},${z}`
}

function posInChunk (pos) {
pos = pos.floored()
pos.x &= 15
pos.z &= 15
return pos
}

function isCube (shapes) {
if (!shapes || shapes.length !== 1) return false
const shape = shapes[0]
Expand All @@ -29,21 +22,46 @@ export type WorldBlock = Block & {
isCube: boolean
}


export class World {
enableLighting = true
skyLight = 15
smoothLighting = true
outputFormat = 'threeJs' as 'threeJs' | 'webgl'
Chunk: any/* import('prismarine-chunk/types/index').PCChunk */
columns = {}
Chunk: typeof import('prismarine-chunk/types/index').PCChunk
columns = {} as { [key: string]: import('prismarine-chunk/types/index').PCChunk }
blockCache = {}
biomeCache: { [id: number]: mcData.Biome }

constructor(version) {
this.Chunk = Chunks(version)
this.Chunk = Chunks(version) as any
this.biomeCache = mcData(version).biomes
}

getLight (pos: Vec3, isNeighbor = false) {
if (!this.enableLighting) return 15
// const key = `${pos.x},${pos.y},${pos.z}`
// if (lightsCache.has(key)) return lightsCache.get(key)
const column = this.getColumnByPos(pos)
if (!column || !hasChunkSection(column, pos)) return 15
let result = Math.min(
15,
Math.max(
column.getBlockLight(posInChunk(pos)),
Math.min(this.skyLight, column.getSkyLight(posInChunk(pos)))
) + 2
)
// lightsCache.set(key, result)
if (result === 2 && this.getBlock(pos)?.name.match(/_stairs|slab/)) { // todo this is obviously wrong
result = this.getLight(pos.offset(0, 1, 0))
}
if (isNeighbor && result === 2) result = 15 // TODO
return result
}

addColumn (x, z, json) {
const chunk = this.Chunk.fromJson(json)
this.columns[columnKey(x, z)] = chunk
this.columns[columnKey(x, z)] = chunk as any
return chunk
}

Expand All @@ -67,6 +85,10 @@ export class World {
return true
}

getColumnByPos (pos: Vec3) {
return this.getColumn(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16)
}

getBlock (pos: Vec3): WorldBlock | null {
const key = columnKey(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16)

Expand All @@ -80,12 +102,18 @@ export class World {

if (!this.blockCache[stateId]) {
const b = column.getBlock(locInChunk)
//@ts-expect-error
b.isCube = isCube(b.shapes)
this.blockCache[stateId] = b
Object.defineProperty(b, 'position', {
get () {
throw new Error('position is not reliable, use pos parameter instead of block.position')
}
})
}

const block = this.blockCache[stateId]
block.position = loc
// block.position = loc // it overrides position of all currently loaded blocks
block.biome = this.biomeCache[column.getBiome(locInChunk)] ?? this.biomeCache[1] ?? this.biomeCache[0]
if (block.name === 'redstone_ore') block.transparent = false
return block
Expand All @@ -95,3 +123,18 @@ export class World {
return block?.isCube && !ignoreAoBlocks.includes(block.name)
}
}

// todo export in chunk instead
const hasChunkSection = (column, pos) => {
if (column._getSection) return column._getSection(pos)
if (column.sections) return column.sections[pos.y >> 4]
if (column.skyLightSections) return column.skyLightSections[getLightSectionIndex(pos, column.minY)]
}

function posInChunk (pos) {
return new Vec3(Math.floor(pos.x) & 15, Math.floor(pos.y), Math.floor(pos.z) & 15)
}

function getLightSectionIndex (pos, minY = 0) {
return Math.floor((pos.y - minY) / 16) + 1
}
21 changes: 21 additions & 0 deletions prismarine-viewer/viewer/lib/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,27 @@ export class Viewer {
this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
})

emitter.on('updateLight', ({ pos }) => {
if (this.world instanceof WorldRendererThree) this.world.updateLight(pos.x, pos.z)
})

emitter.on('time', (timeOfDay) => {
let skyLight = 15
if (timeOfDay < 0 || timeOfDay > 24000) {
throw new Error("Invalid time of day. It should be between 0 and 24000.");
} else if (timeOfDay <= 6000 || timeOfDay >= 18000) {
skyLight = 15;
} else if (timeOfDay > 6000 && timeOfDay < 12000) {
skyLight = 15 - ((timeOfDay - 6000) / 6000) * 15;
} else if (timeOfDay >= 12000 && timeOfDay < 18000) {
skyLight = ((timeOfDay - 12000) / 6000) * 15;
}

if (this.world.skyLight === skyLight) return
this.world.skyLight = skyLight
;(this.world as WorldRendererThree).rerenderAllChunks?.()
})

emitter.emit('listening')
}

Expand Down
8 changes: 8 additions & 0 deletions prismarine-viewer/viewer/lib/worldDataEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,16 @@ export class WorldDataEmitter extends EventEmitter {
const stateId = newBlock.stateId ? newBlock.stateId : ((newBlock.type << 4) | newBlock.metadata)
this.emitter.emit('blockUpdate', { pos: oldBlock.position, stateId })
},
time: () => {
this.emitter.emit('time', bot.time.timeOfDay)
},
} satisfies Partial<BotEvents>

bot._client.on('update_light', ({ chunkX, chunkZ }) => {
const chunkPos = new Vec3(chunkX * 16, 0, chunkZ * 16)
this.loadChunk(chunkPos)
})

this.emitter.on('listening', () => {
this.emitter.emit('blockEntities', new Proxy({}, {
get (_target, posKey, receiver) {
Expand Down
5 changes: 4 additions & 1 deletion prismarine-viewer/viewer/lib/worldrendererCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
texturesVersion?: string
viewDistance = -1
chunksLength = 0
skyLight = 15
smoothLighting = true
enableLighting = true

abstract outputFormat: 'threeJs' | 'webgl'

Expand Down Expand Up @@ -222,7 +225,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
// is always dispatched to the same worker
const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length)
this.sectionsOutstanding.set(key, (this.sectionsOutstanding.get(key) ?? 0) + 1)
this.workers[hash].postMessage({ type: 'dirty', x: pos.x, y: pos.y, z: pos.z, value })
this.workers[hash].postMessage({ type: 'dirty', x: pos.x, y: pos.y, z: pos.z, value, skyLight: this.skyLight, smoothLighting: this.smoothLighting, enableLighting: this.enableLighting })
}

// Listen for chunk rendering updates emitted if a worker finished a render and resolve if the number
Expand Down
14 changes: 14 additions & 0 deletions prismarine-viewer/viewer/lib/worldrendererThree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,20 @@ export class WorldRendererThree extends WorldRendererCommon {
return group
}

updateLight (chunkX: number, chunkZ: number) {
// set all sections in the chunk dirty
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
this.setSectionDirty(new Vec3(chunkX, y, chunkZ))
}
}

rerenderAllChunks () { // todo not clear what to do with loading chunks
for (const key of Object.keys(this.sectionObjects)) {
const [x, y, z] = key.split(',').map(Number)
this.setSectionDirty(new Vec3(x, y, z))
}
}

updateShowChunksBorder (value: boolean) {
this.showChunkBorders = value
for (const object of Object.values(this.sectionObjects)) {
Expand Down
7 changes: 4 additions & 3 deletions src/dayCycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ export default () => {
// todo need to think wisely how to set these values & also move directional light around!
const colorInt = Math.max(int, 0.1)
viewer.scene.background = new THREE.Color(dayColor.r * colorInt, dayColor.g * colorInt, dayColor.b * colorInt)
viewer.ambientLight.intensity = Math.max(int, 0.25)
// directional light
viewer.directionalLight.intensity = Math.min(int, 0.5)
if (!options.newVersionsLighting && bot.supportFeature('blockStateId')) {
viewer.ambientLight.intensity = Math.max(int, 0.25)
viewer.directionalLight.intensity = Math.min(int, 0.5)
}
})
}
14 changes: 9 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ async function connect (connectOptions: {

console.log('Done!')

// todo cleanup these
onGameLoad(async () => {
if (!viewer.world.downloadedBlockStatesData && !viewer.world.customBlockStatesData) {
await new Promise<void>(resolve => {
Expand All @@ -765,11 +766,14 @@ async function connect (connectOptions: {
})

if (!connectOptions.ignoreQs) {
const qs = new URLSearchParams(window.location.search)
for (let command of qs.getAll('command')) {
if (!command.startsWith('/')) command = `/${command}`
bot.chat(command)
}
// todo cleanup
customEvents.on('gameLoaded', () => {
const qs = new URLSearchParams(window.location.search)
for (let command of qs.getAll('command')) {
if (!command.startsWith('/')) command = `/${command}`
bot.chat(command)
}
})
}
}

Expand Down
8 changes: 5 additions & 3 deletions src/menus/components/debug_overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,10 @@ class DebugOverlay extends LitElement {
return html`<p>${name}: ${typeof value === 'boolean' ? html`<span style="color: ${value ? 'lightgreen' : 'red'}">${value}</span>` : value}</p>`
}

const skyL = this.bot.world.getSkyLight(this.bot.entity.position)
const biomeId = this.bot.world.getBiome(this.bot.entity.position)
const botBlock = bot.entity.position.floored()
const skyL = bot.world.getSkyLight(botBlock)
const blockL = bot.world.getBlockLight(botBlock)
const biomeId = bot.world.getBiome(botBlock)

return html`
<div class="debug-left-side">
Expand All @@ -251,7 +253,7 @@ class DebugOverlay extends LitElement {
<p>Packets: ${this.packetsString}</p>
<p>Facing (viewer): ${rot[0].toFixed(3)} ${rot[1].toFixed(3)}</p>
<p>Facing (minecraft): ${quadsDescription[minecraftQuad]} (${minecraftYaw.toFixed(1)} ${(rot[1] * -180 / Math.PI).toFixed(1)})</p>
<p>Light: ${skyL} (${skyL} sky)</p>
<p>Light: ${blockL} (${skyL} sky)</p>
<!-- todo fix biome -->
<p>Biome: minecraft:${window.loadedData.biomesArray[biomeId]?.name ?? 'unknown biome'}</p>
<p>Day: ${this.bot.time.day}</p>
Expand Down
4 changes: 4 additions & 0 deletions src/optionsGuiScheme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export const guiOptionsScheme: {
dayCycleAndLighting: {
text: 'Day Cycle',
},
smoothLighting: {},
newVersionsLighting: {
text: 'Lighting in newer versions',
}
},
],
main: [
Expand Down
Loading

0 comments on commit 53a6d78

Please sign in to comment.