From f6707186371dc67feee5ec6f5c5001aebc286c6e Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Fri, 19 Jul 2019 21:49:59 +0200 Subject: [PATCH] Simplified mesh creation. Some performance improvements. Added debug mode to quest viewer that shows per-section colored area render geometry. --- src/rendering/QuestEntityControls.ts | 136 ++++++++----------- src/rendering/QuestModelManager.ts | 46 ++++++- src/rendering/QuestRenderer.ts | 17 ++- src/rendering/Renderer.ts | 10 ++ src/rendering/conversion/GeometryBuilder.ts | 68 +++++++--- src/rendering/conversion/areas.ts | 35 +++-- src/rendering/conversion/create_mesh.ts | 82 +++++++++++ src/rendering/conversion/entities.ts | 86 ++++++------ src/rendering/conversion/ninja_geometry.ts | 104 ++------------ src/stores/ModelViewerStore.ts | 28 ++-- src/ui/RendererComponent.tsx | 11 +- src/ui/quest_editor/QuestEditorComponent.tsx | 6 +- 12 files changed, 354 insertions(+), 275 deletions(-) create mode 100644 src/rendering/conversion/create_mesh.ts diff --git a/src/rendering/QuestEntityControls.ts b/src/rendering/QuestEntityControls.ts index 9949cae3..3768451a 100644 --- a/src/rendering/QuestEntityControls.ts +++ b/src/rendering/QuestEntityControls.ts @@ -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; - } -} diff --git a/src/rendering/QuestModelManager.ts b/src/rendering/QuestModelManager.ts index b28637b2..cf7917dc 100644 --- a/src/rendering/QuestModelManager.ts +++ b/src/rendering/QuestModelManager.ts @@ -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); diff --git a/src/rendering/QuestRenderer.ts b/src/rendering/QuestRenderer.ts index a58becd8..4ae8ac6c 100644 --- a/src/rendering/QuestRenderer.ts +++ b/src/rendering/QuestRenderer.ts @@ -15,6 +15,18 @@ export function get_quest_renderer(): QuestRenderer { } export class QuestRenderer extends Renderer { + 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 { } 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(); diff --git a/src/rendering/Renderer.ts b/src/rendering/Renderer.ts index b38e9bd7..a26337c3 100644 --- a/src/rendering/Renderer.ts +++ b/src/rendering/Renderer.ts @@ -15,6 +15,16 @@ import OrbitControlsCreator from "three-orbit-controls"; const OrbitControls = OrbitControlsCreator(THREE); export class Renderer { + 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(); diff --git a/src/rendering/conversion/GeometryBuilder.ts b/src/rendering/conversion/GeometryBuilder.ts index 15f8f034..dab40dad 100644 --- a/src/rendering/conversion/GeometryBuilder.ts +++ b/src/rendering/conversion/GeometryBuilder.ts @@ -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; + 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([-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(); + 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(); diff --git a/src/rendering/conversion/areas.ts b/src/rendering/conversion/areas.ts index 6731b5e3..37a879d9 100644 --- a/src/rendering/conversion/areas.ts +++ b/src/rendering/conversion/areas.ts @@ -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]; diff --git a/src/rendering/conversion/create_mesh.ts b/src/rendering/conversion/create_mesh.ts new file mode 100644 index 00000000..c65cde76 --- /dev/null +++ b/src/rendering/conversion/create_mesh.ts @@ -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( + 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; +} diff --git a/src/rendering/conversion/entities.ts b/src/rendering/conversion/entities.ts index a737c746..7ce35d1a 100644 --- a/src/rendering/conversion/entities.ts +++ b/src/rendering/conversion/entities.ts @@ -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; diff --git a/src/rendering/conversion/ninja_geometry.ts b/src/rendering/conversion/ninja_geometry.ts index 2fdc1136..88fda4ee 100644 --- a/src/rendering/conversion/ninja_geometry.ts +++ b/src/rendering/conversion/ninja_geometry.ts @@ -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, 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): BufferGeometry { - return new ModelCreator(new GeometryBuilder()).create_buffer_geometry(object); -} - -export function ninja_object_to_mesh( - object: NjObject, - 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, - 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) { + to_geometry_builder(object: NjObject): void { this.object_to_geometry(object, undefined, new Matrix4()); } @@ -121,44 +71,6 @@ class ModelCreator { return this.builder.build(); } - create_mesh( - object: NjObject, - 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, - 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, 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); } } } diff --git a/src/stores/ModelViewerStore.ts b/src/stores/ModelViewerStore.ts index 36af780a..35609c64 100644 --- a/src/stores/ModelViewerStore.ts +++ b/src/stores/ModelViewerStore.ts @@ -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>> = 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; } }; diff --git a/src/ui/RendererComponent.tsx b/src/ui/RendererComponent.tsx index 6252ff86..24f5fe83 100644 --- a/src/ui/RendererComponent.tsx +++ b/src/ui/RendererComponent.tsx @@ -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; + debug?: boolean; className?: string; on_will_unmount?: () => void; -}> { +}; + +export class RendererComponent extends Component { render(): ReactNode { let className = "RendererComponent"; if (this.props.className) className += " " + this.props.className; @@ -15,6 +18,10 @@ export class RendererComponent extends Component<{ return
; } + componentWillReceiveProps(props: Props): void { + this.props.renderer.debug = !!props.debug; + } + componentDidMount(): void { window.addEventListener("resize", this.onResize); } diff --git a/src/ui/quest_editor/QuestEditorComponent.tsx b/src/ui/quest_editor/QuestEditorComponent.tsx index fd8fc5fd..0a9cc0ad 100644 --- a/src/ui/quest_editor/QuestEditorComponent.tsx +++ b/src/ui/quest_editor/QuestEditorComponent.tsx @@ -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<
- +
({ debug: !state.debug })); } }; }