From 46eb7cfdd07d16c334e5f64761ef48fe288eab2f Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Sat, 13 Jul 2019 17:09:28 +0200 Subject: [PATCH] Textures can now be applied to XJ models in the model viewer. --- src/data_formats/cursor/ArrayBufferCursor.ts | 8 +- src/data_formats/cursor/Cursor.ts | 14 ++- .../cursor/ResizableBufferCursor.ts | 8 +- src/data_formats/parsing/area_geometry.ts | 2 +- src/data_formats/parsing/ninja/index.ts | 15 +--- src/data_formats/parsing/ninja/njcm.ts | 41 +++++---- src/data_formats/parsing/ninja/xj.ts | 88 ++++++++++++++++--- src/rendering/areas.ts | 2 +- src/rendering/models.ts | 52 +++++++++-- src/rendering/xj_model_to_geometry.ts | 31 ++++++- src/stores/ModelViewerStore.ts | 39 ++++---- 11 files changed, 216 insertions(+), 84 deletions(-) diff --git a/src/data_formats/cursor/ArrayBufferCursor.ts b/src/data_formats/cursor/ArrayBufferCursor.ts index 3846a4b0..d75d85de 100644 --- a/src/data_formats/cursor/ArrayBufferCursor.ts +++ b/src/data_formats/cursor/ArrayBufferCursor.ts @@ -7,7 +7,7 @@ import { } from "."; import { Endianness } from ".."; import { Cursor } from "./Cursor"; -import { Vec3 } from "../vector"; +import { Vec3, Vec2 } from "../vector"; /** * A cursor for reading from an array buffer or part of an array buffer. @@ -187,7 +187,11 @@ export class ArrayBufferCursor implements Cursor { return array; } - vec3(): Vec3 { + vec2_f32(): Vec2 { + return new Vec2(this.f32(), this.f32()); + } + + vec3_f32(): Vec3 { return new Vec3(this.f32(), this.f32(), this.f32()); } diff --git a/src/data_formats/cursor/Cursor.ts b/src/data_formats/cursor/Cursor.ts index d3011122..f85bf7f1 100644 --- a/src/data_formats/cursor/Cursor.ts +++ b/src/data_formats/cursor/Cursor.ts @@ -1,5 +1,5 @@ import { Endianness } from ".."; -import { Vec3 } from "../vector"; +import { Vec3, Vec2 } from "../vector"; /** * A cursor for reading binary data. @@ -109,7 +109,7 @@ export interface Cursor { f32(): number; /** - * Reads a 32-bit floating point number/ Doesn't increment position. + * Reads a 32-bit floating point number. Doesn't increment position. */ f32_at(offset: number): number; @@ -128,7 +128,15 @@ export interface Cursor { */ u32_array(n: number): number[]; - vec3(): Vec3; + /** + * Reads 2 32-bit floating point numbers and increments position by 8. + */ + vec2_f32(): Vec2; + + /** + * Reads 3 32-bit floating point numbers and increments position by 12. + */ + vec3_f32(): Vec3; /** * Consumes a variable number of bytes. diff --git a/src/data_formats/cursor/ResizableBufferCursor.ts b/src/data_formats/cursor/ResizableBufferCursor.ts index 167d39f8..8283e2a8 100644 --- a/src/data_formats/cursor/ResizableBufferCursor.ts +++ b/src/data_formats/cursor/ResizableBufferCursor.ts @@ -8,7 +8,7 @@ import { import { Endianness } from ".."; import { ResizableBuffer } from "../ResizableBuffer"; import { Cursor } from "./Cursor"; -import { Vec3 } from "../vector"; +import { Vec3, Vec2 } from "../vector"; export class ResizableBufferCursor implements Cursor { private _offset: number; @@ -214,7 +214,11 @@ export class ResizableBufferCursor implements Cursor { return array; } - vec3(): Vec3 { + vec2_f32(): Vec2 { + return new Vec2(this.f32(), this.f32()); + } + + vec3_f32(): Vec3 { return new Vec3(this.f32(), this.f32(), this.f32()); } diff --git a/src/data_formats/parsing/area_geometry.ts b/src/data_formats/parsing/area_geometry.ts index 9c3c52cf..3a174563 100644 --- a/src/data_formats/parsing/area_geometry.ts +++ b/src/data_formats/parsing/area_geometry.ts @@ -37,7 +37,7 @@ export function parse_area_geometry(cursor: Cursor): RenderObject { cursor.seek_start(section_table_offset + 52 * i); const section_id = cursor.i32(); - const section_position = cursor.vec3(); + const section_position = cursor.vec3_f32(); const section_rotation = new Vec3( cursor.u32() * ANGLE_TO_RAD, cursor.u32() * ANGLE_TO_RAD, diff --git a/src/data_formats/parsing/ninja/index.ts b/src/data_formats/parsing/ninja/index.ts index 5d418e5e..415179d6 100644 --- a/src/data_formats/parsing/ninja/index.ts +++ b/src/data_formats/parsing/ninja/index.ts @@ -1,25 +1,13 @@ import { Cursor } from "../../cursor/Cursor"; import { Vec3 } from "../../vector"; +import { parse_iff } from "../iff"; import { NjcmModel, parse_njcm_model } from "./njcm"; import { parse_xj_model, XjModel } from "./xj"; -import { parse_iff } from "../iff"; - -// TODO: -// - deal with multiple NJCM chunks -// - deal with other types of chunks export const ANGLE_TO_RAD = (2 * Math.PI) / 0xffff; const NJCM = 0x4d434a4e; -export type NjVertex = { - position: Vec3; - normal?: Vec3; - bone_weight: number; - bone_weight_status: number; - calc_continue: boolean; -}; - export type NjModel = NjcmModel | XjModel; export function is_njcm_model(model: NjModel): model is NjcmModel { @@ -126,6 +114,7 @@ function parse_ninja( parse_model: (cursor: Cursor, context: any) => M, context: any ): NjObject[] { + // POF0 and other chunks types are ignored. return parse_iff(cursor) .filter(chunk => chunk.type === NJCM) .flatMap(chunk => parse_sibling_objects(chunk.data, parse_model, context)); diff --git a/src/data_formats/parsing/ninja/njcm.ts b/src/data_formats/parsing/ninja/njcm.ts index da7ae711..f88a1cdf 100644 --- a/src/data_formats/parsing/ninja/njcm.ts +++ b/src/data_formats/parsing/ninja/njcm.ts @@ -1,29 +1,32 @@ import Logger from "js-logger"; -import { NjVertex } from "."; import { Cursor } from "../../cursor/Cursor"; -import { Vec3, Vec2 } from "../../vector"; +import { Vec2, Vec3 } from "../../vector"; const logger = Logger.get("data_formats/parsing/ninja/njcm"); // TODO: -// - textures // - colors // - bump maps -// - animation -// - deal with vertex information contained in triangle strips export type NjcmModel = { type: "njcm"; /** * Sparse array of vertices. */ - vertices: NjVertex[]; + vertices: NjcmVertex[]; meshes: NjcmTriangleStrip[]; - // materials: [], collision_sphere_center: Vec3; collision_sphere_radius: number; }; +export type NjcmVertex = { + position: Vec3; + normal?: Vec3; + bone_weight: number; + bone_weight_status: number; + calc_continue: boolean; +}; + enum NjcmChunkType { Unknown, Null, @@ -95,7 +98,7 @@ type NjcmMaterialChunk = { type NjcmVertexChunk = { type: NjcmChunkType.Vertex; - vertices: NjcmVertex[]; + vertices: NjcmChunkVertex[]; }; type NjcmVolumeChunk = { @@ -111,7 +114,7 @@ type NjcmEndChunk = { type: NjcmChunkType.End; }; -type NjcmVertex = { +type NjcmChunkVertex = { index: number; position: Vec3; normal?: Vec3; @@ -144,9 +147,9 @@ type NjcmMeshVertex = { export function parse_njcm_model(cursor: Cursor, cached_chunk_offsets: number[]): NjcmModel { const vlist_offset = cursor.u32(); // Vertex list const plist_offset = cursor.u32(); // Triangle strip index list - const bounding_sphere_center = cursor.vec3(); + const bounding_sphere_center = cursor.vec3_f32(); const bounding_sphere_radius = cursor.f32(); - const vertices: NjVertex[] = []; + const vertices: NjcmVertex[] = []; const meshes: NjcmTriangleStrip[] = []; if (vlist_offset) { @@ -307,7 +310,11 @@ function parse_chunks( return chunks; } -function parse_vertex_chunk(cursor: Cursor, chunk_type_id: number, flags: number): NjcmVertex[] { +function parse_vertex_chunk( + cursor: Cursor, + chunk_type_id: number, + flags: number +): NjcmChunkVertex[] { if (chunk_type_id < 32 || chunk_type_id > 50) { logger.warn(`Unknown vertex chunk type ${chunk_type_id}.`); return []; @@ -319,12 +326,12 @@ function parse_vertex_chunk(cursor: Cursor, chunk_type_id: number, flags: number const index = cursor.u16(); const vertex_count = cursor.u16(); - const vertices: NjcmVertex[] = []; + const vertices: NjcmChunkVertex[] = []; for (let i = 0; i < vertex_count; ++i) { - const vertex: NjcmVertex = { + const vertex: NjcmChunkVertex = { index: index + i, - position: cursor.vec3(), + position: cursor.vec3_f32(), bone_weight: 1, bone_weight_status, calc_continue, @@ -336,7 +343,7 @@ function parse_vertex_chunk(cursor: Cursor, chunk_type_id: number, flags: number } else if (chunk_type_id === 33) { // NJD_CV_VN_SH cursor.seek(4); // Always 1.0 - vertex.normal = cursor.vec3(); + vertex.normal = cursor.vec3_f32(); cursor.seek(4); // Always 0.0 } else if (35 <= chunk_type_id && chunk_type_id <= 40) { if (chunk_type_id === 37) { @@ -349,7 +356,7 @@ function parse_vertex_chunk(cursor: Cursor, chunk_type_id: number, flags: number cursor.seek(4); } } else if (41 <= chunk_type_id && chunk_type_id <= 47) { - vertex.normal = cursor.vec3(); + vertex.normal = cursor.vec3_f32(); if (chunk_type_id >= 42) { if (chunk_type_id === 44) { diff --git a/src/data_formats/parsing/ninja/xj.ts b/src/data_formats/parsing/ninja/xj.ts index ecaf5f18..1812d496 100644 --- a/src/data_formats/parsing/ninja/xj.ts +++ b/src/data_formats/parsing/ninja/xj.ts @@ -1,28 +1,42 @@ import Logger from "js-logger"; import { Cursor } from "../../cursor/Cursor"; -import { Vec3 } from "../../vector"; -import { NjVertex } from "../ninja"; +import { Vec2, Vec3 } from "../../vector"; const logger = Logger.get("data_formats/parsing/ninja/xj"); // TODO: -// - textures -// - colors +// - vertex colors // - bump maps -// - animation export type XjModel = { type: "xj"; - vertices: NjVertex[]; + vertices: XjVertex[]; meshes: XjMesh[]; collision_sphere_position: Vec3; collision_sphere_radius: number; }; +export type XjVertex = { + position: Vec3; + normal?: Vec3; + uv?: Vec2; +}; + export type XjMesh = { + material_properties: XjMaterialProperties; indices: number[]; }; +export type XjMaterialProperties = { + alpha_src?: number; + alpha_dst?: number; + texture_id?: number; + diffuse_r?: number; + diffuse_g?: number; + diffuse_b?: number; + diffuse_a?: number; +}; + export function parse_xj_model(cursor: Cursor): XjModel { cursor.seek(4); // Flags according to QEdit, seemingly always 0. const vertex_info_table_offset = cursor.u32(); @@ -31,7 +45,7 @@ export function parse_xj_model(cursor: Cursor): XjModel { const triangle_strip_count = cursor.u32(); const transparent_triangle_strip_table_offset = cursor.u32(); const transparent_triangle_strip_count = cursor.u32(); - const collision_sphere_position = cursor.vec3(); + const collision_sphere_position = cursor.vec3_f32(); const collision_sphere_radius = cursor.f32(); const model: XjModel = { @@ -49,6 +63,8 @@ export function parse_xj_model(cursor: Cursor): XjModel { cursor.seek_start(vertex_info_table_offset); cursor.seek(4); // Vertex type. + // Vertex Types + // 3) size: 32, has position, normal, uv const vertex_table_offset = cursor.u32(); const vertex_size = cursor.u32(); const vertex_count = cursor.u32(); @@ -56,19 +72,22 @@ export function parse_xj_model(cursor: Cursor): XjModel { for (let i = 0; i < vertex_count; ++i) { cursor.seek_start(vertex_table_offset + i * vertex_size); - const position = cursor.vec3(); + const position = cursor.vec3_f32(); let normal: Vec3 | undefined; + let uv: Vec2 | undefined; if (vertex_size === 28 || vertex_size === 32 || vertex_size === 36) { - normal = cursor.vec3(); + normal = cursor.vec3_f32(); + } + + if (vertex_size === 24 || vertex_size === 32 || vertex_size === 36) { + uv = cursor.vec2_f32(); } model.vertices.push({ position, normal, - bone_weight: 1.0, - bone_weight_status: 0, - calc_continue: true, + uv, }); } } @@ -102,17 +121,60 @@ function parse_triangle_strip_table( for (let i = 0; i < triangle_strip_count; ++i) { cursor.seek_start(triangle_strip_list_offset + i * 20); - cursor.seek(8); // Skipping flag_and_texture_id_offset and data_type? + const material_table_offset = cursor.u32(); + const material_table_size = cursor.u32(); const index_list_offset = cursor.u32(); const index_count = cursor.u32(); + const material_properties = parse_triangle_strip_material_properties( + cursor, + material_table_offset, + material_table_size + ); + cursor.seek_start(index_list_offset); const indices = cursor.u16_array(index_count); strips.push({ + material_properties, indices, }); } return strips; } + +function parse_triangle_strip_material_properties( + cursor: Cursor, + offset: number, + size: number +): XjMaterialProperties { + const props: XjMaterialProperties = {}; + + for (let i = 0; i < size; ++i) { + cursor.seek_start(offset + i * 16); + + const type = cursor.u32(); + + switch (type) { + case 2: + props.alpha_src = cursor.u32(); + props.alpha_dst = cursor.u32(); + break; + case 3: + props.texture_id = cursor.u32(); + break; + case 5: + { + const rgba = cursor.u32(); + props.diffuse_r = (rgba & 0xff) / 0xff; + props.diffuse_g = ((rgba >>> 8) & 0xff) / 0xff; + props.diffuse_b = ((rgba >>> 16) & 0xff) / 0xff; + props.diffuse_a = ((rgba >>> 24) & 0xff) / 0xff; + } + break; + } + } + + return props; +} diff --git a/src/rendering/areas.ts b/src/rendering/areas.ts index 8f46d87a..6ddd3747 100644 --- a/src/rendering/areas.ts +++ b/src/rendering/areas.ts @@ -119,7 +119,7 @@ export function area_geometry_to_sections_and_object_3d( const indices: number[] = []; for (const model of section.models) { - xj_model_to_geometry(model, new Matrix4(), positions, normals, indices); + xj_model_to_geometry(model, new Matrix4(), positions, normals, [], indices, []); } const geometry = new BufferGeometry(); diff --git a/src/rendering/models.ts b/src/rendering/models.ts index 74ed5b7b..02647aa2 100644 --- a/src/rendering/models.ts +++ b/src/rendering/models.ts @@ -14,6 +14,7 @@ import { Uint16BufferAttribute, Vector3, MeshBasicMaterial, + Mesh, } from "three"; import { vec3_to_threejs } from "."; import { is_njcm_model, NjModel, NjObject } from "../data_formats/parsing/ninja"; @@ -21,8 +22,12 @@ import { NjcmModel } from "../data_formats/parsing/ninja/njcm"; import { xj_model_to_geometry } from "./xj_model_to_geometry"; const DUMMY_MATERIAL = new MeshBasicMaterial({ + color: 0x00ff00, + side: DoubleSide, +}); +const DEFAULT_MATERIAL = new MeshBasicMaterial({ color: 0xff00ff, - transparent: true, + side: DoubleSide, }); const DEFAULT_SKINNED_MATERIAL = new MeshLambertMaterial({ skinning: true, @@ -38,6 +43,10 @@ export function ninja_object_to_buffer_geometry(object: NjObject): Buff return new Object3DCreator([]).create_buffer_geometry(object); } +export function ninja_object_to_mesh(object: NjObject, materials: Material[] = []): Mesh { + return new Object3DCreator(materials).create_mesh(object); +} + export function ninja_object_to_skinned_mesh( object: NjObject, materials: Material[] = [] @@ -45,6 +54,15 @@ export function ninja_object_to_skinned_mesh( return new Object3DCreator(materials).create_skinned_mesh(object); } +export type VertexGroup = { + /** + * Start index. + */ + start: number; + count: number; + material_index: number; +}; + type Vertex = { bone_id: number; position: Vector3; @@ -88,7 +106,7 @@ class Object3DCreator { private indices: number[] = []; private bone_indices: number[] = []; private bone_weights: number[] = []; - private groups: { start: number; count: number; mat_idx: number }[] = []; + private groups: VertexGroup[] = []; constructor(materials: Material[]) { this.materials = [DUMMY_MATERIAL, ...materials]; @@ -106,7 +124,7 @@ class Object3DCreator { geom.setIndex(new Uint16BufferAttribute(this.indices, 1)); for (const group of this.groups) { - geom.addGroup(group.start, group.count, group.mat_idx); + geom.addGroup(group.start, group.count, group.material_index); } geom.computeBoundingBox(); @@ -114,13 +132,25 @@ class Object3DCreator { return geom; } + create_mesh(object: NjObject): Mesh { + const geom = this.create_buffer_geometry(object); + + const max_mat_idx = this.groups.reduce((max, g) => Math.max(max, g.material_index), 0); + + for (let i = this.materials.length - 1; i < max_mat_idx; ++i) { + this.materials.push(DEFAULT_MATERIAL); + } + + return new Mesh(geom, this.materials); + } + create_skinned_mesh(object: NjObject): SkinnedMesh { const geom = this.create_buffer_geometry(object); geom.addAttribute("skinIndex", new Uint16BufferAttribute(this.bone_indices, 4)); geom.addAttribute("skinWeight", new Float32BufferAttribute(this.bone_weights, 4)); - const max_mat_idx = this.groups.reduce((max, g) => Math.max(max, g.mat_idx), 0); + const max_mat_idx = this.groups.reduce((max, g) => Math.max(max, g.material_index), 0); for (let i = this.materials.length - 1; i < max_mat_idx; ++i) { this.materials.push(DEFAULT_SKINNED_MATERIAL); @@ -199,7 +229,15 @@ class Object3DCreator { if (is_njcm_model(model)) { this.njcm_model_to_geometry(model, matrix); } else { - xj_model_to_geometry(model, matrix, this.positions, this.normals, this.indices); + xj_model_to_geometry( + model, + matrix, + this.positions, + this.normals, + this.uvs, + this.indices, + this.groups + ); } } @@ -275,13 +313,13 @@ class Object3DCreator { const last_group = this.groups[this.groups.length - 1]; const mat_idx = mesh.texture_id == null ? 0 : mesh.texture_id + 1; - if (last_group && last_group.mat_idx === mat_idx) { + if (last_group && last_group.material_index === mat_idx) { last_group.count += this.indices.length - start_index_count; } else { this.groups.push({ start: start_index_count, count: this.indices.length - start_index_count, - mat_idx, + material_index: mat_idx, }); } } diff --git a/src/rendering/xj_model_to_geometry.ts b/src/rendering/xj_model_to_geometry.ts index 75cdb9af..0777d9de 100644 --- a/src/rendering/xj_model_to_geometry.ts +++ b/src/rendering/xj_model_to_geometry.ts @@ -1,29 +1,40 @@ import { Matrix3, Matrix4, Vector3 } from "three"; import { vec3_to_threejs } from "."; import { XjModel } from "../data_formats/parsing/ninja/xj"; +import { Vec2 } from "../data_formats/vector"; +import { VertexGroup } from "./models"; const DEFAULT_NORMAL = new Vector3(0, 1, 0); +const DEFAULT_UV = new Vec2(0, 0); export function xj_model_to_geometry( model: XjModel, matrix: Matrix4, positions: number[], normals: number[], - indices: number[] + uvs: number[], + indices: number[], + groups: VertexGroup[] ): void { const index_offset = positions.length / 3; const normal_matrix = new Matrix3().getNormalMatrix(matrix); - for (let { position, normal } of model.vertices) { + for (let { position, normal, uv } of model.vertices) { const p = vec3_to_threejs(position).applyMatrix4(matrix); positions.push(p.x, p.y, p.z); const local_n = normal ? vec3_to_threejs(normal) : DEFAULT_NORMAL; const n = local_n.applyMatrix3(normal_matrix); normals.push(n.x, n.y, n.z); + + const tuv = uv || DEFAULT_UV; + uvs.push(tuv.x, tuv.y); } + let current_mat_idx = 0; + for (const mesh of model.meshes) { + const start_index_count = indices.length; let clockwise = true; for (let j = 2; j < mesh.indices.length; ++j) { @@ -69,5 +80,21 @@ export function xj_model_to_geometry( clockwise = !clockwise; } + + const last_group = groups[groups.length - 1]; + + if (mesh.material_properties.texture_id != null) { + current_mat_idx = mesh.material_properties.texture_id + 1; + } + + if (last_group && last_group.material_index === current_mat_idx) { + last_group.count += indices.length - start_index_count; + } else { + groups.push({ + start: start_index_count, + count: indices.length - start_index_count, + material_index: current_mat_idx, + }); + } } } diff --git a/src/stores/ModelViewerStore.ts b/src/stores/ModelViewerStore.ts index d889f1c9..3b73fac0 100644 --- a/src/stores/ModelViewerStore.ts +++ b/src/stores/ModelViewerStore.ts @@ -5,12 +5,12 @@ import { AnimationClip, AnimationMixer, Clock, + DoubleSide, Mesh, MeshLambertMaterial, SkinnedMesh, Texture, Vector3, - DoubleSide, } from "three"; import { Endianness } from "../data_formats"; import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor"; @@ -20,7 +20,7 @@ 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/animation"; -import { ninja_object_to_buffer_geometry, ninja_object_to_skinned_mesh } from "../rendering/models"; +import { ninja_object_to_mesh, ninja_object_to_skinned_mesh } from "../rendering/models"; import { xvm_to_textures } from "../rendering/textures"; import { get_player_animation_data, get_player_data } from "./binary_assets"; @@ -292,29 +292,22 @@ class ModelViewerStore { let mesh: Mesh; let bb_size = new Vector3(); + const materials = + textures && + textures.map( + tex => + new MeshLambertMaterial({ + skinning: true, + map: tex, + side: DoubleSide, + alphaTest: 0.5, + }) + ); + if (this.has_skeleton) { - mesh = ninja_object_to_skinned_mesh( - this.current_model, - textures && - textures.map( - tex => - new MeshLambertMaterial({ - skinning: true, - map: tex, - side: DoubleSide, - alphaTest: 0.5, - }) - ) - ); + mesh = ninja_object_to_skinned_mesh(this.current_model, materials); } else { - mesh = new Mesh( - ninja_object_to_buffer_geometry(this.current_model), - new MeshLambertMaterial({ - color: 0xff00ff, - side: DoubleSide, - alphaTest: 0.5, - }) - ); + mesh = ninja_object_to_mesh(this.current_model, materials); } mesh.geometry.boundingBox.getSize(bb_size);