Added some functionality to the model viewer to aid in debugging.

This commit is contained in:
Daan Vanden Bosch 2019-07-01 12:13:05 +02:00
parent 43a4c7503d
commit 6579a53d62
6 changed files with 166 additions and 38 deletions

View File

@ -1,6 +1,7 @@
import { Object3D, Vector3, Clock } from "three";
import { Object3D, Vector3, Clock, SkeletonHelper } from "three";
import { model_viewer_store } from "../stores/ModelViewerStore";
import { Renderer } from "./Renderer";
import { autorun } from "mobx";
let renderer: ModelRenderer | undefined;
@ -13,15 +14,33 @@ export class ModelRenderer extends Renderer {
private clock = new Clock();
private model?: Object3D;
private skeleton_helper?: SkeletonHelper;
constructor() {
super();
autorun(() => {
const show = model_viewer_store.show_skeleton;
if (this.skeleton_helper) {
this.skeleton_helper.visible = show;
}
});
}
set_model(model?: Object3D) {
if (this.model !== model) {
if (this.model) {
this.scene.remove(this.model);
this.scene.remove(this.skeleton_helper!);
this.skeleton_helper = undefined;
}
if (model) {
this.scene.add(model);
this.skeleton_helper = new SkeletonHelper(model);
this.skeleton_helper.visible = model_viewer_store.show_skeleton;
(this.skeleton_helper.material as any).linewidth = 3;
this.scene.add(this.skeleton_helper);
this.reset_camera(new Vector3(0, 10, 20), new Vector3(0, 0, 0));
}
@ -32,8 +51,9 @@ export class ModelRenderer extends Renderer {
protected render() {
this.controls.update();
if (model_viewer_store.animation_mixer) {
model_viewer_store.animation_mixer.update(this.clock.getDelta());
if (model_viewer_store.animation) {
model_viewer_store.animation.mixer.update(this.clock.getDelta());
model_viewer_store.update_animation_frame();
}
this.renderer.render(this.scene, this.camera);

View File

@ -1,7 +1,7 @@
import { AnimationClip, Euler, InterpolateLinear, InterpolateSmooth, KeyframeTrack, Quaternion, QuaternionKeyframeTrack, VectorKeyframeTrack } from "three";
import { NjAction, NjInterpolation, NjKeyframeTrackType } from "../data_formats/parsing/ninja/motion";
const PSO_FRAME_RATE = 30;
export const PSO_FRAME_RATE = 30;
export function create_animation_clip(action: NjAction): AnimationClip {
const motion = action.motion;
@ -19,14 +19,14 @@ export function create_animation_clip(action: NjAction): AnimationClip {
for (const keyframe of keyframes) {
times.push(keyframe.frame / PSO_FRAME_RATE);
if (type === NjKeyframeTrackType.Position || type === NjKeyframeTrackType.Scale) {
values.push(keyframe.value.x, keyframe.value.y, keyframe.value.z);
} else {
if (type === NjKeyframeTrackType.Rotation) {
const quat = new Quaternion().setFromEuler(
new Euler(keyframe.value.x, keyframe.value.y, keyframe.value.z)
);
values.push(quat.x, quat.y, quat.z, quat.w);
} else {
values.push(keyframe.value.x, keyframe.value.y, keyframe.value.z);
}
}
@ -48,7 +48,7 @@ export function create_animation_clip(action: NjAction): AnimationClip {
return new AnimationClip(
'Animation',
motion.frame_count / PSO_FRAME_RATE,
(motion.frame_count - 1) / PSO_FRAME_RATE,
tracks
).optimize();
}

View File

@ -1,14 +1,15 @@
import Logger from 'js-logger';
import { action, observable } from "mobx";
import { AnimationClip, AnimationMixer, Object3D } from "three";
import { AnimationAction, AnimationClip, AnimationMixer, Object3D } from "three";
import { BufferCursor } from "../data_formats/BufferCursor";
import { NinjaModel, NinjaObject, parse_nj, parse_xj } from "../data_formats/parsing/ninja";
import { parse_njm_4 } from "../data_formats/parsing/ninja/motion";
import { create_animation_clip } from "../rendering/animation";
import { ninja_object_to_skinned_mesh } from "../rendering/models";
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 HEAD_PART_REGEX = /^pl[A-Z](hed|hai|cap)\d\d.nj$/;
const logger = Logger.get('stores/ModelViewerStore');
const cache: Map<string, Promise<NinjaObject<NinjaModel>>> = new Map();
@ -30,7 +31,35 @@ class ModelViewerStore {
@observable.ref current_model?: NinjaObject<NinjaModel>;
@observable.ref current_model_obj3d?: Object3D;
@observable.ref animation_mixer?: AnimationMixer;
@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);
@ -43,15 +72,28 @@ class ModelViewerStore {
reader.readAsArrayBuffer(file);
}
private set_model = action('set_model', (model?: NinjaObject<NinjaModel>, filename?: string) => {
if (this.current_model_obj3d && this.animation_mixer) {
this.animation_mixer.stopAllAction();
this.animation_mixer.uncacheRoot(this.current_model_obj3d);
this.animation_mixer = undefined;
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;
}
})
if (model) {
if (this.current_model && filename && /^pl[A-Z](hed|hai|cap)\d\d.nj$/.test(filename)) {
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<NinjaModel>, filename?: string) => {
if (this.current_model_obj3d && this.animation) {
this.animation.mixer.stopAllAction();
this.animation.mixer.uncacheRoot(this.current_model_obj3d);
}
if (this.current_model && filename && HEAD_PART_REGEX.test(filename)) {
this.add_to_bone(this.current_model, model, 59);
} else {
this.current_model = model;
@ -60,11 +102,16 @@ class ModelViewerStore {
const mesh = ninja_object_to_skinned_mesh(this.current_model);
mesh.translateY(-mesh.geometry.boundingSphere.radius);
this.current_model_obj3d = mesh;
} else {
this.current_model = undefined;
this.current_model_obj3d = undefined;
if (this.animation) {
this.animation.mixer = new AnimationMixer(mesh);
this.animation.mixer.timeScale = this.animation_frame_rate / PSO_FRAME_RATE;
this.animation.action = this.animation.mixer.clipAction(this.animation.clip);
this.animation.action.paused = !this.animation_playing;
this.animation.action.play();
}
}
})
)
// TODO: notify user of problems.
private loadend = async (file: File, reader: FileReader) => {
@ -115,14 +162,24 @@ class ModelViewerStore {
private add_animation = action('add_animation', (clip: AnimationClip) => {
if (!this.current_model_obj3d) return;
if (this.animation_mixer) {
this.animation_mixer.stopAllAction();
let mixer: AnimationMixer;
if (this.animation) {
this.animation.mixer.stopAllAction();
mixer = this.animation.mixer;
} else {
this.animation_mixer = new AnimationMixer(this.current_model_obj3d);
mixer = new AnimationMixer(this.current_model_obj3d)
}
const action = this.animation_mixer.clipAction(clip);
action.play();
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<NinjaObject<NinjaModel>> {

View File

@ -6,18 +6,28 @@
.mv-ModelViewerComponent-toolbar {
display: flex;
padding: 10px 5px;
}
align-items: center;
.mv-ModelViewerComponent-toolbar > * {
margin: 0 5px;
& > * {
margin: 0 5px;
}
& .group {
display: flex;
align-items: center;
& > * {
margin: 0 5px;
}
}
}
.mv-ModelViewerComponent-main {
flex: 1;
display: flex;
overflow: hidden;
}
.mv-ModelViewerComponent-main > div:nth-child(2) {
flex: 1;
}
& > div:nth-child(2) {
flex: 1;
}
}

View File

@ -1,4 +1,4 @@
import { Button, Upload } from "antd";
import { Button, InputNumber, Upload, Switch } from "antd";
import { UploadChangeParam } from "antd/lib/upload";
import { UploadFile } from "antd/lib/upload/interface";
import { observer } from "mobx-react";
@ -47,8 +47,49 @@ class Toolbar extends React.Component {
// Make sure it doesn't do a POST:
customRequest={() => false}
>
<Button icon="file">{this.state.filename || 'Choose file...'}</Button>
<Button icon="file">{this.state.filename || 'Open file...'}</Button>
</Upload>
{model_viewer_store.animation && (
<>
<Button
icon={model_viewer_store.animation_playing ? 'pause' : 'caret-right'}
onClick={model_viewer_store.toggle_animation_playing}
>
{model_viewer_store.animation_playing ? 'Pause animation' : 'Play animation'}
</Button>
<div className="group">
<span>Frame rate:</span>
<InputNumber
value={model_viewer_store.animation_frame_rate}
onChange={(value) =>
model_viewer_store.set_animation_frame_rate(value || 0)
}
min={1}
step={1}
/>
</div>
<div className="group">
<span>Frame:</span>
<InputNumber
value={model_viewer_store.animation_frame}
onChange={(value) =>
model_viewer_store.set_animation_frame(value || 0)
}
step={1}
/>
<span>
/ {model_viewer_store.animation_frame_count}
</span>
</div>
<div className="group">
<span>Show skeleton:</span>
<Switch
checked={model_viewer_store.show_skeleton}
onChange={(value) => model_viewer_store.show_skeleton = value}
/>
</div>
</>
)}
</div>
);
}

View File

@ -91,7 +91,7 @@ class Toolbar extends React.Component<{ onSaveAsClicked: (filename?: string) =>
// Make sure it doesn't do a POST:
customRequest={() => false}
>
<Button icon="file">{this.state.filename || 'Choose file...'}</Button>
<Button icon="file">{this.state.filename || 'Open file...'}</Button>
</Upload>
{areas && (
<Select