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 { QuestEntity, QuestNpc, QuestObject, Section } from "../domain";
import { quest_editor_store } from "../stores/QuestEditorStore"; import { quest_editor_store } from "../stores/QuestEditorStore";
import { AreaUserData } from "./conversion/areas"; import { AreaUserData } from "./conversion/areas";
import { import { ColorType, EntityUserData, NPC_COLORS, OBJECT_COLORS } from "./conversion/entities";
EntityUserData,
NPC_COLOR,
NPC_HIGHLIGHTED_COLOR,
NPC_SELECTED_COLOR,
OBJECT_COLOR,
OBJECT_HIGHLIGHTED_COLOR,
OBJECT_SELECTED_COLOR,
} from "./conversion/entities";
import { QuestRenderer } from "./QuestRenderer"; import { QuestRenderer } from "./QuestRenderer";
type Selection = { const DOWN_VECTOR = new Vector3(0, -1, 0);
type Highlighted = {
entity: QuestEntity; entity: QuestEntity;
mesh: Mesh; mesh: Mesh;
}; };
@ -32,16 +26,10 @@ type PickResult = Pick & {
mesh: Mesh; mesh: Mesh;
}; };
enum ColorType {
Normal,
Highlighted,
Selected,
}
export class QuestEntityControls { export class QuestEntityControls {
private raycaster = new Raycaster(); private raycaster = new Raycaster();
private selected?: Selection; private selected?: Highlighted;
private highlighted?: Selection; private hovered?: Highlighted;
/** /**
* Iff defined, the user is transforming the selected entity. * Iff defined, the user is transforming the selected entity.
*/ */
@ -118,8 +106,9 @@ export class QuestEntityControls {
const pointer_device_pos = this.renderer.pointer_pos_to_device_coords(e); const pointer_device_pos = this.renderer.pointer_pos_to_device_coords(e);
if (this.selected && this.pick) { if (this.selected && this.pick) {
// User is tranforming selected entity. if (this.moved_since_last_mouse_down) {
if (e.buttons === 1) { if (e.buttons === 1) {
// User is tranforming selected entity.
// User is dragging selected entity. // User is dragging selected entity.
if (e.shiftKey) { if (e.shiftKey) {
// Vertical movement. // Vertical movement.
@ -131,11 +120,12 @@ export class QuestEntityControls {
} }
this.renderer.schedule_render(); this.renderer.schedule_render();
}
} else { } else {
// User is hovering. // User is hovering.
const new_pick = this.pick_entity(pointer_device_pos); const new_pick = this.pick_entity(pointer_device_pos);
if (this.highlight(new_pick)) { if (this.mark_hovered(new_pick)) {
this.renderer.schedule_render(); this.renderer.schedule_render();
} }
} }
@ -159,32 +149,32 @@ export class QuestEntityControls {
/** /**
* @returns true if a render is required. * @returns true if a render is required.
*/ */
private highlight(selection?: Selection): boolean { private mark_hovered(selection?: Highlighted): boolean {
let render_required = false; let render_required = false;
if (!this.selected || !selection_equals(selection, this.selected)) { if (!this.selected || !selection_equals(selection, this.selected)) {
if (!selection_equals(selection, this.highlighted)) { if (!selection_equals(selection, this.hovered)) {
if (this.highlighted) { if (this.hovered) {
set_color(this.highlighted, ColorType.Normal); set_color(this.hovered, ColorType.Normal);
this.highlighted = undefined; this.hovered = undefined;
} }
if (selection) { if (selection) {
set_color(selection, ColorType.Highlighted); set_color(selection, ColorType.Hovered);
} }
render_required = true; render_required = true;
} }
this.highlighted = selection; this.hovered = selection;
} }
return render_required; return render_required;
} }
private select(selection: Selection): void { private select(selection: Highlighted): void {
if (selection_equals(selection, this.highlighted)) { if (selection_equals(selection, this.hovered)) {
this.highlighted = undefined; this.hovered = undefined;
} }
if (!selection_equals(selection, this.selected)) { if (!selection_equals(selection, this.selected)) {
@ -211,7 +201,7 @@ export class QuestEntityControls {
} }
private translate_vertically( private translate_vertically(
selection: Selection, selection: Highlighted,
pick: Pick, pick: Pick,
pointer_position: Vector2 pointer_position: Vector2
): void { ): void {
@ -241,7 +231,7 @@ export class QuestEntityControls {
} }
private translate_horizontally( private translate_horizontally(
selection: Selection, selection: Highlighted,
pick: Pick, pick: Pick,
pointer_position: Vector2 pointer_position: Vector2
): void { ): void {
@ -325,15 +315,15 @@ export class QuestEntityControls {
let drag_y = 0; let drag_y = 0;
// Find vertical distance to terrain. // Find vertical distance to terrain.
this.raycaster.set(intersection.object.position, new Vector3(0, -1, 0)); this.raycaster.set(intersection.object.position, DOWN_VECTOR);
const [terrain] = this.raycaster.intersectObjects( const [collision_geom_intersection] = this.raycaster.intersectObjects(
this.renderer.collision_geometry.children, this.renderer.collision_geometry.children,
true true
); );
if (terrain) { if (collision_geom_intersection) {
drag_adjust.sub(new Vector3(0, terrain.distance, 0)); drag_adjust.y -= collision_geom_intersection.distance;
drag_y += terrain.distance; drag_y += collision_geom_intersection.distance;
} }
return { return {
@ -358,7 +348,7 @@ export class QuestEntityControls {
} { } {
this.raycaster.setFromCamera(pointer_pos, this.renderer.camera); this.raycaster.setFromCamera(pointer_pos, this.renderer.camera);
this.raycaster.ray.origin.add(data.drag_adjust); this.raycaster.ray.origin.add(data.drag_adjust);
const terrains = this.raycaster.intersectObjects( const intersections = this.raycaster.intersectObjects(
this.renderer.collision_geometry.children, this.renderer.collision_geometry.children,
true true
); );
@ -366,19 +356,11 @@ export class QuestEntityControls {
// Don't allow entities to be placed on very steep terrain. // Don't allow entities to be placed on very steep terrain.
// E.g. walls. // E.g. walls.
// TODO: make use of the flags field in the collision data. // TODO: make use of the flags field in the collision data.
for (const terrain of terrains) { for (const intersection of intersections) {
if (terrain.face!.normal.y > 0.75) { if (intersection.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);
return { return {
intersection: terrain, intersection,
section: section: (intersection.object.userData as AreaUserData).section,
render_terrains[0] &&
(render_terrains[0].object.userData as AreaUserData).section,
}; };
} }
} }
@ -387,34 +369,24 @@ export class QuestEntityControls {
} }
} }
function set_color({ entity, mesh }: Selection, type: ColorType): void { function set_color({ entity, mesh }: Highlighted, type: ColorType): void {
const color = get_color(entity, type); const color = entity instanceof QuestNpc ? NPC_COLORS[type] : OBJECT_COLORS[type];
if (mesh) { if (mesh) {
for (const material of mesh.material as MeshLambertMaterial[]) { if (Array.isArray(mesh.material)) {
if (type === ColorType.Normal && material.map) { for (const mat of mesh.material as MeshLambertMaterial[]) {
material.color.set(0xffffff); if (type === ColorType.Normal && mat.map) {
mat.color.set(0xffffff);
} else { } else {
material.color.set(color); 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; 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 Logger from "js-logger";
import { autorun, IReactionDisposer } from "mobx"; 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 { Area, Quest, QuestEntity } from "../domain";
import { load_area_collision_geometry, load_area_render_geometry } from "../loading/areas"; import { load_area_collision_geometry, load_area_render_geometry } from "../loading/areas";
import { import {
@ -11,6 +11,7 @@ import {
} from "../loading/entities"; } from "../loading/entities";
import { create_npc_mesh, create_object_mesh } from "./conversion/entities"; import { create_npc_mesh, create_object_mesh } from "./conversion/entities";
import { QuestRenderer } from "./QuestRenderer"; import { QuestRenderer } from "./QuestRenderer";
import { AreaUserData } from "./conversion/areas";
const logger = Logger.get("rendering/QuestModelManager"); const logger = Logger.get("rendering/QuestModelManager");
@ -55,6 +56,8 @@ export class QuestModelManager {
variant_id variant_id
); );
this.add_sections_to_collision_geometry(collision_geometry, render_geometry);
if (this.quest !== quest || this.area !== area) return; if (this.quest !== quest || this.area !== area) return;
this.renderer.collision_geometry = collision_geometry; 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 { private update_entity_geometry(entity: QuestEntity, model: Mesh): void {
this.renderer.add_entity_model(model); this.renderer.add_entity_model(model);

View File

@ -15,6 +15,18 @@ export function get_quest_renderer(): QuestRenderer {
} }
export class QuestRenderer extends Renderer<PerspectiveCamera> { 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(); private _collision_geometry = new Object3D();
get collision_geometry(): Object3D { get collision_geometry(): Object3D {
@ -34,9 +46,10 @@ export class QuestRenderer extends Renderer<PerspectiveCamera> {
} }
set render_geometry(render_geometry: Object3D) { set render_geometry(render_geometry: Object3D) {
// this.scene.remove(this._render_geometry); this.scene.remove(this._render_geometry);
this._render_geometry = 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(); private _entity_models = new Object3D();

View File

@ -15,6 +15,16 @@ import OrbitControlsCreator from "three-orbit-controls";
const OrbitControls = OrbitControlsCreator(THREE); const OrbitControls = OrbitControlsCreator(THREE);
export class Renderer<C extends Camera> { 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 camera: C;
readonly controls: any; readonly controls: any;
readonly scene = new Scene(); 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 = { export type BuilderVec2 = {
x: number; x: number;
@ -22,10 +37,14 @@ export class GeometryBuilder {
private normals: number[] = []; private normals: number[] = [];
private uvs: number[] = []; private uvs: number[] = [];
private indices: number[] = []; private indices: number[] = [];
private bones: Bone[] = [];
private bone_indices: number[] = []; private bone_indices: number[] = [];
private bone_weights: number[] = []; private bone_weights: number[] = [];
private groups: VertexGroup[] = []; 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 { get vertex_count(): number {
return this.positions.length / 3; return this.positions.length / 3;
@ -35,10 +54,6 @@ export class GeometryBuilder {
return this.indices.length; return this.indices.length;
} }
get max_material_index(): number | undefined {
return this._max_material_index;
}
get_position(index: number): Vector3 { get_position(index: number): Vector3 {
return new Vector3( return new Vector3(
this.positions[3 * index], this.positions[3 * index],
@ -65,14 +80,18 @@ export class GeometryBuilder {
this.indices.push(index); 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_indices.push(index);
this.bone_weights.push(weight); 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 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) { if (last_group && last_group.material_index === mat_idx) {
last_group.size += size; last_group.size += size;
@ -82,15 +101,14 @@ export class GeometryBuilder {
size, size,
material_index: mat_idx, material_index: mat_idx,
}); });
this.material_indices.add(mat_idx);
this._max_material_index = this._max_material_index
? Math.max(this._max_material_index, mat_idx)
: mat_idx;
} }
} }
build(): BufferGeometry { build(): BufferGeometry {
const geom = new 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("position", new Float32BufferAttribute(this.positions, 3));
geom.addAttribute("normal", new Float32BufferAttribute(this.normals, 3)); geom.addAttribute("normal", new Float32BufferAttribute(this.normals, 3));
@ -98,15 +116,29 @@ export class GeometryBuilder {
geom.setIndex(new Uint16BufferAttribute(this.indices, 1)); geom.setIndex(new Uint16BufferAttribute(this.indices, 1));
for (const group of this.groups) { if (this.bone_indices.length && this.bones.length) {
geom.addGroup(group.offset, group.size, group.material_index);
}
if (this.bone_indices.length) {
geom.addAttribute("skinIndex", new Uint16BufferAttribute(this.bone_indices, 4)); geom.addAttribute("skinIndex", new Uint16BufferAttribute(this.bone_indices, 4));
geom.addAttribute("skinWeight", new Float32BufferAttribute(this.bone_weights, 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.computeBoundingSphere();
geom.computeBoundingBox(); geom.computeBoundingBox();

View File

@ -8,12 +8,13 @@ import {
MeshLambertMaterial, MeshLambertMaterial,
Object3D, Object3D,
Vector3, Vector3,
Color,
} from "three"; } from "three";
import { CollisionObject } from "../../data_formats/parsing/area_collision_geometry"; import { CollisionObject } from "../../data_formats/parsing/area_collision_geometry";
import { RenderObject } from "../../data_formats/parsing/area_geometry"; import { RenderObject } from "../../data_formats/parsing/area_geometry";
import { Section } from "../../domain"; import { Section } from "../../domain";
import { ninja_object_to_mesh, ninja_object_to_geometry_builder } from "./ninja_geometry";
import { GeometryBuilder } from "./GeometryBuilder"; import { GeometryBuilder } from "./GeometryBuilder";
import { ninja_object_to_geometry_builder } from "./ninja_geometry";
const materials = [ const materials = [
// Wall // Wall
@ -65,6 +66,10 @@ const wireframe_materials = [
}), }),
]; ];
export type AreaUserData = {
section?: Section;
};
export function area_collision_geometry_to_object_3d(object: CollisionObject): Object3D { export function area_collision_geometry_to_object_3d(object: CollisionObject): Object3D {
const group = new Group(); 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); const mesh = new Mesh(geom, materials);
mesh.renderOrder = 1; mesh.renderOrder = 1;
group.add(mesh); group.add(mesh);
@ -106,20 +114,14 @@ export function area_collision_geometry_to_object_3d(object: CollisionObject): O
return group; return group;
} }
export type AreaUserData = {
section: Section;
};
export function area_geometry_to_sections_and_object_3d( export function area_geometry_to_sections_and_object_3d(
object: RenderObject object: RenderObject
): [Section[], Object3D] { ): [Section[], Object3D] {
const sections: Section[] = []; const sections: Section[] = [];
const group = new Group(); const group = new Group();
let i = 0;
for (const section of object.sections) { for (const section of object.sections) {
const sec = new Section(section.id, section.position, section.rotation.y);
sections.push(sec);
const builder = new GeometryBuilder(); const builder = new GeometryBuilder();
for (const object of section.objects) { for (const object of section.objects) {
@ -128,16 +130,23 @@ export function area_geometry_to_sections_and_object_3d(
const mesh = new Mesh( const mesh = new Mesh(
builder.build(), builder.build(),
new MeshLambertMaterial({ new MeshBasicMaterial({
color: 0x44aaff, color: new Color().setHSL((i++ % 7) / 7, 1, 0.5),
transparent: true, transparent: true,
opacity: 0.75, opacity: 0.25,
side: DoubleSide, side: DoubleSide,
}) })
); );
(mesh.userData as AreaUserData).section = sec;
group.add(mesh); 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]; 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 { import { BufferGeometry, DoubleSide, Mesh, MeshLambertMaterial, Texture } from "three";
BufferGeometry,
DoubleSide,
Mesh,
MeshBasicMaterial,
MeshLambertMaterial,
Texture,
Material,
} from "three";
import { QuestEntity, QuestNpc, QuestObject } from "../../domain"; import { QuestEntity, QuestNpc, QuestObject } from "../../domain";
import { create_mesh } from "./create_mesh";
export const OBJECT_COLOR = 0xffff00; export enum ColorType {
export const OBJECT_HIGHLIGHTED_COLOR = 0xffdf3f; Normal,
export const OBJECT_SELECTED_COLOR = 0xffaa00; Hovered,
export const NPC_COLOR = 0xff0000; Selected,
export const NPC_HIGHLIGHTED_COLOR = 0xff3f5f; }
export const NPC_SELECTED_COLOR = 0xff0054;
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 = { export type EntityUserData = {
entity: QuestEntity; entity: QuestEntity;
@ -25,7 +27,7 @@ export function create_object_mesh(
geometry: BufferGeometry, geometry: BufferGeometry,
textures: Texture[] textures: Texture[]
): Mesh { ): 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( export function create_npc_mesh(
@ -33,27 +35,25 @@ export function create_npc_mesh(
geometry: BufferGeometry, geometry: BufferGeometry,
textures: Texture[] textures: Texture[]
): Mesh { ): 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, entity: QuestEntity,
geometry: BufferGeometry, geometry: BufferGeometry,
textures: Texture[], textures: Texture[],
color: number, color: number,
type: string name: string
): Mesh { ): Mesh {
const max_mat_idx = geometry.groups.reduce((max, g) => Math.max(max, g.materialIndex || 0), 0); const default_material = new MeshLambertMaterial({
const materials: Material[] = [
new MeshBasicMaterial({
color, color,
side: DoubleSide, side: DoubleSide,
}), });
];
materials.push( const mesh = create_mesh(
...textures.map( geometry,
textures.length
? textures.map(
tex => tex =>
new MeshLambertMaterial({ new MeshLambertMaterial({
map: tex, map: tex,
@ -61,19 +61,11 @@ function create_mesh(
alphaTest: 0.5, alphaTest: 0.5,
}) })
) )
: default_material,
default_material
); );
for (let i = materials.length - 1; i < max_mat_idx; ++i) { mesh.name = name;
materials.push(
new MeshLambertMaterial({
color,
side: DoubleSide,
})
);
}
const mesh = new Mesh(geometry, materials);
mesh.name = type;
(mesh.userData as EntityUserData).entity = entity; (mesh.userData as EntityUserData).entity = entity;
const { x, y, z } = entity.position; const { x, y, z } = entity.position;

View File

@ -1,39 +1,10 @@
import { import { Bone, BufferGeometry, Euler, Matrix3, Matrix4, Quaternion, Vector2, Vector3 } from "three";
Bone,
BufferGeometry,
DoubleSide,
Euler,
Material,
Matrix3,
Matrix4,
Mesh,
MeshBasicMaterial,
MeshLambertMaterial,
Quaternion,
Skeleton,
SkinnedMesh,
Vector2,
Vector3,
} from "three";
import { vec3_to_threejs } from "."; import { vec3_to_threejs } from ".";
import { is_njcm_model, NjModel, NjObject } from "../../data_formats/parsing/ninja"; import { is_njcm_model, NjModel, NjObject } from "../../data_formats/parsing/ninja";
import { NjcmModel } from "../../data_formats/parsing/ninja/njcm"; import { NjcmModel } from "../../data_formats/parsing/ninja/njcm";
import { XjModel } from "../../data_formats/parsing/ninja/xj"; import { XjModel } from "../../data_formats/parsing/ninja/xj";
import { GeometryBuilder } from "./GeometryBuilder"; 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_NORMAL = new Vector3(0, 1, 0);
const DEFAULT_UV = new Vector2(0, 0); const DEFAULT_UV = new Vector2(0, 0);
const NO_TRANSLATION = new Vector3(0, 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( export function ninja_object_to_geometry_builder(
object: NjObject<NjModel>, object: NjObject<NjModel>,
builder: GeometryBuilder builder: GeometryBuilder
) { ): void {
new ModelCreator(builder).to_geometry_builder(object); new GeometryCreator(builder).to_geometry_builder(object);
} }
export function ninja_object_to_buffer_geometry(object: NjObject<NjModel>): BufferGeometry { export function ninja_object_to_buffer_geometry(object: NjObject<NjModel>): BufferGeometry {
return new ModelCreator(new GeometryBuilder()).create_buffer_geometry(object); return new GeometryCreator(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
);
} }
type Vertex = { type Vertex = {
@ -102,17 +53,16 @@ class VerticesHolder {
} }
} }
class ModelCreator { class GeometryCreator {
private vertices = new VerticesHolder(); private vertices = new VerticesHolder();
private bone_id: number = 0; private bone_id: number = 0;
private bones: Bone[] = [];
private builder: GeometryBuilder; private builder: GeometryBuilder;
constructor(builder: GeometryBuilder) { constructor(builder: GeometryBuilder) {
this.builder = builder; this.builder = builder;
} }
to_geometry_builder(object: NjObject<NjModel>) { to_geometry_builder(object: NjObject<NjModel>): void {
this.object_to_geometry(object, undefined, new Matrix4()); this.object_to_geometry(object, undefined, new Matrix4());
} }
@ -121,44 +71,6 @@ class ModelCreator {
return this.builder.build(); 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( private object_to_geometry(
object: NjObject<NjModel>, object: NjObject<NjModel>,
parent_bone: Bone | undefined, parent_bone: Bone | undefined,
@ -201,7 +113,7 @@ class ModelCreator {
bone.setRotationFromEuler(euler); bone.setRotationFromEuler(euler);
bone.scale.set(scale.x, scale.y, scale.z); bone.scale.set(scale.x, scale.y, scale.z);
this.bones.push(bone); this.builder.add_bone(bone);
if (parent_bone) { if (parent_bone) {
parent_bone.add(bone); parent_bone.add(bone);
@ -289,7 +201,7 @@ class ModelCreator {
} }
for (const [bone_index, bone_weight] of bones) { 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, MeshLambertMaterial,
SkinnedMesh, SkinnedMesh,
Texture, Texture,
Vector3,
} from "three"; } from "three";
import { Endianness } from "../data_formats"; import { Endianness } from "../data_formats";
import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor"; 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 { NjMotion, parse_njm } from "../data_formats/parsing/ninja/motion";
import { parse_xvm } from "../data_formats/parsing/ninja/texture"; import { parse_xvm } from "../data_formats/parsing/ninja/texture";
import { PlayerAnimation, PlayerModel } from "../domain"; 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 { 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 logger = Logger.get("stores/ModelViewerStore");
const nj_object_cache: Map<string, Promise<NjObject<NjModel>>> = new Map(); const nj_object_cache: Map<string, Promise<NjObject<NjModel>>> = new Map();
@ -294,7 +291,6 @@ class ModelViewerStore {
private set_obj3d = (textures?: Texture[]) => { private set_obj3d = (textures?: Texture[]) => {
if (this.current_model) { if (this.current_model) {
let mesh: Mesh; let mesh: Mesh;
let bb_size = new Vector3();
const materials = const materials =
textures && textures &&
@ -309,13 +305,19 @@ class ModelViewerStore {
); );
if (this.has_skeleton) { 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 { } 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); // Make sure we rotate around the center of the model.
mesh.translateY(-bb_size.y / 2); const bb = mesh.geometry.boundingBox;
const height = bb.max.y - bb.min.y;
mesh.translateY(-height / 2 - bb.min.y);
this.current_obj3d = mesh; this.current_obj3d = mesh;
} }
}; };

View File

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

View File

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