diff --git a/src/data_formats/cursor/ArrayBufferCursor.ts b/src/data_formats/cursor/ArrayBufferCursor.ts index 5406ca73..bcdf420a 100644 --- a/src/data_formats/cursor/ArrayBufferCursor.ts +++ b/src/data_formats/cursor/ArrayBufferCursor.ts @@ -7,6 +7,7 @@ import { } from "."; import { Endianness } from ".."; import { Cursor } from "./Cursor"; +import { Vec3 } from "../Vec3"; /** * A cursor for reading from an array buffer or part of an array buffer. @@ -186,6 +187,10 @@ export class ArrayBufferCursor implements Cursor { return array; } + vec3(): Vec3 { + return new Vec3(this.f32(), this.f32(), this.f32()); + } + take(size: number): ArrayBufferCursor { const offset = this.offset + this.position; const wrapper = new ArrayBufferCursor(this.buffer, this.endianness, offset, size); diff --git a/src/data_formats/cursor/Cursor.ts b/src/data_formats/cursor/Cursor.ts index d6e07ab9..a5156d68 100644 --- a/src/data_formats/cursor/Cursor.ts +++ b/src/data_formats/cursor/Cursor.ts @@ -1,4 +1,5 @@ import { Endianness } from ".."; +import { Vec3 } from "../Vec3"; /** * A cursor for reading binary data. @@ -24,21 +25,21 @@ export interface Cursor { /** * Seek forward or backward by a number of bytes. * - * @param offset - if positive, seeks forward by offset bytes, otherwise seeks backward by -offset bytes. + * @param offset if positive, seeks forward by offset bytes, otherwise seeks backward by -offset bytes. */ seek(offset: number): this; /** * Seek forward from the start of the cursor by a number of bytes. * - * @param offset - greater or equal to 0 and smaller than size + * @param offset greater or equal to 0 and smaller than size */ seek_start(offset: number): this; /** * Seek backward from the end of the cursor by a number of bytes. * - * @param offset - greater or equal to 0 and smaller than size + * @param offset greater or equal to 0 and smaller than size */ seek_end(offset: number): this; @@ -127,10 +128,12 @@ export interface Cursor { */ u32_array(n: number): number[]; + vec3(): Vec3; + /** * Consumes a variable number of bytes. * - * @param size - the amount bytes to consume. + * @param size the amount bytes to consume. * @returns a write-through view containing size bytes. */ take(size: number): Cursor; diff --git a/src/data_formats/cursor/ResizableBufferCursor.ts b/src/data_formats/cursor/ResizableBufferCursor.ts index a89a072d..3abb0d9d 100644 --- a/src/data_formats/cursor/ResizableBufferCursor.ts +++ b/src/data_formats/cursor/ResizableBufferCursor.ts @@ -8,6 +8,7 @@ import { import { Endianness } from ".."; import { ResizableBuffer } from "../ResizableBuffer"; import { Cursor } from "./Cursor"; +import { Vec3 } from "../Vec3"; export class ResizableBufferCursor implements Cursor { private _offset: number; @@ -213,6 +214,10 @@ export class ResizableBufferCursor implements Cursor { return array; } + vec3(): Vec3 { + return new Vec3(this.f32(), this.f32(), this.f32()); + } + take(size: number): ResizableBufferCursor { this.check_size("size", size, size); diff --git a/src/data_formats/parsing/area_collision_geometry.test.ts b/src/data_formats/parsing/area_collision_geometry.test.ts new file mode 100644 index 00000000..f2a5ad5b --- /dev/null +++ b/src/data_formats/parsing/area_collision_geometry.test.ts @@ -0,0 +1,21 @@ +import { readFileSync } from "fs"; +import { parse_area_collision_geometry } from "./area_collision_geometry"; +import { BufferCursor } from "../cursor/BufferCursor"; +import { Endianness } from ".."; + +test("parse_area_collision_geometry", () => { + const buf = readFileSync("test/resources/map_forest01c.rel"); + const object = parse_area_collision_geometry(new BufferCursor(buf, Endianness.Little)); + + expect(object.meshes.length).toBe(69); + expect(object.meshes[0].vertices.length).toBe(11); + expect(object.meshes[0].vertices[0].x).toBeCloseTo(-589.5195, 4); + expect(object.meshes[0].vertices[0].y).toBeCloseTo(16.7166, 4); + expect(object.meshes[0].vertices[0].z).toBeCloseTo(-218.6852, 4); + expect(object.meshes[0].triangles.length).toBe(12); + expect(object.meshes[0].triangles[0].flags).toBe(0b100000001); + expect(object.meshes[0].triangles[0].indices).toEqual([5, 0, 7]); + expect(object.meshes[0].triangles[0].normal.x).toBeCloseTo(0.0137, 4); + expect(object.meshes[0].triangles[0].normal.y).toBeCloseTo(0.9994, 4); + expect(object.meshes[0].triangles[0].normal.z).toBeCloseTo(-0.0307, 4); +}); diff --git a/src/data_formats/parsing/area_collision_geometry.ts b/src/data_formats/parsing/area_collision_geometry.ts index 5d91ae0f..e6e240f1 100644 --- a/src/data_formats/parsing/area_collision_geometry.ts +++ b/src/data_formats/parsing/area_collision_geometry.ts @@ -1,5 +1,6 @@ import { Cursor } from "../cursor/Cursor"; import { Vec3 } from "../Vec3"; +import { parse_rel } from "./rel"; export type CollisionObject = { meshes: CollisionMesh[]; @@ -17,9 +18,8 @@ export type CollisionTriangle = { }; export function parse_area_collision_geometry(cursor: Cursor): CollisionObject { - cursor.seek_end(16); - const main_block_offset = cursor.u32(); - cursor.seek_start(main_block_offset); + const { data_offset } = parse_rel(cursor, false); + cursor.seek_start(data_offset); const main_offset_table_offset = cursor.u32(); cursor.seek_start(main_offset_table_offset); diff --git a/src/data_formats/parsing/area_geometry.ts b/src/data_formats/parsing/area_geometry.ts index b01fb829..12a74b96 100644 --- a/src/data_formats/parsing/area_geometry.ts +++ b/src/data_formats/parsing/area_geometry.ts @@ -1,257 +1,101 @@ -import Logger from "js-logger"; -import { - BufferGeometry, - DoubleSide, - Float32BufferAttribute, - Mesh, - MeshLambertMaterial, - Object3D, - TriangleStripDrawMode, - Uint16BufferAttribute, -} from "three"; -import { Section } from "../../domain"; +import { Cursor } from "../cursor/Cursor"; import { Vec3 } from "../Vec3"; +import { ANGLE_TO_RAD } from "./ninja"; +import { parse_xj_model, XjModel } from "./ninja/xj"; +import { parse_rel } from "./rel"; -const logger = Logger.get("data_formats/parsing/area_geometry"); +export type RenderObject = { + sections: RenderSection[]; +}; -export function parse_area_geometry( - array_buffer: ArrayBuffer -): { sections: Section[]; object_3d: Object3D } { - const dv = new DataView(array_buffer); - const sections = new Map(); +export type RenderSection = { + id: number; + position: Vec3; + rotation: Vec3; + models: XjModel[]; +}; - const object = new Object3D(); +export type Vertex = { + position: Vec3; + normal?: Vec3; +}; - const main_block_offset = dv.getUint32(dv.byteLength - 16, true); - const section_count = dv.getUint32(main_block_offset + 8, true); - const section_table_offset = dv.getUint32(main_block_offset + 16, true); - // const texture_name_offset = dv.getUint32(main_block_offset + 20, true); +export function parse_area_geometry(cursor: Cursor): RenderObject { + const sections: RenderSection[] = []; - for (let i = section_table_offset; i < section_table_offset + section_count * 52; i += 52) { - const section_id = dv.getInt32(i, true); - const section_x = dv.getFloat32(i + 4, true); - const section_y = dv.getFloat32(i + 8, true); - const section_z = dv.getFloat32(i + 12, true); - const section_rotation = (dv.getInt32(i + 20, true) / 0xffff) * 2 * Math.PI; - const section = new Section( - section_id, - new Vec3(section_x, section_y, section_z), - section_rotation + cursor.seek_end(16); + + const { data_offset } = parse_rel(cursor, false); + cursor.seek_start(data_offset); + cursor.seek(8); // Format "fmt2" in UTF-16. + const section_count = cursor.u32(); + cursor.seek(4); + const section_table_offset = cursor.u32(); + // const texture_name_offset = cursor.u32(); + + for (let i = 0; i < section_count; i++) { + cursor.seek_start(section_table_offset + 52 * i); + + const section_id = cursor.i32(); + const section_position = cursor.vec3(); + const section_rotation = new Vec3( + cursor.u32() * ANGLE_TO_RAD, + cursor.u32() * ANGLE_TO_RAD, + cursor.u32() * ANGLE_TO_RAD ); - sections.set(section_id, section); - const index_lists_list = []; - const position_lists_list = []; - const normal_lists_list = []; + cursor.seek(4); - const simple_geometry_offset_table_offset = dv.getUint32(i + 32, true); - // const complex_geometry_offset_table_offset = dv.getUint32(i + 36, true); - const simple_geometry_offset_count = dv.getUint32(i + 40, true); - // const complex_geometry_offset_count = dv.getUint32(i + 44, true); + const simple_geometry_offset_table_offset = cursor.u32(); + // const animated_geometry_offset_table_offset = cursor.u32(); + cursor.seek(4); + const simple_geometry_offset_count = cursor.u32(); + // const animated_geometry_offset_count = cursor.u32(); + // Ignore animated_geometry_offset_count and the last 4 bytes. - for ( - let j = simple_geometry_offset_table_offset; - j < simple_geometry_offset_table_offset + simple_geometry_offset_count * 16; - j += 16 - ) { - let offset = dv.getUint32(j, true); - const flags = dv.getUint32(j + 12, true); + const models = parse_geometry_table( + cursor, + simple_geometry_offset_table_offset, + simple_geometry_offset_count + ); - if (flags & 0b100) { - offset = dv.getUint32(offset, true); - } + sections.push({ + id: section_id, + position: section_position, + rotation: section_rotation, + models, + }); + } - const geometry_offset = dv.getUint32(offset + 4, true); + return { sections }; +} - if (geometry_offset > 0) { - const vertex_info_table_offset = dv.getUint32(geometry_offset + 4, true); - const vertex_info_count = dv.getUint32(geometry_offset + 8, true); - const triangle_strip_table_offset = dv.getUint32(geometry_offset + 12, true); - const triangle_strip_count = dv.getUint32(geometry_offset + 16, true); - // const transparent_object_table_offset = dv.getUint32(blockOffset + 20, true); - // const transparent_object_count = dv.getUint32(blockOffset + 24, true); +function parse_geometry_table( + cursor: Cursor, + table_offset: number, + table_entry_count: number +): XjModel[] { + const models: XjModel[] = []; - const geom_index_lists = []; + for (let i = 0; i < table_entry_count; i++) { + cursor.seek_start(table_offset + 16 * i); - for ( - let k = triangle_strip_table_offset; - k < triangle_strip_table_offset + triangle_strip_count * 20; - k += 20 - ) { - // const flag_and_texture_id_offset = dv.getUint32(k, true); - // const data_type = dv.getUint32(k + 4, true); - const triangle_strip_index_table_offset = dv.getUint32(k + 8, true); - const triangle_strip_index_count = dv.getUint32(k + 12, true); + let offset = cursor.u32(); + cursor.seek(8); + const flags = cursor.u32(); - const triangle_strip_indices = []; - - for ( - let l = triangle_strip_index_table_offset; - l < triangle_strip_index_table_offset + triangle_strip_index_count * 2; - l += 2 - ) { - triangle_strip_indices.push(dv.getUint16(l, true)); - } - - geom_index_lists.push(triangle_strip_indices); - - // TODO: Read texture info. - } - - // TODO: Do the previous for the transparent index table. - - // Assume vertexInfoCount == 1. TODO: Does that make sense? - if (vertex_info_count > 1) { - logger.warn( - `Vertex info count of ${vertex_info_count} was larger than expected.` - ); - } - - // const vertex_type = dv.getUint32(vertexInfoTableOffset, true); - const vertex_table_offset = dv.getUint32(vertex_info_table_offset + 4, true); - const vertex_size = dv.getUint32(vertex_info_table_offset + 8, true); - const vertex_count = dv.getUint32(vertex_info_table_offset + 12, true); - - const geom_positions = []; - const geom_normals = []; - - for ( - let k = vertex_table_offset; - k < vertex_table_offset + vertex_count * vertex_size; - k += vertex_size - ) { - let n_x, n_y, n_z; - - switch (vertex_size) { - case 16: - case 24: - // TODO: are these values sensible? - n_x = 0; - n_y = 1; - n_z = 0; - break; - case 28: - case 36: - n_x = dv.getFloat32(k + 12, true); - n_y = dv.getFloat32(k + 16, true); - n_z = dv.getFloat32(k + 20, true); - // TODO: color, texture coords. - break; - default: - logger.error(`Unexpected vertex size of ${vertex_size}.`); - continue; - } - - const x = dv.getFloat32(k, true); - const y = dv.getFloat32(k + 4, true); - const z = dv.getFloat32(k + 8, true); - const rotated_x = - section.cos_y_axis_rotation * x + section.sin_y_axis_rotation * z; - const rotated_z = - -section.sin_y_axis_rotation * x + section.cos_y_axis_rotation * z; - - geom_positions.push(section_x + rotated_x); - geom_positions.push(section_y + y); - geom_positions.push(section_z + rotated_z); - geom_normals.push(n_x); - geom_normals.push(n_y); - geom_normals.push(n_z); - } - - index_lists_list.push(geom_index_lists); - position_lists_list.push(geom_positions); - normal_lists_list.push(geom_normals); - } + if (flags & 0b100) { + offset = cursor.seek_start(offset).u32(); } - // function vEqual(v, w) { - // return v[0] === w[0] && v[1] === w[1] && v[2] === w[2]; - // } + cursor.seek_start(offset + 4); + const geometry_offset = cursor.u32(); - for (let i = 0; i < position_lists_list.length; ++i) { - const positions = position_lists_list[i]; - const normals = normal_lists_list[i]; - const geom_index_lists = index_lists_list[i]; - // const indices = []; - - geom_index_lists.forEach(object_indices => { - // for (let j = 2; j < objectIndices.length; ++j) { - // const a = objectIndices[j - 2]; - // const b = objectIndices[j - 1]; - // const c = objectIndices[j]; - - // if (a !== b && a !== c && b !== c) { - // const ap = positions.slice(3 * a, 3 * a + 3); - // const bp = positions.slice(3 * b, 3 * b + 3); - // const cp = positions.slice(3 * c, 3 * c + 3); - - // if (!vEqual(ap, bp) && !vEqual(ap, cp) && !vEqual(bp, cp)) { - // if (j % 2 === 0) { - // indices.push(a); - // indices.push(b); - // indices.push(c); - // } else { - // indices.push(b); - // indices.push(a); - // indices.push(c); - // } - // } - // } - // } - - const geometry = new BufferGeometry(); - geometry.addAttribute("position", new Float32BufferAttribute(positions, 3)); - geometry.addAttribute("normal", new Float32BufferAttribute(normals, 3)); - geometry.setIndex(new Uint16BufferAttribute(object_indices, 1)); - - const mesh = new Mesh( - geometry, - new MeshLambertMaterial({ - color: 0x44aaff, - // transparent: true, - opacity: 0.25, - side: DoubleSide, - }) - ); - mesh.setDrawMode(TriangleStripDrawMode); - mesh.userData.section = section; - object.add(mesh); - }); - - // const geometry = new BufferGeometry(); - // geometry.addAttribute( - // 'position', new BufferAttribute(new Float32Array(positions), 3)); - // geometry.addAttribute( - // 'normal', new BufferAttribute(new Float32Array(normals), 3)); - // geometry.setIndex(new BufferAttribute(new Uint16Array(indices), 1)); - - // const mesh = new Mesh( - // geometry, - // new MeshLambertMaterial({ - // color: 0x44aaff, - // transparent: true, - // opacity: 0.25, - // side: DoubleSide - // }) - // ); - // object.add(mesh); - - // const wireframeMesh = new Mesh( - // geometry, - // new MeshBasicMaterial({ - // color: 0x88ccff, - // wireframe: true, - // transparent: true, - // opacity: 0.75, - // }) - // ); - // wireframeMesh.setDrawMode(THREE.TriangleStripDrawMode); - // object.add(wireframeMesh); + if (geometry_offset > 0) { + cursor.seek_start(geometry_offset); + models.push(parse_xj_model(cursor)); } } - return { - sections: [...sections.values()].sort((a, b) => a.id - b.id), - object_3d: object, - }; + return models; } diff --git a/src/data_formats/parsing/itempmt.test.ts b/src/data_formats/parsing/itempmt.test.ts new file mode 100644 index 00000000..855b270b --- /dev/null +++ b/src/data_formats/parsing/itempmt.test.ts @@ -0,0 +1,18 @@ +import { parse_item_pmt } from "./itempmt"; +import { readFileSync } from "fs"; +import { BufferCursor } from "../cursor/BufferCursor"; +import { Endianness } from ".."; + +test("parse_item_pmt", () => { + const buf = readFileSync("test/resources/ItemPMT.bin"); + const item_pmt = parse_item_pmt(new BufferCursor(buf, Endianness.Little)); + + const saber = item_pmt.weapons[1][0]; + + expect(saber.id).toBe(177); + expect(saber.min_atp).toBe(40); + expect(saber.max_atp).toBe(55); + expect(saber.ata).toBe(30); + expect(saber.max_grind).toBe(35); + expect(saber.req_atp).toBe(30); +}); diff --git a/src/data_formats/parsing/itempmt.ts b/src/data_formats/parsing/itempmt.ts index c1d26389..b5a45cd9 100644 --- a/src/data_formats/parsing/itempmt.ts +++ b/src/data_formats/parsing/itempmt.ts @@ -1,4 +1,5 @@ import { Cursor } from "../cursor/Cursor"; +import { parse_rel } from "./rel"; export type ItemPmt = { stat_boosts: PmtStatBoost[]; @@ -95,43 +96,24 @@ export type PmtTool = { }; export function parse_item_pmt(cursor: Cursor): ItemPmt { - cursor.seek_end(32); - const main_table_offset = cursor.u32(); - const main_table_size = cursor.u32(); - // const main_table_count = cursor.u32(); // Should be 1. - - cursor.seek_start(main_table_offset); - - const compact_table_offsets = cursor.u16_array(main_table_size); - const table_offsets: { offset: number; size: number }[] = []; - let expanded_offset = 0; - - for (const compact_offset of compact_table_offsets) { - expanded_offset = expanded_offset + 4 * compact_offset; - cursor.seek_start(expanded_offset - 4); - const size = cursor.u32(); - const offset = cursor.u32(); - table_offsets.push({ offset, size }); - } + const { index } = parse_rel(cursor, true); const item_pmt: ItemPmt = { // This size (65268) of this table seems wrong, so we pass in a hard-coded value. - stat_boosts: parse_stat_boosts(cursor, table_offsets[305].offset, 52), - armors: parse_armors(cursor, table_offsets[7].offset, table_offsets[7].size), - shields: parse_shields(cursor, table_offsets[8].offset, table_offsets[8].size), - units: parse_units(cursor, table_offsets[9].offset, table_offsets[9].size), + stat_boosts: parse_stat_boosts(cursor, index[305].offset, 52), + armors: parse_armors(cursor, index[7].offset, index[7].size), + shields: parse_shields(cursor, index[8].offset, index[8].size), + units: parse_units(cursor, index[9].offset, index[9].size), tools: [], weapons: [], }; for (let i = 11; i <= 37; i++) { - item_pmt.tools.push(parse_tools(cursor, table_offsets[i].offset, table_offsets[i].size)); + item_pmt.tools.push(parse_tools(cursor, index[i].offset, index[i].size)); } for (let i = 38; i <= 275; i++) { - item_pmt.weapons.push( - parse_weapons(cursor, table_offsets[i].offset, table_offsets[i].size) - ); + item_pmt.weapons.push(parse_weapons(cursor, index[i].offset, index[i].size)); } return item_pmt; diff --git a/src/data_formats/parsing/ninja/index.ts b/src/data_formats/parsing/ninja/index.ts index 82d7ee63..95c91a0f 100644 --- a/src/data_formats/parsing/ninja/index.ts +++ b/src/data_formats/parsing/ninja/index.ts @@ -1,13 +1,13 @@ +import { Cursor } from "../../cursor/Cursor"; import { Vec3 } from "../../Vec3"; import { NjcmModel, parse_njcm_model } from "./njcm"; import { parse_xj_model, XjModel } from "./xj"; -import { Cursor } from "../../cursor/Cursor"; // TODO: // - deal with multiple NJCM chunks // - deal with other types of chunks -const ANGLE_TO_RAD = (2 * Math.PI) / 65536; +export const ANGLE_TO_RAD = (2 * Math.PI) / 0xffff; export type NjVertex = { position: Vec3; diff --git a/src/data_formats/parsing/ninja/motion.ts b/src/data_formats/parsing/ninja/motion.ts index 20d03133..5767184e 100644 --- a/src/data_formats/parsing/ninja/motion.ts +++ b/src/data_formats/parsing/ninja/motion.ts @@ -1,7 +1,6 @@ -import { Vec3 } from "../../Vec3"; +import { ANGLE_TO_RAD } from "."; import { Cursor } from "../../cursor/Cursor"; - -const ANGLE_TO_RAD = (2 * Math.PI) / 0xffff; +import { Vec3 } from "../../Vec3"; export type NjMotion = { motion_data: NjMotionData[]; diff --git a/src/data_formats/parsing/ninja/xj.ts b/src/data_formats/parsing/ninja/xj.ts index fe3dfe42..dda64b75 100644 --- a/src/data_formats/parsing/ninja/xj.ts +++ b/src/data_formats/parsing/ninja/xj.ts @@ -1,7 +1,10 @@ +import Logger from "js-logger"; import { Cursor } from "../../cursor/Cursor"; import { Vec3 } from "../../Vec3"; import { NjVertex } from "../ninja"; +const logger = Logger.get("data_formats/parsing/ninja/xj"); + // TODO: // - textures // - colors @@ -11,7 +14,9 @@ import { NjVertex } from "../ninja"; export type XjModel = { type: "xj"; vertices: NjVertex[]; - meshes: XjTriangleStrip[]; + strips: XjTriangleStrip[]; + collision_sphere_position: Vec3; + collision_sphere_radius: number; }; export type XjTriangleStrip = { @@ -20,34 +25,42 @@ export type XjTriangleStrip = { export function parse_xj_model(cursor: Cursor): XjModel { cursor.seek(4); // Flags according to QEdit, seemingly always 0. - const vertex_info_list_offset = cursor.u32(); - cursor.seek(4); // Seems to be the vertexInfoCount, always 1. - const triangle_strip_list_a_offset = cursor.u32(); - const triangle_strip_a_count = cursor.u32(); - const triangle_strip_list_b_offset = cursor.u32(); - const triangle_strip_b_count = cursor.u32(); - cursor.seek(16); // Bounding sphere position and radius in floats. + const vertex_info_table_offset = cursor.u32(); + const vertex_info_count = cursor.u32(); + const triangle_strip_table_offset = cursor.u32(); + 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_radius = cursor.f32(); const model: XjModel = { type: "xj", vertices: [], - meshes: [], + strips: [], + collision_sphere_position, + collision_sphere_radius, }; - if (vertex_info_list_offset) { - cursor.seek_start(vertex_info_list_offset); - cursor.seek(4); // Possibly the vertex type. - const vertexList_offset = cursor.u32(); + if (vertex_info_count >= 1) { + if (vertex_info_count > 1) { + logger.warn(`Vertex info count of ${vertex_info_count} was larger than expected.`); + } + + cursor.seek_start(vertex_info_table_offset); + cursor.seek(4); // Vertex type. + const vertex_table_offset = cursor.u32(); const vertex_size = cursor.u32(); const vertex_count = cursor.u32(); for (let i = 0; i < vertex_count; ++i) { - cursor.seek_start(vertexList_offset + i * vertex_size); - const position = new Vec3(cursor.f32(), cursor.f32(), cursor.f32()); + cursor.seek_start(vertex_table_offset + i * vertex_size); + + const position = cursor.vec3(); let normal: Vec3 | undefined; if (vertex_size === 28 || vertex_size === 32 || vertex_size === 36) { - normal = new Vec3(cursor.f32(), cursor.f32(), cursor.f32()); + normal = cursor.vec3(); } model.vertices.push({ @@ -60,22 +73,18 @@ export function parse_xj_model(cursor: Cursor): XjModel { } } - if (triangle_strip_list_a_offset) { - model.meshes.push( - ...parse_triangle_strip_list( - cursor, - triangle_strip_list_a_offset, - triangle_strip_a_count - ) + if (triangle_strip_table_offset) { + model.strips.push( + ...parse_triangle_strip_table(cursor, triangle_strip_table_offset, triangle_strip_count) ); } - if (triangle_strip_list_b_offset) { - model.meshes.push( - ...parse_triangle_strip_list( + if (transparent_triangle_strip_table_offset) { + model.strips.push( + ...parse_triangle_strip_table( cursor, - triangle_strip_list_b_offset, - triangle_strip_b_count + transparent_triangle_strip_table_offset, + transparent_triangle_strip_count ) ); } @@ -83,7 +92,7 @@ export function parse_xj_model(cursor: Cursor): XjModel { return model; } -function parse_triangle_strip_list( +function parse_triangle_strip_table( cursor: Cursor, triangle_strip_list_offset: number, triangle_strip_count: number @@ -92,7 +101,7 @@ function parse_triangle_strip_list( for (let i = 0; i < triangle_strip_count; ++i) { cursor.seek_start(triangle_strip_list_offset + i * 20); - cursor.seek(8); // Skip material information. + cursor.seek(8); // Skip flag_and_texture_id_offset and data_type. const index_list_offset = cursor.u32(); const index_count = cursor.u32(); // Ignoring 4 bytes. diff --git a/src/data_formats/parsing/rel.ts b/src/data_formats/parsing/rel.ts new file mode 100644 index 00000000..2374f02f --- /dev/null +++ b/src/data_formats/parsing/rel.ts @@ -0,0 +1,44 @@ +import { Cursor } from "../cursor/Cursor"; + +export type Rel = { + data_offset: number; + index: RelIndexEntry[]; +}; + +export type RelIndexEntry = { + offset: number; + size: number; +}; + +export function parse_rel(cursor: Cursor, parse_index: boolean): Rel { + cursor.seek_end(32); + + const index_offset = cursor.u32(); + const index_size = cursor.u32(); + cursor.seek(8); // Typically 1, 0, 0,... + const data_offset = cursor.u32(); + // Typically followed by 12 nul bytes. + + cursor.seek_start(index_offset); + const index = parse_index ? parse_indices(cursor, index_size) : []; + + return { data_offset, index }; +} + +function parse_indices(cursor: Cursor, index_size: number): RelIndexEntry[] { + const compact_offsets = cursor.u16_array(index_size); + const index: RelIndexEntry[] = []; + let expanded_offset = 0; + + for (const compact_offset of compact_offsets) { + expanded_offset = expanded_offset + 4 * compact_offset; + + // Size is not always present. + cursor.seek_start(expanded_offset - 4); + const size = cursor.u32(); + const offset = cursor.u32(); + index.push({ offset, size }); + } + + return index; +} diff --git a/src/domain/index.ts b/src/domain/index.ts index a66235eb..b2d41afd 100644 --- a/src/domain/index.ts +++ b/src/domain/index.ts @@ -59,17 +59,11 @@ export enum Difficulty { export const Difficulties: Difficulty[] = enum_values(Difficulty); export class Section { - id: number; - @observable position: Vec3; - @observable y_axis_rotation: number; - - @computed get sin_y_axis_rotation(): number { - return Math.sin(this.y_axis_rotation); - } - - @computed get cos_y_axis_rotation(): number { - return Math.cos(this.y_axis_rotation); - } + readonly id: number; + readonly position: Vec3; + readonly y_axis_rotation: number; + readonly sin_y_axis_rotation: number; + readonly cos_y_axis_rotation: number; constructor(id: number, position: Vec3, y_axis_rotation: number) { if (!Number.isInteger(id) || id < -1) @@ -80,6 +74,8 @@ export class Section { this.id = id; this.position = position; this.y_axis_rotation = y_axis_rotation; + this.sin_y_axis_rotation = Math.sin(this.y_axis_rotation); + this.cos_y_axis_rotation = Math.cos(this.y_axis_rotation); } } @@ -160,32 +156,32 @@ export class QuestEntity { let { x, y, z } = this.position; if (this.section) { - const relX = x - this.section.position.x; - const relY = y - this.section.position.y; - const relZ = z - this.section.position.z; + const rel_x = x - this.section.position.x; + const rel_y = y - this.section.position.y; + const rel_z = z - this.section.position.z; const sin = -this.section.sin_y_axis_rotation; const cos = this.section.cos_y_axis_rotation; - const rotX = cos * relX + sin * relZ; - const rotZ = -sin * relX + cos * relZ; - x = rotX; - y = relY; - z = rotZ; + const rot_x = cos * rel_x + sin * rel_z; + const rot_z = -sin * rel_x + cos * rel_z; + x = rot_x; + y = rel_y; + z = rot_z; } return new Vec3(x, y, z); } - set section_position(sectPos: Vec3) { - let { x: relX, y: relY, z: relZ } = sectPos; + set section_position(sec_pos: Vec3) { + let { x: rel_x, y: rel_y, z: rel_z } = sec_pos; if (this.section) { const sin = -this.section.sin_y_axis_rotation; const cos = this.section.cos_y_axis_rotation; - const rotX = cos * relX - sin * relZ; - const rotZ = sin * relX + cos * relZ; - const x = rotX + this.section.position.x; - const y = relY + this.section.position.y; - const z = rotZ + this.section.position.z; + const rot_x = cos * rel_x - sin * rel_z; + const rot_z = sin * rel_x + cos * rel_z; + const x = rot_x + this.section.position.x; + const y = rel_y + this.section.position.y; + const z = rot_z + this.section.position.z; this.position = new Vec3(x, y, z); } } diff --git a/src/rendering/QuestRenderer.ts b/src/rendering/QuestRenderer.ts index 58929a52..002a108e 100644 --- a/src/rendering/QuestRenderer.ts +++ b/src/rendering/QuestRenderer.ts @@ -1,4 +1,4 @@ -import { autorun, IReactionDisposer, when } from "mobx"; +import { autorun, IReactionDisposer, when, runInAction } from "mobx"; import { Intersection, Mesh, @@ -98,6 +98,7 @@ export class QuestRenderer extends Renderer { this.scene.add(this.npc_geometry); this.scene.remove(this.collision_geometry); + // this.scene.remove(this.render_geometry); if (this.quest && this.area) { // Add necessary entity geometry when it arrives. @@ -122,6 +123,7 @@ export class QuestRenderer extends Renderer { ); this.scene.remove(this.collision_geometry); + // this.scene.remove(this.render_geometry); this.reset_camera(new Vector3(0, 800, 700), new Vector3(0, 0, 0)); @@ -135,6 +137,7 @@ export class QuestRenderer extends Renderer { ); this.render_geometry = render_geometry; + // this.scene.add(render_geometry); } } @@ -263,12 +266,14 @@ export class QuestRenderer extends Renderer { const { intersection, section } = this.pick_terrain(pointer_pos, data); if (intersection) { - data.entity.position = new Vec3( - intersection.point.x, - intersection.point.y + data.drag_y, - intersection.point.z - ); - data.entity.section = section; + runInAction(() => { + data.entity.position = new Vec3( + intersection.point.x, + intersection.point.y + data.drag_y, + intersection.point.z + ); + data.entity.section = section; + }); } else { // If the cursor is not over any terrain, we translate the entity accross the horizontal plane in which the entity's origin lies. this.raycaster.setFromCamera(pointer_pos, this.camera); diff --git a/src/rendering/areas.ts b/src/rendering/areas.ts index 584d2cbc..b018c83d 100644 --- a/src/rendering/areas.ts +++ b/src/rendering/areas.ts @@ -1,15 +1,22 @@ import { + BufferGeometry, DoubleSide, Face3, + Float32BufferAttribute, Geometry, Group, + Matrix4, Mesh, MeshBasicMaterial, MeshLambertMaterial, Object3D, + Uint16BufferAttribute, Vector3, } from "three"; import { CollisionObject } from "../data_formats/parsing/area_collision_geometry"; +import { RenderObject } from "../data_formats/parsing/area_geometry"; +import { Section } from "../domain"; +import { xj_model_to_geometry } from "./xj_model_to_geometry"; const materials = [ // Wall @@ -99,3 +106,44 @@ export function area_collision_geometry_to_object_3d(object: CollisionObject): O return group; } + +export function area_geometry_to_sections_and_object_3d( + object: RenderObject +): [Section[], Object3D] { + const sections: Section[] = []; + const group = new Group(); + + for (const section of object.sections) { + const positions: number[] = []; + const normals: number[] = []; + const indices: number[] = []; + + for (const model of section.models) { + xj_model_to_geometry(model, new Matrix4(), positions, normals, indices); + } + + const geometry = new BufferGeometry(); + geometry.addAttribute("position", new Float32BufferAttribute(positions, 3)); + geometry.addAttribute("normal", new Float32BufferAttribute(normals, 3)); + geometry.setIndex(new Uint16BufferAttribute(indices, 1)); + + const mesh = new Mesh( + geometry, + new MeshLambertMaterial({ + color: 0x44aaff, + transparent: true, + opacity: 0.25, + side: DoubleSide, + }) + ); + mesh.position.set(section.position.x, section.position.y, section.position.z); + mesh.rotation.set(section.rotation.x, section.rotation.y, section.rotation.z); + group.add(mesh); + + const sec = new Section(section.id, section.position, section.rotation.y); + mesh.userData.section = sec; + sections.push(sec); + } + + return [sections, group]; +} diff --git a/src/rendering/models.ts b/src/rendering/models.ts index 7629888f..a9a534f9 100644 --- a/src/rendering/models.ts +++ b/src/rendering/models.ts @@ -15,9 +15,9 @@ import { Vector3, } from "three"; import { vec3_to_threejs } from "."; -import { NjModel, NjObject, is_njcm_model } 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 { XjModel } from "../data_formats/parsing/ninja/xj"; +import { xj_model_to_geometry } from "./xj_model_to_geometry"; const DEFAULT_MATERIAL = new MeshLambertMaterial({ color: 0xff00ff, @@ -187,7 +187,7 @@ class Object3DCreator { if (is_njcm_model(model)) { this.njcm_model_to_geometry(model, matrix); } else { - this.xj_model_to_geometry(model, matrix); + xj_model_to_geometry(model, matrix, this.positions, this.normals, this.indices); } } @@ -253,102 +253,4 @@ class Object3DCreator { } } } - - private xj_model_to_geometry(model: XjModel, matrix: Matrix4): void { - const positions = this.positions; - const normals = this.normals; - const indices = this.indices; - const index_offset = this.positions.length / 3; - let clockwise = true; - - const normal_matrix = new Matrix3().getNormalMatrix(matrix); - - for (let { position, normal } of model.vertices) { - const p = vec3_to_threejs(position).applyMatrix4(matrix); - positions.push(p.x, p.y, p.z); - - normal = normal || DEFAULT_NORMAL; - const n = vec3_to_threejs(normal).applyMatrix3(normal_matrix); - normals.push(n.x, n.y, n.z); - } - - for (const mesh of model.meshes) { - const strip_indices = mesh.indices; - - for (let j = 2; j < strip_indices.length; ++j) { - const a = index_offset + strip_indices[j - 2]; - const b = index_offset + strip_indices[j - 1]; - const c = index_offset + strip_indices[j]; - const pa = new Vector3( - positions[3 * a], - positions[3 * a + 1], - positions[3 * a + 2] - ); - const pb = new Vector3( - positions[3 * b], - positions[3 * b + 1], - positions[3 * b + 2] - ); - const pc = new Vector3( - positions[3 * c], - positions[3 * c + 1], - positions[3 * c + 2] - ); - const na = new Vector3(normals[3 * a], normals[3 * a + 1], normals[3 * a + 2]); - const nb = new Vector3(normals[3 * a], normals[3 * a + 1], normals[3 * a + 2]); - const nc = new Vector3(normals[3 * a], normals[3 * a + 1], normals[3 * a + 2]); - - // Calculate a surface normal and reverse the vertex winding if at least 2 of the vertex normals point in the opposite direction. - // This hack fixes the winding for most models. - const normal = pb - .clone() - .sub(pa) - .cross(pc.clone().sub(pa)); - - if (clockwise) { - normal.negate(); - } - - const opposite_count = - (normal.dot(na) < 0 ? 1 : 0) + - (normal.dot(nb) < 0 ? 1 : 0) + - (normal.dot(nc) < 0 ? 1 : 0); - - if (opposite_count >= 2) { - clockwise = !clockwise; - } - - if (clockwise) { - indices.push(b); - indices.push(a); - indices.push(c); - } else { - indices.push(a); - indices.push(b); - indices.push(c); - } - - clockwise = !clockwise; - - // The following switch statement fixes model 180.xj (zanba). - // switch (j) { - // case 17: - // case 52: - // case 70: - // case 92: - // case 97: - // case 126: - // case 140: - // case 148: - // case 187: - // case 200: - // console.warn(`swapping winding at: ${j}, (${a}, ${b}, ${c})`); - // break; - // default: - // ccw = !ccw; - // break; - // } - } - } - } } diff --git a/src/rendering/xj_model_to_geometry.ts b/src/rendering/xj_model_to_geometry.ts new file mode 100644 index 00000000..45f14160 --- /dev/null +++ b/src/rendering/xj_model_to_geometry.ts @@ -0,0 +1,94 @@ +import { Matrix3, Matrix4, Vector3 } from "three"; +import { vec3_to_threejs } from "."; +import { XjModel } from "../data_formats/parsing/ninja/xj"; + +const DEFAULT_NORMAL = new Vector3(0, 1, 0); + +export function xj_model_to_geometry( + model: XjModel, + matrix: Matrix4, + positions: number[], + normals: number[], + indices: number[] +): void { + const index_offset = positions.length / 3; + let clockwise = true; + + const normal_matrix = new Matrix3().getNormalMatrix(matrix); + + for (let { position, normal } 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); + } + + for (const mesh of model.strips) { + const strip_indices = mesh.indices; + + for (let j = 2; j < strip_indices.length; ++j) { + const a = index_offset + strip_indices[j - 2]; + const b = index_offset + strip_indices[j - 1]; + const c = index_offset + strip_indices[j]; + const pa = new Vector3(positions[3 * a], positions[3 * a + 1], positions[3 * a + 2]); + const pb = new Vector3(positions[3 * b], positions[3 * b + 1], positions[3 * b + 2]); + const pc = new Vector3(positions[3 * c], positions[3 * c + 1], positions[3 * c + 2]); + const na = new Vector3(normals[3 * a], normals[3 * a + 1], normals[3 * a + 2]); + const nb = new Vector3(normals[3 * a], normals[3 * a + 1], normals[3 * a + 2]); + const nc = new Vector3(normals[3 * a], normals[3 * a + 1], normals[3 * a + 2]); + + // Calculate a surface normal and reverse the vertex winding if at least 2 of the vertex normals point in the opposite direction. + // This hack fixes the winding for most models. + const normal = pb + .clone() + .sub(pa) + .cross(pc.clone().sub(pa)); + + if (clockwise) { + normal.negate(); + } + + const opposite_count = + (normal.dot(na) < 0 ? 1 : 0) + + (normal.dot(nb) < 0 ? 1 : 0) + + (normal.dot(nc) < 0 ? 1 : 0); + + if (opposite_count >= 2) { + clockwise = !clockwise; + } + + if (clockwise) { + indices.push(b); + indices.push(a); + indices.push(c); + } else { + indices.push(a); + indices.push(b); + indices.push(c); + } + + clockwise = !clockwise; + + // The following switch statement fixes model 180.xj (zanba). + // switch (j) { + // case 17: + // case 52: + // case 70: + // case 92: + // case 97: + // case 126: + // case 140: + // case 148: + // case 187: + // case 200: + // console.warn(`swapping winding at: ${j}, (${a}, ${b}, ${c})`); + // break; + // default: + // ccw = !ccw; + // break; + // } + } + } +} diff --git a/src/stores/AreaStore.ts b/src/stores/AreaStore.ts index 6a874752..7f2fbbad 100644 --- a/src/stores/AreaStore.ts +++ b/src/stores/AreaStore.ts @@ -1,11 +1,14 @@ import { Object3D } from "three"; +import { Endianness } from "../data_formats"; +import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor"; import { parse_area_collision_geometry } from "../data_formats/parsing/area_collision_geometry"; import { parse_area_geometry } from "../data_formats/parsing/area_geometry"; import { Area, AreaVariant, Section } from "../domain"; -import { area_collision_geometry_to_object_3d } from "../rendering/areas"; +import { + area_collision_geometry_to_object_3d, + area_geometry_to_sections_and_object_3d, +} from "../rendering/areas"; import { get_area_collision_data, get_area_render_data } from "./binary_assets"; -import { Endianness } from "../data_formats"; -import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor"; function area(id: number, name: string, order: number, variants: number): Area { const area = new Area(id, name, order, []); @@ -104,15 +107,15 @@ class AreaStore { area_id: number, area_variant: number ): Promise { - const sections = sections_cache.get(`${episode}-${area_id}-${area_variant}`); + const key = `${episode}-${area_id}-${area_variant}`; + let sections = sections_cache.get(key); - if (sections) { - return sections; - } else { - return this.get_area_sections_and_render_geometry(episode, area_id, area_variant).then( - ({ sections }) => sections - ); + if (!sections) { + this.load_area_sections_and_render_geometry(episode, area_id, area_variant); + sections = sections_cache.get(key)!; } + + return sections; } async get_area_render_geometry( @@ -120,15 +123,15 @@ class AreaStore { area_id: number, area_variant: number ): Promise { - const object_3d = render_geometry_cache.get(`${episode}-${area_id}-${area_variant}`); + const key = `${episode}-${area_id}-${area_variant}`; + let object_3d = render_geometry_cache.get(key); - if (object_3d) { - return object_3d; - } else { - return this.get_area_sections_and_render_geometry(episode, area_id, area_variant).then( - ({ object_3d }) => object_3d - ); + if (!object_3d) { + this.load_area_sections_and_render_geometry(episode, area_id, area_variant); + object_3d = render_geometry_cache.get(key)!; } + + return object_3d; } async get_area_collision_geometry( @@ -151,26 +154,25 @@ class AreaStore { } } - private get_area_sections_and_render_geometry( + private load_area_sections_and_render_geometry( episode: number, area_id: number, area_variant: number - ): Promise<{ sections: Section[]; object_3d: Object3D }> { - const promise = get_area_render_data(episode, area_id, area_variant).then( - parse_area_geometry + ): void { + const promise = get_area_render_data(episode, area_id, area_variant).then(buffer => + area_geometry_to_sections_and_object_3d( + parse_area_geometry(new ArrayBufferCursor(buffer, Endianness.Little)) + ) ); - const sections = new Promise((resolve, reject) => { - promise.then(({ sections }) => resolve(sections)).catch(reject); - }); - const object_3d = new Promise((resolve, reject) => { - promise.then(({ object_3d }) => resolve(object_3d)).catch(reject); - }); - - sections_cache.set(`${episode}-${area_id}-${area_variant}`, sections); - render_geometry_cache.set(`${episode}-${area_id}-${area_variant}`, object_3d); - - return promise; + sections_cache.set( + `${episode}-${area_id}-${area_variant}`, + promise.then(([sections]) => sections) + ); + render_geometry_cache.set( + `${episode}-${area_id}-${area_variant}`, + promise.then(([, object_3d]) => object_3d) + ); } } diff --git a/src/stores/QuestEditorStore.ts b/src/stores/QuestEditorStore.ts index 4341b29c..4f21c799 100644 --- a/src/stores/QuestEditorStore.ts +++ b/src/stores/QuestEditorStore.ts @@ -1,5 +1,5 @@ import Logger from "js-logger"; -import { action, observable } from "mobx"; +import { action, observable, runInAction } from "mobx"; import { parse_quest, write_quest_qst } from "../data_formats/parsing/quest"; import { Vec3 } from "../data_formats/Vec3"; import { Area, Quest, QuestEntity, Section } from "../domain"; @@ -110,7 +110,6 @@ class QuestEditorStore { let { x, y, z } = entity.position; const section = sections.find(s => s.id === entity.section_id); - entity.section = section; if (section) { const { x: sec_x, y: sec_y, z: sec_z } = section.position; @@ -123,7 +122,10 @@ class QuestEditorStore { logger.warn(`Section ${entity.section_id} not found.`); } - entity.position = new Vec3(x, y, z); + runInAction(() => { + entity.section = section; + entity.position = new Vec3(x, y, z); + }); }; save_current_quest_to_file = (file_name: string) => { diff --git a/test/resources/ItemPMT.bin b/test/resources/ItemPMT.bin new file mode 100644 index 00000000..8d61df1d Binary files /dev/null and b/test/resources/ItemPMT.bin differ diff --git a/test/resources/map_forest01c.rel b/test/resources/map_forest01c.rel new file mode 100644 index 00000000..92d9d34d Binary files /dev/null and b/test/resources/map_forest01c.rel differ