use glTF animations

This commit is contained in:
2025-01-24 12:19:42 +09:00
parent 52a0da10fe
commit de3c175914
3 changed files with 73 additions and 40 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+71 -38
View File
@@ -23,20 +23,9 @@ camera.updateProjectionMatrix()
camera.layers.enable(1) camera.layers.enable(1)
camera.layers.enable(2) camera.layers.enable(2)
const gltfLoader = new GLTFLoader()
const addTo = function addTo(scene) {
return function addToScene(gltf) {
const model = gltf.scene
const scale = 2
model.scale.set(scale, scale, scale)
scene.add(model)
}
}
const projectileMaterial = new THREE.MeshToonMaterial({ color: 0xcccccc }) const projectileMaterial = new THREE.MeshToonMaterial({ color: 0xcccccc })
const terrainMaterial = new THREE.MeshToonMaterial({ color: 0x5c4033 }) const terrainMaterial = new THREE.MeshToonMaterial({ color: 0x5c4033 })
const passableTerrainMaterial = new THREE.MeshToonMaterial({ color: 0x228822, transparent: true, opacity: 0.65 }) const passableTerrainMaterial = new THREE.MeshToonMaterial({ color: 0x228822, transparent: true, opacity: 0.65 })
// const bboxMaterial = new THREE.MeshToonMaterial({ color: 0xffd700, transparent: true, opacity: 0.2 })
const opacity = 0.3 const opacity = 0.3
const teamMaterials = { const teamMaterials = {
blue: new THREE.MeshToonMaterial({ color: 0x4444ff }), blue: new THREE.MeshToonMaterial({ color: 0x4444ff }),
@@ -58,17 +47,50 @@ minimapRenderer.setSize(300, 300)
minimapRenderer.setAnimationLoop(minimapRender) minimapRenderer.setAnimationLoop(minimapRender)
minimapCamera.position.set(10, 10, 10) minimapCamera.position.set(10, 10, 10)
const animationActions = {}
const entities = {} const entities = {}
const projectiles = {} const gltf = {}
const mixers = {}
const positionTweens = {} const positionTweens = {}
const projectiles = {}
const rotationTweens = {} const rotationTweens = {}
const terrains = {} const terrains = {}
var state = { abilities: [], entities: [], terrains: [], projectiles: [] } var state = { abilities: [], entities: [], terrains: [], projectiles: [] }
global.animationActions = animationActions
global.entities = entities global.entities = entities
global.gltf = gltf
global.mixers = mixers
global.projectiles = projectiles global.projectiles = projectiles
global.terrains = terrains
global.state = state global.state = state
global.terrains = terrains
const gltfLoader = new GLTFLoader()
const preloadGLTF = function loadTemplate(path) {
gltfLoader.load(path, (loadedGLTF) => gltf[path] = loadedGLTF)
}
const addGLTF = function addGLTF(scene, path, id) {
if (gltf[path] == null) {
setTimeout(() => addGLTF(scene, path, id), 200)
return
}
const scale = 2
const model = gltf[path].scene.clone()
const mixer = new THREE.AnimationMixer(model)
mixers[id] = mixer
animationActions[id] = {}
gltf[path].animations.forEach((it) => {
const animation = mixer.clipAction(it)
animationActions[id][it.name] = animation
})
model.scale.set(scale, scale, scale)
scene.add(model)
}
const geometry = new THREE.PlaneGeometry(0, 0) const geometry = new THREE.PlaneGeometry(0, 0)
const material = new THREE.MeshToonMaterial({ color: 0x115011 }) const material = new THREE.MeshToonMaterial({ color: 0x115011 })
@@ -102,6 +124,7 @@ function render() {
cameraMovement() cameraMovement()
Object.values(positionTweens).forEach((tween) => tween.update()) Object.values(positionTweens).forEach((tween) => tween.update())
Object.values(rotationTweens).forEach((tween) => tween.update()) Object.values(rotationTweens).forEach((tween) => tween.update())
Object.values(mixers).forEach((mixer) => mixer.update(delta))
renderer.render(scene, camera) renderer.render(scene, camera)
stats.end() stats.end()
} }
@@ -311,6 +334,7 @@ function connectWebSocket() {
else { else {
// const entityMaterial = teamMaterials[e.team] // const entityMaterial = teamMaterials[e.team]
// entity = new THREE.Mesh(new THREE.CylinderGeometry(e.visualRadius / 100, e.visualRadius / 100, e.height / 50), entityMaterial) // entity = new THREE.Mesh(new THREE.CylinderGeometry(e.visualRadius / 100, e.visualRadius / 100, e.height / 50), entityMaterial)
// TODO: change entity material
entity = new THREE.Group() entity = new THREE.Group()
entity.rotation.x = Math.PI / 2 entity.rotation.x = Math.PI / 2
entity.scale.set(e.visualRadius / 100, e.height / 100, e.visualRadius / 100) entity.scale.set(e.visualRadius / 100, e.height / 100, e.visualRadius / 100)
@@ -319,9 +343,9 @@ function connectWebSocket() {
entity.position.set(e.position.x / 100, e.position.y / 100, 0) entity.position.set(e.position.x / 100, e.position.y / 100, 0)
scene.add(entity) scene.add(entity)
const hpMargin = 0.4 const hpMargin = 4.8
const maxHp = new THREE.Sprite(new THREE.SpriteMaterial({ color: 0xd03333 })) const maxHp = new THREE.Sprite(new THREE.SpriteMaterial({ color: 0xd03333 }))
maxHp.position.set(0, (e.visualRadius / 100) + hpMargin, 0) maxHp.position.set(0, hpMargin, 0)
maxHp.scale.set(1.5, 0.2, 1) maxHp.scale.set(1.5, 0.2, 1)
maxHp.layers.set(1) maxHp.layers.set(1)
entity.add(maxHp) entity.add(maxHp)
@@ -338,9 +362,9 @@ function connectWebSocket() {
entity.add(teamMarker) entity.add(teamMarker)
const buffMaterial = new THREE.MeshToonMaterial({ color: 0xffff00, transparent: true, opacity: 0.4 }) const buffMaterial = new THREE.MeshToonMaterial({ color: 0xffff00, transparent: true, opacity: 0.4 })
const buffMarker = new THREE.Mesh(new THREE.CylinderGeometry((e.visualRadius + 10) / 100, (e.visualRadius + 10) / 100, 1), buffMaterial) const buffMarkerRadius = 0.6
const buffMarkerSize = 400 const buffMarker = new THREE.Mesh(new THREE.CylinderGeometry(buffMarkerRadius, buffMarkerRadius, 0.3), buffMaterial)
buffMarker.scale.y = e.height / buffMarkerSize buffMarker.position.y = 2
buffMarker.layers.set(1) buffMarker.layers.set(1)
buffMarker.visible = false buffMarker.visible = false
entity.add(buffMarker) entity.add(buffMarker)
@@ -356,7 +380,7 @@ function connectWebSocket() {
castingMarker.position.y = 1 castingMarker.position.y = 1
castingMarker.scale.y = e.height / castingMarkerSize castingMarker.scale.y = e.height / castingMarkerSize
castingMarker.layers.set(1) castingMarker.layers.set(1)
buffMarker.visible = false castingMarker.visible = false
castingMarkerrotationBase.add(castingMarker) castingMarkerrotationBase.add(castingMarker)
const rangeMaterial = teamMaterials['range'] const rangeMaterial = teamMaterials['range']
@@ -369,10 +393,15 @@ function connectWebSocket() {
entity.add(rangeMarker) entity.add(rangeMarker)
const modelRotationBase = new THREE.Object3D() const modelRotationBase = new THREE.Object3D()
modelRotationBase.rotation.y = e.rotation - (Math.PI / 2)
modelRotationBase.layers.set(1) modelRotationBase.layers.set(1)
entity.add(modelRotationBase) entity.add(modelRotationBase)
gltfLoader.load('models/generic-player-placeholder.gltf', addTo(modelRotationBase)) addGLTF(modelRotationBase, 'models/generic-player-placeholder.gltf', e.id)
const animations = animationActions[e.id] ?? {}
if (e.dead) {
animations.dead?.reset().play()
}
entities[e.id] = entity entities[e.id] = entity
} }
@@ -381,18 +410,30 @@ function connectWebSocket() {
entity.children.at(1).visible = !e.dead entity.children.at(1).visible = !e.dead
entity.children.at(2).visible = e.buffs.some((it) => it.id == 'exposed') // TODO: only works for Exposed now entity.children.at(2).visible = e.buffs.some((it) => it.id == 'exposed') // TODO: only works for Exposed now
const animations = animationActions[e.id] ?? {}
const fadeIn = 0.15
if (e.dead) { if (e.dead) {
entity.rotation.x = 0 if (!animations.dead?.isRunning()) {
entity.children.at(5).children.at(0).rotation.y = Math.PI / 2 Object.values(animations).forEach((it) => it.isRunning() && it.reset().fadeOut(fadeIn).play())
animations.dead?.reset().fadeIn(fadeIn).play()
}
}
else if (e.casting != null) {
if (!animations.cast?.isRunning()) {
Object.values(animations).forEach((it) => it.isRunning() && it.reset().fadeOut(fadeIn).play())
animations.cast?.reset().fadeIn(fadeIn).play()
}
} }
else { else {
entity.rotation.x = Math.PI / 2 if (!animations.default?.isRunning()) {
entity.children.at(5).children.at(0).rotation.y = 0 Object.values(animations).forEach((it) => it.isRunning() && it.reset().fadeOut(fadeIn).play())
animations.default?.reset().fadeIn(fadeIn).play()
}
} }
entity.userData.flaggedForRemoval = false entity.userData.flaggedForRemoval = false
entity.children.at(3).rotation.y = e.rotation entity.children.at(3).rotation.y = e.rotation
// entity.children.at(5).rotation.y = e.rotation - (Math.PI / 2)
const oldRotationY = entity.children.at(5).rotation.y const oldRotationY = entity.children.at(5).rotation.y
const newRotationY = e.rotation - (Math.PI / 2) const newRotationY = e.rotation - (Math.PI / 2)
if (Math.abs((oldRotationY - (2 * Math.PI)) - newRotationY) < Math.abs(oldRotationY - newRotationY)) { if (Math.abs((oldRotationY - (2 * Math.PI)) - newRotationY) < Math.abs(oldRotationY - newRotationY)) {
@@ -411,13 +452,15 @@ function connectWebSocket() {
hp.position.x = -(1 - percentageHp) / 2 hp.position.x = -(1 - percentageHp) / 2
entity.children.at(4).visible = e.id == playerId entity.children.at(4).visible = e.id == playerId
entity.children.at(3).children.at(0).visible = e.casting != null // entity.children.at(3).children.at(0).visible = e.casting != null
} }
for (const e of Object.values(entities)) { for (const e of Object.values(entities)) {
if (e.userData.flaggedForRemoval) { if (e.userData.flaggedForRemoval) {
scene.remove(e) scene.remove(e)
delete animationActions[e.userData.id]
delete entities[e.userData.id] delete entities[e.userData.id]
delete mixers[e.userData.id]
delete positionTweens[e.userData.id] delete positionTweens[e.userData.id]
delete rotationTweens[e.userData.id] delete rotationTweens[e.userData.id]
} }
@@ -480,18 +523,6 @@ function connectWebSocket() {
terrain.userData.id = t.id terrain.userData.id = t.id
scene.add(terrain) scene.add(terrain)
terrains[t.id] = terrain terrains[t.id] = terrain
// // TODO: bboxes aren't tracked and can leak memory
// const bboxValues = Object.values(t.bbox)
// if (bboxValues.length >= 4) {
// const width = (bboxValues[1] - bboxValues[3]) / 100
// const height = (bboxValues[0] - bboxValues[2]) / 100
// const bbox = new THREE.Mesh(new THREE.BoxGeometry(width, height, 0.2), bboxMaterial)
// bbox.position.set((bboxValues[3] / 100) + (width / 2), (bboxValues[2] / 100) + (height / 2), 0)
// bbox.layers.set(1)
// scene.add(bbox)
// }
} }
terrain.position.set(t.position.x / 100, t.position.y / 100, 0) terrain.position.set(t.position.x / 100, t.position.y / 100, 0)
@@ -582,6 +613,8 @@ function connectWebSocket() {
} }
window.addEventListener('load', () => { window.addEventListener('load', () => {
preloadGLTF('models/generic-player-placeholder.gltf')
const params = Object.fromEntries(new URLSearchParams(window.location.search).entries()) const params = Object.fromEntries(new URLSearchParams(window.location.search).entries())
playerId = params.id playerId = params.id
if (playerId == null) { if (playerId == null) {