arx-level-generator
Version:
A tool for creating Arx Fatalis maps
251 lines • 10.4 kB
JavaScript
import fs from 'node:fs/promises';
import path from 'node:path';
import { ArxPolygonFlags } from 'arx-convert/types';
import { Mesh, MeshBasicMaterial, Vector2 } from 'three';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
import { Material } from '../../Material.js';
import { Texture } from '../../Texture.js';
import { Vector3 } from '../../Vector3.js';
import { applyTransformations, fileExists } from '../../helpers.js';
import { getVertices } from '../mesh/getVertices.js';
import { scaleUV as scaleUVTool } from '../mesh/scaleUV.js';
import { toArxCoordinateSystem } from '../mesh/toArxCoordinateSystem.js';
const isTriangulatedMesh = (rawObj) => {
const rows = rawObj.replace(/\\\n/g, '').split(/\r?\n/);
const isNotTriangulated = rows.some((row) => {
if (!row.startsWith('f ')) {
return false;
}
return row.trim().split(' ').length > 4;
});
return !isNotTriangulated;
};
const reversePolygonWinding = (rawObj) => {
let rows = rawObj.replace(/\\\n/g, '').split(/\r?\n/);
rows = rows.map((row) => {
if (!row.startsWith('f')) {
return row;
}
const [, ...coords] = row.trim().split(' ');
return 'f ' + coords.reverse().join(' ');
});
return rows.join('\n');
};
const removeLineElements = (rawObj) => {
let rows = rawObj.replace(/\\\n/g, '').split(/\r?\n/);
rows = rows.map((row) => {
if (!row.startsWith('l')) {
return row;
}
return '# ' + row;
});
return rows.join('\n');
};
const getMaterialFlags = (texture, materialFlags) => {
const defaultFlags = ArxPolygonFlags.DoubleSided | ArxPolygonFlags.Tiled;
return typeof materialFlags === 'function' ? materialFlags(texture, defaultFlags) : materialFlags ?? defaultFlags;
};
const loadMTL = async (filenameWithoutExtension, { materialFlags, fallbackTexture } = {}) => {
const mtlLoader = new MTLLoader();
const { dir, name: filename } = path.parse(filenameWithoutExtension);
const mtlSrc = path.resolve('assets/' + dir + '/' + filename + '.mtl');
const fallbackMaterial = typeof fallbackTexture === 'undefined'
? Material.fromTexture(Texture.missingTexture, {
flags: getMaterialFlags(Texture.missingTexture, materialFlags),
})
: Material.fromTexture(fallbackTexture, {
flags: getMaterialFlags(fallbackTexture, materialFlags),
});
let materials;
if (await fileExists(mtlSrc)) {
try {
const rawMtl = await fs.readFile(mtlSrc, 'utf-8');
const mtl = mtlLoader.parse(rawMtl, '');
const entriesOfMaterials = Object.entries(mtl.materialsInfo);
const nameMaterialPairs = [];
for (const [name, materialInfo] of entriesOfMaterials) {
let material;
if (typeof materialInfo.map_kd !== 'undefined') {
const textureFromFile = Texture.fromCustomFile({
filename: path.parse(materialInfo.map_kd).base,
sourcePath: [dir, path.parse(materialInfo.map_kd).dir].filter((row) => row !== '').join('/'),
});
const flags = getMaterialFlags(textureFromFile, materialFlags);
material = Material.fromTexture(textureFromFile, { flags });
}
else {
console.info(`[info] loadOBJ: Material "${name}" in "${filename}.mtl" doesn't have a texture, using fallback/default texture`);
material = fallbackMaterial;
}
if (material.flags & ArxPolygonFlags.Transparent) {
material.opacity = 50;
}
const meshMaterial = new MeshBasicMaterial({
name,
map: material,
});
nameMaterialPairs.push([name, meshMaterial]);
}
materials = Object.fromEntries(nameMaterialPairs);
}
catch (e) {
console.error(`[error] loadOBJ: error while parsing ${filename}.mtl file:`, e);
materials = new MeshBasicMaterial({
name: filename,
map: fallbackMaterial,
});
}
}
else {
materials = new MeshBasicMaterial({
name: filename,
map: fallbackMaterial,
});
}
return {
materials,
fallbackMaterial,
};
};
/**
* Loads an obj file and an optional mtl file
*
* @see https://en.wikipedia.org/wiki/Wavefront_.obj_file
*/
export const loadOBJ = async (filenameWithoutExtension, { position, scale, scaleUV, orientation, materialFlags, fallbackTexture, reversedPolygonWinding = false, centralize = false, verticalAlign, } = {}) => {
const { materials, fallbackMaterial } = await loadMTL(filenameWithoutExtension, {
materialFlags,
fallbackTexture,
});
const fallbackMeshMaterial = new MeshBasicMaterial({
name: 'fallback-texture',
map: fallbackMaterial,
});
const objLoader = new OBJLoader();
const { dir, name: filename } = path.parse(filenameWithoutExtension);
const objSrc = path.resolve('assets/' + dir + '/' + filename + '.obj');
let rawObj = await fs.readFile(objSrc, 'utf-8');
if (!isTriangulatedMesh(rawObj)) {
console.warn(`[warning] loadOBJ: ${filename}.obj is not triangulated`);
}
// objLoader can't seem to be able to load line elements
rawObj = removeLineElements(rawObj);
if (reversedPolygonWinding) {
rawObj = reversePolygonWinding(rawObj);
}
let obj = objLoader.parse(rawObj);
// the Y axis in Arx is flipped
obj = toArxCoordinateSystem(obj);
// 1 meter in blender = 1 centimeter in Arx
obj.scale.multiply(new Vector3(100, 100, 100));
applyTransformations(obj);
const meshes = [];
const children = obj.children.filter((child) => {
return child instanceof Mesh;
});
children.forEach((child) => {
let material;
if (Array.isArray(child.material)) {
material = child.material.map(({ name }) => {
return materials instanceof MeshBasicMaterial ? materials : materials[name] ?? fallbackMeshMaterial;
});
}
else {
const name = child.material.name;
material = materials instanceof MeshBasicMaterial ? materials : materials[name] ?? fallbackMeshMaterial;
}
const geometry = child.geometry;
if (scale) {
if (typeof scale === 'number') {
geometry.scale(scale, scale, scale);
}
else {
geometry.scale(scale.x, scale.y, scale.z);
}
}
geometry.computeBoundingBox();
const boundingBox = geometry.boundingBox;
const halfDimensions = boundingBox.max.clone().sub(boundingBox.min).divideScalar(2);
let x = 0;
let y = 0;
let z = 0;
if (centralize === true) {
x = -boundingBox.min.x - halfDimensions.x;
z = -boundingBox.min.z - halfDimensions.z;
if (typeof verticalAlign === 'undefined') {
verticalAlign = 'center';
}
}
switch (verticalAlign) {
case 'bottom':
y = -boundingBox.max.y;
break;
case 'center':
y = -boundingBox.min.y - halfDimensions.y;
break;
case 'top':
y = -boundingBox.min.y;
break;
}
if (x !== 0 || y !== 0 || z !== 0) {
geometry.translate(x, y, z);
geometry.computeBoundingBox();
}
if (orientation) {
geometry.rotateX(orientation.x);
geometry.rotateY(orientation.y);
geometry.rotateZ(orientation.z);
}
if (position) {
geometry.translate(position.x, position.y, position.z);
}
if (scaleUV) {
if (typeof scaleUV === 'number') {
scaleUVTool(new Vector2(scaleUV, scaleUV), geometry);
}
else if (scaleUV instanceof Vector2) {
scaleUVTool(scaleUV, geometry);
}
else {
if (Array.isArray(material)) {
// we have multiple materials
material.forEach((singleMaterial, indexOfMaterial) => {
const rawScale = scaleUV(singleMaterial.map);
const scale = typeof rawScale === 'number' ? new Vector2(rawScale, rawScale) : rawScale;
if (geometry.groups.length === 0) {
// the geometry only has 1 material
scaleUVTool(scale, geometry);
}
else {
// the geometry has groups, we only rescale UVs for vertices which have the same
// materialIndex as our currently selected material
const uv = geometry.getAttribute('uv');
getVertices(geometry).forEach(({ idx, materialIndex }) => {
if (indexOfMaterial === materialIndex) {
const u = uv.getX(idx) * scale.x;
const v = uv.getY(idx) * scale.y;
uv.setXY(idx, u, v);
}
});
}
});
}
else {
const rawScale = scaleUV(material.map);
const scale = typeof rawScale === 'number' ? new Vector2(rawScale, rawScale) : rawScale;
scaleUVTool(scale, geometry);
}
}
}
meshes.push(new Mesh(geometry, material));
});
const materialList = [
fallbackMeshMaterial,
...(materials instanceof MeshBasicMaterial ? [materials] : Object.values(materials)),
]
.map(({ map }) => map)
.filter((texture) => texture !== null);
return { meshes, materials: materialList };
};
//# sourceMappingURL=loadOBJ.js.map