UNPKG

skinview3d-blockbench

Version:

SkinView3d animation provider for blockbench bedrock minecraft animations

302 lines (301 loc) 11.2 kB
import { PlayerAnimation } from 'skinview3d'; import { Clock, Euler, Group, MathUtils } from 'three'; import { catmullRom } from './lerp'; import { defaultBonesOverrides, defaultPositions } from './defaults'; /** Provider for bedrock .animation.json files */ export class SkinViewBlockbench extends PlayerAnimation { animations; /** Function called when looped animation loop ends */ onLoopEnd; /** Function call when single-iteration animation ends */ onFinish; /** * Force loop animation, ignoring its settings * (undefined for using loop setting from animation) */ forceLoop; /** Connect cape to body if its not animated */ connectCape; /** Currently playing animation name */ animationName = ''; /** Currently playing animation iteration */ animationIteration = 0; /** Player object */ player; /** Animation progress in milliseconds */ _progress = 0; clock; torsoWrapper; /** Normalize keyframes names by adding explicit lerp mode */ convertKeyframe(input) { if (!input) return undefined; const result = {}; for (const [k, v] of Object.entries(input)) { if (Array.isArray(v)) { result[k] = { post: v, lerp_mode: 'linear' }; } else { result[k] = v; } } return result; } constructor(params) { super(); this.animations = {}; this.onFinish = params.onFinish; this.onLoopEnd = params.onLoopEnd; this.forceLoop = params.forceLoop; this.connectCape = params.connectCape ?? false; this.clock = new Clock(); // Initialize all animations for (const animation_name of Object.keys(params.animation.animations)) { this.processAnimation(params.animation.animations, animation_name, params.bonesOverrides); } // Reset animation this.reset(params.animationName); } /** Reset animation */ reset(animation_name) { const _animation_name = animation_name ?? Object.keys(this.animations).at(0); if (!_animation_name || !(_animation_name in this.animations)) throw new Error('Animation name not specified or no animation found'); this.clock.stop(); this.clock.autoStart = true; this._progress = 0; this.animationIteration = 0; this.animationName = _animation_name; this.paused = false; } /** Resets player' joints */ resetPose() { this.player.resetJoints(); } /** Get list of available animations */ get animationNames() { return Object.keys(this.animations); } /** Prepare single animation */ processAnimation(animations, animation_name, bones_overrides) { const animation = animations[animation_name]; const bones = {}; const keyframes_list = {}; // Iterate by each bone animation for (const [bone, value] of Object.entries(animation.bones)) { // Normalizing bones names let normalizedBoneName = undefined; if (bone in defaultBonesOverrides) { normalizedBoneName = defaultBonesOverrides[bone]; } // Apply overrides if (bones_overrides && Object.values(bones_overrides).includes(bone)) { normalizedBoneName = Object.entries(bones_overrides) .find(([, v]) => v === bone) ?.at(0); } if (!normalizedBoneName) throw Error(`Found unknown bone: ${bone}`); // Normalize keyframes objects bones[normalizedBoneName] = { position: this.convertKeyframe(value.position), rotation: this.convertKeyframe(value.rotation) }; // Prepare keyframes for proper use const rotation_keys = Object.keys(value.rotation ?? {}); const position_keys = Object.keys(value.position ?? {}); keyframes_list[normalizedBoneName] = { rotation: { str: rotation_keys, num: rotation_keys.map(parseFloat) }, position: { str: position_keys, num: position_keys.map(parseFloat) } }; } this.animations[animation_name] = { bones, keyframes_list, animation_length: animation.animation_length, animation_name, animation_loop: animation.loop == true }; } /** Sets the current animation by name from already imported animation set */ setAnimation(animation_name, options) { this.reset(animation_name); if (!options) return; if (options.connectCape) this.connectCape = options.connectCape ?? false; if (options.forceLoop) this.forceLoop = options.forceLoop; } clamp(val, min, max) { return Math.max(min, Math.min(val, max)); } resolveFrame(frame) { if (frame.pre) return frame.pre; if (frame.post) return frame.post; return [0, 0, 0]; } getCurrentKeyframe(value, keyframes_list, insertion, looped_time, loop) { const times = keyframes_list.num; const labels = keyframes_list.str; const n = times.length; let i0, i1, i2, i3; const stepBackwards = (i, step, n) => { const _i = i - step; if (_i <= 0) return n - (1 + step); return _i; }; if (loop) { i0 = stepBackwards(insertion, 1, n); i1 = insertion % n; i2 = (insertion + 1) % n; i3 = (insertion + 2) % n; } else { i0 = Math.max(insertion - 1, 0); i1 = insertion; i2 = Math.min(insertion + 1, n - 1); i3 = Math.min(insertion + 2, n - 1); } const t1 = times[i1]; let t2 = times[i2]; const k0 = value[labels[i0]]; const k1 = value[labels[i1]]; const k2 = value[labels[i2]]; const k3 = value[labels[i3]]; let time = looped_time; if (loop && t2 <= t1) { const duration = times[n - 1] - times[0]; t2 += duration; if (time < t1) time += duration; } let t; if (t2 === t1) { t = 0; } else { t = (time - t1) / (t2 - t1); } const start = this.resolveFrame(k1); const end = this.resolveFrame(k2); if (k2.lerp_mode === 'catmullrom') { const p0 = this.resolveFrame(k0); const p1 = start; const p2 = end; const p3 = this.resolveFrame(k3); return [0, 1, 2].map(i => catmullRom(p0[i], p1[i], p2[i], p3[i], t)); } else { return [0, 1, 2].map(i => start[i] + (end[i] - start[i]) * t); } } /** Group body parts to single torso */ initTorso() { if (this.torsoWrapper || !this.animations[this.animationName].bones.torso) return; this.torsoWrapper = new Group(); this.player.add(this.torsoWrapper); const torso = new Group(); torso.attach(this.player.skin.head); torso.attach(this.player.skin.leftArm); torso.attach(this.player.skin.rightArm); torso.attach(this.player.skin.body); torso.position.y = 8; this.torsoWrapper.add(torso); if (this.connectCape) { const cape = new Group(); cape.attach(this.player.cape); cape.position.y = -1; this.player.skin.body.add(cape); } } animate(player) { const delta = this.clock.getDelta() * this.speed; this.player = player; // Save player object for future this.initTorso(); const current_animation = this.animations[this.animationName]; const looped = this.forceLoop !== undefined ? this.forceLoop : current_animation.animation_loop; const looped_time = looped ? this._progress % current_animation.animation_length : this.clamp(this._progress, 0, current_animation.animation_length); for (const [bone, value] of Object.entries(current_animation.bones)) { for (const type of ['rotation', 'position']) { if (!value[type]) continue; const keyframes_list = current_animation.keyframes_list[bone][type]; let insert_index = this.findInsertPosition(keyframes_list.num, looped_time); if (insert_index === null) insert_index = keyframes_list.num.length - 1; const curr = this.getCurrentKeyframe(value[type], keyframes_list, insert_index, looped_time, current_animation.animation_loop); let skin_bone; switch (bone) { case 'all': skin_bone = this.player; break; case 'torso': skin_bone = this.torsoWrapper; break; case 'cape': skin_bone = this.player.cape; break; default: skin_bone = this.player.skin[bone]; } if (type === 'rotation') { const [x, y, z] = curr.map(MathUtils.degToRad); skin_bone.setRotationFromEuler(new Euler(x, -y, -z, 'ZYX')); } else { const defaults = defaultPositions[bone]; skin_bone.position.set(defaults[0] + curr[0], defaults[1] + curr[1], defaults[2] + -curr[2]); } } } const old_progress = this._progress; this._progress += delta; const animation_iteration = Math.floor(old_progress / current_animation.animation_length); if (animation_iteration > this.animationIteration && looped) { this.animationIteration = animation_iteration; this.onLoopEnd?.(this); } if (old_progress >= current_animation.animation_length && !looped) { this.paused = true; this.onFinish?.(this); } } findInsertPosition(arr, target) { let left = 0; let right = arr.length - 1; if (target < arr[0] || target > arr[arr.length - 1]) { return null; } while (left <= right) { const mid = Math.floor((left + right) / 2); if (arr[mid] === target) { return mid; } if (arr[mid] < target && target < arr[mid + 1]) { return mid; } if (target < arr[mid]) { right = mid - 1; } else { left = mid + 1; } } return null; } }