mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Textures can now be applied to models in the model viewer.
This commit is contained in:
parent
2d1ea81afd
commit
43ca288221
@ -79,6 +79,14 @@ type NjcmDrawPolygonListChunk = {
|
||||
|
||||
type NjcmTinyChunk = {
|
||||
type: NjcmChunkType.Tiny;
|
||||
flip_u: boolean;
|
||||
flip_v: boolean;
|
||||
clamp_u: boolean;
|
||||
clamp_v: boolean;
|
||||
mipmap_d_adjust: number;
|
||||
filter_mode: number;
|
||||
super_sample: boolean;
|
||||
texture_id: number;
|
||||
};
|
||||
|
||||
type NjcmMaterialChunk = {
|
||||
@ -123,6 +131,7 @@ type NjcmTriangleStrip = {
|
||||
clockwise_winding: boolean;
|
||||
has_tex_coords: boolean;
|
||||
has_normal: boolean;
|
||||
texture_id?: number;
|
||||
vertices: NjcmMeshVertex[];
|
||||
};
|
||||
|
||||
@ -161,8 +170,16 @@ export function parse_njcm_model(cursor: Cursor, cached_chunk_offsets: number[])
|
||||
if (plist_offset) {
|
||||
cursor.seek_start(plist_offset);
|
||||
|
||||
let texture_id: number | undefined;
|
||||
|
||||
for (const chunk of parse_chunks(cursor, cached_chunk_offsets, false)) {
|
||||
if (chunk.type === NjcmChunkType.Strip) {
|
||||
if (chunk.type === NjcmChunkType.Tiny) {
|
||||
texture_id = chunk.texture_id;
|
||||
} else if (chunk.type === NjcmChunkType.Strip) {
|
||||
for (const strip of chunk.triangle_strips) {
|
||||
strip.texture_id = texture_id;
|
||||
}
|
||||
|
||||
meshes.push(...chunk.triangle_strips);
|
||||
}
|
||||
}
|
||||
@ -229,9 +246,18 @@ function parse_chunks(
|
||||
});
|
||||
} else if (8 <= type_id && type_id <= 9) {
|
||||
size = 2;
|
||||
const texture_bits_and_id = cursor.u16();
|
||||
chunks.push({
|
||||
type: NjcmChunkType.Tiny,
|
||||
type_id,
|
||||
flip_u: (type_id & 0x80) !== 0,
|
||||
flip_v: (type_id & 0x40) !== 0,
|
||||
clamp_u: (type_id & 0x20) !== 0,
|
||||
clamp_v: (type_id & 0x10) !== 0,
|
||||
mipmap_d_adjust: type_id & 0b1111,
|
||||
filter_mode: texture_bits_and_id >>> 14,
|
||||
super_sample: (texture_bits_and_id & 0x40) !== 0,
|
||||
texture_id: texture_bits_and_id & 0x1fff,
|
||||
});
|
||||
} else if (17 <= type_id && type_id <= 31) {
|
||||
size = 2 + 2 * cursor.u16();
|
||||
@ -429,7 +455,7 @@ function parse_triangle_strip_chunk(
|
||||
vertices.push(vertex);
|
||||
|
||||
if (has_tex_coords) {
|
||||
vertex.tex_coords = new Vec2(cursor.u16(), cursor.u16());
|
||||
vertex.tex_coords = new Vec2(cursor.u16() / 255, cursor.u16() / 255);
|
||||
}
|
||||
|
||||
// Ignore ARGB8888 color.
|
||||
|
@ -5,10 +5,10 @@ import { parse_iff } from "../iff";
|
||||
const logger = Logger.get("data_formats/parsing/ninja/texture");
|
||||
|
||||
export type Xvm = {
|
||||
textures: Texture[];
|
||||
textures: XvmTexture[];
|
||||
};
|
||||
|
||||
export type Texture = {
|
||||
export type XvmTexture = {
|
||||
id: number;
|
||||
format: [number, number];
|
||||
width: number;
|
||||
@ -51,7 +51,7 @@ function parse_header(cursor: Cursor): Header {
|
||||
};
|
||||
}
|
||||
|
||||
function parse_texture(cursor: Cursor): Texture {
|
||||
function parse_texture(cursor: Cursor): XvmTexture {
|
||||
const format_1 = cursor.u32();
|
||||
const format_2 = cursor.u32();
|
||||
const id = cursor.u32();
|
||||
|
@ -1,19 +1,20 @@
|
||||
import Logger from "js-logger";
|
||||
import { autorun } from "mobx";
|
||||
import {
|
||||
CompressedTexture,
|
||||
LinearFilter,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
OrthographicCamera,
|
||||
PlaneGeometry,
|
||||
RGBA_S3TC_DXT1_Format,
|
||||
RGBA_S3TC_DXT3_Format,
|
||||
Texture,
|
||||
Vector2,
|
||||
Vector3,
|
||||
} from "three";
|
||||
import { Texture, Xvm } from "../data_formats/parsing/ninja/texture";
|
||||
import { Xvm } from "../data_formats/parsing/ninja/texture";
|
||||
import { texture_viewer_store } from "../stores/TextureViewerStore";
|
||||
import { Renderer } from "./Renderer";
|
||||
import { xvm_texture_to_texture } from "./textures";
|
||||
|
||||
const logger = Logger.get("rendering/TextureRenderer");
|
||||
|
||||
let renderer: TextureRenderer | undefined;
|
||||
|
||||
@ -66,7 +67,14 @@ export class TextureRenderer extends Renderer<OrthographicCamera> {
|
||||
const y = -Math.floor(total_height / 2);
|
||||
|
||||
for (const tex of xvm.textures) {
|
||||
const tex_3js = this.create_texture(tex);
|
||||
let tex_3js: Texture | undefined;
|
||||
|
||||
try {
|
||||
tex_3js = xvm_texture_to_texture(tex);
|
||||
} catch (e) {
|
||||
logger.warn("Couldn't convert XVM texture.", e);
|
||||
}
|
||||
|
||||
const quad_mesh = new Mesh(
|
||||
this.create_quad(
|
||||
x,
|
||||
@ -74,11 +82,14 @@ export class TextureRenderer extends Renderer<OrthographicCamera> {
|
||||
tex.width,
|
||||
tex.height
|
||||
),
|
||||
new MeshBasicMaterial({
|
||||
map: tex_3js,
|
||||
color: tex_3js ? undefined : 0xff00ff,
|
||||
transparent: true,
|
||||
})
|
||||
tex_3js
|
||||
? new MeshBasicMaterial({
|
||||
map: tex_3js,
|
||||
transparent: true,
|
||||
})
|
||||
: new MeshBasicMaterial({
|
||||
color: 0xff00ff,
|
||||
})
|
||||
);
|
||||
|
||||
this.quad_meshes.push(quad_mesh);
|
||||
@ -88,40 +99,6 @@ export class TextureRenderer extends Renderer<OrthographicCamera> {
|
||||
}
|
||||
};
|
||||
|
||||
private create_texture(tex: Texture): CompressedTexture | undefined {
|
||||
const texture_3js = new CompressedTexture(
|
||||
[
|
||||
{
|
||||
data: new Uint8Array(tex.data) as any,
|
||||
width: tex.width,
|
||||
height: tex.height,
|
||||
},
|
||||
],
|
||||
tex.width,
|
||||
tex.height
|
||||
);
|
||||
|
||||
switch (tex.format[1]) {
|
||||
case 6:
|
||||
texture_3js.format = RGBA_S3TC_DXT1_Format as any;
|
||||
break;
|
||||
case 7:
|
||||
if (tex.format[0] === 2) {
|
||||
texture_3js.format = RGBA_S3TC_DXT3_Format as any;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
texture_3js.minFilter = LinearFilter;
|
||||
texture_3js.needsUpdate = true;
|
||||
|
||||
return texture_3js;
|
||||
}
|
||||
|
||||
private create_quad(x: number, y: number, width: number, height: number): PlaneGeometry {
|
||||
const quad = new PlaneGeometry(width, height, 1, 1);
|
||||
quad.faceVertexUvs = [
|
||||
|
@ -19,9 +19,9 @@ import { is_njcm_model, NjModel, NjObject } from "../data_formats/parsing/ninja"
|
||||
import { NjcmModel } from "../data_formats/parsing/ninja/njcm";
|
||||
import { xj_model_to_geometry } from "./xj_model_to_geometry";
|
||||
|
||||
const DEFAULT_MATERIAL = new MeshLambertMaterial({
|
||||
const DUMMY_MATERIAL = new MeshLambertMaterial({
|
||||
color: 0xff00ff,
|
||||
side: DoubleSide,
|
||||
transparent: true,
|
||||
});
|
||||
const DEFAULT_SKINNED_MATERIAL = new MeshLambertMaterial({
|
||||
skinning: true,
|
||||
@ -35,16 +35,16 @@ const NO_SCALE = new Vector3(1, 1, 1);
|
||||
|
||||
export function ninja_object_to_buffer_geometry(
|
||||
object: NjObject<NjModel>,
|
||||
material: Material = DEFAULT_MATERIAL
|
||||
materials: Material[] = []
|
||||
): BufferGeometry {
|
||||
return new Object3DCreator(material).create_buffer_geometry(object);
|
||||
return new Object3DCreator(materials).create_buffer_geometry(object);
|
||||
}
|
||||
|
||||
export function ninja_object_to_skinned_mesh(
|
||||
object: NjObject<NjModel>,
|
||||
material: Material = DEFAULT_SKINNED_MATERIAL
|
||||
materials: Material[] = []
|
||||
): SkinnedMesh {
|
||||
return new Object3DCreator(material).create_skinned_mesh(object);
|
||||
return new Object3DCreator(materials).create_skinned_mesh(object);
|
||||
}
|
||||
|
||||
type Vertex = {
|
||||
@ -79,18 +79,21 @@ class VerticesHolder {
|
||||
}
|
||||
|
||||
class Object3DCreator {
|
||||
private material: Material;
|
||||
private bone_id: number = 0;
|
||||
private materials: Material[];
|
||||
private vertices = new VerticesHolder();
|
||||
private bone_id: number = 0;
|
||||
private bones: Bone[] = [];
|
||||
|
||||
private positions: number[] = [];
|
||||
private normals: number[] = [];
|
||||
private uvs: number[] = [];
|
||||
private indices: number[] = [];
|
||||
private bone_indices: number[] = [];
|
||||
private bone_weights: number[] = [];
|
||||
private bones: Bone[] = [];
|
||||
private groups: { start: number; count: number; mat_idx: number }[] = [];
|
||||
|
||||
constructor(material: Material) {
|
||||
this.material = material;
|
||||
constructor(materials: Material[]) {
|
||||
this.materials = [DUMMY_MATERIAL, ...materials];
|
||||
}
|
||||
|
||||
create_buffer_geometry(object: NjObject<NjModel>): BufferGeometry {
|
||||
@ -100,23 +103,34 @@ class Object3DCreator {
|
||||
|
||||
geom.addAttribute("position", new Float32BufferAttribute(this.positions, 3));
|
||||
geom.addAttribute("normal", new Float32BufferAttribute(this.normals, 3));
|
||||
geom.addAttribute("uv", new Float32BufferAttribute(this.uvs, 2));
|
||||
|
||||
geom.setIndex(new Uint16BufferAttribute(this.indices, 1));
|
||||
|
||||
geom.computeBoundingSphere();
|
||||
for (const group of this.groups) {
|
||||
geom.addGroup(group.start, group.count, group.mat_idx);
|
||||
}
|
||||
|
||||
geom.computeBoundingBox();
|
||||
|
||||
return geom;
|
||||
}
|
||||
|
||||
create_skinned_mesh(object: NjObject<NjModel>): SkinnedMesh {
|
||||
const geom = this.create_buffer_geometry(object);
|
||||
|
||||
geom.addAttribute("skinIndex", new Uint16BufferAttribute(this.bone_indices, 4));
|
||||
geom.addAttribute("skinWeight", new Float32BufferAttribute(this.bone_weights, 4));
|
||||
|
||||
const mesh = new SkinnedMesh(geom, this.material);
|
||||
const max_mat_idx = this.groups.reduce((max, g) => Math.max(max, g.mat_idx), 0);
|
||||
|
||||
const skeleton = new Skeleton(this.bones);
|
||||
for (let i = this.materials.length - 1; i < max_mat_idx; ++i) {
|
||||
this.materials.push(DEFAULT_SKINNED_MATERIAL);
|
||||
}
|
||||
|
||||
const mesh = new SkinnedMesh(geom, this.materials);
|
||||
mesh.add(this.bones[0]);
|
||||
mesh.bind(skeleton);
|
||||
mesh.bind(new Skeleton(this.bones));
|
||||
|
||||
return mesh;
|
||||
}
|
||||
@ -214,6 +228,8 @@ class Object3DCreator {
|
||||
this.vertices.put(new_vertices);
|
||||
|
||||
for (const mesh of model.meshes) {
|
||||
const start_index_count = this.indices.length;
|
||||
|
||||
for (let i = 0; i < mesh.vertices.length; ++i) {
|
||||
const mesh_vertex = mesh.vertices[i];
|
||||
const vertices = this.vertices.get(mesh_vertex.index);
|
||||
@ -226,6 +242,12 @@ class Object3DCreator {
|
||||
this.positions.push(vertex.position.x, vertex.position.y, vertex.position.z);
|
||||
this.normals.push(normal.x, normal.y, normal.z);
|
||||
|
||||
if (mesh.has_tex_coords) {
|
||||
this.uvs.push(mesh_vertex.tex_coords!.x, mesh_vertex.tex_coords!.y);
|
||||
} else {
|
||||
this.uvs.push(0, 0);
|
||||
}
|
||||
|
||||
if (i >= 2) {
|
||||
if (i % 2 === (mesh.clockwise_winding ? 1 : 0)) {
|
||||
this.indices.push(index - 2);
|
||||
@ -251,6 +273,18 @@ class Object3DCreator {
|
||||
this.bone_weights.push(...bone_weights);
|
||||
}
|
||||
}
|
||||
|
||||
const last_group = this.groups[this.groups.length - 1];
|
||||
|
||||
if (last_group && last_group.mat_idx === mesh.texture_id) {
|
||||
last_group.count += this.indices.length - start_index_count;
|
||||
} else {
|
||||
this.groups.push({
|
||||
start: start_index_count,
|
||||
count: this.indices.length - start_index_count,
|
||||
mat_idx: mesh.texture_id == null ? 0 : mesh.texture_id + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
46
src/rendering/textures.ts
Normal file
46
src/rendering/textures.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Xvm, XvmTexture } from "../data_formats/parsing/ninja/texture";
|
||||
import {
|
||||
Texture,
|
||||
LinearFilter,
|
||||
RGBA_S3TC_DXT3_Format,
|
||||
RGBA_S3TC_DXT1_Format,
|
||||
CompressedTexture,
|
||||
} from "three";
|
||||
|
||||
export function xvm_to_textures(xvm: Xvm): Texture[] {
|
||||
return xvm.textures.map(xvm_texture_to_texture);
|
||||
}
|
||||
|
||||
export function xvm_texture_to_texture(tex: XvmTexture): Texture {
|
||||
const texture_3js = new CompressedTexture(
|
||||
[
|
||||
{
|
||||
data: new Uint8Array(tex.data) as any,
|
||||
width: tex.width,
|
||||
height: tex.height,
|
||||
},
|
||||
],
|
||||
tex.width,
|
||||
tex.height
|
||||
);
|
||||
|
||||
switch (tex.format[1]) {
|
||||
case 6:
|
||||
texture_3js.format = RGBA_S3TC_DXT1_Format as any;
|
||||
break;
|
||||
case 7:
|
||||
if (tex.format[0] === 2) {
|
||||
texture_3js.format = RGBA_S3TC_DXT3_Format as any;
|
||||
} else {
|
||||
throw new Error(`Format[0] ${tex.format[0]} not supported.`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Format[1] ${tex.format[1]} not supported.`);
|
||||
}
|
||||
|
||||
texture_3js.minFilter = LinearFilter;
|
||||
texture_3js.needsUpdate = true;
|
||||
|
||||
return texture_3js;
|
||||
}
|
@ -4,20 +4,24 @@ import {
|
||||
AnimationAction,
|
||||
AnimationClip,
|
||||
AnimationMixer,
|
||||
DoubleSide,
|
||||
Clock,
|
||||
Mesh,
|
||||
MeshLambertMaterial,
|
||||
SkinnedMesh,
|
||||
Clock,
|
||||
Texture,
|
||||
Vector3,
|
||||
DoubleSide,
|
||||
} from "three";
|
||||
import { Endianness } from "../data_formats";
|
||||
import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor";
|
||||
import { NjModel, NjObject, parse_nj, parse_xj } from "../data_formats/parsing/ninja";
|
||||
import { NjMotion, parse_njm } from "../data_formats/parsing/ninja/motion";
|
||||
import { parse_xvm } from "../data_formats/parsing/ninja/texture";
|
||||
import { PlayerAnimation, PlayerModel } from "../domain";
|
||||
import { read_file } from "../read_file";
|
||||
import { create_animation_clip, PSO_FRAME_RATE } from "../rendering/animation";
|
||||
import { ninja_object_to_buffer_geometry, ninja_object_to_skinned_mesh } from "../rendering/models";
|
||||
import { xvm_to_textures } from "../rendering/textures";
|
||||
import { get_player_animation_data, get_player_data } from "./binary_assets";
|
||||
|
||||
const logger = Logger.get("stores/ModelViewerStore");
|
||||
@ -61,6 +65,7 @@ class ModelViewerStore {
|
||||
@observable animation_frame: number = 0;
|
||||
@observable animation_frame_count: number = 0;
|
||||
|
||||
private has_skeleton = false;
|
||||
@observable show_skeleton: boolean = false;
|
||||
|
||||
set_animation_frame_rate = action("set_animation_frame_rate", (rate: number) => {
|
||||
@ -112,6 +117,11 @@ class ModelViewerStore {
|
||||
const njm = parse_njm(cursor, this.current_bone_count);
|
||||
this.set_animation(create_animation_clip(this.current_model, njm));
|
||||
}
|
||||
} else if (file.name.endsWith(".xvm")) {
|
||||
if (this.current_model) {
|
||||
const xvm = parse_xvm(cursor);
|
||||
this.set_textures(xvm_to_textures(xvm));
|
||||
}
|
||||
} else {
|
||||
logger.error(`Unknown file extension in filename "${file.name}".`);
|
||||
}
|
||||
@ -185,23 +195,9 @@ class ModelViewerStore {
|
||||
this.current_player_model = player_model;
|
||||
this.current_model = model;
|
||||
this.current_bone_count = model.bone_count();
|
||||
this.has_skeleton = skeleton;
|
||||
|
||||
let mesh: Mesh;
|
||||
|
||||
if (skeleton) {
|
||||
mesh = ninja_object_to_skinned_mesh(this.current_model);
|
||||
} else {
|
||||
mesh = new Mesh(
|
||||
ninja_object_to_buffer_geometry(this.current_model),
|
||||
new MeshLambertMaterial({
|
||||
color: 0xff00ff,
|
||||
side: DoubleSide,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
mesh.translateY(-mesh.geometry.boundingSphere.radius);
|
||||
this.current_obj3d = mesh;
|
||||
this.set_obj3d();
|
||||
}
|
||||
);
|
||||
|
||||
@ -286,6 +282,45 @@ class ModelViewerStore {
|
||||
return nj_motion;
|
||||
}
|
||||
}
|
||||
|
||||
private set_textures = action("set_textures", (textures: Texture[]) => {
|
||||
this.set_obj3d(textures);
|
||||
});
|
||||
|
||||
private set_obj3d = (textures?: Texture[]) => {
|
||||
if (this.current_model) {
|
||||
let mesh: Mesh;
|
||||
let bb_size = new Vector3();
|
||||
|
||||
if (this.has_skeleton) {
|
||||
mesh = ninja_object_to_skinned_mesh(
|
||||
this.current_model,
|
||||
textures &&
|
||||
textures.map(
|
||||
tex =>
|
||||
new MeshLambertMaterial({
|
||||
skinning: true,
|
||||
map: tex,
|
||||
transparent: true,
|
||||
side: DoubleSide,
|
||||
})
|
||||
)
|
||||
);
|
||||
} else {
|
||||
mesh = new Mesh(
|
||||
ninja_object_to_buffer_geometry(this.current_model),
|
||||
new MeshLambertMaterial({
|
||||
color: 0xff00ff,
|
||||
side: DoubleSide,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
mesh.geometry.boundingBox.getSize(bb_size);
|
||||
mesh.translateY(-bb_size.y / 2);
|
||||
this.current_obj3d = mesh;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const model_viewer_store = new ModelViewerStore();
|
||||
|
Loading…
Reference in New Issue
Block a user