Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,8 @@ node_modules/

# Local scratch images created during browser/UI debugging
tmp-*.png

# Local build and browser diagnostics
/.tmp/
/debug.log
frontend/eqsage-embed/vite.config.js.timestamp-*.mjs
4 changes: 4 additions & 0 deletions frontend/eqsage-embed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@
"dev": "vite --mode development",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build",
"build:watch": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --watch",
"audit:race-skin-textures": "node scripts/audit-race-skin-textures.mjs",
"sync:race-data": "node scripts/sync-race-data.mjs",
"report:race-face-audit": "node scripts/generate-race-face-audit.mjs",
"plan:race-face-zones": "node scripts/plan-unresolved-race-zones.mjs",
"bundle-visualize": "npx vite-bundle-visualizer"
},
"eslintConfig": {
Expand Down
102 changes: 76 additions & 26 deletions frontend/eqsage-embed/sage/lib/eqg/eqg-decoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import { Zone } from "./zone/zone";
import { ZoneData } from "./zone/v4-zone";
import { Model, Animation, Lit } from "./model/model";
import { MDS } from "./model/mds";
import { Eco } from "./eco/eco";
import { PFSArchive } from "../pfs/pfs";
import { deleteEqFileOrFolder, writeEQFile } from "../util/fileHandler";
import { deleteEqFileOrFolder, getEQRootDir, writeEQFile } from "../util/fileHandler";

const getImageProcessor = async () => {
if (typeof window !== 'undefined' && window.imageProcessor) {
Expand All @@ -15,6 +16,8 @@ const getImageProcessor = async () => {
return imageProcessor;
};

const yieldToBrowser = () => new Promise((resolve) => setTimeout(resolve, 0));

export class EQGDecoder {
#options = {
forceWrote: false,
Expand Down Expand Up @@ -67,12 +70,21 @@ export class EQGDecoder {
return this.#fileHandle.name;
}

get options() {
return this.#options;
}

async processBuffer(name, arrayBuffer, skipImages = false) {
this.pfsArchive = new PFSArchive();
this.pfsArchive.openFromFile(arrayBuffer);
const images = [];
this.files = {};
let processedFiles = 0;
for (const [fileName, data] of this.pfsArchive.files.entries()) {
processedFiles++;
if (processedFiles % 10 === 0) {
await yieldToBrowser();
}
this.files[fileName] = this.pfsArchive.getFile(fileName);
if (fileName.endsWith(".lit")) {
const lit = new Lit(
Expand Down Expand Up @@ -133,12 +145,39 @@ export class EQGDecoder {
this.eco[fileName.replace(".eco", "")] = new Eco(this.files[fileName]);
}
if (fileName.endsWith(".mds")) {
//console.log("Had MDS! ", fileName);
const mds = new MDS(
this.files[fileName],
this.#fileHandle,
fileName
);
for (const { name, model } of mds.models) {
this.models[name] = model;
}
}
if (fileName.endsWith('.txt')) {
//console.log('Txt', fileName)
}
console.log('File', fileName)
}

const archiveModelName = this.#fileHandle.name.toLowerCase();
const canonicalModName = `${archiveModelName}.mod`;
if (!this.models[canonicalModName] && !this.models[archiveModelName]) {
const escapedArchiveModelName = archiveModelName.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
);
const embeddedArchiveCodePattern = new RegExp(
`(?:^|_)${escapedArchiveModelName}(?:_|\\d|\\.)`,
'i'
);
const canonicalCandidate = Object.entries(this.models).find(
([modelName]) =>
modelName.endsWith('.mod') &&
embeddedArchiveCodePattern.test(modelName)
);
if (canonicalCandidate) {
this.models[canonicalModName] = canonicalCandidate[1];
}
}

// Post process
Expand All @@ -164,7 +203,7 @@ export class EQGDecoder {
}
}
console.log(`Processed - ${name}`);
console.log('Images', images)
console.log(`Images queued for conversion: ${images.length}`);
if (this.#options.rawImageWrite) {
console.log('Using raw image write');
} else if (!skipImages) {
Expand All @@ -189,25 +228,33 @@ export class EQGDecoder {
*
* @param {import('./common/models').PlaceableGroup} p
*/
async writeModels(p, zoneMetadata, modelFile, writtenModels, mod, v3) {
async writeModels(modelFile, mod, destination) {
const { writeModels } = await import("./gltf-export/common");
return writeModels.apply(this, [
p,
zoneMetadata,
modelFile,
writtenModels,
mod,
v3,
destination,
]);
}

async export() {
if (!this.zone) {
const destination = this.#options.modelDestination ?? 'models';
const canonicalModName = `${this.name.toLowerCase()}.mod`;
if (destination === 'models' && this.models[canonicalModName]) {
await this.writeModels(
canonicalModName,
this.models[canonicalModName],
destination
);
return true;
}
for (const [name, mod] of Object.entries(this.models)) {
if (!name.includes('ter_')) {
await this.writeModels(name, mod);
await this.writeModels(name, mod, destination);
}
}
return true;
}
if (this.zone?.header?.version === 4) {
const { exportv4 } = await import("./gltf-export/v4");
Expand All @@ -222,29 +269,32 @@ export class EQGDecoder {
const micro = performance.now();

for (const file of this.#fileHandle.fileHandles) {
const extension = file.name.split(".").pop();
const extension = file.name.split(".").pop().toLowerCase();
switch (extension) {
case "eqg":
await this.processEQG(file);
break;
case "txt":
if (file.name.endsWith('_assets.txt') && !this.options.skipSubload) {
// const contents = (await file.text()).split('\r\n');
// for (const line of contents) {
// if (line.endsWith('.eqg')) {
// console.log(`Loading dependent asset ${line}`);
// try {
// const dir = getEQRootDir();
// const fh = await dir.getFileHandle(line).then(f => f.getFile());
// await this.processEQG(fh);
// } catch(e) {
// console.log(`Error loading dependent asset`, e);
// }

// }
// }
if (file.name.endsWith('_assets.txt') && !this.#options.skipSubload) {
const contents = (await file.text()).split(/\r?\n/);
for (const entry of contents) {
const line = entry.trim();
if (line.toLowerCase().endsWith('.eqg')) {
console.log(`Loading dependent asset ${line}`);
try {
const dir = getEQRootDir();
const fh = await dir.getFileHandle(line).then(f => f.getFile());
await this.processEQG(fh);
await yieldToBrowser();
} catch(e) {
console.log(`Error loading dependent asset`, e);
}
}
}
}
break;
case "s3d":
break;
case "eff":
break;
case "xmi":
Expand Down
91 changes: 82 additions & 9 deletions frontend/eqsage-embed/sage/lib/eqg/gltf-export/common.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Accessor, Document, WebIO } from '@gltf-transform/core';
import { ALL_EXTENSIONS, KHRMaterialsSpecular } from '@gltf-transform/extensions';

import { mat4, vec3 } from 'gl-matrix';
import { mat4, quat, vec3 } from 'gl-matrix';
import {
appendObjectMetadata,
getEQFileExists,
writeEQFile,
} from '../../util/fileHandler';
import { PREVIEW_CHARACTER_MODEL_CACHE_VERSION } from '../../model/constants';
import { EQGAnimationWriter } from './eqg-animation';

const io = new WebIO().registerExtensions(ALL_EXTENSIONS);
const yieldToBrowser = () => new Promise((resolve) => setTimeout(resolve, 0));

export async function writeMetadata(
infP,
Expand Down Expand Up @@ -50,12 +52,15 @@ export async function writeMetadata(
* @this {import('../eqg-decoder').EQGDecoder}
* @returns
*/
export async function writeModels(modelFile, mod) {
export async function writeModels(modelFile, mod, destination = 'objects') {
modelFile = modelFile.replace('.mod', '');
const diskFileName = `${modelFile}.glb`;
await appendObjectMetadata(modelFile, `${this.name}.eqg`);
if (destination === 'objects') {
await appendObjectMetadata(modelFile, `${this.name}.eqg`);
}
if (
(await getEQFileExists('objects', diskFileName)) &&
(await getEQFileExists(destination, diskFileName)) &&
!this.options?.forceWrite &&
!modelFile.includes('et_drbanner') &&
!modelFile.includes('dest') &&
!modelFile.includes('ggy') &&
Expand All @@ -73,6 +78,7 @@ export async function writeModels(modelFile, mod) {
const node = document.createNode(objectName).setTranslation([0, 0, 0]).setMatrix(flipMatrix);
const skeletonNodes = [];
const boneIndices = [];
const boneBindWorldMatrices = [];

if (mod.bones.length) {
for (const bone of mod.bones) {
Expand All @@ -85,23 +91,59 @@ export async function writeModels(modelFile, mod) {
}
}
let idx = 0;
function recurse(i) {
function recurse(i, parentBindMatrix = null) {
const bone = mod.bones[i];
const node = document.createNode(bone.name);
const translation = vec3.fromValues(bone.x, bone.z, bone.y);
const rotation = quat.normalize(
quat.create(),
quat.fromValues(bone.rotX, bone.rotZ, bone.rotY, bone.rotW)
);
const scale = vec3.fromValues(
Number.isFinite(bone.scaleX) && Math.abs(bone.scaleX) > 0.000001
? bone.scaleX
: 1,
Number.isFinite(bone.scaleZ) && Math.abs(bone.scaleZ) > 0.000001
? bone.scaleZ
: 1,
Number.isFinite(bone.scaleY) && Math.abs(bone.scaleY) > 0.000001
? bone.scaleY
: 1
);
const localBindMatrix = mat4.fromRotationTranslationScale(
mat4.create(),
rotation,
translation,
scale
);
const worldBindMatrix = parentBindMatrix
? mat4.multiply(mat4.create(), parentBindMatrix, localBindMatrix)
: localBindMatrix;
const node = document
.createNode(bone.name)
.setTranslation(translation)
.setRotation(rotation)
.setScale(scale);

skeletonNodes.push(node);
boneIndices[i] = idx;
boneBindWorldMatrices[i] = worldBindMatrix;
idx++;
for (const child of bone.children) {
mod.bones[child].parent = bone;
const [_b, n] = recurse(child);
const [_b, n] = recurse(child, worldBindMatrix);
node.addChild(n);
}
return [bone, node];
}
recurse(0);
}

node.setExtras({
spireCharacterModelCacheVersion: PREVIEW_CHARACTER_MODEL_CACHE_VERSION,
spireEqgSkinningVersion: 1,
spireNativePoseOnly: false,
});

scene.addChild(node);
const materials = {};
for (const mat of mod.geometry.mats) {
Expand All @@ -121,7 +163,16 @@ export async function writeModels(modelFile, mod) {
.setSpecularColorFactor([0, 0, 0]);
gltfMaterial.setExtension('KHR_materials_specular', specular);
for (const prop of mat.properties) {
if (prop.type !== 2) {
continue;
}
const [name] = prop.valueS.toLowerCase().split('.');
if (
!name ||
(mat.name.toLowerCase() === 'failsafeshader' && name === 'grid_standard')
) {
continue;
}
const texture = document
.createTexture(name)
.setMimeType('image/png')
Expand Down Expand Up @@ -161,8 +212,13 @@ export async function writeModels(modelFile, mod) {
}
const primitiveMap = {};
const gltfMesh = document.createMesh(modelFile);
let processedPolygons = 0;

for (const p of mod.geometry.polys) {
processedPolygons++;
if (processedPolygons % 5000 === 0) {
await yieldToBrowser();
}
if (p.material === -1) {
continue;
}
Expand Down Expand Up @@ -340,9 +396,9 @@ export async function writeModels(modelFile, mod) {

for (const [name, ani] of Object.entries(this.animations)) {
// if (name.startsWith(modelFile)) {
console.log('ani', ani);
animWriter.applyAnimation(ani, name);
animWriter.applyAnimation(ani, name, true);
await yieldToBrowser();
// }
}

Expand All @@ -359,10 +415,27 @@ export async function writeModels(modelFile, mod) {
const dummyJoint = document.createNode('DUMMY_PLACEHOLDER');
skin.addJoint(dummyJoint);
node.addChild(dummyJoint);
const inverseBindMatrices = [];
for (let jointIndex = 0; jointIndex < skeletonNodes.length; jointIndex++) {
const sourceBoneIndex = boneIndices.indexOf(jointIndex);
const worldBindMatrix = boneBindWorldMatrices[sourceBoneIndex];
const inverseBindMatrix = worldBindMatrix
? mat4.invert(mat4.create(), worldBindMatrix)
: null;
inverseBindMatrices.push(...(inverseBindMatrix ?? mat4.create()));
}
inverseBindMatrices.push(...mat4.create());
skin.setInverseBindMatrices(
document
.createAccessor('inverse-bind-matrices')
.setType(Accessor.Type.MAT4)
.setArray(new Float32Array(inverseBindMatrices))
.setBuffer(buffer)
);
} else {
node.setMesh(gltfMesh);
}

const bytes = await io.writeBinary(document);
await writeEQFile("objects", diskFileName, bytes.buffer); // eslint-disable-line
await writeEQFile(destination, diskFileName, bytes.buffer);
}
Loading