UNPKG

arx-level-generator

Version:
362 lines 16.3 kB
import { ArxPolygonFlags } from 'arx-convert/types'; import { getCellCoords, MAP_DEPTH_IN_CELLS, MAP_WIDTH_IN_CELLS, isTiled, } from 'arx-convert/utils'; import { Mesh, MeshBasicMaterial, Box3 } from 'three'; import { Color } from './Color.js'; import { Material } from './Material.js'; import { Polygon } from './Polygon.js'; import { Texture } from './Texture.js'; import { Vector3 } from './Vector3.js'; import { Vertex } from './Vertex.js'; import { sum, times } from './faux-ramda.js'; import { applyTransformations, averageVectors, evenAndRemainder, roundToNDecimals } from './helpers.js'; import { getNonIndexedVertices } from './tools/mesh/getVertices.js'; export const QUADIFY = 'quadify'; export const DONT_QUADIFY = "don't quadify"; export const SHADING_FLAT = 'flat'; export const SHADING_SMOOTH = 'smooth'; export class Polygons extends Array { cashedBBox = { numberOfPolygons: 0, value: new Box3(), }; async exportTextures(settings) { const texturesToExport = { tileable: {}, nonTileable: {}, }; for (const polygon of this) { if (typeof polygon.texture === 'undefined' || polygon.texture.isNative) { continue; } const needsToBeTileable = isTiled(polygon); if (needsToBeTileable) { texturesToExport.tileable[polygon.texture.filename] = polygon.texture; } else { texturesToExport.nonTileable[polygon.texture.filename] = polygon.texture; } } const files = {}; for (const filename in texturesToExport.tileable) { const texture = texturesToExport.tileable[filename]; const [source, target] = await texture.exportSourceAndTarget(settings, true); files[target] = source; } for (const filename in texturesToExport.nonTileable) { const texture = texturesToExport.nonTileable[filename]; const [source, target] = await texture.exportSourceAndTarget(settings, false); files[target] = source; } return files; } async toArxData() { const textureContainers = this.getTextureContainers(); // watch out, we're mutating textureContainers! const arxPolygons = []; for (const polygon of this) { arxPolygons.push(await polygon.toArxPolygon(textureContainers)); } const arxTextureContainers = textureContainers .filter(({ remaining, maxRemaining }) => remaining !== maxRemaining) .map(({ id, filename }) => { return { id, filename }; }); return { polygons: arxPolygons, textureContainers: arxTextureContainers, }; } countNindices() { const nindices = {}; this.forEach((polygon) => { if (typeof polygon.texture === 'undefined') { return; } if (!(polygon.texture.filename in nindices)) { nindices[polygon.texture.filename] = { additive: 0, blended: 0, multiplicative: 0, opaque: 0, subtractive: 0, }; } nindices[polygon.texture.filename][polygon.getTransparencyType()] += polygon.getNindices(); }); return nindices; } getTextureContainers() { const textureContainers = []; let cntr = 0; const nindices = this.countNindices(); Object.entries(nindices).forEach(([filename, nindices]) => { const maxNindices = sum(Object.values(nindices)); const [wholeBlocks, remaining] = evenAndRemainder(65535, maxNindices); times(() => { textureContainers.push({ id: ++cntr, filename, remaining: 65535, maxRemaining: 65535 }); textureContainers.push({ id: ++cntr, filename, remaining: 65535, maxRemaining: 65535 }); }, wholeBlocks); textureContainers.push({ id: ++cntr, filename, remaining, maxRemaining: remaining }); textureContainers.push({ id: ++cntr, filename, remaining, maxRemaining: remaining }); }); return textureContainers; } empty() { this.length = 0; } calculateNormals() { this.forEach((polygon) => { polygon.calculateNormals(); polygon.normals = [polygon.norm.clone(), polygon.norm.clone(), polygon.norm.clone(), polygon.norm2.clone()]; }); } addThreeJsMesh(threeJsObj, meshImportProps = {}, isRoot = true) { if (isRoot) { applyTransformations(threeJsObj); } const { tryToQuadify = QUADIFY, shading = SHADING_FLAT, flags = ArxPolygonFlags.None, room } = meshImportProps; const polygons = new Polygons(); if (threeJsObj instanceof Mesh) { const uvs = threeJsObj.geometry.getAttribute('uv'); let texture = undefined; if (threeJsObj.material instanceof MeshBasicMaterial) { if (threeJsObj.material.map instanceof Texture) { texture = threeJsObj.material.map; } else { console.warn('[warning] Polygons: Unsupported texture map in material when adding threejs mesh'); } } else if (Array.isArray(threeJsObj.material)) { texture = threeJsObj.material.map((material) => { if (material instanceof MeshBasicMaterial) { if (material.map instanceof Texture) { return material.map; } else { console.warn('[warning] Polygons: Unsupported texture map in material when adding threejs mesh'); return undefined; } } else { console.warn('[warning] Polygons: Unsupported material found when adding threejs mesh'); return undefined; } }); } else if (typeof threeJsObj.material !== 'undefined') { console.warn('[warning] Polygons: Unsupported material found when adding threejs mesh'); } const vertexPrecision = 10; const vertices = getNonIndexedVertices(threeJsObj.geometry).map(({ idx, vector, materialIndex }) => { return { vertex: new Vertex(roundToNDecimals(vertexPrecision, vector.x), roundToNDecimals(vertexPrecision, vector.y), roundToNDecimals(vertexPrecision, vector.z), uvs.getX(idx), uvs.getY(idx), Color.white), materialIndex, }; }); if (tryToQuadify === QUADIFY) { let previousPolygon = undefined; let currentPolygon; for (let i = 0; i < vertices.length; i += 3) { if (typeof previousPolygon === 'undefined') { previousPolygon = vertices.slice(i, i + 3).reverse(); continue; } currentPolygon = vertices.slice(i, i + 3).reverse(); const materialIndex = currentPolygon[0].materialIndex; let isQuadable = false; if (tryToQuadify === QUADIFY) { // TODO: calculate this instead of having it hardcoded isQuadable = true; } const currentTexture = Array.isArray(texture) ? texture[materialIndex ?? 0] : texture; if (currentTexture instanceof Material && currentTexture.opacity === 100) { // remove opacity currentTexture.flags & ~ArxPolygonFlags.Transparent; } if (isQuadable) { const [a, b, c] = previousPolygon; const d = currentPolygon[1]; const polygon = new Polygon({ vertices: [a, d, c, b].map(({ vertex }) => vertex), texture: currentTexture, flags: currentTexture instanceof Material ? currentTexture.flags | flags : flags, isQuad: true, room, }); if (currentTexture instanceof Material && currentTexture.opacity !== 100) { polygon.setOpacity(currentTexture.opacity, currentTexture.opacityMode); } polygons.push(polygon); previousPolygon = undefined; continue; } const polygon = new Polygon({ vertices: [...previousPolygon.map(({ vertex }) => vertex), new Vertex(0, 0, 0)], texture: currentTexture, flags: currentTexture instanceof Material ? currentTexture.flags | flags : flags, room, }); if (currentTexture instanceof Material && currentTexture.opacity !== 100) { polygon.setOpacity(currentTexture.opacity, currentTexture.opacityMode); } polygons.push(polygon); previousPolygon = currentPolygon; } if (typeof previousPolygon !== 'undefined') { const materialIndex = previousPolygon[0].materialIndex; const currentTexture = Array.isArray(texture) ? texture[materialIndex ?? 0] : texture; if (currentTexture instanceof Material && currentTexture.opacity === 100) { // remove opacity currentTexture.flags & ~ArxPolygonFlags.Transparent; } const polygon = new Polygon({ vertices: [...previousPolygon.map(({ vertex }) => vertex), new Vertex(0, 0, 0)], texture: currentTexture, flags: currentTexture instanceof Material ? currentTexture.flags | flags : flags, room, }); if (currentTexture instanceof Material && currentTexture.opacity !== 100) { polygon.setOpacity(currentTexture.opacity, currentTexture.opacityMode); } polygons.push(polygon); } } else { for (let i = 0; i < vertices.length; i += 3) { const currentPolygon = vertices.slice(i, i + 3).reverse(); const materialIndex = currentPolygon[0].materialIndex; const currentTexture = Array.isArray(texture) ? texture[materialIndex ?? 0] : texture; if (currentTexture instanceof Material && currentTexture.opacity === 100) { // remove opacity currentTexture.flags & ~ArxPolygonFlags.Transparent; } const polygon = new Polygon({ vertices: [...currentPolygon.map(({ vertex }) => vertex), new Vertex(0, 0, 0)], texture: currentTexture, flags: (currentTexture instanceof Material ? currentTexture.flags | flags : flags) & ~ArxPolygonFlags.Quad, room, }); if (currentTexture instanceof Material && currentTexture.opacity !== 100) { polygon.setOpacity(currentTexture.opacity, currentTexture.opacityMode); } polygons.push(polygon); } } } threeJsObj.children.forEach((child) => { polygons.push(...this.addThreeJsMesh(child, meshImportProps, false)); }); if (!isRoot) { return polygons; } if (shading === SHADING_SMOOTH) { const polygonsOfVertices = {}; // TODO: calculate smooth normals for quads too polygons.calculateNormals(); polygons.forEach((polygon) => { const [a, b, c] = polygon.vertices; if (Array.isArray(polygonsOfVertices[a.toString()])) { polygonsOfVertices[a.toString()].push([0, polygon]); } else { polygonsOfVertices[a.toString()] = [[0, polygon]]; } if (Array.isArray(polygonsOfVertices[b.toString()])) { polygonsOfVertices[b.toString()].push([1, polygon]); } else { polygonsOfVertices[b.toString()] = [[1, polygon]]; } if (Array.isArray(polygonsOfVertices[c.toString()])) { polygonsOfVertices[c.toString()].push([2, polygon]); } else { polygonsOfVertices[c.toString()] = [[2, polygon]]; } }); Object.values(polygonsOfVertices).forEach((polygons) => { if (polygons.length === 1) { return; } const normals = polygons.reduce((normals, [vertexIndex, polygon]) => { normals.push(polygon.normals[vertexIndex]); return normals; }, []); const normal = averageVectors(normals); polygons.forEach(([vertexIndex, polygon]) => { ; polygon.normals[vertexIndex] = normal.clone(); }); }); } polygons.forEach((polygon) => { this.push(polygon); }); } getVertexColors() { const cells = {}; this.forEach((polygon, idx) => { const vertices = polygon.vertices.map((vertex) => vertex.toArxVertex()); const [cellX, cellZ] = getCellCoords(vertices); const key = `${cellZ}|${cellX}`; if (key in cells) { cells[key].push(idx); } else { cells[key] = [idx]; } }); const colors = []; for (let z = 0; z < MAP_DEPTH_IN_CELLS; z++) { for (let x = 0; x < MAP_WIDTH_IN_CELLS; x++) { const cell = cells[`${z}|${x}`]; if (typeof cell === 'undefined') { continue; } cell.forEach((idx) => { const polygon = this[idx]; for (let i = 0; i < (polygon.isQuad() ? 4 : 3); i++) { const color = polygon.vertices[i]?.color ?? Color.transparent; colors.push(color.toArxColor()); } }); } } return colors; } getBoundingBox() { // TODO: this isn't ideal when only a vertex gets changed, but not the number of polygons if (this.cashedBBox.numberOfPolygons === this.length) { return this.cashedBBox.value; } const bbox = new Box3(); for (const polygon of this) { const { min, max } = polygon.getBoundingBox(); bbox.expandByPoint(min); bbox.expandByPoint(max); } this.cashedBBox.numberOfPolygons = this.length; this.cashedBBox.value = bbox; return bbox; } getCenter() { const bb = this.getBoundingBox(); const center = new Vector3(); bb.getCenter(center); return center; } getHeight() { const { max, min } = this.getBoundingBox(); return max.y - min.y; } getWidth() { const { max, min } = this.getBoundingBox(); return max.x - min.x; } getDepth() { const { max, min } = this.getBoundingBox(); return max.z - min.z; } } //# sourceMappingURL=Polygons.js.map