Simplified mesh creation. Some performance improvements. Added debug mode to quest viewer that shows per-section colored area render geometry.

This commit is contained in:
Daan Vanden Bosch 2019-07-19 21:49:59 +02:00
parent a181847647
commit f670718637
12 changed files with 354 additions and 275 deletions

View File

@ -4,18 +4,12 @@ import { Vec3 } from "../data_formats/vector";
import { QuestEntity, QuestNpc, QuestObject, Section } from "../domain";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { AreaUserData } from "./conversion/areas";
import {
EntityUserData,
NPC_COLOR,
NPC_HIGHLIGHTED_COLOR,
NPC_SELECTED_COLOR,
OBJECT_COLOR,
OBJECT_HIGHLIGHTED_COLOR,
OBJECT_SELECTED_COLOR,
} from "./conversion/entities";
import { ColorType, EntityUserData, NPC_COLORS, OBJECT_COLORS } from "./conversion/entities";
import { QuestRenderer } from "./QuestRenderer";
type Selection = {
const DOWN_VECTOR = new Vector3(0, -1, 0);
type Highlighted = {
entity: QuestEntity;
mesh: Mesh;
};
@ -32,16 +26,10 @@ type PickResult = Pick & {
mesh: Mesh;
};
enum ColorType {
Normal,
Highlighted,
Selected,
}
export class QuestEntityControls {
private raycaster = new Raycaster();
private selected?: Selection;
private highlighted?: Selection;
private selected?: Highlighted;
private hovered?: Highlighted;
/**
* Iff defined, the user is transforming the selected entity.
*/
@ -118,24 +106,26 @@ export class QuestEntityControls {
const pointer_device_pos = this.renderer.pointer_pos_to_device_coords(e);
if (this.selected && this.pick) {
// User is tranforming selected entity.
if (e.buttons === 1) {
// User is dragging selected entity.
if (e.shiftKey) {
// Vertical movement.
this.translate_vertically(this.selected, this.pick, pointer_device_pos);
} else {
// Horizontal movement accross terrain.
this.translate_horizontally(this.selected, this.pick, pointer_device_pos);
if (this.moved_since_last_mouse_down) {
if (e.buttons === 1) {
// User is tranforming selected entity.
// User is dragging selected entity.
if (e.shiftKey) {
// Vertical movement.
this.translate_vertically(this.selected, this.pick, pointer_device_pos);
} else {
// Horizontal movement accross terrain.
this.translate_horizontally(this.selected, this.pick, pointer_device_pos);
}
}
}
this.renderer.schedule_render();
this.renderer.schedule_render();
}
} else {
// User is hovering.
const new_pick = this.pick_entity(pointer_device_pos);
if (this.highlight(new_pick)) {
if (this.mark_hovered(new_pick)) {
this.renderer.schedule_render();
}
}
@ -159,32 +149,32 @@ export class QuestEntityControls {
/**
* @returns true if a render is required.
*/
private highlight(selection?: Selection): boolean {
private mark_hovered(selection?: Highlighted): boolean {
let render_required = false;
if (!this.selected || !selection_equals(selection, this.selected)) {
if (!selection_equals(selection, this.highlighted)) {
if (this.highlighted) {
set_color(this.highlighted, ColorType.Normal);
this.highlighted = undefined;
if (!selection_equals(selection, this.hovered)) {
if (this.hovered) {
set_color(this.hovered, ColorType.Normal);
this.hovered = undefined;
}
if (selection) {
set_color(selection, ColorType.Highlighted);
set_color(selection, ColorType.Hovered);
}
render_required = true;
}
this.highlighted = selection;
this.hovered = selection;
}
return render_required;
}
private select(selection: Selection): void {
if (selection_equals(selection, this.highlighted)) {
this.highlighted = undefined;
private select(selection: Highlighted): void {
if (selection_equals(selection, this.hovered)) {
this.hovered = undefined;
}
if (!selection_equals(selection, this.selected)) {
@ -211,7 +201,7 @@ export class QuestEntityControls {
}
private translate_vertically(
selection: Selection,
selection: Highlighted,
pick: Pick,
pointer_position: Vector2
): void {
@ -241,7 +231,7 @@ export class QuestEntityControls {
}
private translate_horizontally(
selection: Selection,
selection: Highlighted,
pick: Pick,
pointer_position: Vector2
): void {
@ -325,15 +315,15 @@ export class QuestEntityControls {
let drag_y = 0;
// Find vertical distance to terrain.
this.raycaster.set(intersection.object.position, new Vector3(0, -1, 0));
const [terrain] = this.raycaster.intersectObjects(
this.raycaster.set(intersection.object.position, DOWN_VECTOR);
const [collision_geom_intersection] = this.raycaster.intersectObjects(
this.renderer.collision_geometry.children,
true
);
if (terrain) {
drag_adjust.sub(new Vector3(0, terrain.distance, 0));
drag_y += terrain.distance;
if (collision_geom_intersection) {
drag_adjust.y -= collision_geom_intersection.distance;
drag_y += collision_geom_intersection.distance;
}
return {
@ -358,7 +348,7 @@ export class QuestEntityControls {
} {
this.raycaster.setFromCamera(pointer_pos, this.renderer.camera);
this.raycaster.ray.origin.add(data.drag_adjust);
const terrains = this.raycaster.intersectObjects(
const intersections = this.raycaster.intersectObjects(
this.renderer.collision_geometry.children,
true
);
@ -366,19 +356,11 @@ export class QuestEntityControls {
// Don't allow entities to be placed on very steep terrain.
// E.g. walls.
// TODO: make use of the flags field in the collision data.
for (const terrain of terrains) {
if (terrain.face!.normal.y > 0.75) {
// Find section ID.
this.raycaster.set(terrain.point.clone().setY(1000), new Vector3(0, -1, 0));
const render_terrains = this.raycaster
.intersectObjects(this.renderer.render_geometry.children, true)
.filter(rt => (rt.object.userData as AreaUserData).section.id >= 0);
for (const intersection of intersections) {
if (intersection.face!.normal.y > 0.75) {
return {
intersection: terrain,
section:
render_terrains[0] &&
(render_terrains[0].object.userData as AreaUserData).section,
intersection,
section: (intersection.object.userData as AreaUserData).section,
};
}
}
@ -387,34 +369,24 @@ export class QuestEntityControls {
}
}
function set_color({ entity, mesh }: Selection, type: ColorType): void {
const color = get_color(entity, type);
function set_color({ entity, mesh }: Highlighted, type: ColorType): void {
const color = entity instanceof QuestNpc ? NPC_COLORS[type] : OBJECT_COLORS[type];
if (mesh) {
for (const material of mesh.material as MeshLambertMaterial[]) {
if (type === ColorType.Normal && material.map) {
material.color.set(0xffffff);
} else {
material.color.set(color);
if (Array.isArray(mesh.material)) {
for (const mat of mesh.material as MeshLambertMaterial[]) {
if (type === ColorType.Normal && mat.map) {
mat.color.set(0xffffff);
} else {
mat.color.set(color);
}
}
} else {
(mesh.material as MeshLambertMaterial).color.set(color);
}
}
}
function selection_equals(a?: Selection, b?: Selection): boolean {
function selection_equals(a?: Highlighted, b?: Highlighted): boolean {
return a && b ? a.entity === b.entity : a === b;
}
function get_color(entity: QuestEntity, type: ColorType): number {
const is_npc = entity instanceof QuestNpc;
switch (type) {
default:
case ColorType.Normal:
return is_npc ? NPC_COLOR : OBJECT_COLOR;
case ColorType.Highlighted:
return is_npc ? NPC_HIGHLIGHTED_COLOR : OBJECT_HIGHLIGHTED_COLOR;
case ColorType.Selected:
return is_npc ? NPC_SELECTED_COLOR : OBJECT_SELECTED_COLOR;
}
}

View File

@ -1,6 +1,6 @@
import Logger from "js-logger";
import { autorun, IReactionDisposer } from "mobx";
import { Mesh, Object3D, Vector3 } from "three";
import { Mesh, Object3D, Vector3, Raycaster, Intersection } from "three";
import { Area, Quest, QuestEntity } from "../domain";
import { load_area_collision_geometry, load_area_render_geometry } from "../loading/areas";
import {
@ -11,6 +11,7 @@ import {
} from "../loading/entities";
import { create_npc_mesh, create_object_mesh } from "./conversion/entities";
import { QuestRenderer } from "./QuestRenderer";
import { AreaUserData } from "./conversion/areas";
const logger = Logger.get("rendering/QuestModelManager");
@ -55,6 +56,8 @@ export class QuestModelManager {
variant_id
);
this.add_sections_to_collision_geometry(collision_geometry, render_geometry);
if (this.quest !== quest || this.area !== area) return;
this.renderer.collision_geometry = collision_geometry;
@ -101,6 +104,47 @@ export class QuestModelManager {
}
}
private add_sections_to_collision_geometry(
collision_geom: Object3D,
render_geom: Object3D
): void {
const raycaster = new Raycaster();
const origin = new Vector3();
const down = new Vector3(0, -1, 0);
const up = new Vector3(0, 1, 0);
for (const collision_area of collision_geom.children) {
(collision_area as Mesh).geometry.boundingBox.getCenter(origin);
raycaster.set(origin, down);
const intersection1 = raycaster
.intersectObject(render_geom, true)
.find(i => (i.object.userData as AreaUserData).section != null);
raycaster.set(origin, up);
const intersection2 = raycaster
.intersectObject(render_geom, true)
.find(i => (i.object.userData as AreaUserData).section != null);
let intersection: Intersection | undefined;
if (intersection1 && intersection2) {
intersection =
intersection1.distance <= intersection2.distance
? intersection1
: intersection2;
} else {
intersection = intersection1 || intersection2;
}
if (intersection) {
const cud = collision_area.userData as AreaUserData;
const rud = intersection.object.userData as AreaUserData;
cud.section = rud.section;
}
}
}
private update_entity_geometry(entity: QuestEntity, model: Mesh): void {
this.renderer.add_entity_model(model);

View File

@ -15,6 +15,18 @@ export function get_quest_renderer(): QuestRenderer {
}
export class QuestRenderer extends Renderer<PerspectiveCamera> {
get debug(): boolean {
return this._debug;
}
set debug(debug: boolean) {
if (this._debug !== debug) {
this._debug = debug;
this._render_geometry.visible = debug;
this.schedule_render();
}
}
private _collision_geometry = new Object3D();
get collision_geometry(): Object3D {
@ -34,9 +46,10 @@ export class QuestRenderer extends Renderer<PerspectiveCamera> {
}
set render_geometry(render_geometry: Object3D) {
// this.scene.remove(this._render_geometry);
this.scene.remove(this._render_geometry);
this._render_geometry = render_geometry;
// this.scene.add(render_geometry);
render_geometry.visible = this.debug;
this.scene.add(render_geometry);
}
private _entity_models = new Object3D();

View File

@ -15,6 +15,16 @@ import OrbitControlsCreator from "three-orbit-controls";
const OrbitControls = OrbitControlsCreator(THREE);
export class Renderer<C extends Camera> {
protected _debug = false;
get debug(): boolean {
return this._debug;
}
set debug(debug: boolean) {
this._debug = debug;
}
readonly camera: C;
readonly controls: any;
readonly scene = new Scene();

View File

@ -1,4 +1,19 @@
import { BufferGeometry, Float32BufferAttribute, Uint16BufferAttribute, Vector3 } from "three";
import {
BufferGeometry,
Float32BufferAttribute,
Uint16BufferAttribute,
Vector3,
Bone,
} from "three";
export type BuilderData = {
created_by_geometry_builder: boolean;
/**
* Maps material indices to normalized material indices.
*/
normalized_material_indices: Map<number, number>;
bones: Bone[];
};
export type BuilderVec2 = {
x: number;
@ -22,10 +37,14 @@ export class GeometryBuilder {
private normals: number[] = [];
private uvs: number[] = [];
private indices: number[] = [];
private bones: Bone[] = [];
private bone_indices: number[] = [];
private bone_weights: number[] = [];
private groups: VertexGroup[] = [];
private _max_material_index?: number;
/**
* Will contain all material indices used in {@link this.groups} and -1 for the dummy material.
*/
private material_indices = new Set<number>([-1]);
get vertex_count(): number {
return this.positions.length / 3;
@ -35,10 +54,6 @@ export class GeometryBuilder {
return this.indices.length;
}
get max_material_index(): number | undefined {
return this._max_material_index;
}
get_position(index: number): Vector3 {
return new Vector3(
this.positions[3 * index],
@ -65,14 +80,18 @@ export class GeometryBuilder {
this.indices.push(index);
}
add_bone(index: number, weight: number): void {
add_bone(bone: Bone): void {
this.bones.push(bone);
}
add_bone_weight(index: number, weight: number): void {
this.bone_indices.push(index);
this.bone_weights.push(weight);
}
add_group(offset: number, size: number, material_id?: number): void {
add_group(offset: number, size: number, material_index?: number): void {
const last_group = this.groups[this.groups.length - 1];
const mat_idx = material_id == null ? 0 : material_id + 1;
const mat_idx = material_index == null ? -1 : material_index;
if (last_group && last_group.material_index === mat_idx) {
last_group.size += size;
@ -82,15 +101,14 @@ export class GeometryBuilder {
size,
material_index: mat_idx,
});
this._max_material_index = this._max_material_index
? Math.max(this._max_material_index, mat_idx)
: mat_idx;
this.material_indices.add(mat_idx);
}
}
build(): BufferGeometry {
const geom = new BufferGeometry();
const data = geom.userData as BuilderData;
data.created_by_geometry_builder = true;
geom.addAttribute("position", new Float32BufferAttribute(this.positions, 3));
geom.addAttribute("normal", new Float32BufferAttribute(this.normals, 3));
@ -98,15 +116,29 @@ export class GeometryBuilder {
geom.setIndex(new Uint16BufferAttribute(this.indices, 1));
for (const group of this.groups) {
geom.addGroup(group.offset, group.size, group.material_index);
}
if (this.bone_indices.length) {
if (this.bone_indices.length && this.bones.length) {
geom.addAttribute("skinIndex", new Uint16BufferAttribute(this.bone_indices, 4));
geom.addAttribute("skinWeight", new Float32BufferAttribute(this.bone_weights, 4));
data.bones = this.bones;
} else {
data.bones = [];
}
// Normalize material indices.
const normalized_mat_idxs = new Map<number, number>();
let i = 0;
for (const mat_idx of [...this.material_indices].sort((a, b) => a - b)) {
normalized_mat_idxs.set(mat_idx, i++);
}
// Use normalized material indices in Three.js groups.
for (const group of this.groups) {
geom.addGroup(group.offset, group.size, normalized_mat_idxs.get(group.material_index));
}
data.normalized_material_indices = normalized_mat_idxs;
geom.computeBoundingSphere();
geom.computeBoundingBox();

View File

@ -8,12 +8,13 @@ import {
MeshLambertMaterial,
Object3D,
Vector3,
Color,
} from "three";
import { CollisionObject } from "../../data_formats/parsing/area_collision_geometry";
import { RenderObject } from "../../data_formats/parsing/area_geometry";
import { Section } from "../../domain";
import { ninja_object_to_mesh, ninja_object_to_geometry_builder } from "./ninja_geometry";
import { GeometryBuilder } from "./GeometryBuilder";
import { ninja_object_to_geometry_builder } from "./ninja_geometry";
const materials = [
// Wall
@ -65,6 +66,10 @@ const wireframe_materials = [
}),
];
export type AreaUserData = {
section?: Section;
};
export function area_collision_geometry_to_object_3d(object: CollisionObject): Object3D {
const group = new Group();
@ -94,6 +99,9 @@ export function area_collision_geometry_to_object_3d(object: CollisionObject): O
);
}
geom.computeBoundingBox();
geom.computeBoundingSphere();
const mesh = new Mesh(geom, materials);
mesh.renderOrder = 1;
group.add(mesh);
@ -106,20 +114,14 @@ export function area_collision_geometry_to_object_3d(object: CollisionObject): O
return group;
}
export type AreaUserData = {
section: Section;
};
export function area_geometry_to_sections_and_object_3d(
object: RenderObject
): [Section[], Object3D] {
const sections: Section[] = [];
const group = new Group();
let i = 0;
for (const section of object.sections) {
const sec = new Section(section.id, section.position, section.rotation.y);
sections.push(sec);
const builder = new GeometryBuilder();
for (const object of section.objects) {
@ -128,16 +130,23 @@ export function area_geometry_to_sections_and_object_3d(
const mesh = new Mesh(
builder.build(),
new MeshLambertMaterial({
color: 0x44aaff,
new MeshBasicMaterial({
color: new Color().setHSL((i++ % 7) / 7, 1, 0.5),
transparent: true,
opacity: 0.75,
opacity: 0.25,
side: DoubleSide,
})
);
(mesh.userData as AreaUserData).section = sec;
group.add(mesh);
mesh.position.set(section.position.x, section.position.y, section.position.z);
mesh.rotation.set(section.rotation.x, section.rotation.y, section.rotation.z);
if (section.id >= 0) {
const sec = new Section(section.id, section.position, section.rotation.y);
sections.push(sec);
(mesh.userData as AreaUserData).section = sec;
}
}
return [sections, group];

View File

@ -0,0 +1,82 @@
import {
BufferGeometry,
DoubleSide,
Material,
Mesh,
MeshLambertMaterial,
Skeleton,
SkinnedMesh,
} from "three";
import { BuilderData } from "./GeometryBuilder";
const DUMMY_MATERIAL = new MeshLambertMaterial({
color: 0x00ff00,
side: DoubleSide,
});
const DEFAULT_MATERIAL = new MeshLambertMaterial({
color: 0xff00ff,
side: DoubleSide,
});
const DEFAULT_SKINNED_MATERIAL = new MeshLambertMaterial({
skinning: true,
color: 0xff00ff,
side: DoubleSide,
});
export function create_mesh(
geometry: BufferGeometry,
material?: Material | Material[],
default_material: Material = DEFAULT_MATERIAL
): Mesh {
return create(geometry, material, default_material, Mesh);
}
export function create_skinned_mesh(
geometry: BufferGeometry,
material?: Material | Material[],
default_material: Material = DEFAULT_SKINNED_MATERIAL
): SkinnedMesh {
return create(geometry, material, default_material, SkinnedMesh);
}
function create<M extends Mesh>(
geometry: BufferGeometry,
material: Material | Material[] | undefined,
default_material: Material,
mesh_constructor: new (geometry: BufferGeometry, material: Material | Material[]) => M
): M {
const {
created_by_geometry_builder,
normalized_material_indices: mat_idxs,
bones,
} = geometry.userData as BuilderData;
let mat: Material | Material[];
if (Array.isArray(material)) {
if (created_by_geometry_builder) {
mat = [DUMMY_MATERIAL];
for (const [idx, normalized_idx] of mat_idxs.entries()) {
if (normalized_idx > 0) {
mat[normalized_idx] = material[idx] || default_material;
}
}
} else {
mat = material;
}
} else if (material) {
mat = material;
} else {
mat = default_material;
}
const mesh = new mesh_constructor(geometry, mat);
if (created_by_geometry_builder && bones.length && mesh instanceof SkinnedMesh) {
mesh.add(bones[0]);
mesh.bind(new Skeleton(bones));
}
return mesh;
}

View File

@ -1,20 +1,22 @@
import {
BufferGeometry,
DoubleSide,
Mesh,
MeshBasicMaterial,
MeshLambertMaterial,
Texture,
Material,
} from "three";
import { BufferGeometry, DoubleSide, Mesh, MeshLambertMaterial, Texture } from "three";
import { QuestEntity, QuestNpc, QuestObject } from "../../domain";
import { create_mesh } from "./create_mesh";
export const OBJECT_COLOR = 0xffff00;
export const OBJECT_HIGHLIGHTED_COLOR = 0xffdf3f;
export const OBJECT_SELECTED_COLOR = 0xffaa00;
export const NPC_COLOR = 0xff0000;
export const NPC_HIGHLIGHTED_COLOR = 0xff3f5f;
export const NPC_SELECTED_COLOR = 0xff0054;
export enum ColorType {
Normal,
Hovered,
Selected,
}
export const OBJECT_COLORS: number[] = [];
OBJECT_COLORS[ColorType.Normal] = 0xffff00;
OBJECT_COLORS[ColorType.Hovered] = 0xffdf3f;
OBJECT_COLORS[ColorType.Selected] = 0xffaa00;
export const NPC_COLORS: number[] = [];
NPC_COLORS[ColorType.Normal] = 0xff0000;
NPC_COLORS[ColorType.Hovered] = 0xff3f5f;
NPC_COLORS[ColorType.Selected] = 0xff0054;
export type EntityUserData = {
entity: QuestEntity;
@ -25,7 +27,7 @@ export function create_object_mesh(
geometry: BufferGeometry,
textures: Texture[]
): Mesh {
return create_mesh(object, geometry, textures, OBJECT_COLOR, "Object");
return create(object, geometry, textures, OBJECT_COLORS[ColorType.Normal], object.type.name);
}
export function create_npc_mesh(
@ -33,47 +35,37 @@ export function create_npc_mesh(
geometry: BufferGeometry,
textures: Texture[]
): Mesh {
return create_mesh(npc, geometry, textures, NPC_COLOR, "NPC");
return create(npc, geometry, textures, NPC_COLORS[ColorType.Normal], npc.type.code);
}
function create_mesh(
function create(
entity: QuestEntity,
geometry: BufferGeometry,
textures: Texture[],
color: number,
type: string
name: string
): Mesh {
const max_mat_idx = geometry.groups.reduce((max, g) => Math.max(max, g.materialIndex || 0), 0);
const default_material = new MeshLambertMaterial({
color,
side: DoubleSide,
});
const materials: Material[] = [
new MeshBasicMaterial({
color,
side: DoubleSide,
}),
];
materials.push(
...textures.map(
tex =>
new MeshLambertMaterial({
map: tex,
side: DoubleSide,
alphaTest: 0.5,
})
)
const mesh = create_mesh(
geometry,
textures.length
? textures.map(
tex =>
new MeshLambertMaterial({
map: tex,
side: DoubleSide,
alphaTest: 0.5,
})
)
: default_material,
default_material
);
for (let i = materials.length - 1; i < max_mat_idx; ++i) {
materials.push(
new MeshLambertMaterial({
color,
side: DoubleSide,
})
);
}
const mesh = new Mesh(geometry, materials);
mesh.name = type;
mesh.name = name;
(mesh.userData as EntityUserData).entity = entity;
const { x, y, z } = entity.position;

View File

@ -1,39 +1,10 @@
import {
Bone,
BufferGeometry,
DoubleSide,
Euler,
Material,
Matrix3,
Matrix4,
Mesh,
MeshBasicMaterial,
MeshLambertMaterial,
Quaternion,
Skeleton,
SkinnedMesh,
Vector2,
Vector3,
} from "three";
import { Bone, BufferGeometry, Euler, Matrix3, Matrix4, Quaternion, Vector2, Vector3 } from "three";
import { vec3_to_threejs } from ".";
import { is_njcm_model, NjModel, NjObject } from "../../data_formats/parsing/ninja";
import { NjcmModel } from "../../data_formats/parsing/ninja/njcm";
import { XjModel } from "../../data_formats/parsing/ninja/xj";
import { GeometryBuilder } from "./GeometryBuilder";
const DUMMY_MATERIAL = new MeshBasicMaterial({
color: 0x00ff00,
side: DoubleSide,
});
const DEFAULT_MATERIAL = new MeshBasicMaterial({
color: 0xff00ff,
side: DoubleSide,
});
const DEFAULT_SKINNED_MATERIAL = new MeshLambertMaterial({
skinning: true,
color: 0xff00ff,
side: DoubleSide,
});
const DEFAULT_NORMAL = new Vector3(0, 1, 0);
const DEFAULT_UV = new Vector2(0, 0);
const NO_TRANSLATION = new Vector3(0, 0, 0);
@ -43,32 +14,12 @@ const NO_SCALE = new Vector3(1, 1, 1);
export function ninja_object_to_geometry_builder(
object: NjObject<NjModel>,
builder: GeometryBuilder
) {
new ModelCreator(builder).to_geometry_builder(object);
): void {
new GeometryCreator(builder).to_geometry_builder(object);
}
export function ninja_object_to_buffer_geometry(object: NjObject<NjModel>): BufferGeometry {
return new ModelCreator(new GeometryBuilder()).create_buffer_geometry(object);
}
export function ninja_object_to_mesh(
object: NjObject<NjModel>,
materials: Material[] = [],
default_material: Material = DEFAULT_MATERIAL
): Mesh {
return new ModelCreator(new GeometryBuilder()).create_mesh(object, materials, default_material);
}
export function ninja_object_to_skinned_mesh(
object: NjObject<NjModel>,
materials: Material[] = [],
default_material: Material = DEFAULT_SKINNED_MATERIAL
): SkinnedMesh {
return new ModelCreator(new GeometryBuilder()).create_skinned_mesh(
object,
materials,
default_material
);
return new GeometryCreator(new GeometryBuilder()).create_buffer_geometry(object);
}
type Vertex = {
@ -102,17 +53,16 @@ class VerticesHolder {
}
}
class ModelCreator {
class GeometryCreator {
private vertices = new VerticesHolder();
private bone_id: number = 0;
private bones: Bone[] = [];
private builder: GeometryBuilder;
constructor(builder: GeometryBuilder) {
this.builder = builder;
}
to_geometry_builder(object: NjObject<NjModel>) {
to_geometry_builder(object: NjObject<NjModel>): void {
this.object_to_geometry(object, undefined, new Matrix4());
}
@ -121,44 +71,6 @@ class ModelCreator {
return this.builder.build();
}
create_mesh(
object: NjObject<NjModel>,
materials: Material[],
default_material: Material
): Mesh {
const geom = this.create_buffer_geometry(object);
materials = [DUMMY_MATERIAL, ...materials];
const max_mat_idx = this.builder.max_material_index || 0;
for (let i = materials.length - 1; i < max_mat_idx; ++i) {
materials.push(default_material);
}
return new Mesh(geom, materials);
}
create_skinned_mesh(
object: NjObject<NjModel>,
materials: Material[],
default_material: Material
): SkinnedMesh {
const geom = this.create_buffer_geometry(object);
materials = [DUMMY_MATERIAL, ...materials];
const max_mat_idx = this.builder.max_material_index || 0;
for (let i = materials.length - 1; i < max_mat_idx; ++i) {
materials.push(default_material);
}
const mesh = new SkinnedMesh(geom, materials);
mesh.add(this.bones[0]);
mesh.bind(new Skeleton(this.bones));
return mesh;
}
private object_to_geometry(
object: NjObject<NjModel>,
parent_bone: Bone | undefined,
@ -201,7 +113,7 @@ class ModelCreator {
bone.setRotationFromEuler(euler);
bone.scale.set(scale.x, scale.y, scale.z);
this.bones.push(bone);
this.builder.add_bone(bone);
if (parent_bone) {
parent_bone.add(bone);
@ -289,7 +201,7 @@ class ModelCreator {
}
for (const [bone_index, bone_weight] of bones) {
this.builder.add_bone(bone_index, bone_weight);
this.builder.add_bone_weight(bone_index, bone_weight);
}
}
}

View File

@ -10,7 +10,6 @@ import {
MeshLambertMaterial,
SkinnedMesh,
Texture,
Vector3,
} from "three";
import { Endianness } from "../data_formats";
import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor";
@ -18,14 +17,12 @@ import { NjModel, NjObject, parse_nj, parse_xj } from "../data_formats/parsing/n
import { NjMotion, parse_njm } from "../data_formats/parsing/ninja/motion";
import { parse_xvm } from "../data_formats/parsing/ninja/texture";
import { PlayerAnimation, PlayerModel } from "../domain";
import { read_file } from "../read_file";
import { create_animation_clip, PSO_FRAME_RATE } from "../rendering/conversion/ninja_animation";
import {
ninja_object_to_mesh,
ninja_object_to_skinned_mesh,
} from "../rendering/conversion/ninja_geometry";
import { xvm_to_textures } from "../rendering/conversion/ninja_textures";
import { get_player_animation_data, get_player_data } from "../loading/player";
import { read_file } from "../read_file";
import { create_skinned_mesh, create_mesh } from "../rendering/conversion/create_mesh";
import { create_animation_clip, PSO_FRAME_RATE } from "../rendering/conversion/ninja_animation";
import { ninja_object_to_buffer_geometry } from "../rendering/conversion/ninja_geometry";
import { xvm_to_textures } from "../rendering/conversion/ninja_textures";
const logger = Logger.get("stores/ModelViewerStore");
const nj_object_cache: Map<string, Promise<NjObject<NjModel>>> = new Map();
@ -294,7 +291,6 @@ class ModelViewerStore {
private set_obj3d = (textures?: Texture[]) => {
if (this.current_model) {
let mesh: Mesh;
let bb_size = new Vector3();
const materials =
textures &&
@ -309,13 +305,19 @@ class ModelViewerStore {
);
if (this.has_skeleton) {
mesh = ninja_object_to_skinned_mesh(this.current_model, materials);
mesh = create_skinned_mesh(
ninja_object_to_buffer_geometry(this.current_model),
materials
);
} else {
mesh = ninja_object_to_mesh(this.current_model, materials);
mesh = create_mesh(ninja_object_to_buffer_geometry(this.current_model), materials);
}
mesh.geometry.boundingBox.getSize(bb_size);
mesh.translateY(-bb_size.y / 2);
// Make sure we rotate around the center of the model.
const bb = mesh.geometry.boundingBox;
const height = bb.max.y - bb.min.y;
mesh.translateY(-height / 2 - bb.min.y);
this.current_obj3d = mesh;
}
};

View File

@ -3,11 +3,14 @@ import { Renderer } from "../rendering/Renderer";
import "./RendererComponent.less";
import { Camera } from "three";
export class RendererComponent extends Component<{
type Props = {
renderer: Renderer<Camera>;
debug?: boolean;
className?: string;
on_will_unmount?: () => void;
}> {
};
export class RendererComponent extends Component<Props> {
render(): ReactNode {
let className = "RendererComponent";
if (this.props.className) className += " " + this.props.className;
@ -15,6 +18,10 @@ export class RendererComponent extends Component<{
return <div className={className} ref={this.modifyDom} />;
}
componentWillReceiveProps(props: Props): void {
this.props.renderer.debug = !!props.debug;
}
componentDidMount(): void {
window.addEventListener("resize", this.onResize);
}

View File

@ -15,12 +15,14 @@ import { application_store } from "../../stores/ApplicationStore";
export class QuestEditorComponent extends Component<
{},
{
debug: boolean;
filename?: string;
save_dialog_open: boolean;
save_dialog_filename: string;
}
> {
state = {
debug: false,
save_dialog_open: false,
save_dialog_filename: "Untitled",
};
@ -37,7 +39,7 @@ export class QuestEditorComponent extends Component<
<Toolbar on_save_as_clicked={this.save_as_clicked} />
<div className="qe-QuestEditorComponent-main">
<QuestInfoComponent quest={quest} />
<RendererComponent renderer={get_quest_renderer()} />
<RendererComponent renderer={get_quest_renderer()} debug={this.state.debug} />
<EntityInfoComponent entity={quest_editor_store.selected_entity} />
</div>
<SaveAsForm
@ -82,6 +84,8 @@ export class QuestEditorComponent extends Component<
quest_editor_store.undo_stack.undo();
} else if (e.ctrlKey && e.key === "Z" && !e.altKey) {
quest_editor_store.undo_stack.redo();
} else if (e.ctrlKey && e.altKey && e.key === "d") {
this.setState(state => ({ debug: !state.debug }));
}
};
}