mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Refactored area render geometry code.
This commit is contained in:
parent
f1b3df9754
commit
a60c69a3ef
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
21
src/data_formats/parsing/area_collision_geometry.test.ts
Normal file
21
src/data_formats/parsing/area_collision_geometry.test.ts
Normal file
@ -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);
|
||||
});
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
18
src/data_formats/parsing/itempmt.test.ts
Normal file
18
src/data_formats/parsing/itempmt.test.ts
Normal file
@ -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);
|
||||
});
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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[];
|
||||
|
@ -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.
|
||||
|
44
src/data_formats/parsing/rel.ts
Normal file
44
src/data_formats/parsing/rel.ts
Normal file
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
94
src/rendering/xj_model_to_geometry.ts
Normal file
94
src/rendering/xj_model_to_geometry.ts
Normal file
@ -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;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Section[]> {
|
||||
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<Object3D> {
|
||||
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<Section[]>((resolve, reject) => {
|
||||
promise.then(({ sections }) => resolve(sections)).catch(reject);
|
||||
});
|
||||
const object_3d = new Promise<Object3D>((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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) => {
|
||||
|
BIN
test/resources/ItemPMT.bin
Normal file
BIN
test/resources/ItemPMT.bin
Normal file
Binary file not shown.
BIN
test/resources/map_forest01c.rel
Normal file
BIN
test/resources/map_forest01c.rel
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user