pages235/renderer/viewer/three/entity/EntityMesh.ts

554 lines
17 KiB
TypeScript

import * as THREE from 'three'
import { OBJLoader } from 'three-stdlib'
import huskPng from 'mc-assets/dist/other-textures/latest/entity/zombie/husk.png'
import { Vec3 } from 'vec3'
import arrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/arrow.png'
import spectralArrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/spectral_arrow.png'
import tippedArrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/tipped_arrow.png'
import { loadTexture } from '../../lib/utils'
import { WorldRendererThree } from '../worldrendererThree'
import entities from './entities.json'
import { externalModels } from './objModels'
import externalTexturesJson from './externalTextures.json'
interface ElemFace {
dir: [number, number, number]
u0: [number, number, number]
v0: [number, number, number]
u1: [number, number, number]
v1: [number, number, number]
corners: Array<[number, number, number, number, number]>
}
interface GeoData {
positions: number[]
normals: number[]
uvs: number[]
indices: number[]
skinIndices: number[]
skinWeights: number[]
}
interface JsonBone {
name: string
pivot?: [number, number, number]
bind_pose_rotation?: [number, number, number]
rotation?: [number, number, number]
parent?: string
cubes?: JsonCube[]
mirror?: boolean
}
interface JsonCube {
origin: [number, number, number]
size: [number, number, number]
uv: [number, number]
inflate?: number
rotation?: [number, number, number]
}
interface JsonModel {
texturewidth?: number
textureheight?: number
bones: JsonBone[]
}
interface EntityOverrides {
textures?: Record<string, string>
rotation?: Record<string, { x?: number; y?: number; z?: number }>
}
const elemFaces: Record<string, ElemFace> = {
up: {
dir: [0, 1, 0],
u0: [0, 0, 1],
v0: [0, 0, 0],
u1: [1, 0, 1],
v1: [0, 0, 1],
corners: [
[0, 1, 1, 0, 0],
[1, 1, 1, 1, 0],
[0, 1, 0, 0, 1],
[1, 1, 0, 1, 1]
]
},
down: {
dir: [0, -1, 0],
u0: [1, 0, 1],
v0: [0, 0, 0],
u1: [2, 0, 1],
v1: [0, 0, 1],
corners: [
[1, 0, 1, 0, 0],
[0, 0, 1, 1, 0],
[1, 0, 0, 0, 1],
[0, 0, 0, 1, 1]
]
},
east: {
dir: [1, 0, 0],
u0: [0, 0, 0],
v0: [0, 0, 1],
u1: [0, 0, 1],
v1: [0, 1, 1],
corners: [
[1, 1, 1, 0, 0],
[1, 0, 1, 0, 1],
[1, 1, 0, 1, 0],
[1, 0, 0, 1, 1]
]
},
west: {
dir: [-1, 0, 0],
u0: [1, 0, 1],
v0: [0, 0, 1],
u1: [1, 0, 2],
v1: [0, 1, 1],
corners: [
[0, 1, 0, 0, 0],
[0, 0, 0, 0, 1],
[0, 1, 1, 1, 0],
[0, 0, 1, 1, 1]
]
},
north: {
dir: [0, 0, -1],
u0: [0, 0, 1],
v0: [0, 0, 1],
u1: [1, 0, 1],
v1: [0, 1, 1],
corners: [
[1, 0, 0, 0, 1],
[0, 0, 0, 1, 1],
[1, 1, 0, 0, 0],
[0, 1, 0, 1, 0]
]
},
south: {
dir: [0, 0, 1],
u0: [1, 0, 2],
v0: [0, 0, 1],
u1: [2, 0, 2],
v1: [0, 1, 1],
corners: [
[0, 0, 1, 0, 1],
[1, 0, 1, 1, 1],
[0, 1, 1, 0, 0],
[1, 1, 1, 1, 0]
]
}
}
function dot (a: number[], b: number[]): number {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
function addCube (
attr: GeoData,
boneId: number,
bone: THREE.Bone,
cube: JsonCube,
sameTextureForAllFaces = false,
texWidth = 64,
texHeight = 64,
mirror = false,
errors: string[] = []
): void {
const cubeRotation = new THREE.Euler(0, 0, 0)
if (cube.rotation) {
cubeRotation.x = -cube.rotation[0] * Math.PI / 180
cubeRotation.y = -cube.rotation[1] * Math.PI / 180
cubeRotation.z = -cube.rotation[2] * Math.PI / 180
}
for (const { dir, corners, u0, v0, u1, v1 } of Object.values(elemFaces)) {
const ndx = Math.floor(attr.positions.length / 3)
const eastOrWest = dir[0] !== 0
const faceUvs: number[] = []
for (const pos of corners) {
let u: number
let v: number
if (sameTextureForAllFaces) {
u = (cube.uv[0] + pos[3] * cube.size[0]) / texWidth
v = (cube.uv[1] + pos[4] * cube.size[1]) / texHeight
} else {
u = (cube.uv[0] + dot(pos[3] ? u1 : u0, cube.size)) / texWidth
v = (cube.uv[1] + dot(pos[4] ? v1 : v0, cube.size)) / texHeight
}
// if (isNaN(u) || isNaN(v)) {
// errors.push(`NaN u: ${u}, v: ${v}`)
// continue
// }
// if (u < 0 || u > 1 || v < 0 || v > 1) {
// errors.push(`u: ${u}, v: ${v} out of range`)
// continue
// }
const posX = eastOrWest && mirror ? pos[0] ^ 1 : pos[0]
const posY = pos[1]
const posZ = eastOrWest && mirror ? pos[2] ^ 1 : pos[2]
const inflate = cube.inflate ?? 0
let vecPos = new THREE.Vector3(
cube.origin[0] + posX * cube.size[0] + (posX ? inflate : -inflate),
cube.origin[1] + posY * cube.size[1] + (posY ? inflate : -inflate),
cube.origin[2] + posZ * cube.size[2] + (posZ ? inflate : -inflate)
)
vecPos = vecPos.applyEuler(cubeRotation)
vecPos = vecPos.sub(bone.position)
vecPos = vecPos.applyEuler(bone.rotation)
vecPos = vecPos.add(bone.position)
attr.positions.push(vecPos.x, vecPos.y, vecPos.z)
attr.normals.push(dir[0], dir[1], dir[2])
faceUvs.push(u, v)
attr.skinIndices.push(boneId, 0, 0, 0)
attr.skinWeights.push(1, 0, 0, 0)
}
if (mirror) {
for (let i = 0; i + 1 < corners.length; i += 2) {
const faceIndex = i * 2
const tempFaceUvs = faceUvs.slice(faceIndex, faceIndex + 4)
faceUvs[faceIndex] = tempFaceUvs[2]
faceUvs[faceIndex + 1] = tempFaceUvs[eastOrWest ? 1 : 3]
faceUvs[faceIndex + 2] = tempFaceUvs[0]
faceUvs[faceIndex + 3] = tempFaceUvs[eastOrWest ? 3 : 1]
}
}
attr.uvs.push(...faceUvs)
attr.indices.push(ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3)
}
}
export function getMesh (
worldRenderer: WorldRendererThree | undefined,
texture: string,
jsonModel: JsonModel,
overrides: EntityOverrides = {},
debugFlags: EntityDebugFlags = {}
): THREE.SkinnedMesh {
let textureWidth = jsonModel.texturewidth ?? 64
let textureHeight = jsonModel.textureheight ?? 64
let textureOffset: number[] | undefined
const useBlockTexture = texture.startsWith('block:')
const blocksTexture = worldRenderer?.material.map
if (useBlockTexture) {
if (!worldRenderer) throw new Error('worldRenderer is required for block textures')
const blockName = texture.slice(6)
const textureInfo = worldRenderer.resourcesManager.currentResources!.blocksAtlasParser.getTextureInfo(blockName)
if (textureInfo) {
textureWidth = blocksTexture?.image.width ?? textureWidth
textureHeight = blocksTexture?.image.height ?? textureHeight
textureOffset = [textureInfo.u, textureInfo.v]
} else {
console.error(`Unknown block ${blockName}`)
}
}
const bones: Record<string, THREE.Bone> = {}
const geoData: GeoData = {
positions: [],
normals: [],
uvs: [],
indices: [],
skinIndices: [],
skinWeights: []
}
let i = 0
for (const jsonBone of jsonModel.bones) {
const bone = new THREE.Bone()
if (jsonBone.pivot) {
bone.position.x = jsonBone.pivot[0]
bone.position.y = jsonBone.pivot[1]
bone.position.z = jsonBone.pivot[2]
}
if (jsonBone.bind_pose_rotation) {
bone.rotation.x = -jsonBone.bind_pose_rotation[0] * Math.PI / 180
bone.rotation.y = -jsonBone.bind_pose_rotation[1] * Math.PI / 180
bone.rotation.z = -jsonBone.bind_pose_rotation[2] * Math.PI / 180
} else if (jsonBone.rotation) {
bone.rotation.x = -jsonBone.rotation[0] * Math.PI / 180
bone.rotation.y = -jsonBone.rotation[1] * Math.PI / 180
bone.rotation.z = -jsonBone.rotation[2] * Math.PI / 180
}
if (overrides.rotation?.[jsonBone.name]) {
bone.rotation.x -= (overrides.rotation[jsonBone.name].x ?? 0) * Math.PI / 180
bone.rotation.y -= (overrides.rotation[jsonBone.name].y ?? 0) * Math.PI / 180
bone.rotation.z -= (overrides.rotation[jsonBone.name].z ?? 0) * Math.PI / 180
}
bone.name = `bone_${jsonBone.name}`
bones[jsonBone.name] = bone
if (jsonBone.cubes) {
for (const cube of jsonBone.cubes) {
const errors: string[] = []
addCube(geoData, i, bone, cube, useBlockTexture, textureWidth, textureHeight, jsonBone.mirror, errors)
if (errors.length) {
debugFlags.errors ??= []
debugFlags.errors.push(...errors.map(error => `Bone ${jsonBone.name}: ${error}`))
}
}
}
i++
}
const rootBones: THREE.Object3D[] = []
for (const jsonBone of jsonModel.bones) {
if (jsonBone.parent && bones[jsonBone.parent]) {
bones[jsonBone.parent].add(bones[jsonBone.name])
} else {
rootBones.push(bones[jsonBone.name])
}
}
const skeleton = new THREE.Skeleton(Object.values(bones))
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.Float32BufferAttribute(geoData.positions, 3))
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(geoData.normals, 3))
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(geoData.uvs, 2))
geometry.setAttribute('skinIndex', new THREE.Uint16BufferAttribute(geoData.skinIndices, 4))
geometry.setAttribute('skinWeight', new THREE.Float32BufferAttribute(geoData.skinWeights, 4))
geometry.setIndex(geoData.indices)
const material = new THREE.MeshLambertMaterial({ transparent: true, alphaTest: 0.1 })
const mesh = new THREE.SkinnedMesh(geometry, material)
mesh.add(...rootBones)
mesh.bind(skeleton)
mesh.scale.set(1 / 16, 1 / 16, 1 / 16)
if (textureOffset) {
// todo(memory) dont clone
const loadedTexture = blocksTexture!.clone()
loadedTexture.offset.set(textureOffset[0], textureOffset[1])
loadedTexture.needsUpdate = true
material.map = loadedTexture
} else {
void loadTexture(texture, loadedTexture => {
if (material.map) {
// texture is already loaded
return
}
loadedTexture.magFilter = THREE.NearestFilter
loadedTexture.minFilter = THREE.NearestFilter
loadedTexture.flipY = false
loadedTexture.wrapS = THREE.RepeatWrapping
loadedTexture.wrapT = THREE.RepeatWrapping
material.map = loadedTexture
}, () => {
// This callback runs after the texture is fully loaded
const actualWidth = material.map!.image.width
if (actualWidth && textureWidth !== actualWidth) {
material.map!.repeat.x = textureWidth / actualWidth
}
const actualHeight = material.map!.image.height
if (actualHeight && textureHeight !== actualHeight) {
material.map!.repeat.y = textureHeight / actualHeight
}
material.needsUpdate = true
})
}
return mesh
}
export const rendererSpecialHandled = ['item_frame', 'item', 'player']
type EntityMapping = {
pattern: string | RegExp
target: string
}
const temporaryMappings: EntityMapping[] = [
// Exact matches
{ pattern: 'furnace_minecart', target: 'minecart' },
{ pattern: 'spawner_minecart', target: 'minecart' },
{ pattern: 'chest_minecart', target: 'minecart' },
{ pattern: 'hopper_minecart', target: 'minecart' },
{ pattern: 'command_block_minecart', target: 'minecart' },
{ pattern: 'tnt_minecart', target: 'minecart' },
{ pattern: 'glow_item_frame', target: 'item_frame' },
{ pattern: 'glow_squid', target: 'squid' },
{ pattern: 'trader_llama', target: 'llama' },
{ pattern: 'chest_boat', target: 'boat' },
{ pattern: 'spectral_arrow', target: 'arrow' },
{ pattern: 'husk', target: 'zombie' },
{ pattern: 'zombie_horse', target: 'horse' },
{ pattern: 'donkey', target: 'horse' },
{ pattern: 'skeleton_horse', target: 'horse' },
{ pattern: 'mule', target: 'horse' },
{ pattern: 'ocelot', target: 'cat' },
// Regex patterns
{ pattern: /_minecraft$/, target: 'minecraft' },
{ pattern: /_boat$/, target: 'boat' },
{ pattern: /_raft$/, target: 'boat' },
{ pattern: /_horse$/, target: 'horse' },
{ pattern: /_zombie$/, target: 'zombie' },
{ pattern: /_arrow$/, target: 'zombie' },
]
function getEntityMapping (type: string): string | undefined {
for (const mapping of temporaryMappings) {
if (typeof mapping.pattern === 'string') {
if (mapping.pattern === type) return mapping.target
} else if (mapping.pattern.test(type)) { return mapping.target }
}
return undefined
}
const getEntity = (name: string) => {
return entities[name]
}
const scaleEntity: Record<string, number> = {
zombie: 1.85,
husk: 1.85,
arrow: 0.0025
}
const offsetEntity: Record<string, Vec3> = {
zombie: new Vec3(0, 1, 0),
husk: new Vec3(0, 1, 0),
boat: new Vec3(0, -1, 0),
arrow: new Vec3(0, -0.9, 0)
}
interface EntityGeometry {
geometry: Array<{
name: string;
[key: string]: any;
}>;
}
export type EntityDebugFlags = {
type?: 'obj' | 'bedrock'
tempMap?: string
textureMap?: boolean
errors?: string[]
isHardcodedTexture?: boolean
}
export class EntityMesh {
mesh: THREE.Object3D
constructor (
version: string,
type: string,
worldRenderer?: WorldRendererThree,
overrides: EntityOverrides = {},
debugFlags: EntityDebugFlags = {}
) {
const originalType = type
const mappedValue = getEntityMapping(type)
if (mappedValue) {
type = mappedValue
debugFlags.tempMap = mappedValue
}
if (externalModels[type]) {
const objLoader = new OBJLoader()
const texturePathMap = {
'zombie_horse': `textures/${version}/entity/horse/horse_zombie.png`,
'husk': huskPng,
'skeleton_horse': `textures/${version}/entity/horse/horse_skeleton.png`,
'donkey': `textures/${version}/entity/horse/donkey.png`,
'mule': `textures/${version}/entity/horse/mule.png`,
'ocelot': `textures/${version}/entity/cat/ocelot.png`,
'arrow': arrowTexture,
'spectral_arrow': spectralArrowTexture,
'tipped_arrow': tippedArrowTexture
}
const tempTextureMap = texturePathMap[originalType] || texturePathMap[type]
if (tempTextureMap) {
debugFlags.textureMap = true
}
const texturePath = tempTextureMap || externalTexturesJson[type]
if (externalTexturesJson[type]) {
debugFlags.isHardcodedTexture = true
}
if (!texturePath) throw new Error(`No texture for ${type}`)
const texture = new THREE.TextureLoader().load(texturePath)
texture.minFilter = THREE.NearestFilter
texture.magFilter = THREE.NearestFilter
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
alphaTest: 0.1
})
const obj = objLoader.parse(externalModels[type])
const scale = scaleEntity[originalType] || scaleEntity[type]
if (scale) obj.scale.set(scale, scale, scale)
const offset = offsetEntity[originalType]
if (offset) obj.position.set(offset.x, offset.y, offset.z)
obj.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = material
// todo
if (child.name === 'Head layer') child.visible = false
if (child.name === 'Head' && overrides.rotation?.head) { // todo
child.rotation.x -= (overrides.rotation.head.x ?? 0) * Math.PI / 180
child.rotation.y -= (overrides.rotation.head.y ?? 0) * Math.PI / 180
child.rotation.z -= (overrides.rotation.head.z ?? 0) * Math.PI / 180
}
}
})
this.mesh = obj
debugFlags.type = 'obj'
return
}
if (originalType === 'arrow') {
// overrides.textures = {
// 'default': testArrow,
// ...overrides.textures,
// }
}
const e = getEntity(type)
if (!e) {
// if (knownNotHandled.includes(type)) return
// throw new Error(`Unknown entity ${type}`)
return
}
this.mesh = new THREE.Object3D()
for (const [name, jsonModel] of Object.entries(e.geometry)) {
const texture = overrides.textures?.[name] ?? e.textures[name]
if (!texture) continue
// console.log(JSON.stringify(jsonModel, null, 2))
const mesh = getMesh(worldRenderer,
texture.endsWith('.png') || texture.startsWith('data:image/') || texture.startsWith('block:')
? texture : texture + '.png',
jsonModel,
overrides,
debugFlags)
mesh.name = `geometry_${name}`
this.mesh.add(mesh)
const skeletonHelper = new THREE.SkeletonHelper(mesh)
//@ts-expect-error
skeletonHelper.material.linewidth = 2
skeletonHelper.visible = false
this.mesh.add(skeletonHelper)
}
debugFlags.type = 'bedrock'
}
static getStaticData (name: string): { boneNames: string[] } {
name = getEntityMapping(name) || name
if (externalModels[name]) {
return {
boneNames: [] // todo
}
}
const e = getEntity(name) as EntityGeometry
if (!e) throw new Error(`Unknown entity ${name}`)
return {
boneNames: Object.values(e.geometry).flatMap(x => x.name)
}
}
}
window.EntityMesh = EntityMesh