540 lines
19 KiB
TypeScript
540 lines
19 KiB
TypeScript
import { Vec3 } from 'vec3'
|
|
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
|
import legacyJson from '../../../../src/preflatMap.json'
|
|
import { World, BlockModelPartsResolved, WorldBlock as Block } from './world'
|
|
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
|
|
|
|
let blockProvider: WorldBlockProvider
|
|
|
|
const tints: any = {}
|
|
let needTiles = false
|
|
|
|
let tintsData
|
|
try {
|
|
tintsData = require('esbuild-data').tints
|
|
} catch (err) {
|
|
tintsData = require('minecraft-data/minecraft-data/data/pc/1.16.2/tints.json')
|
|
}
|
|
for (const key of Object.keys(tintsData)) {
|
|
tints[key] = prepareTints(tintsData[key])
|
|
}
|
|
|
|
function prepareTints (tints) {
|
|
const map = new Map()
|
|
const defaultValue = tintToGl(tints.default)
|
|
for (let { keys, color } of tints.data) {
|
|
color = tintToGl(color)
|
|
for (const key of keys) {
|
|
map.set(`${key}`, color)
|
|
}
|
|
}
|
|
return new Proxy(map, {
|
|
get (target, key) {
|
|
return target.has(key) ? target.get(key) : defaultValue
|
|
}
|
|
})
|
|
}
|
|
|
|
function mod (x: number, n: number) {
|
|
return ((x % n) + n) % n
|
|
}
|
|
|
|
const calculatedBlocksEntries = Object.entries(legacyJson.clientCalculatedBlocks)
|
|
export function preflatBlockCalculation (block: Block, world: World, position: Vec3) {
|
|
const type = calculatedBlocksEntries.find(([name, blocks]) => blocks.includes(block.name))?.[0]
|
|
if (!type) return
|
|
switch (type) {
|
|
case 'directional': {
|
|
const isSolidConnection = !block.name.includes('redstone') && !block.name.includes('tripwire')
|
|
const neighbors = [
|
|
world.getBlock(position.offset(0, 0, 1)),
|
|
world.getBlock(position.offset(0, 0, -1)),
|
|
world.getBlock(position.offset(1, 0, 0)),
|
|
world.getBlock(position.offset(-1, 0, 0))
|
|
]
|
|
// set needed props to true: east:'false',north:'false',south:'false',west:'false'
|
|
const props = {}
|
|
for (const [i, neighbor] of neighbors.entries()) {
|
|
const isConnectedToSolid = isSolidConnection ? (neighbor && !neighbor.transparent) : false
|
|
if (isConnectedToSolid || neighbor?.name === block.name) {
|
|
props[['south', 'north', 'east', 'west'][i]] = 'true'
|
|
}
|
|
}
|
|
return props
|
|
}
|
|
// case 'gate_in_wall': {}
|
|
case 'block_snowy': {
|
|
const aboveIsSnow = world.getBlock(position.offset(0, 1, 0))?.name === 'snow'
|
|
return {
|
|
snowy: `${aboveIsSnow}`
|
|
}
|
|
}
|
|
case 'door': {
|
|
// upper half matches lower in
|
|
const { half } = block.getProperties()
|
|
if (half === 'upper') {
|
|
// copy other properties
|
|
const lower = world.getBlock(position.offset(0, -1, 0))
|
|
if (lower?.name === block.name) {
|
|
return {
|
|
...lower.getProperties(),
|
|
half: 'upper'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function tintToGl (tint) {
|
|
const r = (tint >> 16) & 0xff
|
|
const g = (tint >> 8) & 0xff
|
|
const b = tint & 0xff
|
|
return [r / 255, g / 255, b / 255]
|
|
}
|
|
|
|
function getLiquidRenderHeight (world, block, type, pos) {
|
|
if (!block || block.type !== type) return 1 / 9
|
|
if (block.metadata === 0) { // source block
|
|
const blockAbove = world.getBlock(pos.offset(0, 1, 0))
|
|
if (blockAbove && blockAbove.type === type) return 1
|
|
return 8 / 9
|
|
}
|
|
return ((block.metadata >= 8 ? 8 : 7 - block.metadata) + 1) / 9
|
|
}
|
|
|
|
|
|
const isCube = (block: Block) => {
|
|
if (!block || block.transparent) return false
|
|
if (block.isCube) return true
|
|
if (!block.models?.length || block.models.length !== 1) return false
|
|
// all variants
|
|
return block.models[0].every(v => v.elements!.every(e => {
|
|
return e.from[0] === 0 && e.from[1] === 0 && e.from[2] === 0 && e.to[0] === 16 && e.to[1] === 16 && e.to[2] === 16
|
|
}))
|
|
}
|
|
|
|
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: Record<string, any>) {
|
|
const heights: number[] = []
|
|
for (let z = -1; z <= 1; z++) {
|
|
for (let x = -1; x <= 1; x++) {
|
|
const pos = cursor.offset(x, 0, z)
|
|
heights.push(getLiquidRenderHeight(world, world.getBlock(pos), type, pos))
|
|
}
|
|
}
|
|
const cornerHeights = [
|
|
Math.max(Math.max(heights[0], heights[1]), Math.max(heights[3], heights[4])),
|
|
Math.max(Math.max(heights[1], heights[2]), Math.max(heights[4], heights[5])),
|
|
Math.max(Math.max(heights[3], heights[4]), Math.max(heights[6], heights[7])),
|
|
Math.max(Math.max(heights[4], heights[5]), Math.max(heights[7], heights[8]))
|
|
]
|
|
|
|
// eslint-disable-next-line guard-for-in
|
|
for (const face in elemFaces) {
|
|
const { dir, corners } = elemFaces[face]
|
|
const isUp = dir[1] === 1
|
|
|
|
const neighborPos = cursor.offset(...dir as [number, number, number])
|
|
const neighbor = world.getBlock(neighborPos)
|
|
if (!neighbor) continue
|
|
if (neighbor.type === type) continue
|
|
const isGlass = neighbor.name.includes('glass')
|
|
if ((isCube(neighbor) && !isUp) || neighbor.getProperties().waterlogged) continue
|
|
|
|
let tint = [1, 1, 1]
|
|
if (water) {
|
|
let m = 1 // Fake lighting to improve lisibility
|
|
if (Math.abs(dir[0]) > 0) m = 0.6
|
|
else if (Math.abs(dir[2]) > 0) m = 0.8
|
|
tint = tints.water[biome]
|
|
tint = [tint[0] * m, tint[1] * m, tint[2] * m]
|
|
}
|
|
|
|
if (needTiles) {
|
|
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
|
|
block: 'water',
|
|
faces: [],
|
|
}
|
|
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
|
|
face,
|
|
neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`,
|
|
// texture: eFace.texture.name,
|
|
})
|
|
}
|
|
|
|
const { u } = texture
|
|
const { v } = texture
|
|
const { su } = texture
|
|
const { sv } = texture
|
|
|
|
for (const pos of corners) {
|
|
const height = cornerHeights[pos[2] * 2 + pos[0]]
|
|
attr.t_positions.push(
|
|
(pos[0] ? 0.999 : 0.001) + (cursor.x & 15) - 8,
|
|
(pos[1] ? height - 0.001 : 0.001) + (cursor.y & 15) - 8,
|
|
(pos[2] ? 0.999 : 0.001) + (cursor.z & 15) - 8
|
|
)
|
|
attr.t_normals.push(...dir)
|
|
attr.t_uvs.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v)
|
|
attr.t_colors.push(tint[0], tint[1], tint[2])
|
|
}
|
|
}
|
|
}
|
|
|
|
let needRecompute = false
|
|
|
|
function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: boolean, attr: Record<string, any>, globalMatrix: any, globalShift: any, block: Block, biome: string) {
|
|
const position = cursor
|
|
// const key = `${position.x},${position.y},${position.z}`
|
|
// if (!globalThis.allowedBlocks.includes(key)) return
|
|
const cullIfIdentical = block.name.includes('glass')
|
|
|
|
// eslint-disable-next-line guard-for-in
|
|
for (const face in element.faces) {
|
|
const eFace = element.faces[face]
|
|
const { corners, mask1, mask2 } = elemFaces[face]
|
|
const dir = matmul3(globalMatrix, elemFaces[face].dir)
|
|
|
|
if (eFace.cullface) {
|
|
const neighbor = world.getBlock(cursor.plus(new Vec3(...dir)))
|
|
if (neighbor) {
|
|
if (cullIfIdentical && neighbor.type === block.type) continue
|
|
if (!neighbor.transparent && isCube(neighbor)) continue
|
|
} else {
|
|
needRecompute = true
|
|
continue
|
|
}
|
|
}
|
|
|
|
const minx = element.from[0]
|
|
const miny = element.from[1]
|
|
const minz = element.from[2]
|
|
const maxx = element.to[0]
|
|
const maxy = element.to[1]
|
|
const maxz = element.to[2]
|
|
|
|
const texture = eFace.texture as any
|
|
const { u, v, su, sv } = texture
|
|
|
|
const ndx = Math.floor(attr.positions.length / 3)
|
|
|
|
let tint = [1, 1, 1]
|
|
if (eFace.tintindex !== undefined) {
|
|
if (eFace.tintindex === 0) {
|
|
if (block.name === 'redstone_wire') {
|
|
tint = tints.redstone[`${block.getProperties().power}`]
|
|
} else if (block.name === 'birch_leaves' ||
|
|
block.name === 'spruce_leaves' ||
|
|
block.name === 'lily_pad') {
|
|
tint = tints.constant[block.name]
|
|
} else if (block.name.includes('leaves') || block.name === 'vine') {
|
|
tint = tints.foliage[biome]
|
|
} else {
|
|
tint = tints.grass[biome]
|
|
}
|
|
}
|
|
}
|
|
|
|
// UV rotation
|
|
const r = eFace.rotation || 0
|
|
const uvcs = Math.cos(r * Math.PI / 180)
|
|
const uvsn = -Math.sin(r * Math.PI / 180)
|
|
|
|
let localMatrix = null as any
|
|
let localShift = null as any
|
|
|
|
if (element.rotation) {
|
|
// todo do we support rescale?
|
|
localMatrix = buildRotationMatrix(
|
|
element.rotation.axis,
|
|
element.rotation.angle
|
|
)
|
|
|
|
localShift = vecsub3(
|
|
element.rotation.origin,
|
|
matmul3(
|
|
localMatrix,
|
|
element.rotation.origin
|
|
)
|
|
)
|
|
}
|
|
|
|
const aos: number[] = []
|
|
const neighborPos = position.plus(new Vec3(...dir))
|
|
const baseLight = world.getLight(neighborPos, undefined, undefined, block.name) / 15
|
|
for (const pos of corners) {
|
|
let vertex = [
|
|
(pos[0] ? maxx : minx),
|
|
(pos[1] ? maxy : miny),
|
|
(pos[2] ? maxz : minz)
|
|
]
|
|
|
|
vertex = vecadd3(matmul3(localMatrix, vertex), localShift)
|
|
vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift)
|
|
vertex = vertex.map(v => v / 16)
|
|
|
|
attr.positions.push(
|
|
vertex[0] + (cursor.x & 15) - 8,
|
|
vertex[1] + (cursor.y & 15) - 8,
|
|
vertex[2] + (cursor.z & 15) - 8
|
|
)
|
|
|
|
attr.normals.push(...dir)
|
|
|
|
const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5
|
|
const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5
|
|
attr.uvs.push(baseu * su + u, basev * sv + v)
|
|
|
|
let light = 1
|
|
if (doAO) {
|
|
const dx = pos[0] * 2 - 1
|
|
const dy = pos[1] * 2 - 1
|
|
const dz = pos[2] * 2 - 1
|
|
const cornerDir = matmul3(globalMatrix, [dx, dy, dz])
|
|
const side1Dir = matmul3(globalMatrix, [dx * mask1[0], dy * mask1[1], dz * mask1[2]])
|
|
const side2Dir = matmul3(globalMatrix, [dx * mask2[0], dy * mask2[1], dz * mask2[2]])
|
|
const side1 = world.getBlock(cursor.offset(...side1Dir))
|
|
const side2 = world.getBlock(cursor.offset(...side2Dir))
|
|
const corner = world.getBlock(cursor.offset(...cornerDir))
|
|
|
|
let cornerLightResult = 15
|
|
// eslint-disable-next-line no-constant-condition, sonarjs/no-gratuitous-expressions
|
|
if (/* world.config.smoothLighting */false) { // todo fix
|
|
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 = (side1Light + side2Light + cornerLight) / 3
|
|
}
|
|
|
|
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))
|
|
// todo light should go upper on lower blocks
|
|
light = (ao + 1) / 4 * (cornerLightResult / 15)
|
|
aos.push(ao)
|
|
}
|
|
|
|
attr.colors.push(baseLight * tint[0] * light, baseLight * tint[1] * light, baseLight * tint[2] * light)
|
|
}
|
|
|
|
if (needTiles) {
|
|
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
|
|
block: block.name,
|
|
faces: [],
|
|
}
|
|
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
|
|
face,
|
|
neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`,
|
|
light: baseLight
|
|
// texture: eFace.texture.name,
|
|
})
|
|
}
|
|
|
|
if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) {
|
|
attr.indices.push(
|
|
// eslint-disable-next-line @stylistic/function-call-argument-newline
|
|
ndx, ndx + 3, ndx + 2,
|
|
ndx, ndx + 1, ndx + 3
|
|
)
|
|
} else {
|
|
attr.indices.push(
|
|
// eslint-disable-next-line @stylistic/function-call-argument-newline
|
|
ndx, ndx + 1, ndx + 2,
|
|
ndx + 2, ndx + 1, ndx + 3
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
const invisibleBlocks = new Set(['air', 'cave_air', 'void_air', 'barrier'])
|
|
|
|
const isBlockWaterlogged = (block: Block) => block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true'
|
|
|
|
let unknownBlockModel: BlockModelPartsResolved
|
|
let erroredBlockModel: BlockModelPartsResolved
|
|
export function getSectionGeometry (sx, sy, sz, world: World) {
|
|
let delayedRender = [] as Array<() => void>
|
|
|
|
const attr = {
|
|
sx: sx + 8,
|
|
sy: sy + 8,
|
|
sz: sz + 8,
|
|
positions: [],
|
|
normals: [],
|
|
colors: [],
|
|
uvs: [],
|
|
t_positions: [],
|
|
t_normals: [],
|
|
t_colors: [],
|
|
t_uvs: [],
|
|
indices: [],
|
|
tiles: {},
|
|
// todo this can be removed here
|
|
signs: {},
|
|
hadErrors: false
|
|
} as Record<string, any>
|
|
|
|
const cursor = new Vec3(0, 0, 0)
|
|
for (cursor.y = sy; cursor.y < sy + 16; cursor.y++) {
|
|
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
|
|
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
|
|
const block = world.getBlock(cursor)!
|
|
if (invisibleBlocks.has(block.name)) continue
|
|
if (block.name.includes('_sign') || block.name === 'sign') {
|
|
const key = `${cursor.x},${cursor.y},${cursor.z}`
|
|
const props: any = block.getProperties()
|
|
const facingRotationMap = {
|
|
'north': 2,
|
|
'south': 0,
|
|
'west': 1,
|
|
'east': 3
|
|
}
|
|
const isWall = block.name.endsWith('wall_sign') || block.name.endsWith('wall_hanging_sign')
|
|
const isHanging = block.name.endsWith('hanging_sign')
|
|
attr.signs[key] = {
|
|
isWall,
|
|
isHanging,
|
|
rotation: isWall ? facingRotationMap[props.facing] : +props.rotation
|
|
}
|
|
}
|
|
const biome = block.biome.name
|
|
|
|
let preflatRecomputeVariant = !!(block as any)._originalProperties
|
|
if (world.preflat) {
|
|
const patchProperties = preflatBlockCalculation(block, world, cursor)
|
|
if (patchProperties) {
|
|
//@ts-expect-error
|
|
block._originalProperties ??= block._properties
|
|
//@ts-expect-error
|
|
block._properties = { ...block._originalProperties, ...patchProperties }
|
|
preflatRecomputeVariant = true
|
|
} else {
|
|
//@ts-expect-error
|
|
block._properties = block._originalProperties ?? block._properties
|
|
//@ts-expect-error
|
|
block._originalProperties = undefined
|
|
}
|
|
}
|
|
|
|
const isWaterlogged = isBlockWaterlogged(block)
|
|
if (block.name === 'water' || isWaterlogged) {
|
|
const pos = cursor.clone()
|
|
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
|
delayedRender.push(() => {
|
|
renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr)
|
|
})
|
|
} else if (block.name === 'lava') {
|
|
renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr)
|
|
}
|
|
if (block.name !== 'water' && block.name !== 'lava' && !invisibleBlocks.has(block.name)) {
|
|
// cache
|
|
let { models } = block
|
|
if (block.models === undefined || preflatRecomputeVariant) {
|
|
try {
|
|
models = blockProvider.getAllResolvedModels0_1({
|
|
name: block.name,
|
|
properties: block.getProperties(),
|
|
})!
|
|
if (!models.length) models = null
|
|
} catch (err) {
|
|
models ??= erroredBlockModel
|
|
console.error(`Critical assets error. Unable to get block model for ${block.name}[${JSON.stringify(block.getProperties())}]: ` + err.message, err.stack)
|
|
attr.hadErrors = true
|
|
}
|
|
}
|
|
block.models = models ?? null
|
|
|
|
models ??= unknownBlockModel
|
|
|
|
const firstForceVar = world.config.debugModelVariant?.[0]
|
|
let part = 0
|
|
for (const modelVars of models ?? []) {
|
|
const pos = cursor.clone()
|
|
// const variantRuntime = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), modelVars.length)
|
|
const variantRuntime = 0
|
|
const useVariant = world.config.debugModelVariant?.[part] ?? firstForceVar ?? variantRuntime
|
|
part++
|
|
const model = modelVars[useVariant] ?? modelVars[0]
|
|
if (!model) continue
|
|
|
|
let globalMatrix = null as any
|
|
let globalShift = null as any
|
|
for (const axis of ['x', 'y', 'z'] as const) {
|
|
if (axis in model) {
|
|
globalMatrix = globalMatrix ?
|
|
matmulmat3(globalMatrix, buildRotationMatrix(axis, -(model[axis] ?? 0))) :
|
|
buildRotationMatrix(axis, -(model[axis] ?? 0))
|
|
}
|
|
}
|
|
if (globalMatrix) {
|
|
globalShift = [8, 8, 8]
|
|
globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift))
|
|
}
|
|
|
|
for (const element of model.elements ?? []) {
|
|
const ao = model.ao ?? true
|
|
if (block.transparent) {
|
|
const pos = cursor.clone()
|
|
delayedRender.push(() => {
|
|
renderElement(world, pos, element, ao, attr, globalMatrix, globalShift, block, biome)
|
|
})
|
|
} else {
|
|
renderElement(world, cursor, element, ao, attr, globalMatrix, globalShift, block, biome)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const render of delayedRender) {
|
|
render()
|
|
}
|
|
delayedRender = []
|
|
|
|
let ndx = attr.positions.length / 3
|
|
for (let i = 0; i < attr.t_positions.length / 12; i++) {
|
|
attr.indices.push(
|
|
ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3,
|
|
// eslint-disable-next-line @stylistic/function-call-argument-newline
|
|
// back face
|
|
ndx, ndx + 2, ndx + 1, ndx + 2, ndx + 3, ndx + 1
|
|
)
|
|
ndx += 4
|
|
}
|
|
|
|
attr.positions.push(...attr.t_positions)
|
|
attr.normals.push(...attr.t_normals)
|
|
attr.colors.push(...attr.t_colors)
|
|
attr.uvs.push(...attr.t_uvs)
|
|
|
|
delete attr.t_positions
|
|
delete attr.t_normals
|
|
delete attr.t_colors
|
|
delete attr.t_uvs
|
|
|
|
attr.positions = new Float32Array(attr.positions) as any
|
|
attr.normals = new Float32Array(attr.normals) as any
|
|
attr.colors = new Float32Array(attr.colors) as any
|
|
attr.uvs = new Float32Array(attr.uvs) as any
|
|
|
|
return attr
|
|
}
|
|
|
|
export const setBlockStatesData = (blockstatesModels, blocksAtlas: any, _needTiles = false, useUnknownBlockModel = true) => {
|
|
blockProvider = worldBlockProvider(blockstatesModels, blocksAtlas, 'latest')
|
|
globalThis.blockProvider = blockProvider
|
|
if (useUnknownBlockModel) {
|
|
unknownBlockModel = blockProvider.getAllResolvedModels0_1({ name: 'unknown', properties: {} })
|
|
erroredBlockModel = blockProvider.getAllResolvedModels0_1({ name: 'errored', properties: {} })
|
|
}
|
|
|
|
needTiles = _needTiles
|
|
}
|