mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Improved ninja parsing and animation.
This commit is contained in:
parent
a36e177eef
commit
3203e0a649
@ -1,31 +1,32 @@
|
||||
import { BufferGeometry } from 'three';
|
||||
import { NpcType, ObjectType } from '../../domain';
|
||||
import { getNpcData, getObjectData } from './binaryAssets';
|
||||
import { ninja_object_to_buffer_geometry } from '../../rendering/models';
|
||||
import { BufferCursor } from '../BufferCursor';
|
||||
import { parse_nj, parse_xj } from '../parsing/ninja';
|
||||
import { getNpcData, getObjectData } from './binaryAssets';
|
||||
|
||||
const npc_cache: Map<string, Promise<BufferGeometry>> = new Map();
|
||||
const object_cache: Map<string, Promise<BufferGeometry>> = new Map();
|
||||
|
||||
export function get_npc_geometry(npc_type: NpcType): Promise<BufferGeometry> {
|
||||
let geometry = npc_cache.get(String(npc_type.id));
|
||||
let mesh = npc_cache.get(String(npc_type.id));
|
||||
|
||||
if (geometry) {
|
||||
return geometry;
|
||||
if (mesh) {
|
||||
return mesh;
|
||||
} else {
|
||||
geometry = getNpcData(npc_type).then(({ url, data }) => {
|
||||
mesh = getNpcData(npc_type).then(({ url, data }) => {
|
||||
const cursor = new BufferCursor(data, true);
|
||||
const object_3d = url.endsWith('.nj') ? parse_nj(cursor) : parse_xj(cursor);
|
||||
const nj_objects = url.endsWith('.nj') ? parse_nj(cursor) : parse_xj(cursor);
|
||||
|
||||
if (object_3d) {
|
||||
return object_3d;
|
||||
if (nj_objects.length) {
|
||||
return ninja_object_to_buffer_geometry(nj_objects[0]);
|
||||
} else {
|
||||
throw new Error('File could not be parsed into a BufferGeometry.');
|
||||
throw new Error(`Could not parse ${url}.`);
|
||||
}
|
||||
});
|
||||
|
||||
npc_cache.set(String(npc_type.id), geometry);
|
||||
return geometry;
|
||||
npc_cache.set(String(npc_type.id), mesh);
|
||||
return mesh;
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,10 +38,10 @@ export function get_object_geometry(object_type: ObjectType): Promise<BufferGeom
|
||||
} else {
|
||||
geometry = getObjectData(object_type).then(({ url, data }) => {
|
||||
const cursor = new BufferCursor(data, true);
|
||||
const object_3d = url.endsWith('.nj') ? parse_nj(cursor) : parse_xj(cursor);
|
||||
const nj_objects = url.endsWith('.nj') ? parse_nj(cursor) : parse_xj(cursor);
|
||||
|
||||
if (object_3d) {
|
||||
return object_3d;
|
||||
if (nj_objects.length) {
|
||||
return ninja_object_to_buffer_geometry(nj_objects[0]);
|
||||
} else {
|
||||
throw new Error('File could not be parsed into a BufferGeometry.');
|
||||
}
|
||||
|
@ -1,31 +1,52 @@
|
||||
import {
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
Euler,
|
||||
Matrix4,
|
||||
Quaternion,
|
||||
Vector3
|
||||
} from 'three';
|
||||
import { BufferCursor } from '../../BufferCursor';
|
||||
import { parse_nj_model, NjContext } from './nj';
|
||||
import { parse_xj_model, XjContext } from './xj';
|
||||
import { parse_nj_model, NjModel } from './nj';
|
||||
import { parse_xj_model, XjModel } from './xj';
|
||||
import { Vec3 } from '../../../domain';
|
||||
|
||||
// TODO:
|
||||
// - deal with multiple NJCM chunks
|
||||
// - deal with other types of chunks
|
||||
|
||||
export function parse_nj(cursor: BufferCursor): BufferGeometry | undefined {
|
||||
return parse_ninja(cursor, 'nj');
|
||||
const ANGLE_TO_RAD = 2 * Math.PI / 65536;
|
||||
|
||||
export type NinjaVertex = {
|
||||
position: Vec3,
|
||||
normal?: Vec3,
|
||||
}
|
||||
|
||||
export function parse_xj(cursor: BufferCursor): BufferGeometry | undefined {
|
||||
return parse_ninja(cursor, 'xj');
|
||||
export type NinjaModel = NjModel | XjModel;
|
||||
|
||||
export type NinjaObject<M extends NinjaModel> = {
|
||||
evaluation_flags: {
|
||||
no_translate: boolean,
|
||||
no_rotate: boolean,
|
||||
no_scale: boolean,
|
||||
hidden: boolean,
|
||||
break_child_trace: boolean,
|
||||
zxy_rotation_order: boolean,
|
||||
eval_skip: boolean,
|
||||
eval_shape_skip: boolean,
|
||||
},
|
||||
model?: M,
|
||||
position: Vec3,
|
||||
rotation: Vec3, // Euler angles in radians.
|
||||
scale: Vec3,
|
||||
children: NinjaObject<M>[],
|
||||
}
|
||||
|
||||
type Format = 'nj' | 'xj';
|
||||
type Context = NjContext | XjContext;
|
||||
export function parse_nj(cursor: BufferCursor): NinjaObject<NjModel>[] {
|
||||
return parse_ninja(cursor, parse_nj_model, []);
|
||||
}
|
||||
|
||||
function parse_ninja(cursor: BufferCursor, format: Format): BufferGeometry | undefined {
|
||||
export function parse_xj(cursor: BufferCursor): NinjaObject<XjModel>[] {
|
||||
return parse_ninja(cursor, parse_xj_model, undefined);
|
||||
}
|
||||
|
||||
function parse_ninja<M extends NinjaModel>(
|
||||
cursor: BufferCursor,
|
||||
parse_model: (cursor: BufferCursor, context: any) => M,
|
||||
context: any
|
||||
): NinjaObject<M>[] {
|
||||
while (cursor.bytes_left) {
|
||||
// Ninja uses a little endian variant of the IFF format.
|
||||
// IFF files contain chunks preceded by an 8-byte header.
|
||||
@ -34,44 +55,21 @@ function parse_ninja(cursor: BufferCursor, format: Format): BufferGeometry | und
|
||||
const iff_chunk_size = cursor.u32();
|
||||
|
||||
if (iff_type_id === 'NJCM') {
|
||||
return parse_njcm(cursor.take(iff_chunk_size), format);
|
||||
return parse_sibling_objects(cursor.take(iff_chunk_size), parse_model, context);
|
||||
} else {
|
||||
cursor.seek(iff_chunk_size);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function parse_njcm(cursor: BufferCursor, format: Format): BufferGeometry | undefined {
|
||||
if (cursor.bytes_left) {
|
||||
let context: Context;
|
||||
|
||||
if (format === 'nj') {
|
||||
context = {
|
||||
format,
|
||||
positions: [],
|
||||
normals: [],
|
||||
cached_chunk_offsets: [],
|
||||
vertices: []
|
||||
};
|
||||
} else {
|
||||
context = {
|
||||
format,
|
||||
positions: [],
|
||||
normals: [],
|
||||
indices: []
|
||||
};
|
||||
}
|
||||
|
||||
parse_sibling_objects(cursor, new Matrix4(), context);
|
||||
return create_buffer_geometry(context);
|
||||
}
|
||||
}
|
||||
|
||||
function parse_sibling_objects(
|
||||
// TODO: cache model and object offsets so we don't reparse the same data.
|
||||
function parse_sibling_objects<M extends NinjaModel>(
|
||||
cursor: BufferCursor,
|
||||
parent_matrix: Matrix4,
|
||||
context: Context
|
||||
): void {
|
||||
parse_model: (cursor: BufferCursor, context: any) => M,
|
||||
context: any
|
||||
): NinjaObject<M>[] {
|
||||
const eval_flags = cursor.u32();
|
||||
const no_translate = (eval_flags & 0b1) !== 0;
|
||||
const no_rotate = (eval_flags & 0b10) !== 0;
|
||||
@ -79,61 +77,62 @@ function parse_sibling_objects(
|
||||
const hidden = (eval_flags & 0b1000) !== 0;
|
||||
const break_child_trace = (eval_flags & 0b10000) !== 0;
|
||||
const zxy_rotation_order = (eval_flags & 0b100000) !== 0;
|
||||
const eval_skip = (eval_flags & 0b1000000) !== 0;
|
||||
const eval_shape_skip = (eval_flags & 0b1000000) !== 0;
|
||||
|
||||
const model_offset = cursor.u32();
|
||||
const pos_x = cursor.f32();
|
||||
const pos_y = cursor.f32();
|
||||
const pos_z = cursor.f32();
|
||||
const rotation_x = cursor.i32() * (2 * Math.PI / 0xFFFF);
|
||||
const rotation_y = cursor.i32() * (2 * Math.PI / 0xFFFF);
|
||||
const rotation_z = cursor.i32() * (2 * Math.PI / 0xFFFF);
|
||||
const rotation_x = cursor.i32() * ANGLE_TO_RAD;
|
||||
const rotation_y = cursor.i32() * ANGLE_TO_RAD;
|
||||
const rotation_z = cursor.i32() * ANGLE_TO_RAD;
|
||||
const scale_x = cursor.f32();
|
||||
const scale_y = cursor.f32();
|
||||
const scale_z = cursor.f32();
|
||||
const child_offset = cursor.u32();
|
||||
const sibling_offset = cursor.u32();
|
||||
|
||||
const rotation = new Euler(rotation_x, rotation_y, rotation_z, zxy_rotation_order ? 'ZXY' : 'ZYX');
|
||||
const matrix = new Matrix4()
|
||||
.compose(
|
||||
no_translate ? new Vector3() : new Vector3(pos_x, pos_y, pos_z),
|
||||
no_rotate ? new Quaternion(0, 0, 0, 1) : new Quaternion().setFromEuler(rotation),
|
||||
no_scale ? new Vector3(1, 1, 1) : new Vector3(scale_x, scale_y, scale_z)
|
||||
)
|
||||
.premultiply(parent_matrix);
|
||||
let model: M | undefined;
|
||||
let children: NinjaObject<M>[];
|
||||
let siblings: NinjaObject<M>[];
|
||||
|
||||
if (model_offset && !hidden) {
|
||||
if (model_offset) {
|
||||
cursor.seek_start(model_offset);
|
||||
parse_model(cursor, matrix, context);
|
||||
model = parse_model(cursor, context);
|
||||
}
|
||||
|
||||
if (child_offset && !break_child_trace) {
|
||||
if (child_offset) {
|
||||
cursor.seek_start(child_offset);
|
||||
parse_sibling_objects(cursor, matrix, context);
|
||||
children = parse_sibling_objects(cursor, parse_model, context);
|
||||
} else {
|
||||
children = [];
|
||||
}
|
||||
|
||||
if (sibling_offset) {
|
||||
cursor.seek_start(sibling_offset);
|
||||
parse_sibling_objects(cursor, parent_matrix, context);
|
||||
}
|
||||
}
|
||||
|
||||
function create_buffer_geometry(context: Context): BufferGeometry {
|
||||
const geometry = new BufferGeometry();
|
||||
geometry.addAttribute('position', new BufferAttribute(new Float32Array(context.positions), 3));
|
||||
geometry.addAttribute('normal', new BufferAttribute(new Float32Array(context.normals), 3));
|
||||
|
||||
if ('indices' in context) {
|
||||
geometry.setIndex(new BufferAttribute(new Uint16Array(context.indices), 1));
|
||||
}
|
||||
|
||||
return geometry;
|
||||
}
|
||||
|
||||
function parse_model(cursor: BufferCursor, matrix: Matrix4, context: Context): void {
|
||||
if (context.format === 'nj') {
|
||||
parse_nj_model(cursor, matrix, context);
|
||||
siblings = parse_sibling_objects(cursor, parse_model, context);
|
||||
} else {
|
||||
parse_xj_model(cursor, matrix, context);
|
||||
siblings = [];
|
||||
}
|
||||
|
||||
const object: NinjaObject<M> = {
|
||||
evaluation_flags: {
|
||||
no_translate,
|
||||
no_rotate,
|
||||
no_scale,
|
||||
hidden,
|
||||
break_child_trace,
|
||||
zxy_rotation_order,
|
||||
eval_skip,
|
||||
eval_shape_skip,
|
||||
},
|
||||
model,
|
||||
position: new Vec3(pos_x, pos_y, pos_z),
|
||||
rotation: new Vec3(rotation_x, rotation_y, rotation_z),
|
||||
scale: new Vec3(scale_x, scale_y, scale_z),
|
||||
children,
|
||||
};
|
||||
|
||||
return [object, ...siblings];
|
||||
}
|
||||
|
@ -101,13 +101,8 @@ function parse_motion(cursor: BufferCursor): NjMotion {
|
||||
const motion_data_list = [];
|
||||
|
||||
// The mdata array stops where the motion structure starts.
|
||||
while (true) {
|
||||
while (mdata_offset < motion_offset) {
|
||||
cursor.seek_start(mdata_offset);
|
||||
|
||||
if (cursor.position >= motion_offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
mdata_offset = mdata_offset += 8 * element_count;
|
||||
|
||||
let motion_data: NjMotionData = {
|
||||
|
@ -1,63 +1,120 @@
|
||||
import { Matrix3, Matrix4, Vector3 } from 'three';
|
||||
import { BufferCursor } from '../../BufferCursor';
|
||||
import Logger from 'js-logger';
|
||||
import { BufferCursor } from '../../BufferCursor';
|
||||
import { Vec3 } from '../../../domain';
|
||||
import { NinjaVertex } from '.';
|
||||
|
||||
const logger = Logger.get('bin_data/parsing/ninja/nj');
|
||||
|
||||
// TODO:
|
||||
// - deal with multiple NJCM chunks
|
||||
// - deal with other types of chunks
|
||||
// - textures
|
||||
// - colors
|
||||
// - bump maps
|
||||
// - animation
|
||||
// - deal with vertex information contained in triangle strips
|
||||
|
||||
export interface NjContext {
|
||||
format: 'nj';
|
||||
positions: number[];
|
||||
normals: number[];
|
||||
cached_chunk_offsets: number[];
|
||||
vertices: { position: Vector3, normal: Vector3 }[];
|
||||
export type NjModel = {
|
||||
type: 'nj',
|
||||
/**
|
||||
* Sparse array of vertices.
|
||||
*/
|
||||
vertices: NinjaVertex[],
|
||||
meshes: NjTriangleStrip[],
|
||||
// materials: [],
|
||||
bounding_sphere_center: Vec3,
|
||||
bounding_sphere_radius: number,
|
||||
}
|
||||
|
||||
interface Node {
|
||||
vertices: { position: Vector3, normal: Vector3 }[];
|
||||
indices: number[];
|
||||
parent?: Node;
|
||||
children: Node[];
|
||||
enum NjChunkType {
|
||||
Unknown, Null, Bits, CachePolygonList, DrawPolygonList, Tiny, Material, Vertex, Volume, Strip, End
|
||||
}
|
||||
|
||||
interface ChunkVertex {
|
||||
index: number;
|
||||
position: [number, number, number];
|
||||
normal?: [number, number, number];
|
||||
type NjChunk = {
|
||||
type: NjChunkType,
|
||||
type_id: number,
|
||||
} & (NjUnknownChunk | NjNullChunk | NjBitsChunk | NjCachePolygonListChunk | NjDrawPolygonListChunk | NjTinyChunk | NjMaterialChunk | NjVertexChunk | NjVolumeChunk | NjStripChunk | NjEndChunk)
|
||||
|
||||
type NjUnknownChunk = {
|
||||
type: NjChunkType.Unknown,
|
||||
}
|
||||
|
||||
interface ChunkTriangleStrip {
|
||||
clockwise_winding: boolean;
|
||||
indices: number[];
|
||||
type NjNullChunk = {
|
||||
type: NjChunkType.Null,
|
||||
}
|
||||
|
||||
export function parse_nj_model(cursor: BufferCursor, matrix: Matrix4, context: NjContext): void {
|
||||
const { positions, normals, cached_chunk_offsets, vertices } = context;
|
||||
type NjBitsChunk = {
|
||||
type: NjChunkType.Bits,
|
||||
}
|
||||
|
||||
type NjCachePolygonListChunk = {
|
||||
type: NjChunkType.CachePolygonList,
|
||||
cache_index: number,
|
||||
offset: number,
|
||||
}
|
||||
|
||||
type NjDrawPolygonListChunk = {
|
||||
type: NjChunkType.DrawPolygonList,
|
||||
cache_index: number
|
||||
}
|
||||
|
||||
type NjTinyChunk = {
|
||||
type: NjChunkType.Tiny,
|
||||
}
|
||||
|
||||
type NjMaterialChunk = {
|
||||
type: NjChunkType.Material,
|
||||
}
|
||||
|
||||
type NjVertexChunk = {
|
||||
type: NjChunkType.Vertex,
|
||||
vertices: NjVertex[]
|
||||
}
|
||||
|
||||
type NjVolumeChunk = {
|
||||
type: NjChunkType.Volume,
|
||||
}
|
||||
|
||||
type NjStripChunk = {
|
||||
type: NjChunkType.Strip,
|
||||
triangle_strips: NjTriangleStrip[]
|
||||
}
|
||||
|
||||
type NjEndChunk = {
|
||||
type: NjChunkType.End,
|
||||
}
|
||||
|
||||
type NjVertex = {
|
||||
index: number,
|
||||
position: Vec3,
|
||||
normal?: Vec3,
|
||||
}
|
||||
|
||||
type NjTriangleStrip = {
|
||||
clockwise_winding: boolean,
|
||||
indices: number[],
|
||||
}
|
||||
|
||||
export function parse_nj_model(cursor: BufferCursor, cached_chunk_offsets: number[]): NjModel {
|
||||
const vlist_offset = cursor.u32(); // Vertex list
|
||||
const plist_offset = cursor.u32(); // Triangle strip index list
|
||||
|
||||
const normal_matrix = new Matrix3().getNormalMatrix(matrix);
|
||||
const bounding_sphere_center = new Vec3(
|
||||
cursor.f32(),
|
||||
cursor.f32(),
|
||||
cursor.f32()
|
||||
);
|
||||
const bounding_sphere_radius = cursor.f32();
|
||||
const vertices: NinjaVertex[] = [];
|
||||
const meshes: NjTriangleStrip[] = [];
|
||||
|
||||
if (vlist_offset) {
|
||||
cursor.seek_start(vlist_offset);
|
||||
|
||||
for (const chunk of parse_chunks(cursor, cached_chunk_offsets, true)) {
|
||||
if (chunk.chunk_type === 'VERTEX') {
|
||||
const chunk_vertices: ChunkVertex[] = chunk.data;
|
||||
|
||||
for (const vertex of chunk_vertices) {
|
||||
const position = new Vector3(...vertex.position).applyMatrix4(matrix);
|
||||
const normal = vertex.normal ? new Vector3(...vertex.normal).applyMatrix3(normal_matrix) : new Vector3(0, 1, 0);
|
||||
vertices[vertex.index] = { position, normal };
|
||||
if (chunk.type === NjChunkType.Vertex) {
|
||||
for (const vertex of chunk.vertices) {
|
||||
vertices[vertex.index] = {
|
||||
position: vertex.position,
|
||||
normal: vertex.normal
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -67,127 +124,132 @@ export function parse_nj_model(cursor: BufferCursor, matrix: Matrix4, context: N
|
||||
cursor.seek_start(plist_offset);
|
||||
|
||||
for (const chunk of parse_chunks(cursor, cached_chunk_offsets, false)) {
|
||||
if (chunk.chunk_type === 'STRIP') {
|
||||
for (const { clockwiseWinding, indices: stripIndices } of chunk.data) {
|
||||
for (let j = 2; j < stripIndices.length; ++j) {
|
||||
const a = vertices[stripIndices[j - 2]];
|
||||
const b = vertices[stripIndices[j - 1]];
|
||||
const c = vertices[stripIndices[j]];
|
||||
|
||||
if (a && b && c) {
|
||||
if (j % 2 === (clockwiseWinding ? 1 : 0)) {
|
||||
positions.splice(positions.length, 0, a.position.x, a.position.y, a.position.z);
|
||||
positions.splice(positions.length, 0, b.position.x, b.position.y, b.position.z);
|
||||
positions.splice(positions.length, 0, c.position.x, c.position.y, c.position.z);
|
||||
normals.splice(normals.length, 0, a.normal.x, a.normal.y, a.normal.z);
|
||||
normals.splice(normals.length, 0, b.normal.x, b.normal.y, b.normal.z);
|
||||
normals.splice(normals.length, 0, c.normal.x, c.normal.y, c.normal.z);
|
||||
} else {
|
||||
positions.splice(positions.length, 0, b.position.x, b.position.y, b.position.z);
|
||||
positions.splice(positions.length, 0, a.position.x, a.position.y, a.position.z);
|
||||
positions.splice(positions.length, 0, c.position.x, c.position.y, c.position.z);
|
||||
normals.splice(normals.length, 0, b.normal.x, b.normal.y, b.normal.z);
|
||||
normals.splice(normals.length, 0, a.normal.x, a.normal.y, a.normal.z);
|
||||
normals.splice(normals.length, 0, c.normal.x, c.normal.y, c.normal.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chunk.type === NjChunkType.Strip) {
|
||||
meshes.push(...chunk.triangle_strips);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'nj',
|
||||
vertices,
|
||||
meshes,
|
||||
bounding_sphere_center,
|
||||
bounding_sphere_radius
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: don't reparse when DrawPolygonList chunk is encountered.
|
||||
function parse_chunks(
|
||||
cursor: BufferCursor,
|
||||
cached_chunk_offsets: number[],
|
||||
wide_end_chunks: boolean
|
||||
): Array<{
|
||||
chunk_type: string,
|
||||
chunk_sub_type: string | null,
|
||||
chunk_type_id: number,
|
||||
data: any
|
||||
}> {
|
||||
const chunks = [];
|
||||
): NjChunk[] {
|
||||
const chunks: NjChunk[] = [];
|
||||
let loop = true;
|
||||
|
||||
while (loop) {
|
||||
const chunk_type_id = cursor.u8();
|
||||
const type_id = cursor.u8();
|
||||
const flags = cursor.u8();
|
||||
const chunk_start_position = cursor.position;
|
||||
let chunk_type = 'UNKOWN';
|
||||
let chunk_sub_type = null;
|
||||
let data = null;
|
||||
let size = 0;
|
||||
|
||||
if (chunk_type_id === 0) {
|
||||
chunk_type = 'NULL';
|
||||
} else if (1 <= chunk_type_id && chunk_type_id <= 5) {
|
||||
chunk_type = 'BITS';
|
||||
if (type_id === 0) {
|
||||
chunks.push({
|
||||
type: NjChunkType.Null,
|
||||
type_id
|
||||
});
|
||||
} else if (1 <= type_id && type_id <= 3) {
|
||||
chunks.push({
|
||||
type: NjChunkType.Bits,
|
||||
type_id
|
||||
});
|
||||
} else if (type_id === 4) {
|
||||
const cache_index = flags;
|
||||
const offset = cursor.position;
|
||||
chunks.push({
|
||||
type: NjChunkType.CachePolygonList,
|
||||
type_id,
|
||||
cache_index,
|
||||
offset
|
||||
});
|
||||
cached_chunk_offsets[cache_index] = offset;
|
||||
loop = false;
|
||||
} else if (type_id === 5) {
|
||||
const cache_index = flags;
|
||||
const cached_offset = cached_chunk_offsets[cache_index];
|
||||
|
||||
if (chunk_type_id === 4) {
|
||||
chunk_sub_type = 'CACHE_POLYGON_LIST';
|
||||
data = {
|
||||
store_index: flags,
|
||||
offset: cursor.position
|
||||
};
|
||||
cached_chunk_offsets[data.store_index] = data.offset;
|
||||
loop = false;
|
||||
} else if (chunk_type_id === 5) {
|
||||
chunk_sub_type = 'DRAW_POLYGON_LIST';
|
||||
data = {
|
||||
store_index: flags
|
||||
};
|
||||
cursor.seek_start(cached_chunk_offsets[data.store_index]);
|
||||
if (cached_offset != null) {
|
||||
cursor.seek_start(cached_offset);
|
||||
chunks.push(
|
||||
...parse_chunks(cursor, cached_chunk_offsets, wide_end_chunks)
|
||||
);
|
||||
}
|
||||
} else if (8 <= chunk_type_id && chunk_type_id <= 9) {
|
||||
chunk_type = 'TINY';
|
||||
|
||||
chunks.push({
|
||||
type: NjChunkType.DrawPolygonList,
|
||||
type_id,
|
||||
cache_index
|
||||
});
|
||||
} else if (8 <= type_id && type_id <= 9) {
|
||||
size = 2;
|
||||
} else if (17 <= chunk_type_id && chunk_type_id <= 31) {
|
||||
chunk_type = 'MATERIAL';
|
||||
chunks.push({
|
||||
type: NjChunkType.Tiny,
|
||||
type_id
|
||||
});
|
||||
} else if (17 <= type_id && type_id <= 31) {
|
||||
size = 2 + 2 * cursor.u16();
|
||||
} else if (32 <= chunk_type_id && chunk_type_id <= 50) {
|
||||
chunk_type = 'VERTEX';
|
||||
chunks.push({
|
||||
type: NjChunkType.Material,
|
||||
type_id
|
||||
});
|
||||
} else if (32 <= type_id && type_id <= 50) {
|
||||
size = 2 + 4 * cursor.u16();
|
||||
data = parse_chunk_vertex(cursor, chunk_type_id, flags);
|
||||
} else if (56 <= chunk_type_id && chunk_type_id <= 58) {
|
||||
chunk_type = 'VOLUME';
|
||||
chunks.push({
|
||||
type: NjChunkType.Vertex,
|
||||
type_id,
|
||||
vertices: parse_vertex_chunk(cursor, type_id, flags)
|
||||
});
|
||||
} else if (56 <= type_id && type_id <= 58) {
|
||||
size = 2 + 2 * cursor.u16();
|
||||
} else if (64 <= chunk_type_id && chunk_type_id <= 75) {
|
||||
chunk_type = 'STRIP';
|
||||
chunks.push({
|
||||
type: NjChunkType.Volume,
|
||||
type_id
|
||||
});
|
||||
} else if (64 <= type_id && type_id <= 75) {
|
||||
size = 2 + 2 * cursor.u16();
|
||||
data = parse_chunk_triangle_strip(cursor, chunk_type_id);
|
||||
} else if (chunk_type_id === 255) {
|
||||
chunk_type = 'END';
|
||||
chunks.push({
|
||||
type: NjChunkType.Strip,
|
||||
type_id,
|
||||
triangle_strips: parse_triangle_strip_chunk(cursor, type_id)
|
||||
});
|
||||
} else if (type_id === 255) {
|
||||
size = wide_end_chunks ? 2 : 0;
|
||||
chunks.push({
|
||||
type: NjChunkType.End,
|
||||
type_id
|
||||
});
|
||||
loop = false;
|
||||
} else {
|
||||
// Ignore unknown chunks.
|
||||
logger.warn(`Unknown chunk type: ${chunk_type_id}.`);
|
||||
size = 2 + 2 * cursor.u16();
|
||||
chunks.push({
|
||||
type: NjChunkType.Unknown,
|
||||
type_id
|
||||
});
|
||||
logger.warn(`Unknown chunk type ${type_id} at offset ${chunk_start_position}.`);
|
||||
}
|
||||
|
||||
cursor.seek_start(chunk_start_position + size);
|
||||
|
||||
chunks.push({
|
||||
chunk_type,
|
||||
chunk_sub_type,
|
||||
chunk_type_id,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function parse_chunk_vertex(
|
||||
function parse_vertex_chunk(
|
||||
cursor: BufferCursor,
|
||||
chunk_type_id: number,
|
||||
flags: number
|
||||
): ChunkVertex[] {
|
||||
): NjVertex[] {
|
||||
// There are apparently 4 different sets of vertices, ignore all but set 0.
|
||||
if ((flags & 0b11) !== 0) {
|
||||
return [];
|
||||
@ -196,27 +258,27 @@ function parse_chunk_vertex(
|
||||
const index = cursor.u16();
|
||||
const vertex_count = cursor.u16();
|
||||
|
||||
const vertices: ChunkVertex[] = [];
|
||||
const vertices: NjVertex[] = [];
|
||||
|
||||
for (let i = 0; i < vertex_count; ++i) {
|
||||
const vertex: ChunkVertex = {
|
||||
const vertex: NjVertex = {
|
||||
index: index + i,
|
||||
position: [
|
||||
position: new Vec3(
|
||||
cursor.f32(), // x
|
||||
cursor.f32(), // y
|
||||
cursor.f32(), // z
|
||||
]
|
||||
)
|
||||
};
|
||||
|
||||
if (chunk_type_id === 32) {
|
||||
cursor.seek(4); // Always 1.0
|
||||
} else if (chunk_type_id === 33) {
|
||||
cursor.seek(4); // Always 1.0
|
||||
vertex.normal = [
|
||||
vertex.normal = new Vec3(
|
||||
cursor.f32(), // x
|
||||
cursor.f32(), // y
|
||||
cursor.f32(), // z
|
||||
];
|
||||
);
|
||||
cursor.seek(4); // Always 0.0
|
||||
} else if (35 <= chunk_type_id && chunk_type_id <= 40) {
|
||||
if (chunk_type_id === 37) {
|
||||
@ -228,11 +290,11 @@ function parse_chunk_vertex(
|
||||
cursor.seek(4);
|
||||
}
|
||||
} else if (41 <= chunk_type_id && chunk_type_id <= 47) {
|
||||
vertex.normal = [
|
||||
vertex.normal = new Vec3(
|
||||
cursor.f32(), // x
|
||||
cursor.f32(), // y
|
||||
cursor.f32(), // z
|
||||
];
|
||||
);
|
||||
|
||||
if (chunk_type_id >= 42) {
|
||||
if (chunk_type_id === 44) {
|
||||
@ -260,10 +322,10 @@ function parse_chunk_vertex(
|
||||
return vertices;
|
||||
}
|
||||
|
||||
function parse_chunk_triangle_strip(
|
||||
function parse_triangle_strip_chunk(
|
||||
cursor: BufferCursor,
|
||||
chunk_type_id: number
|
||||
): ChunkTriangleStrip[] {
|
||||
): NjTriangleStrip[] {
|
||||
const user_offset_and_strip_count = cursor.u16();
|
||||
const user_flags_size = user_offset_and_strip_count >>> 14;
|
||||
const strip_count = user_offset_and_strip_count & 0x3FFF;
|
||||
@ -292,7 +354,7 @@ function parse_chunk_triangle_strip(
|
||||
parse_texture_coords_hires
|
||||
] = options;
|
||||
|
||||
const strips = [];
|
||||
const strips: NjTriangleStrip[] = [];
|
||||
|
||||
for (let i = 0; i < strip_count; ++i) {
|
||||
const winding_flag_and_index_count = cursor.i16();
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Matrix3, Matrix4, Vector3 } from 'three';
|
||||
import { BufferCursor } from '../../BufferCursor';
|
||||
import { Vec3 } from '../../../domain';
|
||||
import { NinjaVertex } from '.';
|
||||
|
||||
// TODO:
|
||||
// - textures
|
||||
@ -7,16 +8,17 @@ import { BufferCursor } from '../../BufferCursor';
|
||||
// - bump maps
|
||||
// - animation
|
||||
|
||||
export interface XjContext {
|
||||
format: 'xj';
|
||||
positions: number[];
|
||||
normals: number[];
|
||||
indices: number[];
|
||||
export type XjModel = {
|
||||
type: 'xj',
|
||||
vertices: NinjaVertex[],
|
||||
meshes: XjTriangleStrip[],
|
||||
}
|
||||
|
||||
export function parse_xj_model(cursor: BufferCursor, matrix: Matrix4, context: XjContext): void {
|
||||
const { positions, normals, indices } = context;
|
||||
export type XjTriangleStrip = {
|
||||
indices: number[],
|
||||
}
|
||||
|
||||
export function parse_xj_model(cursor: BufferCursor): 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.
|
||||
@ -26,8 +28,11 @@ export function parse_xj_model(cursor: BufferCursor, matrix: Matrix4, context: X
|
||||
const triangle_strip_b_count = cursor.u32();
|
||||
cursor.seek(16); // Bounding sphere position and radius in floats.
|
||||
|
||||
const normal_matrix = new Matrix3().getNormalMatrix(matrix);
|
||||
const index_offset = positions.length / 3;
|
||||
const model: XjModel = {
|
||||
type: 'xj',
|
||||
vertices: [],
|
||||
meshes: []
|
||||
};
|
||||
|
||||
if (vertex_info_list_offset) {
|
||||
cursor.seek_start(vertex_info_list_offset);
|
||||
@ -38,66 +43,55 @@ export function parse_xj_model(cursor: BufferCursor, matrix: Matrix4, context: X
|
||||
|
||||
for (let i = 0; i < vertex_count; ++i) {
|
||||
cursor.seek_start(vertexList_offset + i * vertex_size);
|
||||
const position = new Vector3(
|
||||
const position = new Vec3(
|
||||
cursor.f32(),
|
||||
cursor.f32(),
|
||||
cursor.f32()
|
||||
).applyMatrix4(matrix);
|
||||
let normal;
|
||||
);
|
||||
let normal: Vec3 | undefined;
|
||||
|
||||
if (vertex_size === 28 || vertex_size === 32 || vertex_size === 36) {
|
||||
normal = new Vector3(
|
||||
normal = new Vec3(
|
||||
cursor.f32(),
|
||||
cursor.f32(),
|
||||
cursor.f32()
|
||||
).applyMatrix3(normal_matrix);
|
||||
} else {
|
||||
normal = new Vector3(0, 1, 0);
|
||||
);
|
||||
}
|
||||
|
||||
positions.push(position.x);
|
||||
positions.push(position.y);
|
||||
positions.push(position.z);
|
||||
normals.push(normal.x);
|
||||
normals.push(normal.y);
|
||||
normals.push(normal.z);
|
||||
model.vertices.push({ position, normal });
|
||||
}
|
||||
}
|
||||
|
||||
if (triangle_strip_list_a_offset) {
|
||||
parse_triangle_strip_list(
|
||||
cursor,
|
||||
triangle_strip_list_a_offset,
|
||||
triangle_strip_a_count,
|
||||
positions,
|
||||
normals,
|
||||
indices,
|
||||
index_offset
|
||||
model.meshes.push(
|
||||
...parse_triangle_strip_list(
|
||||
cursor,
|
||||
triangle_strip_list_a_offset,
|
||||
triangle_strip_a_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (triangle_strip_list_b_offset) {
|
||||
parse_triangle_strip_list(
|
||||
cursor,
|
||||
triangle_strip_list_b_offset,
|
||||
triangle_strip_b_count,
|
||||
positions,
|
||||
normals,
|
||||
indices,
|
||||
index_offset
|
||||
model.meshes.push(
|
||||
...parse_triangle_strip_list(
|
||||
cursor,
|
||||
triangle_strip_list_b_offset,
|
||||
triangle_strip_b_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
function parse_triangle_strip_list(
|
||||
cursor: BufferCursor,
|
||||
triangle_strip_list_offset: number,
|
||||
triangle_strip_count: number,
|
||||
positions: number[],
|
||||
normals: number[],
|
||||
indices: number[],
|
||||
index_offset: number
|
||||
): void {
|
||||
): XjTriangleStrip[] {
|
||||
const strips: XjTriangleStrip[] = [];
|
||||
|
||||
for (let i = 0; i < triangle_strip_count; ++i) {
|
||||
cursor.seek_start(triangle_strip_list_offset + i * 20);
|
||||
cursor.seek(8); // Skip material information.
|
||||
@ -106,67 +100,10 @@ function parse_triangle_strip_list(
|
||||
// Ignoring 4 bytes.
|
||||
|
||||
cursor.seek_start(index_list_offset);
|
||||
const strip_indices = cursor.u16_array(index_count);
|
||||
let clockwise = true;
|
||||
const indices = cursor.u16_array(index_count);
|
||||
|
||||
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;
|
||||
// }
|
||||
}
|
||||
strips.push({ indices });
|
||||
}
|
||||
|
||||
return strips;
|
||||
}
|
||||
|
@ -60,10 +60,10 @@ export class Vec3 {
|
||||
y: number;
|
||||
z: number;
|
||||
|
||||
constructor(x?: number, y?: number, z?: number) {
|
||||
this.x = x || 0;
|
||||
this.y = y || 0;
|
||||
this.z = z || 0;
|
||||
constructor(x: number, y: number, z: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
add(v: Vec3): Vec3 {
|
||||
@ -73,11 +73,8 @@ export class Vec3 {
|
||||
return this;
|
||||
}
|
||||
|
||||
clone(x?: number, y?: number, z?: number) {
|
||||
return new Vec3(
|
||||
typeof x === 'number' ? x : this.x,
|
||||
typeof y === 'number' ? y : this.y,
|
||||
typeof z === 'number' ? z : this.z);
|
||||
clone() {
|
||||
return new Vec3(this.x, this.y, this.z);
|
||||
}
|
||||
};
|
||||
|
||||
@ -216,7 +213,7 @@ export class QuestEntity {
|
||||
}
|
||||
}
|
||||
|
||||
object3d?: Object3D;
|
||||
object_3d?: Object3D;
|
||||
|
||||
constructor(
|
||||
area_id: number,
|
||||
|
@ -198,8 +198,8 @@ export class Renderer {
|
||||
|
||||
for (const object of this.quest.objects) {
|
||||
if (object.area_id === this.area.id) {
|
||||
if (object.object3d) {
|
||||
this.objGeometry.add(object.object3d);
|
||||
if (object.object_3d) {
|
||||
this.objGeometry.add(object.object_3d);
|
||||
} else {
|
||||
loaded = false;
|
||||
}
|
||||
@ -208,8 +208,8 @@ export class Renderer {
|
||||
|
||||
for (const npc of this.quest.npcs) {
|
||||
if (npc.area_id === this.area.id) {
|
||||
if (npc.object3d) {
|
||||
this.npcGeometry.add(npc.object3d);
|
||||
if (npc.object_3d) {
|
||||
this.npcGeometry.add(npc.object_3d);
|
||||
} else {
|
||||
loaded = false;
|
||||
}
|
||||
@ -257,7 +257,7 @@ export class Renderer {
|
||||
: oldSelectedData !== data;
|
||||
|
||||
if (selectionChanged) {
|
||||
quest_editor_store.setSelectedEntity(data && data.entity);
|
||||
quest_editor_store.set_selected_entity(data && data.entity);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AnimationClip, InterpolateLinear, InterpolateSmooth, KeyframeTrack, VectorKeyframeTrack } from "three";
|
||||
import { AnimationClip, Euler, InterpolateLinear, InterpolateSmooth, KeyframeTrack, Quaternion, QuaternionKeyframeTrack, VectorKeyframeTrack } from "three";
|
||||
import { NjAction, NjInterpolation, NjKeyframeTrackType } from "../bin_data/parsing/ninja/motion";
|
||||
|
||||
const PSO_FRAME_RATE = 30;
|
||||
@ -8,32 +8,50 @@ export function create_animation_clip(action: NjAction): AnimationClip {
|
||||
const interpolation = motion.interpolation === NjInterpolation.Spline
|
||||
? InterpolateSmooth
|
||||
: InterpolateLinear;
|
||||
// TODO: parse data for all objects.
|
||||
const motion_data = motion.motion_data[0];
|
||||
|
||||
const tracks: KeyframeTrack[] = [];
|
||||
|
||||
motion_data.tracks.forEach(({ type, keyframes }) => {
|
||||
// TODO: rotation
|
||||
if (type === NjKeyframeTrackType.Rotation) return;
|
||||
motion.motion_data.forEach((motion_data, object_id) => {
|
||||
motion_data.tracks.forEach(({ type, keyframes }) => {
|
||||
const times: number[] = [];
|
||||
const values: number[] = [];
|
||||
|
||||
const times: number[] = [];
|
||||
const values: number[] = [];
|
||||
if (type === NjKeyframeTrackType.Position) {
|
||||
const name = `obj_${object_id}.position`;
|
||||
|
||||
for (const keyframe of keyframes) {
|
||||
times.push(keyframe.frame / PSO_FRAME_RATE);
|
||||
values.push(...keyframe.value);
|
||||
}
|
||||
for (const keyframe of keyframes) {
|
||||
times.push(keyframe.frame / PSO_FRAME_RATE);
|
||||
values.push(keyframe.value.x, keyframe.value.y, keyframe.value.z);
|
||||
}
|
||||
|
||||
let name: string;
|
||||
tracks.push(new VectorKeyframeTrack(name, times, values, interpolation));
|
||||
} else if (type === NjKeyframeTrackType.Scale) {
|
||||
const name = `obj_${object_id}.scale`;
|
||||
|
||||
switch (type) {
|
||||
case NjKeyframeTrackType.Position: name = '.position'; break;
|
||||
// case NjKeyframeTrackType.Rotation: name = 'rotation'; break;
|
||||
case NjKeyframeTrackType.Scale: name = '.scale'; break;
|
||||
}
|
||||
for (const keyframe of keyframes) {
|
||||
times.push(keyframe.frame / PSO_FRAME_RATE);
|
||||
values.push(keyframe.value.x, keyframe.value.y, keyframe.value.z);
|
||||
}
|
||||
|
||||
tracks.push(new VectorKeyframeTrack(name!, times, values, interpolation));
|
||||
tracks.push(new VectorKeyframeTrack(name, times, values, interpolation));
|
||||
} else {
|
||||
for (const keyframe of keyframes) {
|
||||
times.push(keyframe.frame / PSO_FRAME_RATE);
|
||||
|
||||
const quat = new Quaternion().setFromEuler(
|
||||
new Euler(keyframe.value.x, keyframe.value.y, keyframe.value.z)
|
||||
);
|
||||
|
||||
values.push(quat.x, quat.y, quat.z, quat.w);
|
||||
}
|
||||
|
||||
tracks.push(
|
||||
new QuaternionKeyframeTrack(
|
||||
`obj_${object_id}.quaternion`, times, values, interpolation
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return new AnimationClip(
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { autorun } from 'mobx';
|
||||
import { BufferGeometry, DoubleSide, Mesh, MeshLambertMaterial } from 'three';
|
||||
import { QuestNpc, QuestObject, QuestEntity } from '../domain';
|
||||
import { QuestEntity, QuestNpc, QuestObject } from '../domain';
|
||||
|
||||
export const OBJECT_COLOR = 0xFFFF00;
|
||||
export const OBJECT_HOVER_COLOR = 0xFFDF3F;
|
||||
@ -23,23 +23,23 @@ function create_mesh(
|
||||
color: number,
|
||||
type: string
|
||||
): Mesh {
|
||||
const object_3d = new Mesh(
|
||||
const mesh = new Mesh(
|
||||
geometry,
|
||||
new MeshLambertMaterial({
|
||||
color,
|
||||
side: DoubleSide
|
||||
})
|
||||
);
|
||||
object_3d.name = type;
|
||||
object_3d.userData.entity = entity;
|
||||
)
|
||||
mesh.name = type;
|
||||
mesh.userData.entity = entity;
|
||||
|
||||
// TODO: dispose autorun?
|
||||
autorun(() => {
|
||||
const { x, y, z } = entity.position;
|
||||
object_3d.position.set(x, y, z);
|
||||
mesh.position.set(x, y, z);
|
||||
const rot = entity.rotation;
|
||||
object_3d.rotation.set(rot.x, rot.y, rot.z);
|
||||
mesh.rotation.set(rot.x, rot.y, rot.z);
|
||||
});
|
||||
|
||||
return object_3d;
|
||||
return mesh;
|
||||
}
|
||||
|
6
src/rendering/index.ts
Normal file
6
src/rendering/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Vec3 } from "../domain";
|
||||
import { Vector3 } from "three";
|
||||
|
||||
export function vec3_to_threejs(v: Vec3): Vector3 {
|
||||
return new Vector3(v.x, v.y, v.z);
|
||||
}
|
@ -1,11 +1,340 @@
|
||||
import { BufferGeometry, DoubleSide, Mesh, MeshLambertMaterial } from 'three';
|
||||
import { BufferAttribute, BufferGeometry, DoubleSide, Euler, Material, Matrix3, Matrix4, Mesh, MeshLambertMaterial, Object3D, Quaternion, Vector3 } from 'three';
|
||||
import { vec3_to_threejs } from '.';
|
||||
import { NinjaModel, NinjaObject } from '../bin_data/parsing/ninja';
|
||||
import { NjModel } from '../bin_data/parsing/ninja/nj';
|
||||
import { XjModel } from '../bin_data/parsing/ninja/xj';
|
||||
import { Vec3 } from '../domain';
|
||||
|
||||
export function create_model_mesh(geometry?: BufferGeometry): Mesh | undefined {
|
||||
return geometry && new Mesh(
|
||||
geometry,
|
||||
new MeshLambertMaterial({
|
||||
color: 0xFF00FF,
|
||||
side: DoubleSide
|
||||
})
|
||||
);
|
||||
const DEFAULT_MATERIAL = new MeshLambertMaterial({
|
||||
color: 0xFF00FF,
|
||||
side: DoubleSide
|
||||
});
|
||||
const DEFAULT_NORMAL = new Vec3(0, 1, 0);
|
||||
|
||||
export function ninja_object_to_object3d(
|
||||
object: NinjaObject<NinjaModel>,
|
||||
material: Material = DEFAULT_MATERIAL
|
||||
): Object3D {
|
||||
return new Object3DCreator(material).create_object_3d(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single BufferGeometry.
|
||||
*/
|
||||
export function ninja_object_to_buffer_geometry(
|
||||
object: NinjaObject<NinjaModel>,
|
||||
material: Material = DEFAULT_MATERIAL
|
||||
): BufferGeometry {
|
||||
return new Object3DCreator(material).create_buffer_geometry(object);
|
||||
}
|
||||
|
||||
class Object3DCreator {
|
||||
private id: number = 0;
|
||||
private vertices: { position: Vector3, normal?: Vector3 }[] = [];
|
||||
private positions: number[] = [];
|
||||
private normals: number[] = [];
|
||||
private indices: number[] = [];
|
||||
private flat: boolean = false;
|
||||
|
||||
constructor(
|
||||
private material: Material
|
||||
) { }
|
||||
|
||||
create_object_3d(object: NinjaObject<NinjaModel>): Object3D {
|
||||
return this.object_to_object3d(object, new Matrix4())!;
|
||||
}
|
||||
|
||||
create_buffer_geometry(object: NinjaObject<NinjaModel>): BufferGeometry {
|
||||
this.flat = true;
|
||||
|
||||
this.object_to_object3d(object, new Matrix4());
|
||||
|
||||
const geom = new BufferGeometry();
|
||||
|
||||
geom.addAttribute('position', new BufferAttribute(new Float32Array(this.positions), 3));
|
||||
geom.addAttribute('normal', new BufferAttribute(new Float32Array(this.normals), 3));
|
||||
|
||||
if (this.indices.length) {
|
||||
geom.setIndex(new BufferAttribute(new Uint16Array(this.indices), 1));
|
||||
}
|
||||
|
||||
// The bounding spheres from the object seem be too small.
|
||||
geom.computeBoundingSphere();
|
||||
|
||||
return geom;
|
||||
}
|
||||
|
||||
private object_to_object3d(object: NinjaObject<NinjaModel>, parent_matrix: Matrix4): Object3D | undefined {
|
||||
const {
|
||||
no_translate, no_rotate, no_scale, hidden, break_child_trace, zxy_rotation_order, eval_skip
|
||||
} = object.evaluation_flags;
|
||||
const { position, rotation, scale } = object;
|
||||
|
||||
const euler = new Euler(
|
||||
rotation.x, rotation.y, rotation.z, zxy_rotation_order ? 'ZXY' : 'ZYX'
|
||||
);
|
||||
const matrix = new Matrix4()
|
||||
.compose(
|
||||
no_translate ? new Vector3() : vec3_to_threejs(position),
|
||||
no_rotate ? new Quaternion(0, 0, 0, 1) : new Quaternion().setFromEuler(euler),
|
||||
no_scale ? new Vector3(1, 1, 1) : vec3_to_threejs(scale)
|
||||
)
|
||||
.premultiply(parent_matrix);
|
||||
|
||||
if (this.flat) {
|
||||
if (object.model && !hidden) {
|
||||
this.model_to_geometry(object.model, matrix);
|
||||
}
|
||||
|
||||
if (!break_child_trace) {
|
||||
for (const child of object.children) {
|
||||
this.object_to_object3d(child, matrix);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
} else {
|
||||
let mesh: Object3D;
|
||||
|
||||
if (object.model && !hidden) {
|
||||
mesh = new Mesh(
|
||||
this.model_to_geometry(object.model, matrix),
|
||||
this.material
|
||||
);
|
||||
} else {
|
||||
mesh = new Object3D();
|
||||
}
|
||||
|
||||
if (!eval_skip) {
|
||||
mesh.name = `obj_${this.id++}`;
|
||||
}
|
||||
|
||||
mesh.position.set(position.x, position.y, position.z);
|
||||
mesh.setRotationFromEuler(euler);
|
||||
mesh.scale.set(scale.x, scale.y, scale.z);
|
||||
|
||||
if (!break_child_trace) {
|
||||
for (const child of object.children) {
|
||||
mesh.add(this.object_to_object3d(child, matrix)!);
|
||||
}
|
||||
}
|
||||
|
||||
return mesh;
|
||||
}
|
||||
}
|
||||
|
||||
private model_to_geometry(model: NinjaModel, matrix: Matrix4): BufferGeometry | undefined {
|
||||
if (model.type === 'nj') {
|
||||
return this.nj_model_to_geometry(model, matrix);
|
||||
} else {
|
||||
return this.xj_model_to_geometry(model, matrix);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use indices and don't add duplicate positions/normals.
|
||||
private nj_model_to_geometry(model: NjModel, matrix: Matrix4): BufferGeometry | undefined {
|
||||
const positions = this.flat ? this.positions : [];
|
||||
const normals = this.flat ? this.normals : [];
|
||||
|
||||
const normal_matrix = new Matrix3().getNormalMatrix(matrix);
|
||||
|
||||
const matrix_inverse = new Matrix4().getInverse(matrix);
|
||||
const normal_matrix_inverse = new Matrix3().getNormalMatrix(matrix_inverse);
|
||||
|
||||
const new_vertices = model.vertices.map(({ position, normal }) => {
|
||||
const new_position = vec3_to_threejs(position).applyMatrix4(matrix);
|
||||
|
||||
const new_normal = normal
|
||||
? vec3_to_threejs(normal).applyMatrix3(normal_matrix)
|
||||
: DEFAULT_NORMAL;
|
||||
|
||||
return {
|
||||
position: new_position,
|
||||
normal: new_normal
|
||||
};
|
||||
});
|
||||
|
||||
if (this.flat) {
|
||||
Object.assign(this.vertices, new_vertices);
|
||||
}
|
||||
|
||||
for (const mesh of model.meshes) {
|
||||
for (let i = 2; i < mesh.indices.length; ++i) {
|
||||
const a_idx = mesh.indices[i - 2];
|
||||
const b_idx = mesh.indices[i - 1];
|
||||
const c_idx = mesh.indices[i];
|
||||
let a;
|
||||
let b;
|
||||
let c;
|
||||
|
||||
if (this.flat) {
|
||||
a = this.vertices[a_idx];
|
||||
b = this.vertices[b_idx];
|
||||
c = this.vertices[c_idx];
|
||||
} else {
|
||||
a = model.vertices[a_idx];
|
||||
b = model.vertices[b_idx];
|
||||
c = model.vertices[c_idx];
|
||||
|
||||
if (!a && this.vertices[a_idx]) {
|
||||
const { position, normal } = this.vertices[a_idx];
|
||||
a = {
|
||||
position: position.clone().applyMatrix4(matrix_inverse),
|
||||
normal: normal && normal.clone().applyMatrix3(normal_matrix_inverse)
|
||||
};
|
||||
}
|
||||
|
||||
if (!b && this.vertices[b_idx]) {
|
||||
const { position, normal } = this.vertices[b_idx];
|
||||
b = {
|
||||
position: position.clone().applyMatrix4(matrix_inverse),
|
||||
normal: normal && normal.clone().applyMatrix3(normal_matrix_inverse)
|
||||
};
|
||||
}
|
||||
|
||||
if (!c && this.vertices[c_idx]) {
|
||||
const { position, normal } = this.vertices[c_idx];
|
||||
c = {
|
||||
position: position.clone().applyMatrix4(matrix_inverse),
|
||||
normal: normal && normal.clone().applyMatrix3(normal_matrix_inverse)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (a && b && c) {
|
||||
const a_n = a.normal || DEFAULT_NORMAL;
|
||||
const b_n = b.normal || DEFAULT_NORMAL;
|
||||
const c_n = c.normal || DEFAULT_NORMAL;
|
||||
|
||||
if (i % 2 === (mesh.clockwise_winding ? 1 : 0)) {
|
||||
positions.push(a.position.x, a.position.y, a.position.z);
|
||||
positions.push(b.position.x, b.position.y, b.position.z);
|
||||
positions.push(c.position.x, c.position.y, c.position.z);
|
||||
normals.push(a_n.x, a_n.y, a_n.z);
|
||||
normals.push(b_n.x, b_n.y, b_n.z);
|
||||
normals.push(c_n.x, c_n.y, c_n.z);
|
||||
} else {
|
||||
positions.push(b.position.x, b.position.y, b.position.z);
|
||||
positions.push(a.position.x, a.position.y, a.position.z);
|
||||
positions.push(c.position.x, c.position.y, c.position.z);
|
||||
normals.push(b_n.x, b_n.y, b_n.z);
|
||||
normals.push(a_n.x, a_n.y, a_n.z);
|
||||
normals.push(c_n.x, c_n.y, c_n.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.flat) {
|
||||
return undefined;
|
||||
} else {
|
||||
Object.assign(this.vertices, new_vertices);
|
||||
|
||||
const geom = new BufferGeometry();
|
||||
|
||||
geom.addAttribute('position', new BufferAttribute(new Float32Array(positions), 3));
|
||||
geom.addAttribute('normal', new BufferAttribute(new Float32Array(normals), 3));
|
||||
// The bounding spheres from the object seem be too small.
|
||||
geom.computeBoundingSphere();
|
||||
|
||||
return geom;
|
||||
}
|
||||
}
|
||||
|
||||
private xj_model_to_geometry(model: XjModel, matrix: Matrix4): BufferGeometry | undefined {
|
||||
const positions = this.flat ? this.positions : [];
|
||||
const normals = this.flat ? this.normals : [];
|
||||
const indices = this.flat ? this.indices : [];
|
||||
const index_offset = this.flat ? this.positions.length / 3 : 0;
|
||||
let clockwise = true;
|
||||
|
||||
const normal_matrix = new Matrix3().getNormalMatrix(matrix);
|
||||
|
||||
for (let { position, normal } of model.vertices) {
|
||||
const p = this.flat ? vec3_to_threejs(position).applyMatrix4(matrix) : position;
|
||||
positions.push(p.x, p.y, p.z);
|
||||
|
||||
normal = normal || DEFAULT_NORMAL;
|
||||
const n = this.flat ? vec3_to_threejs(normal).applyMatrix3(normal_matrix) : normal;
|
||||
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;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
if (this.flat) {
|
||||
return undefined;
|
||||
} else {
|
||||
const geom = new BufferGeometry();
|
||||
|
||||
geom.addAttribute('position', new BufferAttribute(new Float32Array(positions), 3));
|
||||
geom.addAttribute('normal', new BufferAttribute(new Float32Array(normals), 3));
|
||||
geom.setIndex(new BufferAttribute(new Uint16Array(indices), 1));
|
||||
// The bounding spheres from the object seem be too small.
|
||||
geom.computeBoundingSphere();
|
||||
|
||||
return geom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,22 +4,44 @@ import { AnimationClip, AnimationMixer, Object3D } from 'three';
|
||||
import { BufferCursor } from '../bin_data/BufferCursor';
|
||||
import { get_area_sections } from '../bin_data/loading/areas';
|
||||
import { get_npc_geometry, get_object_geometry } from '../bin_data/loading/entities';
|
||||
import { parse_nj, parse_xj } from '../bin_data/parsing/ninja';
|
||||
import { parse_nj, parse_xj, NinjaObject, NinjaModel } from '../bin_data/parsing/ninja';
|
||||
import { parse_njm_4 } from '../bin_data/parsing/ninja/motion';
|
||||
import { parse_quest, write_quest_qst } from '../bin_data/parsing/quest';
|
||||
import { Area, Quest, QuestEntity, Section, Vec3 } from '../domain';
|
||||
import { create_animation_clip } from '../rendering/animation';
|
||||
import { create_npc_mesh, create_object_mesh } from '../rendering/entities';
|
||||
import { create_model_mesh } from '../rendering/models';
|
||||
import { create_npc_mesh as create_npc_object_3d, create_object_mesh as create_object_object_3d } from '../rendering/entities';
|
||||
import { ninja_object_to_object3d as create_model_obj3d } from '../rendering/models';
|
||||
|
||||
const logger = Logger.get('stores/QuestEditorStore');
|
||||
|
||||
function traverse(
|
||||
object: NinjaObject<NinjaModel>,
|
||||
head_part: NinjaObject<NinjaModel>,
|
||||
id_ref: [number]
|
||||
) {
|
||||
if (!object.evaluation_flags.eval_skip) {
|
||||
const id = id_ref[0]++;
|
||||
|
||||
if (id === 59) {
|
||||
object.evaluation_flags.hidden = false;
|
||||
object.evaluation_flags.break_child_trace = false;
|
||||
object.children.push(head_part);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of object.children) {
|
||||
traverse(child, head_part, id_ref);
|
||||
}
|
||||
}
|
||||
|
||||
class QuestEditorStore {
|
||||
@observable current_quest?: Quest;
|
||||
@observable current_area?: Area;
|
||||
@observable selected_entity?: QuestEntity;
|
||||
|
||||
@observable.ref current_model?: Object3D;
|
||||
@observable.ref current_model?: NinjaObject<NinjaModel>;
|
||||
@observable.ref current_model_obj3d?: Object3D;
|
||||
@observable.ref animation_mixer?: AnimationMixer;
|
||||
|
||||
set_quest = action('set_quest', (quest?: Quest) => {
|
||||
@ -31,19 +53,28 @@ class QuestEditorStore {
|
||||
}
|
||||
})
|
||||
|
||||
set_model = action('set_model', (model?: Object3D) => {
|
||||
set_model = action('set_model', (model?: NinjaObject<NinjaModel>) => {
|
||||
this.reset_model_and_quest_state();
|
||||
this.current_model = model;
|
||||
|
||||
if (model) {
|
||||
if (this.current_model) {
|
||||
traverse(this.current_model, model, [0]);
|
||||
} else {
|
||||
this.current_model = model;
|
||||
}
|
||||
|
||||
this.current_model_obj3d = create_model_obj3d(this.current_model);
|
||||
}
|
||||
})
|
||||
|
||||
add_animation = action('add_animation', (clip: AnimationClip) => {
|
||||
if (!this.current_model) return;
|
||||
if (!this.current_model_obj3d) return;
|
||||
|
||||
if (this.animation_mixer) {
|
||||
this.animation_mixer.stopAllAction();
|
||||
this.animation_mixer.uncacheRoot(this.current_model);
|
||||
this.animation_mixer.uncacheRoot(this.current_model_obj3d);
|
||||
} else {
|
||||
this.animation_mixer = new AnimationMixer(this.current_model);
|
||||
this.animation_mixer = new AnimationMixer(this.current_model_obj3d);
|
||||
}
|
||||
|
||||
const action = this.animation_mixer.clipAction(clip);
|
||||
@ -59,11 +90,11 @@ class QuestEditorStore {
|
||||
this.animation_mixer.uncacheRoot(this.current_model);
|
||||
}
|
||||
|
||||
this.current_model = undefined;
|
||||
this.current_model_obj3d = undefined;
|
||||
this.animation_mixer = undefined;
|
||||
}
|
||||
|
||||
setSelectedEntity = (entity?: QuestEntity) => {
|
||||
set_selected_entity = (entity?: QuestEntity) => {
|
||||
this.selected_entity = entity;
|
||||
}
|
||||
|
||||
@ -94,12 +125,16 @@ class QuestEditorStore {
|
||||
}
|
||||
|
||||
if (file.name.endsWith('.nj')) {
|
||||
this.set_model(create_model_mesh(parse_nj(new BufferCursor(reader.result, true))));
|
||||
const model = parse_nj(new BufferCursor(reader.result, true))[0];
|
||||
this.set_model(model);
|
||||
} else if (file.name.endsWith('.xj')) {
|
||||
this.set_model(create_model_mesh(parse_xj(new BufferCursor(reader.result, true))));
|
||||
const model = parse_xj(new BufferCursor(reader.result, true))[0];
|
||||
this.set_model(model);
|
||||
} else if (file.name.endsWith('.njm')) {
|
||||
this.add_animation(
|
||||
create_animation_clip(parse_njm_4(new BufferCursor(reader.result, true)))
|
||||
create_animation_clip(
|
||||
parse_njm_4(new BufferCursor(reader.result, true))
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const quest = parse_quest(new BufferCursor(reader.result, true));
|
||||
@ -118,9 +153,9 @@ class QuestEditorStore {
|
||||
// Generate object geometry.
|
||||
for (const object of quest.objects.filter(o => o.area_id === variant.area.id)) {
|
||||
try {
|
||||
const geometry = await get_object_geometry(object.type);
|
||||
const object_geom = await get_object_geometry(object.type);
|
||||
this.set_section_on_visible_quest_entity(object, sections);
|
||||
object.object3d = create_object_mesh(object, geometry);
|
||||
object.object_3d = create_object_object_3d(object, object_geom);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
@ -129,9 +164,9 @@ class QuestEditorStore {
|
||||
// Generate NPC geometry.
|
||||
for (const npc of quest.npcs.filter(npc => npc.area_id === variant.area.id)) {
|
||||
try {
|
||||
const geometry = await get_npc_geometry(npc.type);
|
||||
const npc_geom = await get_npc_geometry(npc.type);
|
||||
this.set_section_on_visible_quest_entity(npc, sections);
|
||||
npc.object3d = create_npc_mesh(npc, geometry);
|
||||
npc.object_3d = create_npc_object_3d(npc, npc_geom);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
|
@ -22,8 +22,6 @@ export class QuestEditorComponent extends React.Component<{}, {
|
||||
|
||||
render() {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
const model = quest_editor_store.current_model;
|
||||
const area = quest_editor_store.current_area;
|
||||
|
||||
return (
|
||||
<div className="qe-QuestEditorComponent">
|
||||
@ -32,8 +30,8 @@ export class QuestEditorComponent extends React.Component<{}, {
|
||||
<QuestInfoComponent quest={quest} />
|
||||
<RendererComponent
|
||||
quest={quest}
|
||||
area={area}
|
||||
model={model}
|
||||
area={quest_editor_store.current_area}
|
||||
model={quest_editor_store.current_model_obj3d}
|
||||
/>
|
||||
<EntityInfoComponent entity={quest_editor_store.selected_entity} />
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user