UNPKG

arx-level-generator

Version:
329 lines 11.7 kB
import { ArxPolygonFlags } from 'arx-convert/types'; import { MAP_DEPTH_IN_CELLS, MAP_WIDTH_IN_CELLS, isQuad } from 'arx-convert/utils'; import { Box3, Triangle } from 'three'; import { NO_TEXTURE_CONTAINER, Texture } from './Texture.js'; import { Vector3 } from './Vector3.js'; import { Vertex } from './Vertex.js'; import { isBetween, percentOf } from './helpers.js'; const DEFAULT_ROOM_ID = 1; export class Polygon { vertices; /** face normal for the 1st half of the polygon enclosed by vertices a, b and c */ norm; /** face normal for the 2nd half of the polygon enclosed by vertices d, b and c when polygon is a quad */ norm2; texture; flags; /** vertex normals */ normals; transval; area; room; paddy; config = { /** setting this to true will prevent calculation of norm, norm2 and normals properties */ areNormalsCalculated: false, }; constructor(props) { this.vertices = props.vertices; this.norm = props.norm ?? new Vector3(0, 0, 0); this.norm2 = props.norm2 ?? new Vector3(0, 0, 0); this.texture = props.texture; this.flags = props.flags ?? ArxPolygonFlags.None; if (props.isQuad === true) { this.flags = this.flags | ArxPolygonFlags.Quad; } this.normals = props.normals; this.transval = props.transval ?? 0; this.area = props.area ?? 0; this.room = props.room ?? DEFAULT_ROOM_ID; this.paddy = props.paddy; this.config = { ...this.config, ...(props.config ?? {}) }; } clone() { return new Polygon({ vertices: this.vertices.map((v) => v.clone()), norm: this.norm.clone(), norm2: this.norm2.clone(), texture: this.texture, flags: this.flags, normals: this.normals?.map((n) => n.clone()), transval: this.transval, area: this.area, room: this.room, paddy: this.paddy, config: { areNormalsCalculated: this.config.areNormalsCalculated, }, }); } static fromArxPolygon(polygon, colors, textures, areNormalsCalculated) { const extendedVertices = polygon.vertices.map(({ llfColorIdx, ...vertex }) => { const extendedVertex = vertex; if (typeof llfColorIdx === 'number') { extendedVertex.color = colors[llfColorIdx]; } return Vertex.fromArxVertex(extendedVertex); }); let texture = undefined; const textureContainer = textures.find(({ id }) => id === polygon.textureContainerId); if (textureContainer) { texture = Texture.fromArxTextureContainer(textureContainer); } let normals = undefined; if (polygon.normals) { normals = polygon.normals.map(Vector3.fromArxVector3); } return new Polygon({ vertices: extendedVertices, norm: Vector3.fromArxVector3(polygon.norm), norm2: Vector3.fromArxVector3(polygon.norm2), texture, flags: polygon.flags, normals, transval: polygon.transval, area: polygon.area, room: polygon.room, paddy: polygon.paddy, config: { areNormalsCalculated, }, }); } hasTexture() { return typeof this.texture !== 'undefined'; } async toArxPolygon(textureContainers) { const vertices = this.vertices.map((vertex) => vertex.toArxVertex()); let textureContainerId = NO_TEXTURE_CONTAINER; if (this.hasTexture()) { const textureFilename = this.texture.filename; const nindices = this.getNindices(); const textureContainer = textureContainers.find(({ filename, remaining }) => { return remaining - nindices >= 0 && filename === textureFilename; }); if (typeof textureContainer !== 'undefined') { textureContainer.remaining -= nindices; textureContainerId = textureContainer.id; } } let normals = undefined; if (this.normals) { normals = this.normals.map((normal) => normal.toArxVector3()); } return { vertices, norm: this.norm.toArxVector3(), norm2: this.norm2.toArxVector3(), textureContainerId, flags: this.flags, normals, transval: this.transval, area: this.area, room: this.room, paddy: this.paddy, }; } isQuad() { return isQuad(this); } isTransparent() { return (this.flags & ArxPolygonFlags.Transparent) !== 0; } calculateNormals() { if (this.config.areNormalsCalculated === true) { return; } const [a, b, c, d] = this.vertices; const triangle = new Triangle(a, b, c); triangle.getNormal(this.norm); if (this.isQuad()) { const triangle2 = new Triangle(d, c, b); triangle2.getNormal(this.norm2); } } /** * @see https://github.com/arx/ArxLibertatis/blob/1.2.1/src/graphics/data/Mesh.cpp#L1100 */ getNindices() { return this.isQuad() ? 6 : 3; } /** * @see https://github.com/arx/ArxLibertatis/blob/1.2.1/src/graphics/data/Mesh.cpp#L1102 */ getTransparencyType() { if (!this.isTransparent()) { return 'opaque'; } if (this.transval >= 2) { return 'multiplicative'; } if (this.transval >= 1) { return 'additive'; } if (this.transval > 0) { return 'blended'; } return 'subtractive'; } setOpacity(percent, transparencyType = 'additive') { if (percent === 100) { this.flags &= ~ArxPolygonFlags.Transparent; return; } this.flags |= ArxPolygonFlags.Transparent; const value = percentOf(percent, 1); switch (transparencyType) { case 'additive': this.transval = 1 + value; break; case 'blended': this.transval = value; break; case 'multiplicative': this.transval = 2 + value; break; case 'subtractive': this.transval = -value; break; } } /** * Quote from Dscharrer: * * Looks like this is the "correct" formula for trinagles: * @see https://github.com/arx/ArxLibertatis/blob/ArxFatalis-1.21/Sources/EERIE/EERIEPoly.cpp#L3134 * * And for quads: * @see https://github.com/arx/ArxLibertatis/blob/ArxFatalis-1.21/Sources/EERIE/EERIEDraw.cpp#L267 * * At least the triangle formula looks like it was supposed to be the actual area but it only works for * specific kinds of triangles. The quad "area" is probably annoying or impossible to replicate as it will * depend on the order of the vertices in the triangle before they were reordered when merging into the quad. * * AFAICT the area value is only used for collisions to to do additional checks for larger polygons. * I don't think the exact value matters in practice. */ calculateArea() { this.area = this.getHalfPolygonArea(false) + (this.isQuad() ? this.getHalfPolygonArea(true) : 0); } /** * assuming the order of vertices taking up a russian i (И) shape: * ``` * 0 2 * 1 3 * ``` * `isQuadPart` === false -> calculate the area of 0-1-2 * `isQuadPart` === true -> calculate the area of 1-2-3 */ getHalfPolygonArea(isQuadPart) { const triangle = new Triangle(...this.vertices.slice(isQuadPart ? 1 : 0, 3)); return triangle.getArea(); // const [i, j, k] = isQuadPart ? [1, 2, 3] : [0, 1, 2] // const a = this.vertices[i].clone().add(this.vertices[j]).divideScalar(2).distanceTo(this.vertices[k]) // const b = this.vertices[isQuadPart ? i : k].distanceTo(this.vertices[j]) // return (a * b) / 2 } /** * All the vertices are inside or on the surface of the box * If exludeOnSurface (default true) is true, then we ignore checking the surface by shrinking * the box by 1 on each side */ isWithin(box, excludeOnSurface = true) { const copyOfBox = box.clone(); if (excludeOnSurface) { copyOfBox.min.add(new Vector3(1, 1, 1)); copyOfBox.max.sub(new Vector3(1, 1, 1)); } const [a, b, c, d] = this.vertices; if (!copyOfBox.containsPoint(a) || !copyOfBox.containsPoint(b) || !copyOfBox.containsPoint(c)) { return false; } if (this.isQuad() && !copyOfBox.containsPoint(d)) { return false; } return true; } /** * At least one of the vertices are inside or on the surface of the box * If `exludeOnSurface` (default true) is true, then we ignore checking the surface by shrinking * the box by a specific value on each side */ isPartiallyWithin(box, excludeOnSurface = true) { const padding = 0.1; const copyOfBox = box.clone(); if (excludeOnSurface) { copyOfBox.min.add(new Vector3(padding, padding, padding)); copyOfBox.max.sub(new Vector3(padding, padding, padding)); } const [a, b, c, d] = this.vertices; if (copyOfBox.containsPoint(a) || copyOfBox.containsPoint(b) || copyOfBox.containsPoint(c)) { return true; } if (this.isQuad() && copyOfBox.containsPoint(d)) { return true; } return false; } setColor(color) { this.vertices.forEach((vertex) => { vertex.color = color; }); } move(offset) { this.vertices.forEach((vertex) => { vertex.add(offset); }); } scale(scale) { this.vertices.forEach((vertex) => { vertex.multiplyScalar(scale); }); } equals(polygon, epsilon = 0) { if (this.isQuad() !== polygon.isQuad()) { return false; } for (let i = 0; i < (this.isQuad() ? 4 : 3); i++) { const aVertex = this.vertices[i]; if (!polygon.vertices.some((bVertex) => aVertex.equals(bVertex, epsilon))) { return false; } } return true; } isOutOfBounds() { const outOfBoundVertex = this.vertices .slice(0, this.isQuad() ? 4 : 3) .find((vertex) => !isBetween(0, MAP_WIDTH_IN_CELLS * 100 - 1, vertex.x) || !isBetween(0, MAP_DEPTH_IN_CELLS * 100 - 1, vertex.z)); return outOfBoundVertex !== undefined; } makeDoubleSided() { this.flags = this.flags | ArxPolygonFlags.DoubleSided; } flipUVHorizontally() { this.vertices[0].uv.x *= -1; this.vertices[1].uv.x *= -1; this.vertices[2].uv.x *= -1; if (this.isQuad()) { this.vertices[3].uv.x *= -1; } } flipUVVertically() { this.vertices[0].uv.y *= -1; this.vertices[1].uv.y *= -1; this.vertices[2].uv.y *= -1; if (this.isQuad()) { this.vertices[3].uv.y *= -1; } } getBoundingBox() { const box = new Box3(); for (let i = 0; i < (this.isQuad() ? 4 : 3); i++) { box.expandByPoint(this.vertices[i]); } return box; } } //# sourceMappingURL=Polygon.js.map