Added textures to character class models in model viewer.

This commit is contained in:
Daan Vanden Bosch 2020-01-03 00:50:19 +01:00
parent 70e6eef27c
commit 20d5b0d52d
24 changed files with 376 additions and 133 deletions

BIN
assets/player/FOmarTex.afs Normal file

Binary file not shown.

BIN
assets/player/FOmarlTex.afs Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/player/FOnewmTex.afs Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/player/HUcastTex.afs Normal file

Binary file not shown.

BIN
assets/player/HUmarTex.afs Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/player/RAcastTex.afs Normal file

Binary file not shown.

BIN
assets/player/RAmarTex.afs Normal file

Binary file not shown.

BIN
assets/player/RAmarlTex.afs Normal file

Binary file not shown.

View File

@ -19,14 +19,16 @@ export function is_xj_model(model: NjModel): model is XjModel {
}
export class NjObject<M extends NjModel = NjModel> {
private readonly _children: NjObject<M>[];
readonly evaluation_flags: NjEvaluationFlags;
readonly model: M | undefined;
readonly position: Vec3;
readonly rotation: Vec3; // Euler angles in radians.
readonly scale: Vec3;
readonly children: NjObject<M>[];
readonly children: readonly NjObject<M>[];
private bone_cache = new Map<number, NjObject<M> | null>();
private readonly bone_cache = new Map<number, NjObject<M> | null>();
private _bone_count = -1;
constructor(
@ -42,13 +44,14 @@ export class NjObject<M extends NjModel = NjModel> {
this.position = position;
this.rotation = rotation;
this.scale = scale;
this.children = children;
this._children = children;
this.children = this._children;
}
bone_count(): number {
if (this._bone_count === -1) {
const id_ref: [number] = [0];
this.get_bone_internal(this, Infinity, id_ref);
this.get_bone_internal(this, Number.MAX_SAFE_INTEGER, id_ref);
this._bone_count = id_ref[0];
}
@ -58,7 +61,7 @@ export class NjObject<M extends NjModel = NjModel> {
get_bone(bone_id: number): NjObject<M> | undefined {
let bone = this.bone_cache.get(bone_id);
// Strict check because null means there's no bone with this id.
// Strict === check because null means there's no bone with this id.
if (bone === undefined) {
bone = this.get_bone_internal(this, bone_id, [0]);
this.bone_cache.set(bone_id, bone || null);
@ -67,6 +70,12 @@ export class NjObject<M extends NjModel = NjModel> {
return bone || undefined;
}
add_child(child: NjObject<M>): void {
this._bone_count = -1;
this.bone_cache.clear();
this._children.push(child);
}
private get_bone_internal(
object: NjObject<M>,
bone_id: number,

View File

@ -27,6 +27,27 @@ export type NjcmVertex = {
calc_continue: boolean;
};
export type NjcmTriangleStrip = {
ignore_light: boolean;
ignore_specular: boolean;
ignore_ambient: boolean;
use_alpha: boolean;
double_side: boolean;
flat_shading: boolean;
environment_mapping: boolean;
clockwise_winding: boolean;
has_tex_coords: boolean;
has_normal: boolean;
texture_id?: number;
vertices: NjcmMeshVertex[];
};
export type NjcmMeshVertex = {
index: number;
normal?: Vec3;
tex_coords?: Vec2;
};
enum NjcmChunkType {
Unknown,
Null,
@ -124,27 +145,6 @@ type NjcmChunkVertex = {
calc_continue: boolean;
};
type NjcmTriangleStrip = {
ignore_light: boolean;
ignore_specular: boolean;
ignore_ambient: boolean;
use_alpha: boolean;
double_side: boolean;
flat_shading: boolean;
environment_mapping: boolean;
clockwise_winding: boolean;
has_tex_coords: boolean;
has_normal: boolean;
texture_id?: number;
vertices: NjcmMeshVertex[];
};
type NjcmMeshVertex = {
index: number;
normal?: Vec3;
tex_coords?: Vec2;
};
export function parse_njcm_model(cursor: Cursor, cached_chunk_offsets: number[]): NjcmModel {
const vlist_offset = cursor.u32(); // Vertex list
const plist_offset = cursor.u32(); // Triangle strip index list

View File

@ -27,6 +27,16 @@ export function array_buffers_equal(a: ArrayBuffer, b: ArrayBuffer): boolean {
return true;
}
export function create_array<T>(length: number, value: (index: number) => T): T[] {
const array = Array(length);
for (let i = 0; i < length; i++) {
array[i] = value(i);
}
return array;
}
/**
* Returns the given filename without the file extension.
*/

View File

@ -11,7 +11,7 @@ export class Model3DToolBar extends ToolBar {
constructor(model_3d_store: Model3DStore) {
const open_file_button = new FileButton("Open file...", {
icon_left: Icon.File,
accept: ".nj, .njm, .xj, .xvm",
accept: ".afs, .nj, .njm, .xj, .xvm",
});
const skeleton_checkbox = new CheckBox(false, { label: "Show skeleton" });
const play_animation_checkbox = new CheckBox(true, { label: "Play animation" });

View File

@ -60,8 +60,6 @@ export class Model3DView extends ResizableWidget {
),
);
model_3d_store.set_current_model(model_3d_store.models[5]);
this.renderer_view.start_rendering();
this.disposable(

View File

@ -7,9 +7,18 @@ import { NjMotion, parse_njm } from "../../core/data_formats/parsing/ninja/motio
import { Disposable } from "../../core/observable/Disposable";
import { DisposablePromise } from "../../core/DisposablePromise";
import { CharacterClassModel } from "../model/CharacterClassModel";
import { parse_xvm, XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
import { parse_afs } from "../../core/data_formats/parsing/afs";
export class CharacterClassAssetLoader implements Disposable {
private readonly nj_object_cache: Map<string, DisposablePromise<NjObject>> = new Map();
private readonly nj_object_cache: Map<
string,
DisposablePromise<NjObject<NjcmModel>>
> = new Map();
private readonly xvr_texture_cache: Map<
string,
DisposablePromise<readonly XvrTexture[]>
> = new Map();
private readonly nj_motion_cache: Map<number, DisposablePromise<NjMotion>> = new Map();
constructor(private readonly http_client: HttpClient) {}
@ -27,7 +36,7 @@ export class CharacterClassAssetLoader implements Disposable {
this.nj_motion_cache.clear();
}
load_geometry(model: CharacterClassModel): DisposablePromise<NjObject> {
load_geometry(model: CharacterClassModel): Promise<NjObject<NjcmModel>> {
let nj_object = this.nj_object_cache.get(model.name);
if (!nj_object) {
@ -41,26 +50,37 @@ export class CharacterClassAssetLoader implements Disposable {
/**
* Loads the separate body parts and joins them together at the right bones.
*/
private load_all_nj_objects(model: CharacterClassModel): DisposablePromise<NjObject> {
private load_all_nj_objects(
model: CharacterClassModel,
): DisposablePromise<NjObject<NjcmModel>> {
return this.load_body_part_geometry(model.name, "Body").then(body => {
if (!body) {
throw new Error(`Couldn't load body for player class ${model.name}.`);
}
return this.load_body_part_geometry(model.name, "Head", 0).then(head => {
if (head) {
this.add_to_bone(body, head, 59);
if (!head) {
return body;
}
if (model.hair_styles_count === 0) {
// Shift by 1 for the section ID and once for every body texture ID.
let shift = 1 + model.body_tex_ids.length;
this.shift_texture_ids(head, shift);
this.add_to_bone(body, head, 59);
if (model.hair_style_count === 0) {
return body;
}
return this.load_body_part_geometry(model.name, "Hair", 0).then(hair => {
if (hair) {
this.add_to_bone(body, hair, 59);
if (!hair) {
return body;
}
shift += model.head_tex_ids.length;
this.shift_texture_ids(hair, shift);
this.add_to_bone(head, hair, 0);
if (!model.hair_styles_with_accessory.has(0)) {
return body;
}
@ -68,7 +88,9 @@ export class CharacterClassAssetLoader implements Disposable {
return this.load_body_part_geometry(model.name, "Accessory", 0).then(
accessory => {
if (accessory) {
this.add_to_bone(body, accessory, 59);
shift += model.hair_tex_ids.length;
this.shift_texture_ids(accessory, shift);
this.add_to_bone(hair, accessory, 0);
}
return body;
@ -90,16 +112,56 @@ export class CharacterClassAssetLoader implements Disposable {
.then(buffer => parse_nj(new ArrayBufferCursor(buffer, Endianness.Little))[0]);
}
/**
* Shift texture IDs so that the IDs of different body parts don't overlap.
*/
private shift_texture_ids(nj_object: NjObject<NjcmModel>, shift: number): void {
if (nj_object.model) {
for (const mesh of nj_object.model.meshes) {
if (mesh.texture_id != undefined) {
mesh.texture_id += shift;
}
}
}
for (const child of nj_object.children) {
this.shift_texture_ids(child, shift);
}
}
private add_to_bone(object: NjObject, head_part: NjObject, bone_id: number): void {
const bone = object.get_bone(bone_id);
if (bone) {
bone.evaluation_flags.hidden = false;
bone.evaluation_flags.break_child_trace = false;
bone.children.push(head_part);
bone.add_child(head_part);
}
}
load_textures(model: CharacterClassModel): Promise<readonly XvrTexture[]> {
let xvr_texture = this.xvr_texture_cache.get(model.name);
if (!xvr_texture) {
xvr_texture = this.http_client
.get(`/player/${model.name}Tex.afs`)
.array_buffer()
.then(buffer => {
const afs = parse_afs(new ArrayBufferCursor(buffer, Endianness.Little));
const textures: XvrTexture[] = [];
for (const file of afs) {
const xvm = parse_xvm(new ArrayBufferCursor(file, Endianness.Little));
textures.push(...xvm.textures);
}
return textures;
});
}
return xvr_texture;
}
load_animation(animation_id: number, bone_count: number): Promise<NjMotion> {
let nj_motion = this.nj_motion_cache.get(animation_id);

View File

@ -1,8 +1,39 @@
import { SectionIds } from "../../core/model";
import { create_array } from "../../core/util";
export class CharacterClassModel {
constructor(
readonly name: string,
readonly head_style_count: number,
readonly hair_styles_count: number,
readonly hair_styles_with_accessory: Set<number>,
) {}
readonly name: string;
readonly head_style_count: number;
readonly hair_style_count: number;
readonly hair_styles_with_accessory: Set<number>;
/**
* Can be indexed with {@link SectionId}
*/
readonly section_id_tex_ids: number[];
readonly body_tex_ids: readonly number[];
readonly head_tex_ids: readonly number[];
readonly hair_tex_ids: readonly (number | undefined)[];
readonly accessory_tex_ids: readonly (number | undefined)[];
constructor(props: {
name: string;
head_style_count: number;
hair_style_count: number;
hair_styles_with_accessory: Set<number>;
section_id_tex_id: number;
body_tex_ids: number[];
head_tex_ids?: number[];
hair_tex_ids?: (number | undefined)[];
accessory_tex_ids?: (number | undefined)[];
}) {
this.name = props.name;
this.head_style_count = props.head_style_count;
this.hair_style_count = props.hair_style_count;
this.hair_styles_with_accessory = props.hair_styles_with_accessory;
this.section_id_tex_ids = create_array(SectionIds.length, i => props.section_id_tex_id + i);
this.body_tex_ids = props.body_tex_ids;
this.head_tex_ids = props.head_tex_ids ?? [];
this.hair_tex_ids = props.hair_tex_ids ?? [];
this.accessory_tex_ids = props.accessory_tex_ids ?? [];
}
}

View File

@ -4,6 +4,7 @@ import {
Clock,
DoubleSide,
Mesh,
MeshBasicMaterial,
MeshLambertMaterial,
Object3D,
PerspectiveCamera,
@ -13,7 +14,7 @@ import {
} from "three";
import { Disposable } from "../../core/observable/Disposable";
import { NjMotion } from "../../core/data_formats/parsing/ninja/motion";
import { xvm_to_textures } from "../../core/rendering/conversion/ninja_textures";
import { xvr_texture_to_texture } from "../../core/rendering/conversion/ninja_textures";
import { create_mesh, create_skinned_mesh } from "../../core/rendering/conversion/create_mesh";
import { ninja_object_to_buffer_geometry } from "../../core/rendering/conversion/ninja_geometry";
import {
@ -24,6 +25,19 @@ import { DisposableThreeRenderer, Renderer } from "../../core/rendering/Renderer
import { Disposer } from "../../core/observable/Disposer";
import { ChangeEvent } from "../../core/observable/Observable";
import { Model3DStore } from "../stores/Model3DStore";
import { LogManager } from "../../core/Logger";
const logger = LogManager.get("viewer/rendering/Model3DRenderer");
const DEFAULT_MATERIAL = new MeshLambertMaterial({
color: 0xffffff,
side: DoubleSide,
});
const DEFAULT_SKINNED_MATERIAL = new MeshLambertMaterial({
skinning: true,
color: 0xffffff,
side: DoubleSide,
});
export class Model3DRenderer extends Renderer implements Disposable {
private readonly disposer = new Disposer();
@ -46,7 +60,7 @@ export class Model3DRenderer extends Renderer implements Disposable {
this.disposer.add_all(
model_3d_store.current_nj_data.observe(this.nj_data_or_xvm_changed),
model_3d_store.current_xvm.observe(this.nj_data_or_xvm_changed),
model_3d_store.current_textures.observe(this.nj_data_or_xvm_changed),
model_3d_store.current_nj_motion.observe(this.nj_motion_changed),
model_3d_store.show_skeleton.observe(this.show_skeleton_changed),
model_3d_store.animation_playing.observe(this.animation_playing_changed),
@ -103,25 +117,45 @@ export class Model3DRenderer extends Renderer implements Disposable {
let mesh: Mesh;
const xvm = this.model_3d_store.current_xvm.val;
const textures = xvm ? xvm_to_textures(xvm) : undefined;
const textures = this.model_3d_store.current_textures.val.map(tex => {
if (tex) {
try {
return xvr_texture_to_texture(tex);
} catch (e) {
logger.error("Couldn't convert XVR texture.", e);
}
}
const materials =
textures &&
textures.map(
tex =>
new MeshLambertMaterial({
skinning: has_skeleton,
map: tex,
side: DoubleSide,
alphaTest: 0.5,
}),
);
return undefined;
});
const materials = textures.map(tex =>
tex
? new MeshBasicMaterial({
skinning: has_skeleton,
map: tex,
side: DoubleSide,
alphaTest: 0.1,
transparent: true,
})
: new MeshLambertMaterial({
skinning: has_skeleton,
side: DoubleSide,
}),
);
if (has_skeleton) {
mesh = create_skinned_mesh(ninja_object_to_buffer_geometry(nj_object), materials);
mesh = create_skinned_mesh(
ninja_object_to_buffer_geometry(nj_object),
materials,
DEFAULT_SKINNED_MATERIAL,
);
} else {
mesh = create_mesh(ninja_object_to_buffer_geometry(nj_object), materials);
mesh = create_mesh(
ninja_object_to_buffer_geometry(nj_object),
materials,
DEFAULT_MATERIAL,
);
}
// Make sure we rotate around the center of the model instead of its origin.

View File

@ -3,13 +3,19 @@ import {
MeshBasicMaterial,
OrthographicCamera,
PlaneGeometry,
Texture,
Vector2,
Vector3,
} from "three";
import { Disposable } from "../../core/observable/Disposable";
import { DisposableThreeRenderer, Renderer } from "../../core/rendering/Renderer";
import { Disposer } from "../../core/observable/Disposer";
import { TextureStore, TextureWithSize } from "../stores/TextureStore";
import { TextureStore } from "../stores/TextureStore";
import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
import { xvr_texture_to_texture } from "../../core/rendering/conversion/ninja_textures";
import { LogManager } from "../../core/Logger";
const logger = LogManager.get("viewer/rendering/TextureRenderer");
export class TextureRenderer extends Renderer implements Disposable {
private readonly disposer = new Disposer();
@ -51,7 +57,7 @@ export class TextureRenderer extends Renderer implements Disposable {
this.disposer.dispose();
}
private render_textures(textures: readonly TextureWithSize[]): void {
private render_textures(textures: readonly XvrTexture[]): void {
let total_width = 10 * (textures.length - 1); // 10px spacing between textures.
let total_height = 0;
@ -64,6 +70,14 @@ export class TextureRenderer extends Renderer implements Disposable {
const y = -Math.floor(total_height / 2);
for (const tex of textures) {
let texture: Texture | undefined = undefined;
try {
texture = xvr_texture_to_texture(tex);
} catch (e) {
logger.error("Couldn't convert XVR texture.", e);
}
const quad_mesh = new Mesh(
this.create_quad(
x,
@ -71,9 +85,9 @@ export class TextureRenderer extends Renderer implements Disposable {
tex.width,
tex.height,
),
tex.texture
texture
? new MeshBasicMaterial({
map: tex.texture,
map: texture,
transparent: true,
})
: new MeshBasicMaterial({

View File

@ -6,13 +6,16 @@ import { CharacterClassModel } from "../model/CharacterClassModel";
import { CharacterClassAnimationModel } from "../model/CharacterClassAnimationModel";
import { WritableProperty } from "../../core/observable/property/WritableProperty";
import { read_file } from "../../core/read_file";
import { property } from "../../core/observable";
import { list_property, property } from "../../core/observable";
import { Property } from "../../core/observable/property/Property";
import { PSO_FRAME_RATE } from "../../core/rendering/conversion/ninja_animation";
import { parse_xvm, Xvm } from "../../core/data_formats/parsing/ninja/texture";
import { parse_xvm, XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
import { CharacterClassAssetLoader } from "../loading/CharacterClassAssetLoader";
import { Store } from "../../core/stores/Store";
import { LogManager } from "../../core/Logger";
import { ListProperty } from "../../core/observable/property/list/ListProperty";
import { parse_afs } from "../../core/data_formats/parsing/afs";
import { SectionIds } from "../../core/model";
const logger = LogManager.get("viewer/stores/ModelStore");
@ -27,7 +30,7 @@ export class Model3DStore extends Store {
undefined,
);
private readonly _current_nj_data = property<NjData | undefined>(undefined);
private readonly _current_xvm = property<Xvm | undefined>(undefined);
private readonly _current_textures = list_property<XvrTexture | undefined>();
private readonly _show_skeleton: WritableProperty<boolean> = property(false);
private readonly _current_animation: WritableProperty<
CharacterClassAnimationModel | undefined
@ -38,18 +41,112 @@ export class Model3DStore extends Store {
private readonly _animation_frame: WritableProperty<number> = property(0);
readonly models: readonly CharacterClassModel[] = [
new CharacterClassModel("HUmar", 1, 10, new Set([6])),
new CharacterClassModel("HUnewearl", 1, 10, new Set()),
new CharacterClassModel("HUcast", 5, 0, new Set()),
new CharacterClassModel("HUcaseal", 5, 0, new Set()),
new CharacterClassModel("RAmar", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])),
new CharacterClassModel("RAmarl", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])),
new CharacterClassModel("RAcast", 5, 0, new Set()),
new CharacterClassModel("RAcaseal", 5, 0, new Set()),
new CharacterClassModel("FOmar", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])),
new CharacterClassModel("FOmarl", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])),
new CharacterClassModel("FOnewm", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])),
new CharacterClassModel("FOnewearl", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])),
new CharacterClassModel({
name: "HUmar",
head_style_count: 1,
hair_style_count: 10,
hair_styles_with_accessory: new Set([6]),
section_id_tex_id: 126,
body_tex_ids: [0, 1, 2, 108],
head_tex_ids: [54, 55],
hair_tex_ids: [94, 95],
}),
new CharacterClassModel({
name: "HUnewearl",
head_style_count: 1,
hair_style_count: 10,
hair_styles_with_accessory: new Set(),
section_id_tex_id: 299,
body_tex_ids: [],
}),
new CharacterClassModel({
name: "HUcast",
head_style_count: 5,
hair_style_count: 0,
hair_styles_with_accessory: new Set(),
section_id_tex_id: 275,
body_tex_ids: [],
}),
new CharacterClassModel({
name: "HUcaseal",
head_style_count: 5,
hair_style_count: 0,
hair_styles_with_accessory: new Set(),
section_id_tex_id: 375,
body_tex_ids: [0, 1, 2],
head_tex_ids: [3, 4],
}),
new CharacterClassModel({
name: "RAmar",
head_style_count: 1,
hair_style_count: 10,
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
section_id_tex_id: 197,
body_tex_ids: [],
}),
new CharacterClassModel({
name: "RAmarl",
head_style_count: 1,
hair_style_count: 10,
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
section_id_tex_id: 322,
body_tex_ids: [15, 1, 0],
head_tex_ids: [288],
hair_tex_ids: [308, 309],
accessory_tex_ids: [undefined, undefined, 8],
}),
new CharacterClassModel({
name: "RAcast",
head_style_count: 5,
hair_style_count: 0,
hair_styles_with_accessory: new Set(),
section_id_tex_id: 300,
body_tex_ids: [0, 1, 2, 3, 275],
head_tex_ids: [4],
}),
new CharacterClassModel({
name: "RAcaseal",
head_style_count: 5,
hair_style_count: 0,
hair_styles_with_accessory: new Set(),
section_id_tex_id: 375,
body_tex_ids: [],
}),
new CharacterClassModel({
name: "FOmar",
head_style_count: 1,
hair_style_count: 10,
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
section_id_tex_id: 310,
body_tex_ids: [12, 13, 14, 0],
head_tex_ids: [276, 272],
hair_tex_ids: [undefined, 296, 297],
accessory_tex_ids: [4],
}),
new CharacterClassModel({
name: "FOmarl",
head_style_count: 1,
hair_style_count: 10,
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
section_id_tex_id: 326,
body_tex_ids: [],
}),
new CharacterClassModel({
name: "FOnewm",
head_style_count: 1,
hair_style_count: 10,
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
section_id_tex_id: 344,
body_tex_ids: [],
}),
new CharacterClassModel({
name: "FOnewearl",
head_style_count: 1,
hair_style_count: 10,
hair_styles_with_accessory: new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
section_id_tex_id: 505,
body_tex_ids: [],
}),
];
readonly animations: readonly CharacterClassAnimationModel[] = new Array(572)
@ -58,7 +155,7 @@ export class Model3DStore extends Store {
readonly current_model: Property<CharacterClassModel | undefined> = this._current_model;
readonly current_nj_data: Property<NjData | undefined> = this._current_nj_data;
readonly current_xvm: Property<Xvm | undefined> = this._current_xvm;
readonly current_textures: ListProperty<XvrTexture | undefined> = this._current_textures;
readonly show_skeleton: Property<boolean> = this._show_skeleton;
readonly current_animation: Property<CharacterClassAnimationModel | undefined> = this
._current_animation;
@ -77,6 +174,8 @@ export class Model3DStore extends Store {
this.current_model.observe(({ value }) => this.load_model(value)),
this.current_animation.observe(({ value }) => this.load_animation(value)),
);
this.set_current_model(this.models[[3, 5, 6, 8][Math.floor(Math.random() * 4)]]);
}
set_current_model = (current_model: CharacterClassModel): void => {
@ -149,7 +248,20 @@ export class Model3DStore extends Store {
}
} else if (file.name.endsWith(".xvm")) {
if (this.current_model) {
this._current_xvm.val = parse_xvm(cursor);
this._current_textures.val = parse_xvm(cursor).textures;
}
} else if (file.name.endsWith(".afs")) {
if (this.current_model) {
const files = parse_afs(cursor);
const textures: XvrTexture[] = [];
for (const file of files) {
textures.push(
...parse_xvm(new ArrayBufferCursor(file, Endianness.Little)).textures,
);
}
this._current_textures.val = textures;
}
} else {
logger.error(`Unknown file extension in filename "${file.name}".`);
@ -172,6 +284,20 @@ export class Model3DStore extends Store {
bone_count: model ? 64 : nj_object.bone_count(),
has_skeleton: true,
});
const textures = await this.asset_loader.load_textures(model);
this._current_textures.val = [
textures[
model.section_id_tex_ids[Math.floor(Math.random() * SectionIds.length)]
],
...[
...model.body_tex_ids,
...model.head_tex_ids,
...model.hair_tex_ids,
...model.accessory_tex_ids,
].map(id => (id == undefined ? undefined : textures[id])),
];
} catch (e) {
logger.error(`Couldn't load model for ${model.name}.`);
this._current_nj_data.val = undefined;
@ -182,7 +308,7 @@ export class Model3DStore extends Store {
};
private set_current_nj_data(nj_data: NjData): void {
this._current_xvm.val = undefined;
this._current_textures.clear();
this._current_nj_data.val = nj_data;
}

View File

@ -1,28 +1,20 @@
import { list_property } from "../../core/observable";
import { parse_xvm } from "../../core/data_formats/parsing/ninja/texture";
import { parse_xvm, XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
import { read_file } from "../../core/read_file";
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { Endianness } from "../../core/data_formats/Endianness";
import { Store } from "../../core/stores/Store";
import { LogManager } from "../../core/Logger";
import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty";
import { Texture } from "three";
import { ListProperty } from "../../core/observable/property/list/ListProperty";
import { filename_extension } from "../../core/util";
import { xvr_texture_to_texture } from "../../core/rendering/conversion/ninja_textures";
import { parse_afs } from "../../core/data_formats/parsing/afs";
const logger = LogManager.get("viewer/stores/TextureStore");
export type TextureWithSize = {
readonly texture?: Texture;
readonly width: number;
readonly height: number;
};
export class TextureStore extends Store {
private readonly _textures: WritableListProperty<TextureWithSize> = list_property();
readonly textures: ListProperty<TextureWithSize> = this._textures;
private readonly _textures: WritableListProperty<XvrTexture> = list_property();
readonly textures: ListProperty<XvrTexture> = this._textures;
load_file = async (file: File): Promise<void> => {
try {
@ -32,50 +24,17 @@ export class TextureStore extends Store {
if (ext === "xvm") {
const xvm = parse_xvm(new ArrayBufferCursor(buffer, Endianness.Little));
this._textures.splice(
0,
Infinity,
...xvm.textures.map(tex => {
let texture: Texture | undefined = undefined;
try {
texture = xvr_texture_to_texture(tex);
} catch (e) {
logger.error("Couldn't convert XVR texture.", e);
}
return {
texture,
width: tex.width,
height: tex.height,
};
}),
);
this._textures.splice(0, Infinity, ...xvm.textures);
} else if (ext === "afs") {
const afs = parse_afs(new ArrayBufferCursor(buffer, Endianness.Little));
const textures: TextureWithSize[] = [];
const textures: XvrTexture[] = [];
for (const buffer of afs) {
const xvm = parse_xvm(new ArrayBufferCursor(buffer, Endianness.Little));
for (const tex of xvm.textures) {
let texture: Texture | undefined = undefined;
try {
texture = xvr_texture_to_texture(tex);
} catch (e) {
logger.error("Couldn't convert XVR texture.", e);
}
textures.push({
texture,
width: tex.width,
height: tex.height,
});
}
textures.push(...xvm.textures);
}
this._textures.splice(0, Infinity, ...textures);
this._textures.val = textures;
}
} catch (e) {
logger.error("Couldn't read file.", e);

View File

@ -1 +1 @@
39
40