The texture viewer can now deal with textures in an AFS archive.

This commit is contained in:
Daan Vanden Bosch 2020-01-02 00:55:55 +01:00
parent 53ca625d43
commit 16b1d0934b
7 changed files with 121 additions and 67 deletions

View File

@ -3,7 +3,7 @@ import { LogManager } from "../../Logger";
const logger = LogManager.get("core/data_formats/parsing/afs"); const logger = LogManager.get("core/data_formats/parsing/afs");
const AFS = 0x41465300; const AFS = 0x00534641;
type AfsFileEntry = { type AfsFileEntry = {
readonly offset: number; readonly offset: number;

View File

@ -5,10 +5,10 @@ import { LogManager } from "../../../Logger";
const logger = LogManager.get("core/data_formats/parsing/ninja/texture"); const logger = LogManager.get("core/data_formats/parsing/ninja/texture");
export type Xvm = { export type Xvm = {
textures: XvmTexture[]; textures: XvrTexture[];
}; };
export type XvmTexture = { export type XvrTexture = {
id: number; id: number;
format: [number, number]; format: [number, number];
width: number; width: number;
@ -24,34 +24,7 @@ type Header = {
const XVMH = 0x484d5658; const XVMH = 0x484d5658;
const XVRT = 0x54525658; const XVRT = 0x54525658;
export function parse_xvm(cursor: Cursor): Xvm { export function parse_xvr(cursor: Cursor): XvrTexture {
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 {
const format_1 = cursor.u32(); const format_1 = cursor.u32();
const format_2 = cursor.u32(); const format_2 = cursor.u32();
const id = cursor.u32(); const id = cursor.u32();
@ -69,3 +42,28 @@ function parse_texture(cursor: Cursor): XvmTexture {
data, 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,
};
}

View File

@ -7,13 +7,13 @@ import {
Texture, Texture,
CompressedPixelFormat, CompressedPixelFormat,
} from "three"; } 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[] { 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 format: CompressedPixelFormat;
let data_size: number; let data_size: number;

View File

@ -42,6 +42,18 @@ export function basename(filename: string): string {
return filename; 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 { export function assert(condition: any, msg?: string | (() => string)): asserts condition {
if (!condition) { if (!condition) {
let full_msg = "Assertion Error"; let full_msg = "Assertion Error";

View File

@ -13,7 +13,7 @@ export class TextureView extends ResizableWidget {
private readonly open_file_button = new FileButton("Open file...", { private readonly open_file_button = new FileButton("Open file...", {
icon_left: Icon.File, icon_left: Icon.File,
accept: ".xvm", accept: ".afs, .xvm",
}); });
private readonly tool_bar = this.disposable(new ToolBar(this.open_file_button)); 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.renderer_view.start_rendering();
this.disposable( this.disposables(
gui_store.tool.observe(({ value: tool }) => { gui_store.tool.observe(({ value: tool }) => {
if (tool === GuiTool.Viewer) { if (tool === GuiTool.Viewer) {
this.renderer_view.start_rendering(); this.renderer_view.start_rendering();

View File

@ -3,19 +3,13 @@ import {
MeshBasicMaterial, MeshBasicMaterial,
OrthographicCamera, OrthographicCamera,
PlaneGeometry, PlaneGeometry,
Texture,
Vector2, Vector2,
Vector3, Vector3,
} from "three"; } from "three";
import { Disposable } from "../../core/observable/Disposable"; import { Disposable } from "../../core/observable/Disposable";
import { DisposableThreeRenderer, Renderer } from "../../core/rendering/Renderer"; import { DisposableThreeRenderer, Renderer } from "../../core/rendering/Renderer";
import { Disposer } from "../../core/observable/Disposer"; import { Disposer } from "../../core/observable/Disposer";
import { Xvm } from "../../core/data_formats/parsing/ninja/texture"; import { TextureStore, TextureWithSize } from "../stores/TextureStore";
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");
export class TextureRenderer extends Renderer implements Disposable { export class TextureRenderer extends Renderer implements Disposable {
private readonly disposer = new Disposer(); private readonly disposer = new Disposer();
@ -27,12 +21,10 @@ export class TextureRenderer extends Renderer implements Disposable {
super(three_renderer); super(three_renderer);
this.disposer.add_all( this.disposer.add_all(
texture_store.current_xvm.observe(({ value: xvm }) => { texture_store.textures.observe(({ value: textures }) => {
this.scene.remove(...this.quad_meshes); this.scene.remove(...this.quad_meshes);
if (xvm) { this.render_textures(textures);
this.render_textures(xvm);
}
this.reset_camera(new Vector3(0, 0, 5), new Vector3()); this.reset_camera(new Vector3(0, 0, 5), new Vector3());
this.schedule_render(); this.schedule_render();
@ -59,11 +51,11 @@ export class TextureRenderer extends Renderer implements Disposable {
this.disposer.dispose(); this.disposer.dispose();
} }
private render_textures(xvm: Xvm): void { private render_textures(textures: readonly TextureWithSize[]): void {
let total_width = 10 * (xvm.textures.length - 1); // 10px spacing between textures. let total_width = 10 * (textures.length - 1); // 10px spacing between textures.
let total_height = 0; let total_height = 0;
for (const tex of xvm.textures) { for (const tex of textures) {
total_width += tex.width; total_width += tex.width;
total_height = Math.max(total_height, tex.height); 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); let x = -Math.floor(total_width / 2);
const y = -Math.floor(total_height / 2); const y = -Math.floor(total_height / 2);
for (const tex of xvm.textures) { for (const tex of textures) {
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( const quad_mesh = new Mesh(
this.create_quad( this.create_quad(
x, x,
@ -87,9 +71,9 @@ export class TextureRenderer extends Renderer implements Disposable {
tex.width, tex.width,
tex.height, tex.height,
), ),
tex_3js tex.texture
? new MeshBasicMaterial({ ? new MeshBasicMaterial({
map: tex_3js, map: tex.texture,
transparent: true, transparent: true,
}) })
: new MeshBasicMaterial({ : new MeshBasicMaterial({

View File

@ -1,22 +1,82 @@
import { property } from "../../core/observable"; import { list_property } from "../../core/observable";
import { parse_xvm, Xvm } from "../../core/data_formats/parsing/ninja/texture"; import { parse_xvm } from "../../core/data_formats/parsing/ninja/texture";
import { Property } from "../../core/observable/property/Property";
import { read_file } from "../../core/read_file"; import { read_file } from "../../core/read_file";
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { Endianness } from "../../core/data_formats/Endianness"; import { Endianness } from "../../core/data_formats/Endianness";
import { Store } from "../../core/stores/Store"; import { Store } from "../../core/stores/Store";
import { LogManager } from "../../core/Logger"; 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"); const logger = LogManager.get("viewer/stores/TextureStore");
export type TextureWithSize = {
readonly texture?: Texture;
readonly width: number;
readonly height: number;
};
export class TextureStore extends Store { export class TextureStore extends Store {
private readonly _current_xvm = property<Xvm | undefined>(undefined); private readonly _textures: WritableListProperty<TextureWithSize> = list_property();
readonly current_xvm: Property<Xvm | undefined> = this._current_xvm; readonly textures: ListProperty<TextureWithSize> = this._textures;
load_file = async (file: File): Promise<void> => { load_file = async (file: File): Promise<void> => {
try { try {
const ext = filename_extension(file.name);
const buffer = await read_file(file); 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) { } catch (e) {
logger.error("Couldn't read file.", e); logger.error("Couldn't read file.", e);
} }