import Logger from 'js-logger'; import { action, observable } from "mobx"; import { AnimationAction, AnimationClip, AnimationMixer, SkinnedMesh } from "three"; import { BufferCursor } from "../data_formats/BufferCursor"; import { NinjaModel, NinjaObject, parse_nj, parse_xj } from "../data_formats/parsing/ninja"; import { parse_njm } from "../data_formats/parsing/ninja/motion"; import { PlayerModel } from '../domain'; import { create_animation_clip, PSO_FRAME_RATE } from "../rendering/animation"; import { ninja_object_to_skinned_mesh } from "../rendering/models"; import { get_player_data } from './binary_assets'; const logger = Logger.get('stores/ModelViewerStore'); const cache: Map>> = new Map(); class ModelViewerStore { readonly models: PlayerModel[] = [ new PlayerModel('HUmar', 1, 10, new Set([6])), new PlayerModel('HUnewearl', 1, 10, new Set()), new PlayerModel('HUcast', 5, 0, new Set()), new PlayerModel('HUcaseal', 5, 0, new Set()), new PlayerModel('RAmar', 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), new PlayerModel('RAmarl', 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), new PlayerModel('RAcast', 5, 0, new Set()), new PlayerModel('RAcaseal', 5, 0, new Set()), new PlayerModel('FOmarl', 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), new PlayerModel('FOmar', 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), new PlayerModel('FOnewm', 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), new PlayerModel('FOnewearl', 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), ]; @observable.ref current_model?: NinjaObject; @observable.ref current_bone_count: number = 0; @observable.ref current_obj3d?: SkinnedMesh; @observable.ref animation?: { mixer: AnimationMixer, clip: AnimationClip, action: AnimationAction, } @observable animation_playing: boolean = false; @observable animation_frame_rate: number = PSO_FRAME_RATE; @observable animation_frame: number = 0; @observable animation_frame_count: number = 0; @observable show_skeleton: boolean = false; set_animation_frame_rate = action('set_animation_frame_rate', (rate: number) => { if (this.animation) { this.animation.mixer.timeScale = rate / PSO_FRAME_RATE; this.animation_frame_rate = rate; } }) set_animation_frame = action('set_animation_frame', (frame: number) => { if (this.animation) { const frame_count = this.animation_frame_count; frame = (frame - 1) % frame_count + 1; if (frame < 1) frame = frame_count + frame; this.animation.action.time = (frame - 1) / (frame_count - 1); this.animation_frame = frame; } }) load_model = async (model: PlayerModel) => { const object = await this.get_player_ninja_object(model); this.set_model(object); // Ignore the bones from the head parts. this.current_bone_count = 64; } load_file = (file: File) => { const reader = new FileReader(); reader.addEventListener('loadend', () => { this.loadend(file, reader) }); reader.readAsArrayBuffer(file); } toggle_animation_playing = action('toggle_animation_playing', () => { if (this.animation) { this.animation.action.paused = !this.animation.action.paused; this.animation_playing = !this.animation.action.paused; } }) update_animation_frame = action('update_animation_frame', () => { if (this.animation) { const frame_count = this.animation_frame_count; this.animation_frame = Math.floor(this.animation.action.time * (frame_count - 1) + 1); } }) private set_model = action('set_model', (model: NinjaObject) => { if (this.current_obj3d && this.animation) { this.animation.mixer.stopAllAction(); this.animation.mixer.uncacheRoot(this.current_obj3d); this.animation = undefined; } this.current_model = model; this.current_bone_count = model.bone_count(); const mesh = ninja_object_to_skinned_mesh(this.current_model); mesh.translateY(-mesh.geometry.boundingSphere.radius); this.current_obj3d = mesh; }) // TODO: notify user of problems. private loadend = async (file: File, reader: FileReader) => { if (!(reader.result instanceof ArrayBuffer)) { logger.error('Couldn\'t read file.'); return; } if (file.name.endsWith('.nj')) { const model = parse_nj(new BufferCursor(reader.result, true))[0]; this.set_model(model); } else if (file.name.endsWith('.xj')) { const model = parse_xj(new BufferCursor(reader.result, true))[0]; this.set_model(model); } else if (file.name.endsWith('.njm')) { if (this.current_model) { const njm = parse_njm( new BufferCursor(reader.result, true), this.current_bone_count ); this.add_animation(create_animation_clip(this.current_model, njm)); } } else { logger.error(`Unknown file extension in filename "${file.name}".`); } } private add_to_bone( object: NinjaObject, head_part: NinjaObject, bone_id: number ) { const bone = object.get_bone(bone_id); if (bone) { bone.evaluation_flags.hidden = false; bone.evaluation_flags.break_child_trace = false; bone.children.push(head_part); } } private add_animation = action('add_animation', (clip: AnimationClip) => { if (!this.current_obj3d) return; let mixer: AnimationMixer; if (this.animation) { this.animation.mixer.stopAllAction(); mixer = this.animation.mixer; } else { mixer = new AnimationMixer(this.current_obj3d) } this.animation = { mixer, clip, action: mixer.clipAction(clip) } this.animation.action.play(); this.animation_playing = true; this.animation_frame_count = PSO_FRAME_RATE * clip.duration + 1; }) private get_player_ninja_object(model: PlayerModel): Promise> { let ninja_object = cache.get(model.name); if (ninja_object) { return ninja_object; } else { ninja_object = this.get_all_assets(model); cache.set(model.name, ninja_object); return ninja_object; } } private async get_all_assets(model: PlayerModel): Promise> { const body_data = await get_player_data(model.name, 'Body'); const body = parse_nj(new BufferCursor(body_data, true))[0]; if (!body) { throw new Error(`Couldn't parse body for player class ${model.name}.`); } const head_data = await get_player_data(model.name, 'Head', 0); const head = parse_nj(new BufferCursor(head_data, true))[0]; if (head) { this.add_to_bone(body, head, 59); } if (model.hair_styles_count > 0) { const hair_data = await get_player_data(model.name, 'Hair', 0); const hair = parse_nj(new BufferCursor(hair_data, true))[0]; if (hair) { this.add_to_bone(body, hair, 59); } if (model.hair_styles_with_accessory.has(0)) { const accessory_data = await get_player_data(model.name, 'Accessory', 0); const accessory = parse_nj(new BufferCursor(accessory_data, true))[0]; if (accessory) { this.add_to_bone(body, accessory, 59); } } } return body; } } export const model_viewer_store = new ModelViewerStore();