Created
September 22, 2025 18:29
-
-
Save ariankordi/15c713e1208d7a5d534152dc276bab4a to your computer and use it in GitHub Desktop.
(2025-05-13) Wii RFL_Res.dat resource reader in JS using Kaitai. Displays each shape in the resource. This REQUIRES having RFL_Res.dat in the directory you're using it in.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| meta: | |
| id: r_f_l_resource | |
| #id: c_f_li_archive | |
| title: Resource archive image format for RFL, NFL, CFL. | |
| endian: be # Set to BE for RFL. | |
| #bit-endian: le # No bitfields. | |
| seq: | |
| - id: total_archive_count | |
| type: u2 | |
| doc: | | |
| Amount of total archives within the file. | |
| RFL = 18 (RFLiArcID_Max), NFL = 20 (?), CFL = 20 (CFLi_PARTS_ID_COUNT) | |
| - id: version | |
| type: u2 | |
| doc: | | |
| Resource version. Set in RFL and CFL, only seen used in CFL debug mode. | |
| CFL bootloadDB2Res_: nn::dbg::detail::Printf("CFL: Cached Resource Version = 0x%04x\n"); | |
| / Debug assert; "cfl_resource.cpp",0x19d,"%s", "loader->mVersion >= 0x509" | |
| #- id: archives | |
| # type: archive | |
| # repeat: expr | |
| # repeat-expr: 20 | |
| - id: offsets | |
| doc: | | |
| Offsets for each part type. | |
| type: u4 | |
| repeat: expr | |
| repeat-expr: total_archive_count | |
| instances: | |
| archives: | |
| pos: offsets[_index] # Reads from each offset in the list. | |
| type: r_f_li_archive | |
| repeat: expr | |
| repeat-expr: total_archive_count # Should match offsets' length. | |
| archive0: | |
| pos: offsets[0] | |
| type: r_f_li_archive | |
| archive1: | |
| pos: offsets[1] | |
| type: r_f_li_archive | |
| archive2: | |
| pos: offsets[2] | |
| type: r_f_li_archive | |
| archive3: | |
| pos: offsets[3] | |
| type: r_f_li_archive | |
| archive4: | |
| pos: offsets[4] | |
| type: r_f_li_archive | |
| archive5: | |
| pos: offsets[5] | |
| type: r_f_li_archive | |
| archive6: | |
| pos: offsets[6] | |
| type: r_f_li_archive | |
| archive7: | |
| pos: offsets[7] | |
| type: r_f_li_archive | |
| archive8: | |
| pos: offsets[8] | |
| type: r_f_li_archive | |
| archive9: | |
| pos: offsets[9] | |
| type: r_f_li_archive | |
| archive10: | |
| pos: offsets[10] | |
| type: r_f_li_archive | |
| archive11: | |
| pos: offsets[11] | |
| type: r_f_li_archive | |
| archive12: | |
| pos: offsets[12] | |
| type: r_f_li_archive | |
| archive13: | |
| pos: offsets[13] | |
| type: r_f_li_archive | |
| archive14: | |
| pos: offsets[14] | |
| type: r_f_li_archive | |
| archive15: | |
| pos: offsets[15] | |
| type: r_f_li_archive | |
| archive16: | |
| pos: offsets[16] | |
| type: r_f_li_archive | |
| archive17: | |
| pos: offsets[17] | |
| type: r_f_li_archive | |
| archive18: | |
| pos: offsets[18] | |
| type: r_f_li_archive | |
| archive19: | |
| pos: offsets[19] | |
| type: r_f_li_archive | |
| archive1texture0: | |
| # Read texture header for archive 1 (eye) index 0. | |
| # Seek to offsets (8). Go to offset of archive 1. | |
| # Then estimate file count * sizeof(u32) | |
| pos: | | |
| 8 | |
| + offsets[1] | |
| + (archive1.num * 4) | |
| #type: r_f_li_texture | |
| type: c_f_li_tex_header | |
| archive3shape1: | |
| # Read header for archive 3 (faceline) index 1. | |
| pos: 8 + offsets[3] + (archive3.num * sizeof<u4>) + 0x653 | |
| type: r_f_li_shape | |
| archive1shape1: | |
| # Read header for archive 1 (beard) index 1. | |
| pos: 8 + offsets[0] + (archive0.num * sizeof<u4>) + 0x9 | |
| type: r_f_li_shape | |
| # Default M hair. | |
| archive8shape33: | |
| # Read header for archive 8 (hair) index 33. | |
| pos: 8 + offsets[8] + (archive8.num * sizeof<u4>) + 0x11F27 | |
| type: r_f_li_shape | |
| types: | |
| r_f_li_archive: | |
| seq: | |
| - id: num | |
| type: u2 | |
| doc: Number of files in the archive. | |
| - id: maxsize | |
| type: u2 | |
| doc: Size of the biggest entry in the archive. | |
| - id: offset | |
| type: u4 | |
| doc: Offsets of each part in the archive. | |
| repeat: expr | |
| repeat-expr: num + 1 | |
| #instances: | |
| # texture0: | |
| # pos: _io.pos + offset[0] # Read each part entry at the offset | |
| # type: r_f_li_texture | |
| c_f_li_tex_header: | |
| doc: | | |
| Image width and height are calculated by | |
| getting the next power of two. In JS: | |
| const nextPow2 = x => x <= 1 ? 1 : 1 << (32 - Math.clz32(x - 1)); | |
| seq: | |
| - id: image_w | |
| type: u2 | |
| - id: image_h | |
| type: u2 | |
| - id: m_mipmap_size | |
| type: u1 | |
| - id: m_format | |
| type: u1 | |
| - id: m_wrap_s | |
| type: u1 | |
| - id: m_wrap_t | |
| type: u1 | |
| r_f_li_texture: | |
| seq: | |
| - id: format | |
| type: u1 | |
| - id: alpha | |
| type: u1 | |
| - id: width | |
| type: u2 | |
| - id: height | |
| type: u2 | |
| - id: wrap_s | |
| type: u1 | |
| - id: wrap_t | |
| type: u1 | |
| - id: index_texture | |
| type: u1 | |
| - id: color_format | |
| type: u1 | |
| - id: num_colors | |
| type: u2 | |
| - id: palette_ofs | |
| type: u4 | |
| - id: enable_lod | |
| type: u1 | |
| - id: enable_edge_lod | |
| type: u1 | |
| - id: enable_bias_clamp | |
| type: u1 | |
| - id: enable_max_aniso | |
| type: u1 | |
| - id: min_filt | |
| type: u1 | |
| - id: mag_filt | |
| type: u1 | |
| - id: min_lod | |
| type: s1 | |
| - id: max_lod | |
| type: s1 | |
| - id: mipmap_level | |
| type: u1 | |
| - id: reserved | |
| type: s1 | |
| - id: lod_bias | |
| type: s2 | |
| - id: image_ofs | |
| type: u4 | |
| r_f_li_shape: | |
| doc: | | |
| NOTE: Untyped. Custom type. | |
| Real data is loaded from "res" in RFLiInitShapeRes | |
| seq: | |
| - id: identifier | |
| type: str | |
| size: 4 | |
| encoding: ascii | |
| doc: | | |
| 4-byte identifier, unused by RFL. | |
| 'nose', 'frhd', 'face', 'hair', 'cap_', 'berd', 'nsln', 'mask', 'glas' | |
| https://github.com/SMGCommunity/Petari/blob/6b6a7635d3ab985a5866be9ae4db09d52d678f6c/src/RVLFaceLib/RFL_Model.c#L835 | |
| # Faceline transform fields. | |
| - id: nose_trans | |
| type: f4 | |
| repeat: expr | |
| repeat-expr: 3 | |
| if: is_faceline | |
| - id: beard_trans | |
| type: f4 | |
| repeat: expr | |
| repeat-expr: 3 | |
| if: is_faceline | |
| - id: hair_trans | |
| type: f4 | |
| repeat: expr | |
| repeat-expr: 3 | |
| if: is_faceline | |
| # Positions. | |
| - id: num_vtx_pos | |
| type: u2 | |
| - id: vtx_pos | |
| #type: vec3_s16 | |
| type: u2 | |
| repeat: expr | |
| #repeat-expr: num_vtx_pos | |
| repeat-expr: num_vtx_pos * 3 | |
| # normals | |
| - id: num_vtx_nrm | |
| type: u2 | |
| - id: vtx_nrm | |
| type: u2 | |
| repeat: expr | |
| repeat-expr: num_vtx_nrm * 3 | |
| # texcoords (unless skip_txc) | |
| - id: num_vtx_txc | |
| type: u2 | |
| if: not skip_txc | |
| - id: vtx_txc | |
| type: u2 | |
| repeat: expr | |
| repeat-expr: num_vtx_txc * 2 | |
| if: not skip_txc | |
| # primitives | |
| - id: prim_count | |
| type: u1 | |
| - id: primitives | |
| type: primitive | |
| repeat: expr | |
| repeat-expr: prim_count | |
| instances: | |
| # NOTE: RFL does not use the identifier. | |
| # Should use RFLArcID instead. | |
| skip_txc: | |
| value: | | |
| identifier == "frhd" or | |
| identifier == "hair" or | |
| identifier == "berd" or | |
| identifier == "nose" | |
| is_faceline: | |
| value: identifier == "face" | |
| # one primitive entry in the DL | |
| primitive: | |
| seq: | |
| - id: vtx_count | |
| type: u1 | |
| - id: prim_type | |
| type: u1 | |
| enum: g_x_primitive | |
| - id: vertices | |
| type: vertex | |
| repeat: expr | |
| repeat-expr: vtx_count | |
| # one vertex‐index tuple | |
| vertex: | |
| seq: | |
| - id: pos_idx | |
| type: u1 | |
| - id: nrm_idx | |
| type: u1 | |
| - id: tex_idx | |
| type: u1 | |
| if: not _parent._parent.skip_txc # match the r_f_li_shape skip_txc | |
| enums: | |
| g_x_primitive: | |
| 0xB8: points | |
| 0xA8: lines | |
| 0xB0: linestrip | |
| 0x90: triangles | |
| 0x98: trianglestrip | |
| 0xA0: trianglefan | |
| 0x80: quads | |
| rfl_i_arc_id: | |
| 0: beard | |
| 1: eye | |
| 2: eyebrow | |
| 3: faceline | |
| 4: face_tex | |
| 5: fore_head | |
| 6: glass | |
| 7: glass_tex | |
| 8: hair | |
| 9: mask | |
| 10: mole | |
| 11: mouth | |
| 12: mustache | |
| 13: nose | |
| 14: nline | |
| 15: nline_tex | |
| 16: cap | |
| 17: cap_tex | |
| 18: max |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta name="viewport" content="width=device-width,initial-scale=1.0"> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "kaitai-struct": "https://esm.sh/[email protected]", | |
| "three": "https://esm.sh/[email protected]", | |
| "three/": "https://esm.sh/[email protected]/" | |
| } | |
| } | |
| </script> | |
| <script type="module"> | |
| // Export modules to window. | |
| /* | |
| import * as THREE from 'three'; | |
| window.THREE = THREE; | |
| globalThis.THREE = THREE; | |
| */ | |
| import { KaitaiStream } from 'kaitai-struct'; | |
| window.KaitaiStream = KaitaiStream; globalThis.KaitaiStream = KaitaiStream; | |
| </script> | |
| <!--<script type="module" src="KaitaiStream.min.js"></script> | |
| <script type="module" src="RFLResource.js"></script>--> | |
| <script type="module" src="rfl-resource.js"></script> | |
| <style> | |
| /* The semi-transparent, resizable overlay */ | |
| #shape-browser-overlay { | |
| position: absolute; | |
| top: 5%; | |
| left: 5%; | |
| width: 28%; | |
| height: 90%; | |
| background-color: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| overflow-y: auto; | |
| padding: 1rem; | |
| resize: both; | |
| box-sizing: border-box; | |
| z-index: 1000; | |
| /* Use native system font. */ | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Header and control panel */ | |
| #shape-browser-header { | |
| margin-bottom: 1rem; | |
| } | |
| #shape-browser-controls { | |
| margin-top: 0.5rem; | |
| display: flex; | |
| gap: 0.5rem; | |
| align-items: center; | |
| } | |
| #shape-browser-controls label { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.25rem; | |
| font-size: 0.9rem; | |
| } | |
| /* Remove bullets from the list */ | |
| #shape-browser-archive-list { | |
| list-style: none; | |
| padding-left: 0; | |
| flex-grow: 1; | |
| } | |
| /* Each archive <details> */ | |
| #shape-browser-archive-list details { | |
| margin-bottom: 0.5rem; | |
| } | |
| #shape-browser-archive-list summary { | |
| cursor: pointer; | |
| font-weight: bold; | |
| font-size: 1rem; | |
| padding: 0.25rem 0; | |
| } | |
| /* Each file entry */ | |
| .shape-file-entry { | |
| font-size: 0.9rem; | |
| cursor: pointer; | |
| } | |
| .shape-file-entry:hover { | |
| background-color: rgba(255, 255, 255, 0.1); | |
| } | |
| /* Highlight the currently selected file */ | |
| .shape-file-entry.selected { | |
| background-color: rgba(0, 128, 255, 0.3); | |
| } | |
| body { | |
| margin: 0; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="shape-browser-overlay"> | |
| <div id="shape-browser-header"> | |
| <div id="shape-browser-controls"> | |
| <!-- Wireframe toggle --> | |
| <label> | |
| <input type="checkbox" id="control-wireframe"> | |
| Wireframe | |
| </label> | |
| <!-- Rotate toggle --> | |
| <label> | |
| <input type="checkbox" id="control-rotate"> | |
| Auto-Rotate | |
| </label> | |
| <!-- Texture test button --> | |
| <button id="control-add-texture">test UVs</button> | |
| <input type="file" id="control-texture-input" accept="image/*" style="display: none;"> | |
| </div> | |
| </div> | |
| <!-- Archive list will go here --> | |
| <ul id="shape-browser-archive-list"></ul> | |
| </div> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // @ts-check | |
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; | |
| // import SPECTOR from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm'; | |
| // const spector = new SPECTOR.Spector(); | |
| import { KaitaiStream } from 'kaitai-struct'; | |
| import * as RFLResourceImport from './RFLResource.js'; | |
| // Import as UMD or ESM. | |
| let RFLResource = /** @type {*} */ (globalThis).RFLResource; | |
| RFLResource = (!RFLResource) ? RFLResourceImport : RFLResource; | |
| /** | |
| * {@link https://github.com/SMGCommunity/Petari/blob/98fe1905624e3b499869eee6b74b12fcaf94f38a/libs/RVLFaceLib/include/RFLi_Types.h#L23C1-L43C13} | |
| * @enum {number} | |
| */ | |
| const RFLiArcID = { | |
| Beard: 0, | |
| Eye: 1, | |
| Eyebrow: 2, | |
| Faceline: 3, | |
| FaceTex: 4, | |
| ForeHead: 5, | |
| Glass: 6, | |
| GlassTex: 7, | |
| Hair: 8, | |
| Mask: 9, | |
| Mole: 10, | |
| Mouth: 11, | |
| Mustache: 12, | |
| Nose: 13, | |
| Nline: 14, | |
| NlineTex: 15, | |
| Cap: 16, | |
| CapTex: 17, | |
| Max: 18 | |
| }; | |
| /** | |
| * Gets an offset of an individual file/element/part within the archive. | |
| * @param {RFLResource} res | |
| * @param {RFLiArcID} arcID | |
| * @param {number} index | |
| * @returns {number} Absolute offset of file within the archive. | |
| */ | |
| function getOffsetArchiveFile(res, arcID, index) { | |
| /** Initial position. */ | |
| const _pos = res._io.pos; | |
| /** Offsets begin at this location. */ | |
| const initialOffset = 8; | |
| /** Offset of the beginning of the archive. */ | |
| const archiveOffset = res.offsets[arcID]; | |
| console.assert(archiveOffset); | |
| res._io.seek(/* initialOffset + */archiveOffset); // Seek and read archive. | |
| /** Construct archive to get its offsets. */ | |
| const archive = new RFLResource.RFLiArchive(res._io, res, res._root); | |
| console.assert(archive.num > 0); | |
| /** Location after the archive's file offsets. Value = num files * sizeof(u32). */ | |
| const filesOffset = archive.num * 4; | |
| /** Individual file's offset. */ | |
| const fileOffset = archive.offset[index]; | |
| console.assert(typeof fileOffset === 'number'); | |
| /** Total offset of file. */ | |
| const finalOffset = initialOffset + archiveOffset + filesOffset + fileOffset; | |
| res._io.seek(_pos); // Seek to initial position before returning. | |
| return finalOffset; | |
| } | |
| /** | |
| * @param {RFLResource} res | |
| * @param {RFLiArcID} arcID | |
| * @param {number} index | |
| * @returns {typeof RFLResource.RFLiShape} | |
| * @todo Makes use of types VIA KAITAI: RFLResource, RFLiShape (custom) | |
| */ | |
| function getResourceShape(res, arcID, index) { | |
| /** Initial position. */ | |
| const _pos = res._io.pos; | |
| const finalOffset = getOffsetArchiveFile(res, arcID, index); | |
| /** Seek to the offset of the file. */ | |
| res._io.seek(finalOffset); | |
| /** Read the shape at the offset. */ | |
| const shape = new RFLResource.RFLiShape(res._io, this, res._root); | |
| res._io.seek(_pos); // Seek to initial position before returning. | |
| return shape; | |
| } | |
| // ==> LOADING <== | |
| /** @type {Object<RFLiArcID, string>} */ | |
| const RFLiArcIDShapesAndNames = { | |
| [RFLiArcID.Faceline]: 'Faceline', | |
| [RFLiArcID.ForeHead]: 'ForeHead', | |
| [RFLiArcID.Glass]: 'Glass', | |
| [RFLiArcID.Hair]: 'Hair', | |
| [RFLiArcID.Mask]: 'Mask', | |
| [RFLiArcID.Nose]: 'Nose', | |
| [RFLiArcID.Nline]: 'Nline', | |
| [RFLiArcID.Cap]: 'Cap' | |
| }; | |
| /** List of RFLiArcIDs that contain shapes. */ | |
| const shapeArchiveIds = Object.keys(RFLiArcIDShapesAndNames) | |
| .map(key => Number(key)); | |
| /** | |
| * Helper: returns how many files are in archive `arcID`. | |
| * @param {RFLResource} resource | |
| * @param {number} arcID | |
| * @returns {number} | |
| */ | |
| function getFileCountInArchive(resource, arcID) { | |
| const archiveOffset = resource.offsets[arcID]; | |
| // Seek to archive header | |
| const savedPosition = resource._io.pos; | |
| resource._io.seek(archiveOffset); | |
| const archiveHeader = new RFLResource.RFLiArchive(resource._io, resource, resource._root); | |
| resource._io.seek(savedPosition); | |
| return archiveHeader.num; | |
| } | |
| /** | |
| * Helper: returns the byte size of file `fileIndex` within archive `arcID`. | |
| * @param {RFLResource} resource | |
| * @param {number} arcID | |
| * @param {number} fileIndex | |
| * @returns {number} | |
| */ | |
| function getFileSizeInArchive(resource, arcID, fileIndex) { | |
| const archiveOffset = resource.offsets[arcID]; | |
| // We need the offset of this file and the offset of the next file | |
| const savedPosition = resource._io.pos; | |
| resource._io.seek(archiveOffset); | |
| const archiveHeader = new RFLResource.RFLiArchive(resource._io, resource, resource._root); | |
| const offsetsArray = archiveHeader.offset; // length = num+1 | |
| let startOffset = offsetsArray[fileIndex]; | |
| let endOffset; | |
| if (fileIndex < offsetsArray.length - 1) { | |
| endOffset = offsetsArray[fileIndex + 1]; | |
| } else { | |
| // Last file: size is archiveHeader.maxsize or subtract from fileSize | |
| endOffset = offsetsArray[fileIndex] + archiveHeader.maxsize; | |
| } | |
| resource._io.seek(savedPosition); | |
| return endOffset - startOffset; | |
| } | |
| /** | |
| * Populate the <ul> with all archives and files. | |
| * @param {RFLResource} resource | |
| */ | |
| function populateShapeArchiveList(resource) { | |
| const archiveListElement = document.getElementById('shape-browser-archive-list'); | |
| archiveListElement.innerHTML = ''; // Clear any existing entries | |
| // For all shape archive IDs... | |
| for (const arcID of shapeArchiveIds) { | |
| const archiveName = RFLiArcIDShapesAndNames[arcID]; | |
| const fileCount = getFileCountInArchive(resource, arcID); | |
| // Create <details> with a <summary> | |
| const detailsElement = document.createElement('details'); | |
| const summaryElement = document.createElement('summary'); | |
| summaryElement.textContent = `ID ${arcID}: ${archiveName} (${fileCount} files)`; | |
| detailsElement.appendChild(summaryElement); | |
| // Inner list of files | |
| const filesListElement = document.createElement('ul'); | |
| for (let fileIndex = 0; fileIndex < fileCount; fileIndex++) { | |
| const fileSize = getFileSizeInArchive(resource, arcID, fileIndex); | |
| const fileEntryElement = document.createElement('li'); | |
| fileEntryElement.classList.add('shape-file-entry'); | |
| // e.g. "Hair 33 (2048 bytes)" | |
| fileEntryElement.textContent = | |
| `${archiveName} ${fileIndex} (${fileSize} bytes)`; | |
| // On click: load & render shape, update hash, highlight | |
| fileEntryElement.addEventListener('click', () => { | |
| // Remove old “selected” class | |
| document.querySelectorAll('.shape-file-entry.selected') | |
| .forEach(el => el.classList.remove('selected')); | |
| // Add to this one | |
| fileEntryElement.classList.add('selected'); | |
| // Deep-link via hash | |
| location.hash = `#arc=${arcID}&file=${fileIndex}`; | |
| // Load and display | |
| const shape = getResourceShape(resource, arcID, fileIndex); | |
| const mesh = rflShapeToMesh(shape); | |
| if (mesh) { | |
| applyWireframeSetting(mesh); | |
| switchMeshInScene(mesh); | |
| } | |
| }); | |
| filesListElement.appendChild(fileEntryElement); | |
| } | |
| detailsElement.appendChild(filesListElement); | |
| const listItemElement = document.createElement('li'); | |
| listItemElement.appendChild(detailsElement); | |
| archiveListElement.appendChild(listItemElement); | |
| } | |
| } | |
| /** | |
| * Reads location.hash and, if present, auto-selects that shape. | |
| * @param {RFLResource} resource | |
| */ | |
| function applyDeepLinkSelection(resource) { | |
| const hash = location.hash.slice(1); | |
| const params = new URLSearchParams(hash); | |
| const arcID = Number(params.get('arc')); | |
| const fileIndex = Number(params.get('file')); | |
| if (shapeArchiveIds.includes(arcID) && !isNaN(fileIndex)) { | |
| // Find the matching <li> element and click it | |
| const archiveName = RFLiArcIDShapesAndNames[arcID]; | |
| const selector = `.shape-file-entry`; | |
| document.querySelectorAll(selector).forEach((el) => { | |
| if (el.textContent.startsWith(`${archiveName} ${fileIndex} `)) { | |
| el.click(); | |
| } | |
| }); | |
| } | |
| } | |
| /** Toggles wireframe mode on the current mesh’s material. */ | |
| function applyWireframeSetting(mesh) { | |
| if (mesh && mesh.material instanceof THREE.Material) { | |
| mesh.material.wireframe = document.getElementById('control-wireframe').checked; | |
| } | |
| } | |
| /** Adds event listeners for the control panel. */ | |
| function initializeControlPanel(resource) { | |
| // Wireframe toggle | |
| document.getElementById('control-wireframe') | |
| .addEventListener('change', () => { | |
| if (currentMesh) { | |
| applyWireframeSetting(currentMesh); | |
| } | |
| }); | |
| // Auto-rotate toggle | |
| document.getElementById('control-rotate') | |
| .addEventListener('change', (event) => { | |
| if (orbitControls) { | |
| orbitControls.autoRotate = event.target.checked; | |
| } | |
| }); | |
| // “Add Texture” button | |
| const textureInputElement = document.getElementById('control-texture-input'); | |
| document.getElementById('control-add-texture') | |
| .addEventListener('click', () => { | |
| textureInputElement.click(); | |
| }); | |
| // Handle file selection | |
| textureInputElement.addEventListener('change', () => { | |
| const file = textureInputElement.files[0]; | |
| if (!file) { | |
| return; | |
| } | |
| if (!currentMesh.geometry.attributes.uv) { | |
| alert('This mesh has no UV coordinates to apply a texture.'); | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = () => { | |
| const texture = new THREE.TextureLoader().load(reader.result); | |
| currentMesh.material.map = texture; | |
| currentMesh.material.needsUpdate = true; | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |
| // ==> INDICES <== | |
| /** | |
| * @param {Array<typeof RFLResource.Primitive>} primitives | |
| * @returns {number} The number of indices based on primitives. | |
| */ | |
| function getIndexCountFromPrimitives(primitives) { | |
| let total = 0; | |
| for (const prim of primitives) { | |
| const numVertices = prim.vertices.length; | |
| switch (prim.primType) { | |
| case RFLResource.GXPrimitive.TRIANGLES: | |
| total += Math.floor(numVertices / 3) * 3; | |
| break; | |
| case RFLResource.GXPrimitive.TRIANGLESTRIP: | |
| case RFLResource.GXPrimitive.TRIANGLEFAN: | |
| total += Math.max(0, numVertices - 2) * 3; | |
| break; | |
| case RFLResource.GXPrimitive.QUADS: | |
| total += Math.floor(numVertices / 4) * 6; | |
| break; | |
| default: | |
| break; // skip lines/points | |
| } | |
| } | |
| return total; | |
| } | |
| /** | |
| * Build a flat Uint16Array of triangle indices from RFLiShape.primitives. | |
| * @param {Array<typeof RFLResource.Primitive>} primitives - | |
| * Each Primitive must have: | |
| * - primType (one of GXPrimitive.*) | |
| * - vertices: Array of { posIdx: number, … } | |
| * @param {Uint16Array} indices - List of indices to populate. | |
| * @param {number} offset - Offset to begin in `indices` array. | |
| * @returns {Uint16Array} | |
| */ | |
| function buildToIndexBuffer(primitives, indices, offset = 0) { | |
| /** Current offset in the output array. */ | |
| let i = offset; | |
| for (const prim of primitives) { | |
| // Use just indices for positions. | |
| const idx = /** @type {Array<{posIdx: number}>} */ | |
| (prim.vertices).map(v => v.posIdx); | |
| switch (prim.primType) { | |
| // Reverse winding when writing indices. | |
| case RFLResource.GXPrimitive.TRIANGLES: | |
| // every group of 3 is one triangle | |
| for (let j = 0; j + 2 < idx.length; j += 3) { | |
| indices[i] = idx[j + 2]; | |
| indices[i + 1] = idx[j + 1]; | |
| indices[i + 2] = idx[j]; | |
| i += 3; | |
| } | |
| break; | |
| case RFLResource.GXPrimitive.TRIANGLESTRIP: | |
| // strip: (0,1,2), (2,1,3), (2,3,4) | |
| for (let j = 0; j + 2 < idx.length; j++) { | |
| if (j % 2 === 0) { | |
| indices[i] = idx[j + 2]; | |
| indices[i + 1] = idx[j + 1]; | |
| indices[i + 2] = idx[j]; | |
| } else { | |
| // flip winding on odd tris | |
| indices[i] = idx[j + 1]; | |
| indices[i + 1] = idx[j + 2]; | |
| indices[i + 2] = idx[j]; | |
| } | |
| i += 3; | |
| } | |
| break; | |
| case RFLResource.GXPrimitive.TRIANGLEFAN: | |
| // fan: (0,1,2), (0,2,3), (0,3,4) | |
| for (let j = 1; j + 1 < idx.length; j++) { | |
| indices[i] = idx[j + 1]; | |
| indices[i + 1] = idx[j]; | |
| indices[i + 2] = idx[0]; | |
| i += 3; | |
| } | |
| break; | |
| case RFLResource.GXPrimitive.QUADS: | |
| // quads: split each group of 4 into two tris | |
| for (let j = 0; j + 3 < idx.length; j += 4) { | |
| // triangle A: 0,1,2 | |
| indices[i] = idx[j + 2]; | |
| indices[i + 1] = idx[j + 1]; | |
| indices[i + 2] = idx[j]; | |
| // triangle B: 0,2,3 | |
| indices[i + 3] = idx[j + 3]; | |
| indices[i + 4] = idx[j + 2]; | |
| indices[i + 5] = idx[j]; | |
| i += 6; | |
| } | |
| break; | |
| default: | |
| // lines, points, strips-of-lines: unexpected, assert | |
| console.assert(false, | |
| `Unexpected primitive type: ${prim.primType} (${RFLResource.GXPrimitive[prim.primType]})`); | |
| break; | |
| } | |
| } | |
| return new Uint16Array(indices); | |
| } | |
| /** | |
| * Build de-indexed Int16 buffers from a RFLiShape. | |
| * Each triangle emits 3 vertices in sequence. We allocate once, | |
| * then copy raw s16 data into each attribute array. | |
| * @param {typeof RFLResource.RFLiShape} shape - | |
| * The Kaitai-parsed shape, with: | |
| * - shape.vtxPos: Int16Array of [x,y,z,x,y,z,…] | |
| * - shape.vtxNrm: Int16Array of [nx,ny,nz,…] | |
| * - shape.vtxTxc: (optional) Int16Array of [u,v,u,v,…] | |
| * - shape.primitives: Array of GX DL primitives | |
| * @returns {{ | |
| * position: Int16Array, // length = triCount * 3 coords | |
| * normal: Int16Array, // same length as positions | |
| * texcoord: Int16Array|undefined, // length = triCount * 2 coords | |
| * index: Uint16Array // sequential 0..N-1 | |
| * }} | |
| * @todo REVIEW and REPLACE ????? | |
| */ | |
| function buildIndexedAttributesFromShape(shape) { | |
| // 1) Count total output vertices (triangles × 3) | |
| let totalVerts = 0; | |
| for (const prim of shape.primitives) { | |
| const count = prim.vertices.length; | |
| switch (prim.primType) { | |
| case RFLResource.GXPrimitive.TRIANGLES: | |
| totalVerts += Math.floor(count / 3) * 3; | |
| break; | |
| case RFLResource.GXPrimitive.TRIANGLESTRIP: | |
| case RFLResource.GXPrimitive.TRIANGLEFAN: | |
| totalVerts += Math.max(0, count - 2) * 3; | |
| break; | |
| case RFLResource.GXPrimitive.QUADS: | |
| totalVerts += Math.floor(count / 4) * 2 * 3; | |
| break; | |
| default: | |
| // skip lines/points | |
| break; | |
| } | |
| } | |
| // 2) Pre-allocate output buffers | |
| const position = new Int16Array(totalVerts * 3); | |
| const normal = new Int16Array(totalVerts * 3); | |
| const hasUVs = Array.isArray(shape.vtxTxc) && shape.vtxTxc.length > 0; | |
| const texcoord = hasUVs ? new Int16Array(totalVerts * 2) : undefined; | |
| const index = new Uint16Array(totalVerts); | |
| // 3) Walk primitives and copy data | |
| let vertexCursor = 0; // counts output vertices 0..totalVerts-1 | |
| let outPosOff = 0; // positions[offset..offset+2] | |
| let outNrmOff = 0; // normals[offset..offset+2] | |
| let outUvOff = 0; // uvs[offset..offset+1] | |
| const rawPos = shape.vtxPos; // Int16Array | |
| const rawNrm = shape.vtxNrm; // Int16Array | |
| const rawUvs = shape.vtxTxc; // maybe Int16Array | |
| // helper to emit one vertex by raw index triple | |
| function emitVertex(posIndex, nrmIndex, uvIndex) { | |
| // copy XYZ | |
| position[outPosOff] = rawPos[posIndex * 3]; | |
| position[outPosOff + 1] = rawPos[posIndex * 3 + 1]; | |
| position[outPosOff + 2] = rawPos[posIndex * 3 + 2]; | |
| // copy normals | |
| normal[outNrmOff] = rawNrm[nrmIndex * 3]; | |
| normal[outNrmOff + 1] = rawNrm[nrmIndex * 3 + 1]; | |
| normal[outNrmOff + 2] = rawNrm[nrmIndex * 3 + 2]; | |
| // copy UV if present | |
| if (texcoord) { | |
| texcoord[outUvOff] = rawUvs[uvIndex * 2]; | |
| texcoord[outUvOff + 1] = rawUvs[uvIndex * 2 + 1]; | |
| } | |
| // set the index for this new vertex | |
| index[vertexCursor] = vertexCursor; | |
| // advance all cursors | |
| vertexCursor += 1; | |
| outPosOff += 3; | |
| outNrmOff += 3; | |
| if (hasUVs) outUvOff += 2; | |
| } | |
| // 4) Expand each primitive to triangles | |
| for (const prim of shape.primitives) { | |
| const verts = prim.vertices; | |
| switch (prim.primType) { | |
| case RFLResource.GXPrimitive.TRIANGLES: | |
| // emit every group of 3, reversed for CCW | |
| for (let i = 0; i + 2 < verts.length; i += 3) { | |
| emitVertex(verts[i + 2].posIdx, verts[i + 2].nrmIdx, verts[i + 2].texIdx); | |
| emitVertex(verts[i + 1].posIdx, verts[i + 1].nrmIdx, verts[i + 1].texIdx); | |
| emitVertex(verts[i].posIdx, verts[i].nrmIdx, verts[i].texIdx); | |
| } | |
| break; | |
| case RFLResource.GXPrimitive.TRIANGLESTRIP: | |
| // (0,1,2),(2,1,3),(2,3,4),… with winding flip | |
| for (let i = 0; i + 2 < verts.length; i++) { | |
| if (i % 2 === 0) { | |
| emitVertex(verts[i + 2].posIdx, verts[i + 2].nrmIdx, verts[i + 2].texIdx); | |
| emitVertex(verts[i + 1].posIdx, verts[i + 1].nrmIdx, verts[i + 1].texIdx); | |
| emitVertex(verts[i].posIdx, verts[i].nrmIdx, verts[i].texIdx); | |
| } else { | |
| emitVertex(verts[i + 1].posIdx, verts[i + 1].nrmIdx, verts[i + 1].texIdx); | |
| emitVertex(verts[i + 2].posIdx, verts[i + 2].nrmIdx, verts[i + 2].texIdx); | |
| emitVertex(verts[i].posIdx, verts[i].nrmIdx, verts[i].texIdx); | |
| } | |
| } | |
| break; | |
| case RFLResource.GXPrimitive.TRIANGLEFAN: | |
| // (0,1,2),(0,2,3),(0,3,4),… | |
| for (let i = 1; i + 1 < verts.length; i++) { | |
| emitVertex(verts[i + 1].posIdx, verts[i + 1].nrmIdx, verts[i + 1].texIdx); | |
| emitVertex(verts[i].posIdx, verts[i].nrmIdx, verts[i].texIdx); | |
| emitVertex(verts[0].posIdx, verts[0].nrmIdx, verts[0].texIdx); | |
| } | |
| break; | |
| case RFLResource.GXPrimitive.QUADS: | |
| // [0,1,2,3] → (2,1,0),(3,2,0) | |
| for (let i = 0; i + 3 < verts.length; i += 4) { | |
| emitVertex(verts[i + 2].posIdx, verts[i + 2].nrmIdx, verts[i + 2].texIdx); | |
| emitVertex(verts[i + 1].posIdx, verts[i + 1].nrmIdx, verts[i + 1].texIdx); | |
| emitVertex(verts[i].posIdx, verts[i].nrmIdx, verts[i].texIdx); | |
| emitVertex(verts[i + 3].posIdx, verts[i + 3].nrmIdx, verts[i + 3].texIdx); | |
| emitVertex(verts[i + 2].posIdx, verts[i + 2].nrmIdx, verts[i + 2].texIdx); | |
| emitVertex(verts[i].posIdx, verts[i].nrmIdx, verts[i].texIdx); | |
| } | |
| break; | |
| default: | |
| // skip lines & points | |
| break; | |
| } | |
| } | |
| return { | |
| position, | |
| normal, | |
| texcoord, | |
| index | |
| }; | |
| } | |
| // ==> MY CODE ? <== | |
| /** | |
| * Main entrypoint. | |
| * @param {string} resPath - Path to RFL_Res.dat. | |
| */ | |
| async function main(resPath = 'RFL_Res.dat') { | |
| // Fetch resource from response. | |
| const response = await fetch(resPath); | |
| if (!response.ok) { | |
| const err2 = new Error(`failed to fetch resource, HTTP status = ${response.status}`); | |
| throw err2; | |
| } | |
| const buffer = await response.arrayBuffer(); | |
| /** Construct RFLResource Kaitai type. Uses new KaitaiStream from ArrayBuffer. */ | |
| const resource = new RFLResource(new KaitaiStream(buffer, null)); | |
| // Create Three.js scene. | |
| if (!scene) { | |
| // spector.displayUI(); | |
| initializeThree(); | |
| } | |
| populateShapeArchiveList(resource); | |
| initializeControlPanel(resource); | |
| applyDeepLinkSelection(resource); | |
| // get a shape that can be drawn | |
| // const shape = res.archive8shape33; | |
| const shape = getResourceShape(resource, RFLiArcID.Hair, 33); | |
| // Add mesh to the scene. | |
| const mesh = rflShapeToMesh(shape); | |
| console.assert(mesh !== null); | |
| switchMeshInScene(mesh); | |
| } | |
| /** @type {THREE.Scene} */ | |
| let scene; | |
| /** @type {import('three/addons/controls/OrbitControls.js')} */ | |
| let orbitControls = null; | |
| /** @type {THREE.Mesh|null} */ | |
| let currentMesh = null; | |
| /** Simple triangle (apparently). */ | |
| const sample = new Int16Array([ | |
| 256, 0, 0, | |
| 0, 256, 0, | |
| 0, 0, 256 | |
| ]); | |
| /** | |
| * Creates a mesh from RFL shape data. | |
| * @param {Object} shape - RFL shape to add. | |
| * @returns {THREE.Mesh|null} The Three.js mesh, or null if an invalid shape is passed in. | |
| */ | |
| function rflShapeToMesh(shape) { | |
| // function addMeshToScene(positioni16 = sample, normali16, texcoordi16, indexu16) { | |
| // The position count must be valid before continuing. | |
| if (!shape || !shape.numVtxPos) { | |
| return null; | |
| } | |
| /* | |
| // Create arrays for all attributes. | |
| const position = new Int16Array(shape.vtxPos); | |
| const normal = new Int16Array(shape.vtxNrm); | |
| // UVs/texCoord are optional. | |
| const texcoord = shape.vtxTxc ? new Int16Array(shape.vtxTxc) : undefined; | |
| // Create indices. | |
| const indexCount = getIndexCountFromPrimitives(shape.primitives); | |
| const index = new Uint16Array(indexCount); | |
| buildToIndexBuffer(shape.primitives, index); | |
| */ | |
| const { position, normal, texcoord, index } = buildIndexedAttributesFromShape(shape); | |
| console.assert(position && position.length); | |
| /* | |
| // Convert to Float32 and scale (assume 1 unit = 256) | |
| const positionF32 = new Float32Array(position.length); | |
| const normalF32 = new Float32Array(normal.length); | |
| for (let i = 0; i < position.length; i++) { | |
| positionF32[i] = position[i] / 256.0; | |
| } | |
| for (let i = 0; i < normal.length; i++) { | |
| normalF32[i] = normal[i] / 256.0; | |
| } | |
| */ | |
| /** Create BufferGeometry. Normalize all attributes. */ | |
| const geometry = new THREE.BufferGeometry(); | |
| geometry.setAttribute('position', | |
| new THREE.BufferAttribute(position, 3, true)); | |
| // new THREE.BufferAttribute(positionF32, 3)); | |
| if (normal) { | |
| geometry.setAttribute('normal', | |
| new THREE.BufferAttribute(normal, 3, true)); | |
| // new THREE.BufferAttribute(normalF32, 3)); | |
| } | |
| if (texcoord) { | |
| geometry.setAttribute('uv', | |
| new THREE.BufferAttribute(texcoord, 2, true)); | |
| } | |
| // if (index) { | |
| geometry.setIndex(new THREE.BufferAttribute(index, 1)); | |
| // geometry.computeVertexNormals(); | |
| /** Create material and THREE.Mesh. */ | |
| const mesh = new THREE.Mesh(geometry, | |
| new THREE.MeshPhongMaterial({ | |
| color: 0xffffff, | |
| // wireframe: true, | |
| // flatShading: true, | |
| // side: THREE.BackSide | |
| })); | |
| return mesh; | |
| } | |
| /** | |
| * Adds mesh to the global {@link scene} and disposes the previous one. | |
| * @param {THREE.Mesh} mesh - New mesh to add to scene. | |
| */ | |
| function switchMeshInScene(mesh) { | |
| // Remove current mesh before adding new one to scene. | |
| if (currentMesh) { | |
| scene.remove(currentMesh); | |
| // Dispose the currentMesh. | |
| if (currentMesh.material instanceof THREE.Material && | |
| !Array.isArray(currentMesh.material)) { | |
| currentMesh.material.dispose(); | |
| /** @type {THREE.MeshBasicMaterial} */ (currentMesh.material).map?.dispose(); | |
| } else { | |
| console.assert(false, 'expected currentMesh.material to be a single non-array THREE.Material'); | |
| } | |
| } | |
| scene.add(mesh); // Add mesh to scene. | |
| currentMesh = mesh; | |
| } | |
| /** | |
| * Entrypoint that will create new THREE.Scene, renderer, and lights. | |
| * @param {HTMLElement} canvasTarget - Where to add the renderer's canvas. | |
| */ | |
| function initializeThree(canvasTarget = document.body) { | |
| scene = new THREE.Scene(); // Create scene. | |
| scene.background = new THREE.Color(0x202020); // Set scene background. | |
| /** Scene camera. */ | |
| const camera = new THREE.PerspectiveCamera(45, | |
| window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(0.46, 0.75, 1.2); | |
| // Add lighting to scene for Three.js materials. | |
| const intensity = Number(THREE.REVISION) >= 155 ? Math.PI : 1.0; | |
| const ambientLight = new THREE.AmbientLight(new THREE.Color(0.73, 0.73, 0.73), intensity); | |
| const directionalLight = new THREE.DirectionalLight( | |
| new THREE.Color(0.60, 0.60, 0.60), intensity); | |
| directionalLight.position.set(-0.455, 0.348, 0.5); | |
| scene.add(ambientLight, directionalLight); | |
| /** Create THREE.WebGLRenderer. */ | |
| const renderer = new THREE.WebGLRenderer(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| // Append to DOM. | |
| canvasTarget.appendChild(renderer.domElement); | |
| // Create OrbitControls. | |
| // @ts-expect-error -- 'PerspectiveCamera' not assignable to 'Camera', 'matrixWorldInverse.copy' incompatible | |
| orbitControls = new OrbitControls(camera, renderer.domElement); | |
| orbitControls.autoRotateSpeed = 8; | |
| orbitControls.target.set(-0.08, 0.15, -0.09); | |
| // Set resize handler. | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| /** requestAnimationFrame handler function. */ | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| orbitControls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| } | |
| main(); // Run async entrypoint. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild | |
| (function (root, factory) { | |
| if (typeof define === 'function' && define.amd) { | |
| define(['kaitai-struct/KaitaiStream'], factory); | |
| } else if (typeof module === 'object' && module.exports) { | |
| module.exports = factory(require('kaitai-struct/KaitaiStream')); | |
| } else { | |
| root.RFLResource = factory(root.KaitaiStream); | |
| } | |
| }(typeof self !== 'undefined' ? self : this, function (KaitaiStream) { | |
| var RFLResource = (function() { | |
| RFLResource.GXPrimitive = Object.freeze({ | |
| QUADS: 128, | |
| TRIANGLES: 144, | |
| TRIANGLESTRIP: 152, | |
| TRIANGLEFAN: 160, | |
| LINES: 168, | |
| LINESTRIP: 176, | |
| POINTS: 184, | |
| 128: "QUADS", | |
| 144: "TRIANGLES", | |
| 152: "TRIANGLESTRIP", | |
| 160: "TRIANGLEFAN", | |
| 168: "LINES", | |
| 176: "LINESTRIP", | |
| 184: "POINTS", | |
| }); | |
| RFLResource.RflIArcId = Object.freeze({ | |
| BEARD: 0, | |
| EYE: 1, | |
| EYEBROW: 2, | |
| FACELINE: 3, | |
| FACE_TEX: 4, | |
| FORE_HEAD: 5, | |
| GLASS: 6, | |
| GLASS_TEX: 7, | |
| HAIR: 8, | |
| MASK: 9, | |
| MOLE: 10, | |
| MOUTH: 11, | |
| MUSTACHE: 12, | |
| NOSE: 13, | |
| NLINE: 14, | |
| NLINE_TEX: 15, | |
| CAP: 16, | |
| CAP_TEX: 17, | |
| MAX: 18, | |
| 0: "BEARD", | |
| 1: "EYE", | |
| 2: "EYEBROW", | |
| 3: "FACELINE", | |
| 4: "FACE_TEX", | |
| 5: "FORE_HEAD", | |
| 6: "GLASS", | |
| 7: "GLASS_TEX", | |
| 8: "HAIR", | |
| 9: "MASK", | |
| 10: "MOLE", | |
| 11: "MOUTH", | |
| 12: "MUSTACHE", | |
| 13: "NOSE", | |
| 14: "NLINE", | |
| 15: "NLINE_TEX", | |
| 16: "CAP", | |
| 17: "CAP_TEX", | |
| 18: "MAX", | |
| }); | |
| function RFLResource(_io, _parent, _root) { | |
| this._io = _io; | |
| this._parent = _parent; | |
| this._root = _root || this; | |
| this._read(); | |
| } | |
| RFLResource.prototype._read = function() { | |
| this.totalArchiveCount = this._io.readU2be(); | |
| this.version = this._io.readU2be(); | |
| this.offsets = []; | |
| for (var i = 0; i < this.totalArchiveCount; i++) { | |
| this.offsets.push(this._io.readU4be()); | |
| } | |
| } | |
| var RFLiTexture = RFLResource.RFLiTexture = (function() { | |
| function RFLiTexture(_io, _parent, _root) { | |
| this._io = _io; | |
| this._parent = _parent; | |
| this._root = _root || this; | |
| this._read(); | |
| } | |
| RFLiTexture.prototype._read = function() { | |
| this.format = this._io.readU1(); | |
| this.alpha = this._io.readU1(); | |
| this.width = this._io.readU2be(); | |
| this.height = this._io.readU2be(); | |
| this.wrapS = this._io.readU1(); | |
| this.wrapT = this._io.readU1(); | |
| this.indexTexture = this._io.readU1(); | |
| this.colorFormat = this._io.readU1(); | |
| this.numColors = this._io.readU2be(); | |
| this.paletteOfs = this._io.readU4be(); | |
| this.enableLod = this._io.readU1(); | |
| this.enableEdgeLod = this._io.readU1(); | |
| this.enableBiasClamp = this._io.readU1(); | |
| this.enableMaxAniso = this._io.readU1(); | |
| this.minFilt = this._io.readU1(); | |
| this.magFilt = this._io.readU1(); | |
| this.minLod = this._io.readS1(); | |
| this.maxLod = this._io.readS1(); | |
| this.mipmapLevel = this._io.readU1(); | |
| this.reserved = this._io.readS1(); | |
| this.lodBias = this._io.readS2be(); | |
| this.imageOfs = this._io.readU4be(); | |
| } | |
| return RFLiTexture; | |
| })(); | |
| var Vertex = RFLResource.Vertex = (function() { | |
| function Vertex(_io, _parent, _root) { | |
| this._io = _io; | |
| this._parent = _parent; | |
| this._root = _root || this; | |
| this._read(); | |
| } | |
| Vertex.prototype._read = function() { | |
| this.posIdx = this._io.readU1(); | |
| this.nrmIdx = this._io.readU1(); | |
| if (!(this._parent._parent.skipTxc)) { | |
| this.texIdx = this._io.readU1(); | |
| } | |
| } | |
| return Vertex; | |
| })(); | |
| /** | |
| * NOTE: Untyped. Custom type. | |
| * Real data is loaded from "res" in RFLiInitShapeRes | |
| */ | |
| var RFLiShape = RFLResource.RFLiShape = (function() { | |
| function RFLiShape(_io, _parent, _root) { | |
| this._io = _io; | |
| this._parent = _parent; | |
| this._root = _root || this; | |
| this._read(); | |
| } | |
| RFLiShape.prototype._read = function() { | |
| this.identifier = KaitaiStream.bytesToStr(this._io.readBytes(4), "ascii"); | |
| if (this.isFaceline) { | |
| this.noseTrans = []; | |
| for (var i = 0; i < 3; i++) { | |
| this.noseTrans.push(this._io.readF4be()); | |
| } | |
| } | |
| if (this.isFaceline) { | |
| this.beardTrans = []; | |
| for (var i = 0; i < 3; i++) { | |
| this.beardTrans.push(this._io.readF4be()); | |
| } | |
| } | |
| if (this.isFaceline) { | |
| this.hairTrans = []; | |
| for (var i = 0; i < 3; i++) { | |
| this.hairTrans.push(this._io.readF4be()); | |
| } | |
| } | |
| this.numVtxPos = this._io.readU2be(); | |
| this.vtxPos = []; | |
| for (var i = 0; i < (this.numVtxPos * 3); i++) { | |
| this.vtxPos.push(this._io.readU2be()); | |
| } | |
| this.numVtxNrm = this._io.readU2be(); | |
| this.vtxNrm = []; | |
| for (var i = 0; i < (this.numVtxNrm * 3); i++) { | |
| this.vtxNrm.push(this._io.readU2be()); | |
| } | |
| if (!(this.skipTxc)) { | |
| this.numVtxTxc = this._io.readU2be(); | |
| } | |
| if (!(this.skipTxc)) { | |
| this.vtxTxc = []; | |
| for (var i = 0; i < (this.numVtxTxc * 2); i++) { | |
| this.vtxTxc.push(this._io.readU2be()); | |
| } | |
| } | |
| this.primCount = this._io.readU1(); | |
| this.primitives = []; | |
| for (var i = 0; i < this.primCount; i++) { | |
| this.primitives.push(new Primitive(this._io, this, this._root)); | |
| } | |
| } | |
| Object.defineProperty(RFLiShape.prototype, 'skipTxc', { | |
| get: function() { | |
| if (this._m_skipTxc !== undefined) | |
| return this._m_skipTxc; | |
| this._m_skipTxc = ((this.identifier == "frhd") || (this.identifier == "hair") || (this.identifier == "berd") || (this.identifier == "nose")) ; | |
| return this._m_skipTxc; | |
| } | |
| }); | |
| Object.defineProperty(RFLiShape.prototype, 'isFaceline', { | |
| get: function() { | |
| if (this._m_isFaceline !== undefined) | |
| return this._m_isFaceline; | |
| this._m_isFaceline = this.identifier == "face"; | |
| return this._m_isFaceline; | |
| } | |
| }); | |
| /** | |
| * 4-byte identifier, unused by RFL. | |
| * 'nose', 'frhd', 'face', 'hair', 'cap_', 'berd', 'nsln', 'mask', 'glas' | |
| * https://github.com/SMGCommunity/Petari/blob/6b6a7635d3ab985a5866be9ae4db09d52d678f6c/src/RVLFaceLib/RFL_Model.c#L835 | |
| */ | |
| return RFLiShape; | |
| })(); | |
| var RFLiArchive = RFLResource.RFLiArchive = (function() { | |
| function RFLiArchive(_io, _parent, _root) { | |
| this._io = _io; | |
| this._parent = _parent; | |
| this._root = _root || this; | |
| this._read(); | |
| } | |
| RFLiArchive.prototype._read = function() { | |
| this.num = this._io.readU2be(); | |
| this.maxsize = this._io.readU2be(); | |
| this.offset = []; | |
| for (var i = 0; i < (this.num + 1); i++) { | |
| this.offset.push(this._io.readU4be()); | |
| } | |
| } | |
| /** | |
| * Number of files in the archive. | |
| */ | |
| /** | |
| * Size of the biggest entry in the archive. | |
| */ | |
| /** | |
| * Offsets of each part in the archive. | |
| */ | |
| return RFLiArchive; | |
| })(); | |
| /** | |
| * Image width and height are calculated by | |
| * getting the next power of two. In JS: | |
| * const nextPow2 = x => x <= 1 ? 1 : 1 << (32 - Math.clz32(x - 1)); | |
| */ | |
| var CFLiTexHeader = RFLResource.CFLiTexHeader = (function() { | |
| function CFLiTexHeader(_io, _parent, _root) { | |
| this._io = _io; | |
| this._parent = _parent; | |
| this._root = _root || this; | |
| this._read(); | |
| } | |
| CFLiTexHeader.prototype._read = function() { | |
| this.imageW = this._io.readU2be(); | |
| this.imageH = this._io.readU2be(); | |
| this.mMipmapSize = this._io.readU1(); | |
| this.mFormat = this._io.readU1(); | |
| this.mWrapS = this._io.readU1(); | |
| this.mWrapT = this._io.readU1(); | |
| } | |
| return CFLiTexHeader; | |
| })(); | |
| var Primitive = RFLResource.Primitive = (function() { | |
| function Primitive(_io, _parent, _root) { | |
| this._io = _io; | |
| this._parent = _parent; | |
| this._root = _root || this; | |
| this._read(); | |
| } | |
| Primitive.prototype._read = function() { | |
| this.vtxCount = this._io.readU1(); | |
| this.primType = this._io.readU1(); | |
| this.vertices = []; | |
| for (var i = 0; i < this.vtxCount; i++) { | |
| this.vertices.push(new Vertex(this._io, this, this._root)); | |
| } | |
| } | |
| return Primitive; | |
| })(); | |
| Object.defineProperty(RFLResource.prototype, 'archive4', { | |
| get: function() { | |
| if (this._m_archive4 !== undefined) | |
| return this._m_archive4; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[4]); | |
| this._m_archive4 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive4; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive18', { | |
| get: function() { | |
| if (this._m_archive18 !== undefined) | |
| return this._m_archive18; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[18]); | |
| this._m_archive18 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive18; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive9', { | |
| get: function() { | |
| if (this._m_archive9 !== undefined) | |
| return this._m_archive9; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[9]); | |
| this._m_archive9 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive9; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive16', { | |
| get: function() { | |
| if (this._m_archive16 !== undefined) | |
| return this._m_archive16; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[16]); | |
| this._m_archive16 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive16; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive3shape1', { | |
| get: function() { | |
| if (this._m_archive3shape1 !== undefined) | |
| return this._m_archive3shape1; | |
| var _pos = this._io.pos; | |
| this._io.seek((((8 + this.offsets[3]) + (this.archive3.num * 4)) + 1619)); | |
| this._m_archive3shape1 = new RFLiShape(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive3shape1; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive8shape33', { | |
| get: function() { | |
| if (this._m_archive8shape33 !== undefined) | |
| return this._m_archive8shape33; | |
| var _pos = this._io.pos; | |
| this._io.seek((((8 + this.offsets[8]) + (this.archive8.num * 4)) + 73511)); | |
| this._m_archive8shape33 = new RFLiShape(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive8shape33; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive1shape1', { | |
| get: function() { | |
| if (this._m_archive1shape1 !== undefined) | |
| return this._m_archive1shape1; | |
| var _pos = this._io.pos; | |
| this._io.seek((((8 + this.offsets[0]) + (this.archive0.num * 4)) + 9)); | |
| this._m_archive1shape1 = new RFLiShape(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive1shape1; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive1', { | |
| get: function() { | |
| if (this._m_archive1 !== undefined) | |
| return this._m_archive1; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[1]); | |
| this._m_archive1 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive1; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive6', { | |
| get: function() { | |
| if (this._m_archive6 !== undefined) | |
| return this._m_archive6; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[6]); | |
| this._m_archive6 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive6; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive12', { | |
| get: function() { | |
| if (this._m_archive12 !== undefined) | |
| return this._m_archive12; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[12]); | |
| this._m_archive12 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive12; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive0', { | |
| get: function() { | |
| if (this._m_archive0 !== undefined) | |
| return this._m_archive0; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[0]); | |
| this._m_archive0 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive0; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive13', { | |
| get: function() { | |
| if (this._m_archive13 !== undefined) | |
| return this._m_archive13; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[13]); | |
| this._m_archive13 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive13; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive10', { | |
| get: function() { | |
| if (this._m_archive10 !== undefined) | |
| return this._m_archive10; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[10]); | |
| this._m_archive10 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive10; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive11', { | |
| get: function() { | |
| if (this._m_archive11 !== undefined) | |
| return this._m_archive11; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[11]); | |
| this._m_archive11 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive11; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive8', { | |
| get: function() { | |
| if (this._m_archive8 !== undefined) | |
| return this._m_archive8; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[8]); | |
| this._m_archive8 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive8; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive1texture0', { | |
| get: function() { | |
| if (this._m_archive1texture0 !== undefined) | |
| return this._m_archive1texture0; | |
| var _pos = this._io.pos; | |
| this._io.seek(((8 + this.offsets[1]) + (this.archive1.num * 4))); | |
| this._m_archive1texture0 = new CFLiTexHeader(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive1texture0; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive7', { | |
| get: function() { | |
| if (this._m_archive7 !== undefined) | |
| return this._m_archive7; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[7]); | |
| this._m_archive7 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive7; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive19', { | |
| get: function() { | |
| if (this._m_archive19 !== undefined) | |
| return this._m_archive19; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[19]); | |
| this._m_archive19 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive19; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive3', { | |
| get: function() { | |
| if (this._m_archive3 !== undefined) | |
| return this._m_archive3; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[3]); | |
| this._m_archive3 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive3; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive17', { | |
| get: function() { | |
| if (this._m_archive17 !== undefined) | |
| return this._m_archive17; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[17]); | |
| this._m_archive17 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive17; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive15', { | |
| get: function() { | |
| if (this._m_archive15 !== undefined) | |
| return this._m_archive15; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[15]); | |
| this._m_archive15 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive15; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archives', { | |
| get: function() { | |
| if (this._m_archives !== undefined) | |
| return this._m_archives; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[i]); | |
| this._m_archives = []; | |
| for (var i = 0; i < this.totalArchiveCount; i++) { | |
| this._m_archives.push(new RFLiArchive(this._io, this, this._root)); | |
| } | |
| this._io.seek(_pos); | |
| return this._m_archives; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive5', { | |
| get: function() { | |
| if (this._m_archive5 !== undefined) | |
| return this._m_archive5; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[5]); | |
| this._m_archive5 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive5; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive14', { | |
| get: function() { | |
| if (this._m_archive14 !== undefined) | |
| return this._m_archive14; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[14]); | |
| this._m_archive14 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive14; | |
| } | |
| }); | |
| Object.defineProperty(RFLResource.prototype, 'archive2', { | |
| get: function() { | |
| if (this._m_archive2 !== undefined) | |
| return this._m_archive2; | |
| var _pos = this._io.pos; | |
| this._io.seek(this.offsets[2]); | |
| this._m_archive2 = new RFLiArchive(this._io, this, this._root); | |
| this._io.seek(_pos); | |
| return this._m_archive2; | |
| } | |
| }); | |
| /** | |
| * Amount of total archives within the file. | |
| * RFL = 18 (RFLiArcID_Max), NFL = 20 (?), CFL = 20 (CFLi_PARTS_ID_COUNT) | |
| */ | |
| /** | |
| * Resource version. Set in RFL and CFL, only seen used in CFL debug mode. | |
| * CFL bootloadDB2Res_: nn::dbg::detail::Printf("CFL: Cached Resource Version = 0x%04x\n"); | |
| * / Debug assert; "cfl_resource.cpp",0x19d,"%s", "loader->mVersion >= 0x509" | |
| */ | |
| /** | |
| * Offsets for each part type. | |
| */ | |
| return RFLResource; | |
| })(); | |
| return RFLResource; | |
| })); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment