diff --git a/src/core/data_formats/parsing/afs.ts b/src/core/data_formats/parsing/afs.ts index 7f7fbcd7..d009a1f5 100644 --- a/src/core/data_formats/parsing/afs.ts +++ b/src/core/data_formats/parsing/afs.ts @@ -3,7 +3,7 @@ import { LogManager } from "../../Logger"; const logger = LogManager.get("core/data_formats/parsing/afs"); -const AFS = 0x41465300; +const AFS = 0x00534641; type AfsFileEntry = { readonly offset: number; diff --git a/src/core/data_formats/parsing/ninja/texture.ts b/src/core/data_formats/parsing/ninja/texture.ts index 0bd03f6e..12703f23 100644 --- a/src/core/data_formats/parsing/ninja/texture.ts +++ b/src/core/data_formats/parsing/ninja/texture.ts @@ -5,10 +5,10 @@ import { LogManager } from "../../../Logger"; const logger = LogManager.get("core/data_formats/parsing/ninja/texture"); export type Xvm = { - textures: XvmTexture[]; + textures: XvrTexture[]; }; -export type XvmTexture = { +export type XvrTexture = { id: number; format: [number, number]; width: number; @@ -24,34 +24,7 @@ type Header = { const XVMH = 0x484d5658; const XVRT = 0x54525658; -export function parse_xvm(cursor: Cursor): Xvm { - const chunks = parse_iff(cursor); - const header_chunk = chunks.find(chunk => chunk.type === XVMH); - const header = header_chunk && parse_header(header_chunk.data); - - const textures = chunks - .filter(chunk => chunk.type === XVRT) - .map(chunk => parse_texture(chunk.data)); - - if (!header) { - logger.warn("No header found."); - } else if (header.texture_count !== textures.length) { - logger.warn( - `Found ${textures.length} textures instead of ${header.texture_count} as defined in the header.`, - ); - } - - return { textures }; -} - -function parse_header(cursor: Cursor): Header { - const texture_count = cursor.u16(); - return { - texture_count, - }; -} - -function parse_texture(cursor: Cursor): XvmTexture { +export function parse_xvr(cursor: Cursor): XvrTexture { const format_1 = cursor.u32(); const format_2 = cursor.u32(); const id = cursor.u32(); @@ -69,3 +42,28 @@ function parse_texture(cursor: Cursor): XvmTexture { data, }; } + +export function parse_xvm(cursor: Cursor): Xvm { + const chunks = parse_iff(cursor); + const header_chunk = chunks.find(chunk => chunk.type === XVMH); + const header = header_chunk && parse_header(header_chunk.data); + + const textures = chunks + .filter(chunk => chunk.type === XVRT) + .map(chunk => parse_xvr(chunk.data)); + + if (header && header.texture_count !== textures.length) { + logger.warn( + `Found ${textures.length} textures instead of ${header.texture_count} as defined in the header.`, + ); + } + + return { textures }; +} + +function parse_header(cursor: Cursor): Header { + const texture_count = cursor.u16(); + return { + texture_count, + }; +} diff --git a/src/core/rendering/conversion/ninja_textures.ts b/src/core/rendering/conversion/ninja_textures.ts index b108a2fc..ae8dc3e1 100644 --- a/src/core/rendering/conversion/ninja_textures.ts +++ b/src/core/rendering/conversion/ninja_textures.ts @@ -7,13 +7,13 @@ import { Texture, CompressedPixelFormat, } from "three"; -import { Xvm, XvmTexture } from "../../data_formats/parsing/ninja/texture"; +import { Xvm, XvrTexture } from "../../data_formats/parsing/ninja/texture"; export function xvm_to_textures(xvm: Xvm): Texture[] { - return xvm.textures.map(xvm_texture_to_texture); + return xvm.textures.map(xvr_texture_to_texture); } -export function xvm_texture_to_texture(tex: XvmTexture): Texture { +export function xvr_texture_to_texture(tex: XvrTexture): Texture { let format: CompressedPixelFormat; let data_size: number; diff --git a/src/core/util.ts b/src/core/util.ts index c9332187..85531490 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -42,6 +42,18 @@ export function basename(filename: string): string { return filename; } +export function filename_extension(filename: string): string { + const dot_idx = filename.lastIndexOf("."); + + // < 0 means filenames doesn't contain any "." + // also skip index 0 because that would mean the basename is empty + if (dot_idx > 1) { + return filename.slice(dot_idx + 1); + } + + return filename; +} + export function assert(condition: any, msg?: string | (() => string)): asserts condition { if (!condition) { let full_msg = "Assertion Error"; diff --git a/src/viewer/gui/TextureView.ts b/src/viewer/gui/TextureView.ts index 79d23a58..8c9345c6 100644 --- a/src/viewer/gui/TextureView.ts +++ b/src/viewer/gui/TextureView.ts @@ -13,7 +13,7 @@ export class TextureView extends ResizableWidget { private readonly open_file_button = new FileButton("Open file...", { icon_left: Icon.File, - accept: ".xvm", + accept: ".afs, .xvm", }); private readonly tool_bar = this.disposable(new ToolBar(this.open_file_button)); @@ -41,7 +41,7 @@ export class TextureView extends ResizableWidget { this.renderer_view.start_rendering(); - this.disposable( + this.disposables( gui_store.tool.observe(({ value: tool }) => { if (tool === GuiTool.Viewer) { this.renderer_view.start_rendering(); diff --git a/src/viewer/rendering/TextureRenderer.ts b/src/viewer/rendering/TextureRenderer.ts index 1bfe5ce1..1c107d9b 100644 --- a/src/viewer/rendering/TextureRenderer.ts +++ b/src/viewer/rendering/TextureRenderer.ts @@ -3,19 +3,13 @@ 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 { Xvm } from "../../core/data_formats/parsing/ninja/texture"; -import { xvm_texture_to_texture } from "../../core/rendering/conversion/ninja_textures"; -import { TextureStore } from "../stores/TextureStore"; -import { LogManager } from "../../core/Logger"; - -const logger = LogManager.get("viewer/rendering/TextureRenderer"); +import { TextureStore, TextureWithSize } from "../stores/TextureStore"; export class TextureRenderer extends Renderer implements Disposable { private readonly disposer = new Disposer(); @@ -27,12 +21,10 @@ export class TextureRenderer extends Renderer implements Disposable { super(three_renderer); this.disposer.add_all( - texture_store.current_xvm.observe(({ value: xvm }) => { + texture_store.textures.observe(({ value: textures }) => { this.scene.remove(...this.quad_meshes); - if (xvm) { - this.render_textures(xvm); - } + this.render_textures(textures); this.reset_camera(new Vector3(0, 0, 5), new Vector3()); this.schedule_render(); @@ -59,11 +51,11 @@ export class TextureRenderer extends Renderer implements Disposable { this.disposer.dispose(); } - private render_textures(xvm: Xvm): void { - let total_width = 10 * (xvm.textures.length - 1); // 10px spacing between textures. + private render_textures(textures: readonly TextureWithSize[]): void { + let total_width = 10 * (textures.length - 1); // 10px spacing between textures. let total_height = 0; - for (const tex of xvm.textures) { + for (const tex of textures) { total_width += tex.width; total_height = Math.max(total_height, tex.height); } @@ -71,15 +63,7 @@ export class TextureRenderer extends Renderer implements Disposable { let x = -Math.floor(total_width / 2); const y = -Math.floor(total_height / 2); - for (const tex of xvm.textures) { - let tex_3js: Texture | undefined; - - try { - tex_3js = xvm_texture_to_texture(tex); - } catch (e) { - logger.warn("Couldn't convert XVM texture.", e); - } - + for (const tex of textures) { const quad_mesh = new Mesh( this.create_quad( x, @@ -87,9 +71,9 @@ export class TextureRenderer extends Renderer implements Disposable { tex.width, tex.height, ), - tex_3js + tex.texture ? new MeshBasicMaterial({ - map: tex_3js, + map: tex.texture, transparent: true, }) : new MeshBasicMaterial({ diff --git a/src/viewer/stores/TextureStore.ts b/src/viewer/stores/TextureStore.ts index 98bc3336..aaa2e164 100644 --- a/src/viewer/stores/TextureStore.ts +++ b/src/viewer/stores/TextureStore.ts @@ -1,22 +1,82 @@ -import { property } from "../../core/observable"; -import { parse_xvm, Xvm } from "../../core/data_formats/parsing/ninja/texture"; -import { Property } from "../../core/observable/property/Property"; +import { list_property } from "../../core/observable"; +import { parse_xvm } 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 _current_xvm = property(undefined); - readonly current_xvm: Property = this._current_xvm; + private readonly _textures: WritableListProperty = list_property(); + readonly textures: ListProperty = this._textures; load_file = async (file: File): Promise => { try { + const ext = filename_extension(file.name); const buffer = await read_file(file); - this._current_xvm.val = parse_xvm(new ArrayBufferCursor(buffer, Endianness.Little)); + + 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, + }; + }), + ); + } else if (ext === "afs") { + const afs = parse_afs(new ArrayBufferCursor(buffer, Endianness.Little)); + const textures: TextureWithSize[] = []; + + 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, + }); + } + } + + this._textures.splice(0, Infinity, ...textures); + } } catch (e) { logger.error("Couldn't read file.", e); }