feat: Display holding block (experimental setting) (#190)
This commit is contained in:
parent
bbd01d9682
commit
ee966395c6
15 changed files with 489 additions and 57 deletions
1
experiments/three.html
Normal file
1
experiments/three.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<script type="module" src="three.ts"></script>
|
||||
101
experiments/three.ts
Normal file
101
experiments/three.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import * as THREE from 'three'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
import * as THREE from 'three';
|
||||
import Jimp from 'jimp';
|
||||
|
||||
const scene = new THREE.Scene()
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||
camera.position.set(0, 0, 5)
|
||||
const renderer = new THREE.WebGLRenderer()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
const controls = new OrbitControls(camera, renderer.domElement)
|
||||
|
||||
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
||||
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
|
||||
const cube = new THREE.Mesh(geometry, material)
|
||||
cube.position.set(0.5, 0.5, 0.5);
|
||||
const group = new THREE.Group()
|
||||
group.add(cube)
|
||||
group.position.set(-0.5, -0.5, -0.5);
|
||||
const outerGroup = new THREE.Group()
|
||||
outerGroup.add(group)
|
||||
outerGroup.scale.set(0.2, 0.2, 0.2)
|
||||
outerGroup.position.set(1, 1, 0)
|
||||
scene.add(outerGroup)
|
||||
|
||||
// const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 }))
|
||||
// mesh.position.set(0.5, 1, 0.5)
|
||||
// const group = new THREE.Group()
|
||||
// group.add(mesh)
|
||||
// group.position.set(-0.5, -1, -0.5)
|
||||
// const outerGroup = new THREE.Group()
|
||||
// outerGroup.add(group)
|
||||
// // outerGroup.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z)
|
||||
// scene.add(outerGroup)
|
||||
|
||||
new tweenJs.Tween(group.rotation).to({ z: THREE.MathUtils.degToRad(90) }, 1000).yoyo(true).repeat(Infinity).start()
|
||||
|
||||
const tweenGroup = new tweenJs.Group()
|
||||
function animate () {
|
||||
tweenGroup.update()
|
||||
requestAnimationFrame(animate)
|
||||
// cube.rotation.x += 0.01
|
||||
// cube.rotation.y += 0.01
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
animate()
|
||||
|
||||
// let animation
|
||||
|
||||
window.animate = () => {
|
||||
// new Tween.Tween(group.position).to({ y: group.position.y - 1}, 1000 * 0.35/2).yoyo(true).repeat(1).start()
|
||||
new tweenJs.Tween(group.rotation, tweenGroup).to({ z: THREE.MathUtils.degToRad(90) }, 1000 * 0.35 / 2).yoyo(true).repeat(Infinity).start().onRepeat(() => {
|
||||
console.log('done')
|
||||
})
|
||||
}
|
||||
|
||||
window.stop = () => {
|
||||
tweenGroup.removeAll()
|
||||
}
|
||||
|
||||
|
||||
function createGeometryFromImage() {
|
||||
return new Promise<THREE.ShapeGeometry>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABEElEQVQ4jWNkIAPw2Zv9J0cfXPOSvx/+L/n74T+HqsJ/JlI1T9u3i6H91B7ybdY+vgZuO1majV+fppFmPnuz/+ihy2dv9t/49Wm8mlECkV1FHh5FfPZm/1XXTGX4cechA4eKPMNVq1CGH7cfMBJ0rlxX+X8OVYX/xq9P/5frKifoZ0Z0AwS8HRkYGBgYvt+8xyDXUUbQZgwJPnuz/+wq8gw/7zxk+PXsFUFno0h6mon+l5fgZFhwnYmBTUqMgYGBgaAhLMiaHQyFGOZvf8Lw49FXRgYGhv8MDAwwg/7jMoQFFury/C8Y5m9/wnADohnZVryJhoWBARJ9Cw69gtmMAgiFAcuvZ68Yfj17hU8NXgAATdKfkzbQhBEAAAAASUVORK5CYII='
|
||||
console.log('img.complete', img.complete)
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const context = canvas.getContext('2d');
|
||||
context.drawImage(img, 0, 0, img.width, img.height);
|
||||
const imgData = context.getImageData(0, 0, img.width, img.height);
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
for (let y = 0; y < img.height; y++) {
|
||||
for (let x = 0; x < img.width; x++) {
|
||||
const index = (y * img.width + x) * 4;
|
||||
const alpha = imgData.data[index + 3];
|
||||
if (alpha !== 0) {
|
||||
shape.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const geometry = new THREE.ShapeGeometry(shape);
|
||||
resolve(geometry);
|
||||
};
|
||||
img.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
// Usage:
|
||||
const shapeGeomtry = createGeometryFromImage().then(geometry => {
|
||||
const material = new THREE.MeshBasicMaterial({ color: 0xffffff });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
scene.add(mesh);
|
||||
})
|
||||
|
|
@ -282,6 +282,46 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
getItemMesh(item) {
|
||||
const textureUv = this.getItemUv?.(item.itemId ?? item.blockId)
|
||||
if (textureUv) {
|
||||
// todo use geometry buffer uv instead!
|
||||
const { u, v, size, su, sv, texture } = textureUv
|
||||
const itemsTexture = texture.clone()
|
||||
itemsTexture.flipY = true
|
||||
itemsTexture.offset.set(u, 1 - v - (sv ?? size))
|
||||
itemsTexture.repeat.set(su ?? size, sv ?? size)
|
||||
itemsTexture.needsUpdate = true
|
||||
itemsTexture.magFilter = THREE.NearestFilter
|
||||
itemsTexture.minFilter = THREE.NearestFilter
|
||||
const itemsTextureFlipped = itemsTexture.clone()
|
||||
itemsTextureFlipped.repeat.x *= -1
|
||||
itemsTextureFlipped.needsUpdate = true
|
||||
itemsTextureFlipped.offset.set(u + (su ?? size), 1 - v - (sv ?? size))
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
map: itemsTexture,
|
||||
transparent: true,
|
||||
alphaTest: 0.1,
|
||||
})
|
||||
const materialFlipped = new THREE.MeshStandardMaterial({
|
||||
map: itemsTextureFlipped,
|
||||
transparent: true,
|
||||
alphaTest: 0.1,
|
||||
})
|
||||
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0), [
|
||||
// top left and right bottom are black box materials others are transparent
|
||||
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
||||
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
||||
material, materialFlipped,
|
||||
])
|
||||
return {
|
||||
mesh,
|
||||
itemsTexture,
|
||||
itemsTextureFlipped,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update(/** @type {import('prismarine-entity').Entity & {delete?, pos}} */entity, overrides) {
|
||||
let isPlayerModel = entity.name === 'player'
|
||||
if (entity.name === 'zombie' || entity.name === 'zombie_villager' || entity.name === 'husk') {
|
||||
|
|
@ -296,52 +336,23 @@ export class Entities extends EventEmitter {
|
|||
//@ts-expect-error
|
||||
const item = entity.metadata?.find(m => typeof m === 'object' && m?.itemCount)
|
||||
if (item) {
|
||||
const textureUv = this.getItemUv?.(item.itemId ?? item.blockId)
|
||||
if (textureUv) {
|
||||
// todo use geometry buffer uv instead!
|
||||
const { u, v, size, su, sv, texture } = textureUv
|
||||
const itemsTexture = texture.clone()
|
||||
itemsTexture.flipY = true
|
||||
itemsTexture.offset.set(u, 1 - v - (sv ?? size))
|
||||
itemsTexture.repeat.set(su ?? size, sv ?? size)
|
||||
itemsTexture.needsUpdate = true
|
||||
itemsTexture.magFilter = THREE.NearestFilter
|
||||
itemsTexture.minFilter = THREE.NearestFilter
|
||||
const itemsTextureFlipped = itemsTexture.clone()
|
||||
itemsTextureFlipped.repeat.x *= -1
|
||||
itemsTextureFlipped.needsUpdate = true
|
||||
itemsTextureFlipped.offset.set(u + (su ?? size), 1 - v - (sv ?? size))
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
map: itemsTexture,
|
||||
transparent: true,
|
||||
alphaTest: 0.1,
|
||||
})
|
||||
const materialFlipped = new THREE.MeshStandardMaterial({
|
||||
map: itemsTextureFlipped,
|
||||
transparent: true,
|
||||
alphaTest: 0.1,
|
||||
})
|
||||
mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0), [
|
||||
// top left and right bottom are black box materials others are transparent
|
||||
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
||||
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
||||
material, materialFlipped,
|
||||
])
|
||||
mesh.scale.set(0.5, 0.5, 0.5)
|
||||
mesh.position.set(0, 0.2, 0)
|
||||
const object = this.getItemMesh(item)
|
||||
if (object) {
|
||||
object.scale.set(0.5, 0.5, 0.5)
|
||||
object.position.set(0, 0.2, 0)
|
||||
// set faces
|
||||
// mesh.position.set(targetPos.x + 0.5 + 2, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
// viewer.scene.add(mesh)
|
||||
const clock = new THREE.Clock()
|
||||
mesh.onBeforeRender = () => {
|
||||
object.onBeforeRender = () => {
|
||||
const delta = clock.getDelta()
|
||||
mesh.rotation.y += delta
|
||||
object.rotation.y += delta
|
||||
}
|
||||
//@ts-expect-error
|
||||
group.additionalCleanup = () => {
|
||||
// important: avoid texture memory leak and gpu slowdown
|
||||
itemsTexture.dispose()
|
||||
itemsTextureFlipped.dispose()
|
||||
object.itemsTexture.dispose()
|
||||
object.itemsTextureFlipped.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
207
prismarine-viewer/viewer/lib/holdingBlock.ts
Normal file
207
prismarine-viewer/viewer/lib/holdingBlock.ts
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import * as THREE from 'three'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer'
|
||||
|
||||
export type HandItemBlock = {
|
||||
name
|
||||
properties
|
||||
}
|
||||
|
||||
export default class HoldingBlock {
|
||||
holdingBlock: THREE.Object3D | undefined = undefined
|
||||
swingAnimation: tweenJs.Group | undefined = undefined
|
||||
blockSwapAnimation: {
|
||||
tween: tweenJs.Group
|
||||
hidden: boolean
|
||||
} | undefined = undefined
|
||||
cameraGroup = new THREE.Mesh()
|
||||
objectOuterGroup = new THREE.Group()
|
||||
objectInnerGroup = new THREE.Group()
|
||||
camera: THREE.Group | THREE.PerspectiveCamera
|
||||
stopUpdate = false
|
||||
lastHeldItem: HandItemBlock | undefined
|
||||
toBeRenderedItem: HandItemBlock | undefined
|
||||
isSwinging = false
|
||||
nextIterStopCallbacks: Array<() => void> | undefined
|
||||
|
||||
constructor (public scene: THREE.Scene) {
|
||||
this.initCameraGroup()
|
||||
}
|
||||
|
||||
initCameraGroup () {
|
||||
this.cameraGroup = new THREE.Mesh()
|
||||
this.scene.add(this.cameraGroup)
|
||||
}
|
||||
|
||||
startSwing () {
|
||||
this.nextIterStopCallbacks = undefined // forget about cancelling
|
||||
if (this.isSwinging) return
|
||||
this.swingAnimation = new tweenJs.Group()
|
||||
this.isSwinging = true
|
||||
const cube = this.cameraGroup.children[0]
|
||||
if (cube) {
|
||||
// const DURATION = 1000 * 0.35 / 2
|
||||
const DURATION = 1000 * 0.35 / 3
|
||||
// const DURATION = 1000
|
||||
const initialPos = {
|
||||
x: this.objectInnerGroup.position.x,
|
||||
y: this.objectInnerGroup.position.y,
|
||||
z: this.objectInnerGroup.position.z
|
||||
}
|
||||
const initialRot = {
|
||||
x: this.objectInnerGroup.rotation.x,
|
||||
y: this.objectInnerGroup.rotation.y,
|
||||
z: this.objectInnerGroup.rotation.z
|
||||
}
|
||||
const mainAnim = new tweenJs.Tween(this.objectInnerGroup.position, this.swingAnimation).to({ y: this.objectInnerGroup.position.y - this.objectInnerGroup.scale.y / 2 }, DURATION).yoyo(true).repeat(Infinity).start()
|
||||
let i = 0
|
||||
mainAnim.onRepeat(() => {
|
||||
i++
|
||||
if (this.nextIterStopCallbacks && i % 2 === 0) {
|
||||
for (const callback of this.nextIterStopCallbacks) {
|
||||
callback()
|
||||
}
|
||||
this.nextIterStopCallbacks = undefined
|
||||
this.isSwinging = false
|
||||
this.swingAnimation!.removeAll()
|
||||
this.swingAnimation = undefined
|
||||
// todo refactor to be more generic for animations
|
||||
this.objectInnerGroup.position.set(initialPos.x, initialPos.y, initialPos.z)
|
||||
// this.objectInnerGroup.rotation.set(initialRot.x, initialRot.y, initialRot.z)
|
||||
Object.assign(this.objectInnerGroup.rotation, initialRot)
|
||||
}
|
||||
})
|
||||
|
||||
new tweenJs.Tween(this.objectInnerGroup.rotation, this.swingAnimation).to({ z: THREE.MathUtils.degToRad(90) }, DURATION).yoyo(true).repeat(Infinity).start()
|
||||
new tweenJs.Tween(this.objectInnerGroup.rotation, this.swingAnimation).to({ x: -THREE.MathUtils.degToRad(90) }, DURATION).yoyo(true).repeat(Infinity).start()
|
||||
}
|
||||
}
|
||||
|
||||
async stopSwing () {
|
||||
if (!this.isSwinging) return
|
||||
// might never resolve!
|
||||
/* return */void new Promise<void>((resolve) => {
|
||||
this.nextIterStopCallbacks ??= []
|
||||
this.nextIterStopCallbacks.push(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
update (camera: typeof this.camera) {
|
||||
this.camera = camera
|
||||
this.swingAnimation?.update()
|
||||
this.blockSwapAnimation?.tween.update()
|
||||
this.updateCameraGroup()
|
||||
}
|
||||
|
||||
// worldTest () {
|
||||
// const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhongMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 }))
|
||||
// mesh.position.set(0.5, 0.5, 0.5)
|
||||
// const group = new THREE.Group()
|
||||
// group.add(mesh)
|
||||
// group.position.set(-0.5, -0.5, -0.5)
|
||||
// const outerGroup = new THREE.Group()
|
||||
// outerGroup.add(group)
|
||||
// outerGroup.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z)
|
||||
// this.scene.add(outerGroup)
|
||||
|
||||
// new tweenJs.Tween(group.rotation).to({ z: THREE.MathUtils.degToRad(90) }, 1000).yoyo(true).repeat(Infinity).start()
|
||||
// }
|
||||
|
||||
async playBlockSwapAnimation () {
|
||||
// if (this.blockSwapAnimation) return
|
||||
this.blockSwapAnimation ??= {
|
||||
tween: new tweenJs.Group(),
|
||||
hidden: false
|
||||
}
|
||||
const DURATION = 1000 * 0.35 / 2
|
||||
const tween = new tweenJs.Tween(this.objectInnerGroup.position, this.blockSwapAnimation.tween).to({
|
||||
y: this.objectInnerGroup.position.y + (this.objectInnerGroup.scale.y * 1.5 * (this.blockSwapAnimation.hidden ? 1 : -1))
|
||||
}, DURATION).start()
|
||||
return new Promise<void>((resolve) => {
|
||||
tween.onComplete(() => {
|
||||
if (this.blockSwapAnimation!.hidden) {
|
||||
this.blockSwapAnimation = undefined
|
||||
} else {
|
||||
this.blockSwapAnimation!.hidden = !this.blockSwapAnimation!.hidden
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
isDifferentItem (block: HandItemBlock | undefined) {
|
||||
return this.lastHeldItem && (this.lastHeldItem.name !== block?.name || JSON.stringify(this.lastHeldItem.properties) !== JSON.stringify(block?.properties ?? '{}'))
|
||||
}
|
||||
|
||||
updateCameraGroup () {
|
||||
if (this.stopUpdate) return
|
||||
const { camera } = this
|
||||
this.cameraGroup.position.copy(camera.position)
|
||||
this.cameraGroup.rotation.copy(camera.rotation)
|
||||
|
||||
const viewerSize = viewer.renderer.getSize(new THREE.Vector2())
|
||||
// const x = window.x ?? 0.25 * viewerSize.width / viewerSize.height
|
||||
// const x = 0 * viewerSize.width / viewerSize.height
|
||||
const x = 0.2 * viewerSize.width / viewerSize.height
|
||||
this.objectOuterGroup.position.set(x, -0.3, -0.45)
|
||||
}
|
||||
|
||||
async initHandObject (material: THREE.Material, blockstatesModels: any, blocksAtlases: any, block?: HandItemBlock) {
|
||||
let animatingCurrent = false
|
||||
if (!this.swingAnimation && !this.blockSwapAnimation && this.isDifferentItem(block)) {
|
||||
animatingCurrent = true
|
||||
await this.playBlockSwapAnimation()
|
||||
this.holdingBlock?.removeFromParent()
|
||||
this.holdingBlock = undefined
|
||||
}
|
||||
this.lastHeldItem = block
|
||||
if (!block) {
|
||||
this.holdingBlock?.removeFromParent()
|
||||
this.holdingBlock = undefined
|
||||
this.swingAnimation = undefined
|
||||
this.blockSwapAnimation = undefined
|
||||
return
|
||||
}
|
||||
const blockProvider = worldBlockProvider(blockstatesModels, blocksAtlases, 'latest')
|
||||
const models = blockProvider.getAllResolvedModels0_1(block, true)
|
||||
const blockInner = getThreeBlockModelGroup(material, models, undefined, 'plains', loadedData)
|
||||
// const { mesh: itemMesh } = viewer.entities.getItemMesh({
|
||||
// itemId: 541,
|
||||
// })!
|
||||
// itemMesh.position.set(0.5, 0.5, 0.5)
|
||||
// const blockInner = itemMesh
|
||||
blockInner.name = 'holdingBlock'
|
||||
const blockOuterGroup = new THREE.Group()
|
||||
blockOuterGroup.add(blockInner)
|
||||
this.holdingBlock = blockInner
|
||||
this.objectInnerGroup = new THREE.Group()
|
||||
this.objectInnerGroup.add(blockOuterGroup)
|
||||
this.objectInnerGroup.position.set(-0.5, -0.5, -0.5)
|
||||
// todo cleanup
|
||||
if (animatingCurrent) {
|
||||
this.objectInnerGroup.position.y -= this.objectInnerGroup.scale.y * 1.5
|
||||
}
|
||||
Object.assign(blockOuterGroup.position, { x: 0.5, y: 0.5, z: 0.5 })
|
||||
|
||||
this.objectOuterGroup = new THREE.Group()
|
||||
this.objectOuterGroup.add(this.objectInnerGroup)
|
||||
|
||||
this.cameraGroup.add(this.objectOuterGroup)
|
||||
const rotation = -45 + -90
|
||||
// const rotation = -45 // should be for item
|
||||
this.holdingBlock.rotation.set(0, THREE.MathUtils.degToRad(rotation), 0, 'ZYX')
|
||||
|
||||
// const scale = window.scale ?? 0.2
|
||||
const scale = 0.2
|
||||
this.objectOuterGroup.scale.set(scale, scale, scale)
|
||||
// this.objectOuterGroup.position.set(x, window.y ?? -0.41, window.z ?? -0.45)
|
||||
// this.objectOuterGroup.position.set(x, 0, -0.45)
|
||||
|
||||
if (animatingCurrent) {
|
||||
await this.playBlockSwapAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -272,3 +272,19 @@ export const renderBlockThree = (...args: Parameters<typeof renderBlockThreeAttr
|
|||
|
||||
return geometry
|
||||
}
|
||||
|
||||
export const getThreeBlockModelGroup = (material: THREE.Material, ...args: Parameters<typeof renderBlockThree>) => {
|
||||
const geometry = renderBlockThree(...args)
|
||||
const mesh = new THREE.Mesh(geometry, material)
|
||||
mesh.position.set(-0.5, -0.5, -0.5)
|
||||
const group = new THREE.Group()
|
||||
group.add(mesh)
|
||||
group.rotation.set(0, -THREE.MathUtils.degToRad(90), 0, 'ZYX')
|
||||
globalThis.mesh = group
|
||||
return group
|
||||
// return new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhongMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 }))
|
||||
}
|
||||
|
||||
export const setBlockPosition = (object: THREE.Object3D, position: { x: number, y: number, z: number }) => {
|
||||
object.position.set(position.x + 0.5, position.y + 0.5, position.z + 0.5)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Entities } from './entities'
|
|||
import { Primitives } from './primitives'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
import { WorldRendererCommon, WorldRendererConfig, defaultWorldRendererConfig } from './worldrendererCommon'
|
||||
import { renderBlockThree } from './mesher/standaloneRenderer'
|
||||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer'
|
||||
|
||||
export class Viewer {
|
||||
scene: THREE.Scene
|
||||
|
|
@ -101,18 +101,32 @@ export class Viewer {
|
|||
}
|
||||
|
||||
demoModel () {
|
||||
//@ts-expect-error
|
||||
const pos = cursorBlockRel(0, 1, 0).position
|
||||
const blockProvider = worldBlockProvider(this.world.blockstatesModels, this.world.blocksAtlases, 'latest')
|
||||
const models = blockProvider.getAllResolvedModels0_1({
|
||||
name: 'item_frame',
|
||||
name: 'furnace',
|
||||
properties: {
|
||||
map: false
|
||||
// map: false
|
||||
}
|
||||
})
|
||||
const geometry = renderBlockThree(models, undefined, 'plains', loadedData)
|
||||
}, true)
|
||||
const { material } = this.world
|
||||
// block material
|
||||
const mesh = new THREE.Mesh(geometry, material)
|
||||
mesh.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z)
|
||||
const mesh = getThreeBlockModelGroup(material, models, undefined, 'plains', loadedData)
|
||||
// mesh.rotation.y = THREE.MathUtils.degToRad(90)
|
||||
setBlockPosition(mesh, pos)
|
||||
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
||||
mesh.add(helper)
|
||||
this.scene.add(mesh)
|
||||
}
|
||||
|
||||
demoItem () {
|
||||
//@ts-expect-error
|
||||
const pos = cursorBlockRel(0, 1, 0).position
|
||||
const { mesh } = this.entities.getItemMesh({
|
||||
itemId: 541,
|
||||
})!
|
||||
mesh.position.set(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5)
|
||||
// mesh.scale.set(0.5, 0.5, 0.5)
|
||||
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
||||
mesh.add(helper)
|
||||
this.scene.add(mesh)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { EventEmitter } from 'events'
|
|||
import { generateSpiralMatrix, ViewRect } from 'flying-squid/dist/utils'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { BotEvents } from 'mineflayer'
|
||||
import { getItemFromBlock } from '../../../src/botUtils'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
|
||||
export type ChunkPosKey = string
|
||||
|
|
@ -20,6 +21,14 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
private eventListeners: Record<string, any> = {}
|
||||
private readonly emitter: WorldDataEmitter
|
||||
keepChunksDistance = 0
|
||||
_handDisplay = false
|
||||
get handDisplay () {
|
||||
return this._handDisplay
|
||||
}
|
||||
set handDisplay (newVal) {
|
||||
this._handDisplay = newVal
|
||||
this.eventListeners.heldItemChanged?.()
|
||||
}
|
||||
|
||||
constructor (public world: typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) {
|
||||
super()
|
||||
|
|
@ -55,7 +64,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
})
|
||||
}
|
||||
|
||||
this.eventListeners[bot.username] = {
|
||||
this.eventListeners = {
|
||||
// 'move': botPosition,
|
||||
entitySpawn (e: any) {
|
||||
emitEntity(e)
|
||||
|
|
@ -70,7 +79,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
this.emitter.emit('entity', { id: e.id, delete: true })
|
||||
},
|
||||
chunkColumnLoad: (pos: Vec3) => {
|
||||
this.loadChunk(pos)
|
||||
void this.loadChunk(pos)
|
||||
},
|
||||
chunkColumnUnload: (pos: Vec3) => {
|
||||
this.unloadChunk(pos)
|
||||
|
|
@ -82,7 +91,24 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
time: () => {
|
||||
this.emitter.emit('time', bot.time.timeOfDay)
|
||||
},
|
||||
heldItemChanged: () => {
|
||||
if (!this.handDisplay) {
|
||||
viewer.world.onHandItemSwitch(undefined)
|
||||
return
|
||||
}
|
||||
const newItem = bot.heldItem
|
||||
if (!newItem) {
|
||||
viewer.world.onHandItemSwitch(undefined)
|
||||
return
|
||||
}
|
||||
const block = loadedData.blocksByName[newItem.name]
|
||||
// todo clean types
|
||||
const blockProperties = block ? new window.PrismarineBlock(block.id, 'void', newItem.metadata).getProperties() : {}
|
||||
viewer.world.onHandItemSwitch({ name: newItem.name, properties: blockProperties })
|
||||
},
|
||||
} satisfies Partial<BotEvents>
|
||||
this.eventListeners.heldItemChanged()
|
||||
|
||||
|
||||
bot._client.on('update_light', ({ chunkX, chunkZ }) => {
|
||||
const chunkPos = new Vec3(chunkX * 16, 0, chunkZ * 16)
|
||||
|
|
@ -105,7 +131,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
this.emitter.emit('listening')
|
||||
}
|
||||
|
||||
for (const [evt, listener] of Object.entries(this.eventListeners[bot.username])) {
|
||||
for (const [evt, listener] of Object.entries(this.eventListeners)) {
|
||||
bot.on(evt as any, listener)
|
||||
}
|
||||
|
||||
|
|
@ -116,10 +142,9 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
}
|
||||
|
||||
removeListenersFromBot (bot: import('mineflayer').Bot) {
|
||||
for (const [evt, listener] of Object.entries(this.eventListeners[bot.username])) {
|
||||
for (const [evt, listener] of Object.entries(this.eventListeners)) {
|
||||
bot.removeListener(evt as any, listener)
|
||||
}
|
||||
delete this.eventListeners[bot.username]
|
||||
}
|
||||
|
||||
async init (pos: Vec3) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { toMajorVersion } from '../../../src/utils'
|
|||
import { buildCleanupDecorator } from './cleanupDecorator'
|
||||
import { defaultMesherConfig } from './mesher/shared'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
import { HandItemBlock } from './holdingBlock'
|
||||
|
||||
function mod (x, n) {
|
||||
return ((x % n) + n) % n
|
||||
|
|
@ -37,6 +38,7 @@ type CustomTexturesData = {
|
|||
|
||||
export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> {
|
||||
worldConfig = { minY: 0, worldHeight: 256 }
|
||||
// todo need to cleanup
|
||||
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
|
||||
|
||||
@worldCleanup()
|
||||
|
|
@ -59,6 +61,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
textureDownloaded (): void
|
||||
}>
|
||||
customTexturesDataUrl = undefined as string | undefined
|
||||
@worldCleanup()
|
||||
currentTextureImage = undefined as any
|
||||
workers: any[] = []
|
||||
viewerPosition?: Vec3
|
||||
|
|
@ -157,6 +160,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
}
|
||||
|
||||
onHandItemSwitch (item: HandItemBlock | undefined): void { }
|
||||
changeHandSwingingState (isAnimationPlaying: boolean): void { }
|
||||
|
||||
abstract handleWorkerMessage (data: WorkerReceive): void
|
||||
|
||||
abstract updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { renderSign } from '../sign-renderer'
|
|||
import { chunkPos, sectionPos } from './simpleUtils'
|
||||
import { WorldRendererCommon, WorldRendererConfig } from './worldrendererCommon'
|
||||
import { disposeObject } from './threeJsUtils'
|
||||
import { renderBlockThree } from './mesher/standaloneRenderer'
|
||||
import HoldingBlock, { HandItemBlock } from './holdingBlock'
|
||||
|
||||
export class WorldRendererThree extends WorldRendererCommon {
|
||||
outputFormat = 'threeJs' as const
|
||||
|
|
@ -19,7 +19,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
signsCache = new Map<string, any>()
|
||||
starField: StarField
|
||||
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
|
||||
cameraGroup = new THREE.Group()
|
||||
holdingBlock: HoldingBlock
|
||||
|
||||
get tilesRendered () {
|
||||
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
|
||||
|
|
@ -28,8 +28,34 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
constructor (public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public config: WorldRendererConfig) {
|
||||
super(config)
|
||||
this.starField = new StarField(scene)
|
||||
// this.initCameraGroup()
|
||||
// this.initHandObject()
|
||||
this.holdingBlock = new HoldingBlock(this.scene)
|
||||
this.onHandItemSwitch({
|
||||
name: 'furnace',
|
||||
properties: {}
|
||||
})
|
||||
|
||||
this.renderUpdateEmitter.on('textureDownloaded', () => {
|
||||
if (this.holdingBlock.toBeRenderedItem) {
|
||||
this.onHandItemSwitch(this.holdingBlock.toBeRenderedItem)
|
||||
this.holdingBlock.toBeRenderedItem = undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onHandItemSwitch (item: HandItemBlock | undefined) {
|
||||
if (!this.currentTextureImage) {
|
||||
this.holdingBlock.toBeRenderedItem = item
|
||||
return
|
||||
}
|
||||
void this.holdingBlock.initHandObject(this.material, this.blockstatesModels, this.blocksAtlases, item)
|
||||
}
|
||||
|
||||
changeHandSwingingState (isAnimationPlaying: boolean) {
|
||||
if (isAnimationPlaying) {
|
||||
this.holdingBlock.startSwing()
|
||||
} else {
|
||||
void this.holdingBlock.stopSwing()
|
||||
}
|
||||
}
|
||||
|
||||
timeUpdated (newTime: number): void {
|
||||
|
|
@ -173,6 +199,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
|
||||
render () {
|
||||
tweenJs.update()
|
||||
this.holdingBlock.update(this.camera)
|
||||
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
|
||||
const cam = this.camera instanceof THREE.Group ? this.camera.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
|
||||
this.renderer.render(this.scene, cam)
|
||||
|
|
|
|||
|
|
@ -87,8 +87,13 @@ export const onGameLoad = (onLoad) => {
|
|||
return
|
||||
}
|
||||
const craftingSlots = bot.inventory.slots.slice(1, 5)
|
||||
const resultingItem = getResultingRecipe(craftingSlots, 2)
|
||||
void bot.creative.setInventorySlot(craftingResultSlot, resultingItem ?? null)
|
||||
try {
|
||||
const resultingItem = getResultingRecipe(craftingSlots, 2)
|
||||
void bot.creative.setInventorySlot(craftingResultSlot, resultingItem ?? null)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
// todo resolve the error! and why would we ever get here on every update?
|
||||
}
|
||||
}) as any)
|
||||
|
||||
bot.on('windowClose', () => {
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ export const guiOptionsScheme: {
|
|||
unit: '',
|
||||
tooltip: 'Additional distance to keep the chunks loading before unloading them by marking them as too far',
|
||||
},
|
||||
handDisplay: {},
|
||||
},
|
||||
],
|
||||
main: [
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ const defaultOptions = {
|
|||
enabledResourcepack: null as string | null,
|
||||
useVersionsTextures: 'latest',
|
||||
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
|
||||
handDisplay: false,
|
||||
|
||||
// antiAliasing: false,
|
||||
|
||||
|
|
|
|||
|
|
@ -75,3 +75,14 @@ export const statsEnd = () => {
|
|||
stats2.end()
|
||||
statsGl.end()
|
||||
}
|
||||
|
||||
window.statsPerSec = {}
|
||||
let statsPerSec = {}
|
||||
window.addStatPerSec = (name) => {
|
||||
statsPerSec[name] ??= 0
|
||||
statsPerSec[name]++
|
||||
}
|
||||
setInterval(() => {
|
||||
window.statsPerSec = statsPerSec
|
||||
statsPerSec = {}
|
||||
}, 1000)
|
||||
|
|
|
|||
|
|
@ -66,11 +66,11 @@ export const watchOptionsAfterViewerInit = () => {
|
|||
|
||||
let viewWatched = false
|
||||
export const watchOptionsAfterWorldViewInit = () => {
|
||||
worldView!.keepChunksDistance = options.keepChunksDistance
|
||||
if (viewWatched) return
|
||||
viewWatched = true
|
||||
watchValue(options, o => {
|
||||
if (!worldView) return
|
||||
worldView.keepChunksDistance = o.keepChunksDistance
|
||||
worldView.handDisplay = o.handDisplay
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -294,6 +294,8 @@ class WorldInteraction {
|
|||
bot.lookAt = oldLookAt
|
||||
}).catch(console.warn)
|
||||
}
|
||||
viewer.world.changeHandSwingingState(true)
|
||||
viewer.world.changeHandSwingingState(false)
|
||||
} else if (!stop) {
|
||||
const offhand = activate ? false : activatableItems(bot.inventory.slots[45]?.name ?? '')
|
||||
bot.activateItem(offhand) // todo offhand
|
||||
|
|
@ -351,11 +353,15 @@ class WorldInteraction {
|
|||
})
|
||||
customEvents.emit('digStart')
|
||||
this.lastDigged = Date.now()
|
||||
viewer.world.changeHandSwingingState(true)
|
||||
} else if (performance.now() - this.lastSwing > 200) {
|
||||
bot.swingArm('right')
|
||||
this.lastSwing = performance.now()
|
||||
}
|
||||
}
|
||||
if (!this.buttons[0] && this.lastButtons[0]) {
|
||||
viewer.world.changeHandSwingingState(false)
|
||||
}
|
||||
this.prevOnGround = onGround
|
||||
|
||||
// Show cursor
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue