UNPKG

arx-level-generator

Version:
243 lines 11.3 kB
import fs from 'node:fs/promises'; import path from 'node:path'; import { FTL } from 'arx-convert'; import { ArxFaceType } from 'arx-convert/types'; import objectHash from 'object-hash'; import { MathUtils, MeshBasicMaterial, Vector2 } from 'three'; import { Polygons } from './Polygons.js'; import { Texture } from './Texture.js'; import { Vector3 } from './Vector3.js'; import { repeat } from './faux-ramda.js'; import { arrayPadRight, fileExists, roundToNDecimals } from './helpers.js'; import { getCacheStats, hashingAlgorithm, saveHashOf } from './services/cache.js'; import { getNonIndexedVertices } from './tools/mesh/getVertices.js'; /** * a, b and c are normal vectors * @see https://stackoverflow.com/a/35205710/1806628 */ const getFaceNormal = (a, b, c) => { return new Vector3().crossVectors(b.clone().sub(a), c.clone().sub(a)).normalize(); }; export class EntityModel { static targetPath = 'game/graph/obj3d/interactive'; filename; sourcePath; originIdx; mesh; actionPoints; constructor(props) { this.filename = props.filename; this.sourcePath = props.sourcePath ?? './'; this.actionPoints = props.actionPoints ?? []; this.originIdx = 0; } /** * props.originIdx is optional, its default value is 0 * * use `@tools/mesh/getLowestPolygonIdx` to get the index of the lowest point of a mesh */ static fromThreeJsObj(threeJsObj, props) { const model = new EntityModel(props); model.mesh = threeJsObj; model.originIdx = props.originIdx ?? 0; return model; } /** * props.originIdx is optional, its default value is 0 * * use `@tools/mesh/getLowestPolygonIdx` to get the index of the lowest point of a mesh */ static fromPolygons(polygons, props) { const model = new EntityModel(props); model.mesh = polygons; model.originIdx = props.originIdx ?? 0; return model; } clone() { const copy = new EntityModel({ filename: this.filename, sourcePath: this.sourcePath, }); copy.mesh = this.mesh; return copy; } /** * targetName is the folder relative to EntityModel.targetPath without the filename, * for example `items/quest_item/mirror` */ async exportSourceAndTarget(settings, targetName, exportJsonFiles = false, prettify = false) { const files = {}; const { name: entityName } = path.parse(targetName); const binaryTarget = path.resolve(settings.outputDir, EntityModel.targetPath, targetName, `${entityName}.ftl`); const jsonTarget = `${binaryTarget}.json`; if (typeof this.mesh === 'undefined') { const binarySource = path.resolve(settings.assetsDir, this.sourcePath, this.filename); files[binaryTarget] = binarySource; } else { const cachedBinary = await getCacheStats(path.join(EntityModel.targetPath, targetName, `${entityName}.ftl`), settings); const ftlData = this.generateFtl(entityName); const hashOfFtlData = objectHash(ftlData, { algorithm: hashingAlgorithm }); let binaryChanged = false; if (hashOfFtlData !== cachedBinary.hash || !cachedBinary.exists) { const ftl = FTL.save(ftlData); await fs.writeFile(cachedBinary.filename, ftl); await saveHashOf(cachedBinary.filename, hashOfFtlData, settings); binaryChanged = true; } files[binaryTarget] = cachedBinary.filename; if (exportJsonFiles) { const cachedJsonTarget = `${cachedBinary.filename}.json`; const cachedJsonExists = await fileExists(cachedJsonTarget); if (binaryChanged || !cachedJsonExists) { const stringifiedFtl = prettify ? JSON.stringify(ftlData, null, 2) : JSON.stringify(ftlData); await fs.writeFile(cachedJsonTarget, stringifiedFtl); } files[jsonTarget] = cachedJsonTarget; } } return files; } /** * this method assumes that this.mesh is defined */ generateFtl(entityName) { const ftlData = { header: { origin: this.originIdx, name: entityName, }, vertices: [], faces: [], textureContainers: [], groups: [], actions: this.actionPoints, selections: [], }; const vertexPrecision = 5; const mesh = this.mesh; if (mesh instanceof Polygons) { mesh.calculateNormals(); // TODO: rotate +90 degrees on Y axis const vertices = mesh.flatMap((polygon) => polygon.vertices.slice(0, polygon.isQuad() ? 3 : 4)); const origin = vertices[ftlData.header.origin].clone(); ftlData.vertices = mesh.flatMap((polygon) => { const vertices = []; const normals = polygon.normals; vertices.push({ vector: polygon.vertices[0].clone().sub(origin).toArxVector3(), norm: normals[0] }, { vector: polygon.vertices[1].clone().sub(origin).toArxVector3(), norm: normals[1] }, { vector: polygon.vertices[2].clone().sub(origin).toArxVector3(), norm: normals[2] }); if (polygon.isQuad()) { vertices.push({ vector: polygon.vertices[2].clone().sub(origin).toArxVector3(), norm: normals[2] }, { vector: polygon.vertices[1].clone().sub(origin).toArxVector3(), norm: normals[1] }, { vector: polygon.vertices[3].clone().sub(origin).toArxVector3(), norm: normals[3] }); } return vertices; }); ftlData.textureContainers = mesh.getTextureContainers(); let vertexIdxCntr = 0; ftlData.faces = mesh.flatMap((polygon) => { const faces = []; const normals = polygon.normals; const faceNormal = getFaceNormal(normals[0], normals[1], normals[2]); const textureIdx = ftlData.textureContainers.findIndex(({ filename }) => polygon.texture?.equals(filename)); faces.push({ faceType: ArxFaceType.Flat, vertexIdx: [vertexIdxCntr, vertexIdxCntr + 1, vertexIdxCntr + 2], textureIdx, u: [polygon.vertices[0].uv.x, polygon.vertices[1].uv.x, polygon.vertices[2].uv.x], v: [polygon.vertices[0].uv.y, polygon.vertices[1].uv.y, polygon.vertices[2].uv.y], norm: faceNormal.toArxVector3(), }); vertexIdxCntr += 3; if (polygon.isQuad()) { const faceNormal = getFaceNormal(normals[2], normals[1], normals[3]); faces.push({ faceType: ArxFaceType.Flat, vertexIdx: [vertexIdxCntr, vertexIdxCntr + 1, vertexIdxCntr + 2], textureIdx, u: [polygon.vertices[2].uv.x, polygon.vertices[1].uv.x, polygon.vertices[3].uv.x], v: [polygon.vertices[2].uv.y, polygon.vertices[1].uv.y, polygon.vertices[3].uv.y], norm: faceNormal.toArxVector3(), }); vertexIdxCntr += 3; } return faces; }); } else { const { geometry, material } = mesh; geometry.rotateY(MathUtils.degToRad(90)); const normals = geometry.getAttribute('normal'); const uvs = geometry.getAttribute('uv'); const vertices = []; const faceIndexes = []; getNonIndexedVertices(geometry).forEach(({ idx, vector, materialIndex }, i) => { vertices.push({ vector: new Vector3(roundToNDecimals(vertexPrecision, vector.x), roundToNDecimals(vertexPrecision, vector.y), roundToNDecimals(vertexPrecision, vector.z)), norm: new Vector3(normals.getX(idx), normals.getY(idx), normals.getZ(idx)), uv: new Vector2(uvs.getX(idx), uvs.getY(idx)), textureIdx: materialIndex ?? 0, }); if (i % 3 === 0) { faceIndexes.push([-1, -1, i]); } else { faceIndexes[faceIndexes.length - 1][2 - (i % 3)] = i; } }); const origin = vertices[ftlData.header.origin].vector.clone(); ftlData.vertices = vertices.map(({ vector, norm }) => ({ vector: vector.clone().sub(origin).toArxVector3(), norm: norm.toArxVector3(), })); ftlData.faces = faceIndexes.map(([aIdx, bIdx, cIdx]) => { const a = vertices[aIdx]; const b = vertices[bIdx]; const c = vertices[cIdx]; const faceNormal = getFaceNormal(a.norm, b.norm, c.norm); return { faceType: ArxFaceType.Flat, vertexIdx: [aIdx, bIdx, cIdx], textureIdx: a.textureIdx, u: [a.uv.x, b.uv.x, c.uv.x], v: [a.uv.y, b.uv.y, c.uv.y], norm: faceNormal.toArxVector3(), }; }); const numberOfGroups = geometry.groups.length === 0 ? 1 : geometry.groups.length; let textures = []; if (material instanceof MeshBasicMaterial) { if (material.map instanceof Texture) { textures = repeat(material.map, numberOfGroups); } else { console.warn('[warning] EntityModel: Unsupported texture map in material when adding threejs mesh'); } } else if (Array.isArray(material)) { textures = material.map((material) => { if (material instanceof MeshBasicMaterial) { if (material.map instanceof Texture) { return material.map; } else { console.warn('[warning] EntityModel: Unsupported texture map in material when adding threejs mesh'); return undefined; } } else { console.warn('[warning] EntityModel: Unsupported material found when adding threejs mesh'); return undefined; } }); } else if (typeof material !== 'undefined') { console.warn('[warning] EntityModel: Unsupported material found when adding threejs mesh'); } ftlData.textureContainers = arrayPadRight(numberOfGroups, undefined, textures).map((t) => { return { filename: t?.filename ?? '<missing material>', }; }); } return ftlData; } } //# sourceMappingURL=EntityModel.js.map