import { CustomGeometry } from '../../geometry/custom-geometry.js';
import { PointsGeometry } from '../../geometry/points-geometry.js';
import { PolylineGeometry } from '../../geometry/polyline-geometry.js';
import { Object3D } from '../../scene/object3d.js';
import { Vector3 } from '../../math/vector3.js';
/**
* Number of components for position vectors.
*
* @type {number}
*/
const POSITION_COMPONENT_COUNT = 3;
/**
* Number of components for UV vectors.
*
* @type {number}
*/
const UV_COMPONENT_COUNT = 2;
/**
* Number of components for normal vectors.
*
* @type {number}
*/
const NORMAL_COMPONENT_COUNT = 3;
/**
* Number of components for RGB color.
*
* @type {number}
*/
const COLOR_COMPONENT_COUNT = 3;
/**
* Default UV coordinates, when missing in OBJ data.
*
* @type {number[]}
*/
const DEFAULT_UV = [0.0, 0.0];
/**
* Default normal vector used as placeholder.
*
* @type {number[]}
*/
const DEFAULT_NORMAL = [0.0, 0.0, 1.0];
/**
* Value indicating, that an OBJ index is not provided.
*
* @type {number}
*/
const OBJ_INDEX_NOT_PROVIDED = -1;
/**
* Zero value used for numeric comparisons.
*
* @type {number}
*/
const ZERO_VALUE = 0;
/**
* Index used to reference the first element in arrays.
*
* @type {number}
*/
const FIRST_INDEX = 0;
/**
* Index used to reference the second element in arrays.
*
* @type {number}
*/
const SECOND_INDEX = 1;
/**
* Index used to reference the third element in arrays.
*
* @type {number}
*/
const THIRD_INDEX = 2;
/**
* Index positions used in float triplets (X).
*
* @type {number}
*/
const COMPONENT_INDEX_X = 0;
/**
* Index positions used in float triplets (Y).
*
* @type {number}
*/
const COMPONENT_INDEX_Y = 1;
/**
* Index positions used in float triplets (Z).
*
* @type {number}
*/
const COMPONENT_INDEX_Z = 2;
/**
* Separator for unique vertex keys.
*
* @type {string}
*/
const VERTEX_KEY_SEPARATOR = '|';
/**
* Index increment for loops.
*
* @type {number}
*/
const LOOP_INCREMENT = 1;
/**
* Minimum vertex count required for line geometry.
*
* @type {number}
*/
const LINE_MIN_VERTEX_COUNT = 2;
/**
* Geometry entry type for meshes.
*
* @type {string}
*/
const ENTRY_TYPE_MESH = 'mesh';
/**
* Geometry entry type for points.
*
* @type {string}
*/
const ENTRY_TYPE_POINTS = 'points';
/**
* Geometry entry type for lines.
*
* @type {string}
*/
const ENTRY_TYPE_LINE = 'line';
/**
* Error message for invalid WebGL context.
*
* @type {string}
*/
const ERROR_WEBGL_CONTEXT_TYPE = '`ObjGeometryBuilder` expects a `WebGL2RenderingContext`.';
/**
* Error message for invalid parsed data.
*
* @type {string}
*/
const ERROR_PARSED_DATA_TYPE = '`ObjGeometryBuilder.build` expects parsed OBJ data as an object.';
/**
* String literal for typeof checks (object).
*
* @type {string}
*/
const TYPEOF_OBJECT = 'object';
/**
* Type definition for parsed OBJ face vertex.
*
* @typedef {Object} ObjFaceVertex
* @property {number} positionIndex - Position index.
* @property {number} uvIndex - UV index.
* @property {number} normalIndex - Normal index.
*/
/**
* Type definition for parsed OBJ material chunk.
*
* @typedef {Object} ObjMaterialChunk
* @property {string} materialName - Material name.
* @property {number} smoothingGroup - Smoothing group (0 = off).
* @property {ObjFaceVertex[][]} triangles - Triangulated faces.
* @property {number[]} points - Point indices.
* @property {number[][]} lines - Line index arrays.
*/
/**
* Type definition for parsed OBJ group.
*
* @typedef {Object} ObjParsedGroup
* @property {string} name - Group name.
* @property {ObjMaterialChunk[]} materialChunks - Material chunks in this group.
*/
/**
* Type definition for parsed OBJ object.
*
* @typedef {Object} ObjParsedObject
* @property {string} name - Object name.
* @property {ObjParsedGroup[]} groups - Group list.
*/
/**
* Parsed OBJ data.
*
* @typedef {Object} ObjParsedData
* @property {number[]} positions - Flat positions array.
* @property {number[]} uvs - Flat UV array.
* @property {number[]} normals - Flat normal array.
* @property {number[]} colors - Flat vertex color array.
* @property {boolean} hasVertexColors - True, when vertex colors are provided.
* @property {ObjParsedObject[]} objects - Parsed objects.
*/
/**
* Output entry for mesh creation.
*
* @typedef {Object} ObjMeshEntry
* @property {string} entryType - Entry type (mesh/points/line).
* @property {Object3D} parent - Parent node for the mesh.
* @property {CustomGeometry | PointsGeometry | PolylineGeometry} geometry - Geometry instance.
* @property {string} materialName - Material name used by the chunk.
* @property {boolean} usesVertexColors - True, when geometry includes vertex colors.
*/
/**
* Result of building OBJ geometry.
*
* @typedef {Object} ObjGeometryBuildResult
* @property {Object3D} root - Root object containing all nodes.
* @property {ObjMeshEntry[]} entries - Mesh creation entries.
* @property {CustomGeometry[]} geometries - Created geometries.
*/
/**
* Builds geometry buffers from parsed OBJ data.
*/
export class ObjGeometryBuilder {
/**
* WebGL2 rendering context used to create the geometries.
*
* @type {WebGL2RenderingContext}
* @private
*/
#webglContext;
/**
* @param {WebGL2RenderingContext} webglContext - WebGL2 rendering context.
* @throws {TypeError} When `webglContext` is not a `WebGL2RenderingContext`.
*/
constructor(webglContext) {
if (!(webglContext instanceof WebGL2RenderingContext)) {
throw new TypeError(ERROR_WEBGL_CONTEXT_TYPE);
}
this.#webglContext = webglContext;
}
/**
* Builds the geometry for parsed OBJ data.
*
* @param {ObjParsedData} parsedData - Parsed OBJ data.
* @returns {ObjGeometryBuildResult} - Build result, which is containing the root node, mesh creation entries and created geometries.
* @throws {TypeError} When `parsedData` is invalid.
*/
build(parsedData) {
if (parsedData === null || typeof parsedData !== TYPEOF_OBJECT || Array.isArray(parsedData)) {
throw new TypeError(ERROR_PARSED_DATA_TYPE);
}
const root = new Object3D();
const entries = [];
const geometries = [];
for (const objectData of parsedData.objects) {
const objectNode = new Object3D();
root.add(objectNode);
for (const groupData of objectData.groups) {
const groupNode = new Object3D();
objectNode.add(groupNode);
for (const chunk of groupData.materialChunks) {
if (chunk.triangles.length) {
const geometryData = this.#buildGeometryForChunk(chunk, parsedData);
const geometry = new CustomGeometry(this.#webglContext, geometryData);
const usesVertexColors = Boolean(geometryData.colors);
entries.push({
entryType : ENTRY_TYPE_MESH,
parent : groupNode,
geometry,
materialName : chunk.materialName,
usesVertexColors
});
geometries.push(geometry);
}
if (chunk.points.length) {
const geometry = this.#buildPointsGeometry(chunk, parsedData);
entries.push({
entryType : ENTRY_TYPE_POINTS,
parent : groupNode,
geometry,
materialName : chunk.materialName,
usesVertexColors: false
});
geometries.push(geometry);
}
if (chunk.lines.length) {
const lineGeometries = this.#buildLineGeometries(chunk, parsedData);
for (const geometry of lineGeometries) {
entries.push({
entryType : ENTRY_TYPE_LINE,
parent : groupNode,
geometry,
materialName : chunk.materialName,
usesVertexColors: false
});
geometries.push(geometry);
}
}
}
}
}
return {
root,
entries,
geometries
};
}
/**
* Builds the geometry data for a material chunk.
*
* @param {ObjMaterialChunk} chunk - Material chunk.
* @param {ObjParsedData} parsedData - Parsed OBJ data.
* @returns {Object} - Geometry buffers for the chunk.
* @private
*/
#buildGeometryForChunk(chunk, parsedData) {
const positions = [];
const uvs = [];
const normals = [];
const colors = parsedData.hasVertexColors ? [] : null;
const indices = [];
if (chunk.smoothingGroup === ZERO_VALUE) {
this.#appendFlatGeometry(chunk, parsedData, positions, uvs, normals, colors, indices);
} else {
this.#appendSmoothGeometry(chunk, parsedData, positions, uvs, normals, colors, indices);
}
return {
positions : new Float32Array(positions),
indices,
uvs : new Float32Array(uvs),
normals : new Float32Array(normals),
colors : colors ? new Float32Array(colors) : null
};
}
/**
* Builds points geometry for a chunk.
*
* @param {ObjMaterialChunk} chunk - Material chunk.
* @param {ObjParsedData} parsedData - Parsed OBJ data.
* @returns {PointsGeometry} - Points geometry.
* @private
*/
#buildPointsGeometry(chunk, parsedData) {
const positions = ObjGeometryBuilder.#buildVectorPositions(chunk.points, parsedData.positions);
return new PointsGeometry(this.#webglContext, { positions });
}
/**
* Builds line geometries for a chunk.
*
* @param {ObjMaterialChunk} chunk - Material chunk.
* @param {ObjParsedData} parsedData - Parsed OBJ data.
* @returns {PolylineGeometry[]} - Line geometries.
* @private
*/
#buildLineGeometries(chunk, parsedData) {
const result = [];
for (const lineIndices of chunk.lines) {
const positions = ObjGeometryBuilder.#buildVectorPositions(lineIndices, parsedData.positions);
if (positions.length >= LINE_MIN_VERTEX_COUNT) {
result.push(new PolylineGeometry(this.#webglContext, { positions }));
}
}
return result;
}
/**
* Appends the flat-shaded geometry data.
*
* @param {ObjMaterialChunk} chunk - Material chunk.
* @param {ObjParsedData} parsedData - Parsed OBJ data.
* @param {number[]} positionsOut - Output positions.
* @param {number[]} uvsOut - Output UVs.
* @param {number[]} normalsOut - Output normals.
* @param {number[] | null} colorsOut - Output colors.
* @param {number[]} indicesOut - Output indices.
* @returns {void} - Appends the flat-shaded vertex data into the provided output buffers.
* @private
*/
#appendFlatGeometry(chunk, parsedData, positionsOut, uvsOut, normalsOut, colorsOut, indicesOut) {
const positions = parsedData.positions;
const uvs = parsedData.uvs;
const normals = parsedData.normals;
const colors = parsedData.colors;
let vertexIndex = ZERO_VALUE;
for (const triangle of chunk.triangles) {
const faceNormal = this.#computeFaceNormal(triangle, positions);
for (const vertex of triangle) {
const positionIndex = vertex.positionIndex;
const uvIndex = vertex.uvIndex;
const normalIndex = vertex.normalIndex;
ObjGeometryBuilder.#appendPosition(positions, positionIndex, positionsOut);
ObjGeometryBuilder.#appendUv(uvs, uvIndex, uvsOut);
if (normalIndex !== OBJ_INDEX_NOT_PROVIDED) {
ObjGeometryBuilder.#appendNormal(normals, normalIndex, normalsOut);
} else {
normalsOut.push(
faceNormal[COMPONENT_INDEX_X],
faceNormal[COMPONENT_INDEX_Y],
faceNormal[COMPONENT_INDEX_Z]
);
}
if (colorsOut) {
ObjGeometryBuilder.#appendColor(colors, positionIndex, colorsOut);
}
indicesOut.push(vertexIndex);
vertexIndex += LOOP_INCREMENT;
}
}
}
/**
* Appends the smooth-shaded geometry data.
*
* @param {ObjMaterialChunk} chunk - Material chunk.
* @param {ObjParsedData} parsedData - Parsed OBJ data.
* @param {number[]} positionsOut - Output positions.
* @param {number[]} uvsOut - Output UVs.
* @param {number[]} normalsOut - Output normals.
* @param {number[] | null} colorsOut - Output colors.
* @param {number[]} indicesOut - Output indices.
* @returns {void} - Appends the smooth-shaded vertex data into the provided output buffers, reusing the shared vertices, when possible.
* @private
*/
#appendSmoothGeometry(chunk, parsedData, positionsOut, uvsOut, normalsOut, colorsOut, indicesOut) {
const positions = parsedData.positions;
const uvs = parsedData.uvs;
const normals = parsedData.normals;
const colors = parsedData.colors;
const vertexMap = new Map();
const normalAccumulator = this.#buildNormalAccumulator(chunk, positions);
for (const triangle of chunk.triangles) {
for (const vertex of triangle) {
const key = ObjGeometryBuilder.#buildVertexKey(vertex);
if (vertexMap.has(key)) {
indicesOut.push(vertexMap.get(key));
continue;
}
const positionIndex = vertex.positionIndex;
const uvIndex = vertex.uvIndex;
const normalIndex = vertex.normalIndex;
const nextIndex = positionsOut.length / POSITION_COMPONENT_COUNT;
vertexMap.set(key, nextIndex);
ObjGeometryBuilder.#appendPosition(positions, positionIndex, positionsOut);
ObjGeometryBuilder.#appendUv(uvs, uvIndex, uvsOut);
if (normalIndex !== OBJ_INDEX_NOT_PROVIDED) {
ObjGeometryBuilder.#appendNormal(normals, normalIndex, normalsOut);
} else {
const smoothNormal = normalAccumulator.get(key) || DEFAULT_NORMAL;
normalsOut.push(
smoothNormal[COMPONENT_INDEX_X],
smoothNormal[COMPONENT_INDEX_Y],
smoothNormal[COMPONENT_INDEX_Z]
);
}
if (colorsOut) {
ObjGeometryBuilder.#appendColor(colors, positionIndex, colorsOut);
}
indicesOut.push(nextIndex);
}
}
}
/**
* Builds a normal accumulator map for smooth shading.
*
* @param {ObjMaterialChunk} chunk - Material chunk.
* @param {number[]} positions - Source positions.
* @returns {Map<string, number[]>} - Map of the `accumulated normalized` normals, keyed by the unique vertex key.
* @private
*/
#buildNormalAccumulator(chunk, positions) {
const accumulators = new Map();
for (const triangle of chunk.triangles) {
const faceNormal = this.#computeFaceNormal(triangle, positions);
for (const vertex of triangle) {
if (vertex.normalIndex !== OBJ_INDEX_NOT_PROVIDED) {
continue;
}
const key = ObjGeometryBuilder.#buildVertexKey(vertex);
const current = accumulators.get(key) || [ZERO_VALUE, ZERO_VALUE, ZERO_VALUE];
current[COMPONENT_INDEX_X] += faceNormal[COMPONENT_INDEX_X];
current[COMPONENT_INDEX_Y] += faceNormal[COMPONENT_INDEX_Y];
current[COMPONENT_INDEX_Z] += faceNormal[COMPONENT_INDEX_Z];
accumulators.set(key, current);
}
}
for (const [key, normal] of accumulators.entries()) {
const length = Math.hypot(normal[COMPONENT_INDEX_X], normal[COMPONENT_INDEX_Y], normal[COMPONENT_INDEX_Z]);
if (length > ZERO_VALUE) {
normal[COMPONENT_INDEX_X] /= length;
normal[COMPONENT_INDEX_Y] /= length;
normal[COMPONENT_INDEX_Z] /= length;
}
accumulators.set(key, normal);
}
return accumulators;
}
/**
* Computes a face normal for a triangle.
*
* @param {ObjFaceVertex[]} triangle - Triangle vertices.
* @param {number[]} positions - Source positions.
* @returns {number[]} - Normalized face normal as an `[x, y, z]` array.
* @private
*/
#computeFaceNormal(triangle, positions) {
// Resolving the triangle vertices (A, B, C):
const vertexA = triangle[FIRST_INDEX];
const vertexB = triangle[SECOND_INDEX];
const vertexC = triangle[THIRD_INDEX];
// Reading the vertex positions (A, B, C) from the flat positions buffer:
const ax = ObjGeometryBuilder.#getPositionComponent(positions, vertexA.positionIndex, COMPONENT_INDEX_X);
const ay = ObjGeometryBuilder.#getPositionComponent(positions, vertexA.positionIndex, COMPONENT_INDEX_Y);
const az = ObjGeometryBuilder.#getPositionComponent(positions, vertexA.positionIndex, COMPONENT_INDEX_Z);
const bx = ObjGeometryBuilder.#getPositionComponent(positions, vertexB.positionIndex, COMPONENT_INDEX_X);
const by = ObjGeometryBuilder.#getPositionComponent(positions, vertexB.positionIndex, COMPONENT_INDEX_Y);
const bz = ObjGeometryBuilder.#getPositionComponent(positions, vertexB.positionIndex, COMPONENT_INDEX_Z);
const cx = ObjGeometryBuilder.#getPositionComponent(positions, vertexC.positionIndex, COMPONENT_INDEX_X);
const cy = ObjGeometryBuilder.#getPositionComponent(positions, vertexC.positionIndex, COMPONENT_INDEX_Y);
const cz = ObjGeometryBuilder.#getPositionComponent(positions, vertexC.positionIndex, COMPONENT_INDEX_Z);
// Computing the edge vectors (AB and AC):
const abx = bx - ax;
const aby = by - ay;
const abz = bz - az;
const acx = cx - ax;
const acy = cy - ay;
const acz = cz - az;
// Computing the face normal as a cross product (AB * AC):
const nx = (aby * acz) - (abz * acy);
const ny = (abz * acx) - (abx * acz);
const nz = (abx * acy) - (aby * acx);
const length = Math.hypot(nx, ny, nz);
// Normalizing the `normal-vector` (fallbacking to the default normal for degenerate triangles):
if (length > ZERO_VALUE) {
return [nx / length, ny / length, nz / length];
}
return DEFAULT_NORMAL;
}
/**
* Appends a position to the target buffer.
*
* @param {number[]} sourcePositions - Source positions.
* @param {number} index - Position index.
* @param {number[]} target - Target positions buffer.
* @returns {void} - Appends the referenced position triplet to the target buffer.
* @private
*/
static #appendPosition(sourcePositions, index, target) {
const baseIndex = index * POSITION_COMPONENT_COUNT;
target.push(
sourcePositions[baseIndex + COMPONENT_INDEX_X],
sourcePositions[baseIndex + COMPONENT_INDEX_Y],
sourcePositions[baseIndex + COMPONENT_INDEX_Z]
);
}
/**
* Appends a UV to the target buffer.
*
* @param {number[]} sourceUvs - Source UVs.
* @param {number} index - UV-index.
* @param {number[]} target - Target UV-buffer.
* @returns {void} - Appends the referenced UV-pair to the target buffer or the default UV, when missing/invalid.
* @private
*/
static #appendUv(sourceUvs, index, target) {
if (index !== OBJ_INDEX_NOT_PROVIDED && index >= ZERO_VALUE && (index * UV_COMPONENT_COUNT) < sourceUvs.length) {
const baseIndex = index * UV_COMPONENT_COUNT;
target.push(sourceUvs[baseIndex + COMPONENT_INDEX_X], sourceUvs[baseIndex + COMPONENT_INDEX_Y]);
return;
}
target.push(DEFAULT_UV[COMPONENT_INDEX_X], DEFAULT_UV[COMPONENT_INDEX_Y]);
}
/**
* Appends a normal to the target buffer.
*
* @param {number[]} sourceNormals - Source normals.
* @param {number} index - Normal index.
* @param {number[]} target - Target normal buffer.
* @returns {void} - Appends the referenced normal triplet to the target buffer or the default normal, when missing/invalid.
* @private
*/
static #appendNormal(sourceNormals, index, target) {
if (index !== OBJ_INDEX_NOT_PROVIDED && index >= ZERO_VALUE && (index * NORMAL_COMPONENT_COUNT) < sourceNormals.length) {
const baseIndex = index * NORMAL_COMPONENT_COUNT;
target.push(
sourceNormals[baseIndex + COMPONENT_INDEX_X],
sourceNormals[baseIndex + COMPONENT_INDEX_Y],
sourceNormals[baseIndex + COMPONENT_INDEX_Z]
);
return;
}
target.push(
DEFAULT_NORMAL[COMPONENT_INDEX_X],
DEFAULT_NORMAL[COMPONENT_INDEX_Y],
DEFAULT_NORMAL[COMPONENT_INDEX_Z]
);
}
/**
* Appends a color to the target buffer.
*
* @param {number[]} sourceColors - Source colors.
* @param {number} index - Color index.
* @param {number[]} target - Target colors buffer.
* @returns {void} - Appends the referenced RGB-triplet to the target buffer.
* @private
*/
static #appendColor(sourceColors, index, target) {
const baseIndex = index * COLOR_COMPONENT_COUNT;
target.push(
sourceColors[baseIndex + COMPONENT_INDEX_X],
sourceColors[baseIndex + COMPONENT_INDEX_Y],
sourceColors[baseIndex + COMPONENT_INDEX_Z]
);
}
/**
* Builds a unique vertex key from the indices.
*
* @param {ObjFaceVertex} vertex - Face vertex.
* @returns {string} - Unique vertex key built from the OBJ indices.
* @private
*/
static #buildVertexKey(vertex) {
return String(vertex.positionIndex)
+ VERTEX_KEY_SEPARATOR + String(vertex.uvIndex)
+ VERTEX_KEY_SEPARATOR + String(vertex.normalIndex);
}
/**
* Reads the position component from the source array.
*
* @param {number[]} positions - Source positions.
* @param {number} index - Vertex index.
* @param {number} component - Component offset.
* @returns {number} - Requested position component value for the given vertex index.
* @private
*/
static #getPositionComponent(positions, index, component) {
return positions[(index * POSITION_COMPONENT_COUNT) + component];
}
/**
* Builds Vector3 positions from indices.
*
* @param {number[]} indices - Position indices.
* @param {number[]} positions - Flat positions array.
* @returns {Vector3[]} - Vector3 positions.
* @private
*/
static #buildVectorPositions(indices, positions) {
const result = [];
for (const index of indices) {
const baseIndex = index * POSITION_COMPONENT_COUNT;
const x = positions[baseIndex + COMPONENT_INDEX_X];
const y = positions[baseIndex + COMPONENT_INDEX_Y];
const z = positions[baseIndex + COMPONENT_INDEX_Z];
result.push(new Vector3(x, y, z));
}
return result;
}
}