diff --git a/assets/player/FOmarTex.afs b/assets/player/FOmarTex.afs new file mode 100644 index 00000000..e973f380 Binary files /dev/null and b/assets/player/FOmarTex.afs differ diff --git a/assets/player/FOmarlTex.afs b/assets/player/FOmarlTex.afs new file mode 100644 index 00000000..dd4b438e Binary files /dev/null and b/assets/player/FOmarlTex.afs differ diff --git a/assets/player/FOnewearlTex.afs b/assets/player/FOnewearlTex.afs new file mode 100644 index 00000000..e20d3749 Binary files /dev/null and b/assets/player/FOnewearlTex.afs differ diff --git a/assets/player/FOnewmTex.afs b/assets/player/FOnewmTex.afs new file mode 100644 index 00000000..c1d4d126 Binary files /dev/null and b/assets/player/FOnewmTex.afs differ diff --git a/assets/player/HUcasealTex.afs b/assets/player/HUcasealTex.afs new file mode 100644 index 00000000..6c6d81dd Binary files /dev/null and b/assets/player/HUcasealTex.afs differ diff --git a/assets/player/HUcastTex.afs b/assets/player/HUcastTex.afs new file mode 100644 index 00000000..18a4c9ef Binary files /dev/null and b/assets/player/HUcastTex.afs differ diff --git a/assets/player/HUmarTex.afs b/assets/player/HUmarTex.afs new file mode 100644 index 00000000..0a6fce0b Binary files /dev/null and b/assets/player/HUmarTex.afs differ diff --git a/assets/player/HUnewearlTex.afs b/assets/player/HUnewearlTex.afs new file mode 100644 index 00000000..5eb60061 Binary files /dev/null and b/assets/player/HUnewearlTex.afs differ diff --git a/assets/player/RAcasealTex.afs b/assets/player/RAcasealTex.afs new file mode 100644 index 00000000..ef05254e Binary files /dev/null and b/assets/player/RAcasealTex.afs differ diff --git a/assets/player/RAcastTex.afs b/assets/player/RAcastTex.afs new file mode 100644 index 00000000..ab41654e Binary files /dev/null and b/assets/player/RAcastTex.afs differ diff --git a/assets/player/RAmarTex.afs b/assets/player/RAmarTex.afs new file mode 100644 index 00000000..fac0c18e Binary files /dev/null and b/assets/player/RAmarTex.afs differ diff --git a/assets/player/RAmarlTex.afs b/assets/player/RAmarlTex.afs new file mode 100644 index 00000000..05e6e671 Binary files /dev/null and b/assets/player/RAmarlTex.afs differ diff --git a/src/core/data_formats/parsing/ninja/index.ts b/src/core/data_formats/parsing/ninja/index.ts index a93a4929..ce30dd12 100644 --- a/src/core/data_formats/parsing/ninja/index.ts +++ b/src/core/data_formats/parsing/ninja/index.ts @@ -19,14 +19,16 @@ export function is_xj_model(model: NjModel): model is XjModel { } export class NjObject { + private readonly _children: NjObject[]; + readonly evaluation_flags: NjEvaluationFlags; readonly model: M | undefined; readonly position: Vec3; readonly rotation: Vec3; // Euler angles in radians. readonly scale: Vec3; - readonly children: NjObject[]; + readonly children: readonly NjObject[]; - private bone_cache = new Map | null>(); + private readonly bone_cache = new Map | null>(); private _bone_count = -1; constructor( @@ -42,13 +44,14 @@ export class NjObject { 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 { get_bone(bone_id: number): NjObject | 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 { return bone || undefined; } + add_child(child: NjObject): void { + this._bone_count = -1; + this.bone_cache.clear(); + this._children.push(child); + } + private get_bone_internal( object: NjObject, bone_id: number, diff --git a/src/core/data_formats/parsing/ninja/njcm.ts b/src/core/data_formats/parsing/ninja/njcm.ts index 3dea38f0..058197ba 100644 --- a/src/core/data_formats/parsing/ninja/njcm.ts +++ b/src/core/data_formats/parsing/ninja/njcm.ts @@ -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 diff --git a/src/core/util.ts b/src/core/util.ts index 85531490..125f8160 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -27,6 +27,16 @@ export function array_buffers_equal(a: ArrayBuffer, b: ArrayBuffer): boolean { return true; } +export function create_array(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. */ diff --git a/src/viewer/gui/model_3d/Model3DToolBar.ts b/src/viewer/gui/model_3d/Model3DToolBar.ts index 02222067..11ec936d 100644 --- a/src/viewer/gui/model_3d/Model3DToolBar.ts +++ b/src/viewer/gui/model_3d/Model3DToolBar.ts @@ -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" }); diff --git a/src/viewer/gui/model_3d/Model3DView.ts b/src/viewer/gui/model_3d/Model3DView.ts index 8780fb7a..2b8beaa3 100644 --- a/src/viewer/gui/model_3d/Model3DView.ts +++ b/src/viewer/gui/model_3d/Model3DView.ts @@ -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( diff --git a/src/viewer/loading/CharacterClassAssetLoader.ts b/src/viewer/loading/CharacterClassAssetLoader.ts index 6c607681..51f60392 100644 --- a/src/viewer/loading/CharacterClassAssetLoader.ts +++ b/src/viewer/loading/CharacterClassAssetLoader.ts @@ -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> = new Map(); + private readonly nj_object_cache: Map< + string, + DisposablePromise> + > = new Map(); + private readonly xvr_texture_cache: Map< + string, + DisposablePromise + > = new Map(); private readonly nj_motion_cache: Map> = 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 { + load_geometry(model: CharacterClassModel): Promise> { 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 { + private load_all_nj_objects( + model: CharacterClassModel, + ): DisposablePromise> { 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, 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 { + 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 { let nj_motion = this.nj_motion_cache.get(animation_id); diff --git a/src/viewer/model/CharacterClassModel.ts b/src/viewer/model/CharacterClassModel.ts index 57c0a76b..31df6024 100644 --- a/src/viewer/model/CharacterClassModel.ts +++ b/src/viewer/model/CharacterClassModel.ts @@ -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, - ) {} + readonly name: string; + readonly head_style_count: number; + readonly hair_style_count: number; + readonly hair_styles_with_accessory: Set; + /** + * 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; + 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 ?? []; + } } diff --git a/src/viewer/rendering/Model3DRenderer.ts b/src/viewer/rendering/Model3DRenderer.ts index 0404f429..af284f47 100644 --- a/src/viewer/rendering/Model3DRenderer.ts +++ b/src/viewer/rendering/Model3DRenderer.ts @@ -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. diff --git a/src/viewer/rendering/TextureRenderer.ts b/src/viewer/rendering/TextureRenderer.ts index 1c107d9b..42d534fb 100644 --- a/src/viewer/rendering/TextureRenderer.ts +++ b/src/viewer/rendering/TextureRenderer.ts @@ -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({ diff --git a/src/viewer/stores/Model3DStore.ts b/src/viewer/stores/Model3DStore.ts index d44d4923..13bafb06 100644 --- a/src/viewer/stores/Model3DStore.ts +++ b/src/viewer/stores/Model3DStore.ts @@ -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(undefined); - private readonly _current_xvm = property(undefined); + private readonly _current_textures = list_property(); private readonly _show_skeleton: WritableProperty = property(false); private readonly _current_animation: WritableProperty< CharacterClassAnimationModel | undefined @@ -38,18 +41,112 @@ export class Model3DStore extends Store { private readonly _animation_frame: WritableProperty = 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 = this._current_model; readonly current_nj_data: Property = this._current_nj_data; - readonly current_xvm: Property = this._current_xvm; + readonly current_textures: ListProperty = this._current_textures; readonly show_skeleton: Property = this._show_skeleton; readonly current_animation: Property = 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; } diff --git a/src/viewer/stores/TextureStore.ts b/src/viewer/stores/TextureStore.ts index f108665a..7176427d 100644 --- a/src/viewer/stores/TextureStore.ts +++ b/src/viewer/stores/TextureStore.ts @@ -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 = list_property(); - readonly textures: ListProperty = this._textures; + private readonly _textures: WritableListProperty = list_property(); + readonly textures: ListProperty = this._textures; load_file = async (file: File): Promise => { 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); diff --git a/version.txt b/version.txt index a2720097..425151f3 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -39 +40