@loaders.gl/obj
Version:
Framework-independent loader for the OBJ format
721 lines (709 loc) • 23.5 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if (typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd) define([], factory);
else if (typeof exports === 'object') exports['loaders'] = factory();
else root['loaders'] = factory();})(globalThis, function () {
"use strict";
var __exports__ = (() => {
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// external-global-plugin:@loaders.gl/core
var require_core = __commonJS({
"external-global-plugin:@loaders.gl/core"(exports, module) {
module.exports = globalThis.loaders;
}
});
// bundle.ts
var bundle_exports = {};
__export(bundle_exports, {
MTLLoader: () => MTLLoader2,
OBJLoader: () => OBJLoader2,
OBJWorkerLoader: () => OBJLoader
});
__reExport(bundle_exports, __toESM(require_core(), 1));
// ../schema/src/lib/table/simple-table/data-type.ts
function getDataTypeFromValue(value, defaultNumberType = "float32") {
if (value instanceof Date) {
return "date-millisecond";
}
if (value instanceof Number) {
return defaultNumberType;
}
if (typeof value === "string") {
return "utf8";
}
if (value === null || value === "undefined") {
return "null";
}
return "null";
}
function getDataTypeFromArray(array) {
let type = getDataTypeFromTypedArray(array);
if (type !== "null") {
return { type, nullable: false };
}
if (array.length > 0) {
type = getDataTypeFromValue(array[0]);
return { type, nullable: true };
}
return { type: "null", nullable: true };
}
function getDataTypeFromTypedArray(array) {
switch (array.constructor) {
case Int8Array:
return "int8";
case Uint8Array:
case Uint8ClampedArray:
return "uint8";
case Int16Array:
return "int16";
case Uint16Array:
return "uint16";
case Int32Array:
return "int32";
case Uint32Array:
return "uint32";
case Float32Array:
return "float32";
case Float64Array:
return "float64";
default:
return "null";
}
}
// ../schema/src/lib/mesh/mesh-utils.ts
function getMeshBoundingBox(attributes) {
let minX = Infinity;
let minY = Infinity;
let minZ = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
let maxZ = -Infinity;
const positions = attributes.POSITION ? attributes.POSITION.value : [];
const len = positions && positions.length;
for (let i = 0; i < len; i += 3) {
const x = positions[i];
const y = positions[i + 1];
const z = positions[i + 2];
minX = x < minX ? x : minX;
minY = y < minY ? y : minY;
minZ = z < minZ ? z : minZ;
maxX = x > maxX ? x : maxX;
maxY = y > maxY ? y : maxY;
maxZ = z > maxZ ? z : maxZ;
}
return [
[minX, minY, minZ],
[maxX, maxY, maxZ]
];
}
// src/lib/parse-obj-meshes.ts
var OBJECT_RE = /^[og]\s*(.+)?/;
var MATERIAL_RE = /^mtllib /;
var MATERIAL_USE_RE = /^usemtl /;
var MeshMaterial = class {
constructor({ index, name = "", mtllib, smooth, groupStart }) {
this.index = index;
this.name = name;
this.mtllib = mtllib;
this.smooth = smooth;
this.groupStart = groupStart;
this.groupEnd = -1;
this.groupCount = -1;
this.inherited = false;
}
clone(index = this.index) {
return new MeshMaterial({
index,
name: this.name,
mtllib: this.mtllib,
smooth: this.smooth,
groupStart: 0
});
}
};
var MeshObject = class {
constructor(name = "") {
this.name = name;
this.geometry = {
vertices: [],
normals: [],
colors: [],
uvs: []
};
this.materials = [];
this.smooth = true;
this.fromDeclaration = null;
}
startMaterial(name, libraries) {
const previous = this._finalize(false);
if (previous && (previous.inherited || previous.groupCount <= 0)) {
this.materials.splice(previous.index, 1);
}
const material = new MeshMaterial({
index: this.materials.length,
name,
mtllib: Array.isArray(libraries) && libraries.length > 0 ? libraries[libraries.length - 1] : "",
smooth: previous !== void 0 ? previous.smooth : this.smooth,
groupStart: previous !== void 0 ? previous.groupEnd : 0
});
this.materials.push(material);
return material;
}
currentMaterial() {
if (this.materials.length > 0) {
return this.materials[this.materials.length - 1];
}
return void 0;
}
_finalize(end) {
const lastMultiMaterial = this.currentMaterial();
if (lastMultiMaterial && lastMultiMaterial.groupEnd === -1) {
lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3;
lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart;
lastMultiMaterial.inherited = false;
}
if (end && this.materials.length > 1) {
for (let mi = this.materials.length - 1; mi >= 0; mi--) {
if (this.materials[mi].groupCount <= 0) {
this.materials.splice(mi, 1);
}
}
}
if (end && this.materials.length === 0) {
this.materials.push({
name: "",
smooth: this.smooth
});
}
return lastMultiMaterial;
}
};
var ParserState = class {
constructor() {
this.objects = [];
this.object = null;
this.vertices = [];
this.normals = [];
this.colors = [];
this.uvs = [];
this.materialLibraries = [];
this.startObject("", false);
}
startObject(name, fromDeclaration = true) {
if (this.object && !this.object.fromDeclaration) {
this.object.name = name;
this.object.fromDeclaration = fromDeclaration;
return;
}
const previousMaterial = this.object && typeof this.object.currentMaterial === "function" ? this.object.currentMaterial() : void 0;
if (this.object && typeof this.object._finalize === "function") {
this.object._finalize(true);
}
this.object = new MeshObject(name);
this.object.fromDeclaration = fromDeclaration;
if (previousMaterial && previousMaterial.name && typeof previousMaterial.clone === "function") {
const declared = previousMaterial.clone(0);
declared.inherited = true;
this.object.materials.push(declared);
}
this.objects.push(this.object);
}
finalize() {
if (this.object && typeof this.object._finalize === "function") {
this.object._finalize(true);
}
}
parseVertexIndex(value, len) {
const index = parseInt(value);
return (index >= 0 ? index - 1 : index + len / 3) * 3;
}
parseNormalIndex(value, len) {
const index = parseInt(value);
return (index >= 0 ? index - 1 : index + len / 3) * 3;
}
parseUVIndex(value, len) {
const index = parseInt(value);
return (index >= 0 ? index - 1 : index + len / 2) * 2;
}
addVertex(a, b, c) {
const src = this.vertices;
const dst = this.object.geometry.vertices;
dst.push(src[a + 0], src[a + 1], src[a + 2]);
dst.push(src[b + 0], src[b + 1], src[b + 2]);
dst.push(src[c + 0], src[c + 1], src[c + 2]);
}
addVertexPoint(a) {
const src = this.vertices;
const dst = this.object.geometry.vertices;
dst.push(src[a + 0], src[a + 1], src[a + 2]);
}
addVertexLine(a) {
const src = this.vertices;
const dst = this.object.geometry.vertices;
dst.push(src[a + 0], src[a + 1], src[a + 2]);
}
addNormal(a, b, c) {
const src = this.normals;
const dst = this.object.geometry.normals;
dst.push(src[a + 0], src[a + 1], src[a + 2]);
dst.push(src[b + 0], src[b + 1], src[b + 2]);
dst.push(src[c + 0], src[c + 1], src[c + 2]);
}
addColor(a, b, c) {
const src = this.colors;
const dst = this.object.geometry.colors;
dst.push(src[a + 0], src[a + 1], src[a + 2]);
dst.push(src[b + 0], src[b + 1], src[b + 2]);
dst.push(src[c + 0], src[c + 1], src[c + 2]);
}
addUV(a, b, c) {
const src = this.uvs;
const dst = this.object.geometry.uvs;
dst.push(src[a + 0], src[a + 1]);
dst.push(src[b + 0], src[b + 1]);
dst.push(src[c + 0], src[c + 1]);
}
addUVLine(a) {
const src = this.uvs;
const dst = this.object.geometry.uvs;
dst.push(src[a + 0], src[a + 1]);
}
// eslint-disable-next-line max-params
addFace(a, b, c, ua, ub, uc, na, nb, nc) {
const vLen = this.vertices.length;
let ia = this.parseVertexIndex(a, vLen);
let ib = this.parseVertexIndex(b, vLen);
let ic = this.parseVertexIndex(c, vLen);
this.addVertex(ia, ib, ic);
if (ua !== void 0 && ua !== "") {
const uvLen = this.uvs.length;
ia = this.parseUVIndex(ua, uvLen);
ib = this.parseUVIndex(ub, uvLen);
ic = this.parseUVIndex(uc, uvLen);
this.addUV(ia, ib, ic);
}
if (na !== void 0 && na !== "") {
const nLen = this.normals.length;
ia = this.parseNormalIndex(na, nLen);
ib = na === nb ? ia : this.parseNormalIndex(nb, nLen);
ic = na === nc ? ia : this.parseNormalIndex(nc, nLen);
this.addNormal(ia, ib, ic);
}
if (this.colors.length > 0) {
this.addColor(ia, ib, ic);
}
}
addPointGeometry(vertices) {
this.object.geometry.type = "Points";
const vLen = this.vertices.length;
for (const vertex of vertices) {
this.addVertexPoint(this.parseVertexIndex(vertex, vLen));
}
}
addLineGeometry(vertices, uvs) {
this.object.geometry.type = "Line";
const vLen = this.vertices.length;
const uvLen = this.uvs.length;
for (const vertex of vertices) {
this.addVertexLine(this.parseVertexIndex(vertex, vLen));
}
for (const uv of uvs) {
this.addUVLine(this.parseUVIndex(uv, uvLen));
}
}
};
function parseOBJMeshes(text) {
const state = new ParserState();
if (text.indexOf("\r\n") !== -1) {
text = text.replace(/\r\n/g, "\n");
}
if (text.indexOf("\\\n") !== -1) {
text = text.replace(/\\\n/g, "");
}
const lines = text.split("\n");
let line = "";
let lineFirstChar = "";
let lineLength = 0;
let result = [];
const trimLeft = typeof "".trimLeft === "function";
for (let i = 0, l = lines.length; i < l; i++) {
line = lines[i];
line = trimLeft ? line.trimLeft() : line.trim();
lineLength = line.length;
if (lineLength === 0)
continue;
lineFirstChar = line.charAt(0);
if (lineFirstChar === "#")
continue;
if (lineFirstChar === "v") {
const data = line.split(/\s+/);
switch (data[0]) {
case "v":
state.vertices.push(parseFloat(data[1]), parseFloat(data[2]), parseFloat(data[3]));
if (data.length >= 7) {
state.colors.push(parseFloat(data[4]), parseFloat(data[5]), parseFloat(data[6]));
}
break;
case "vn":
state.normals.push(parseFloat(data[1]), parseFloat(data[2]), parseFloat(data[3]));
break;
case "vt":
state.uvs.push(parseFloat(data[1]), parseFloat(data[2]));
break;
default:
}
} else if (lineFirstChar === "f") {
const lineData = line.substr(1).trim();
const vertexData = lineData.split(/\s+/);
const faceVertices = [];
for (let j = 0, jl = vertexData.length; j < jl; j++) {
const vertex = vertexData[j];
if (vertex.length > 0) {
const vertexParts = vertex.split("/");
faceVertices.push(vertexParts);
}
}
const v1 = faceVertices[0];
for (let j = 1, jl = faceVertices.length - 1; j < jl; j++) {
const v2 = faceVertices[j];
const v3 = faceVertices[j + 1];
state.addFace(v1[0], v2[0], v3[0], v1[1], v2[1], v3[1], v1[2], v2[2], v3[2]);
}
} else if (lineFirstChar === "l") {
const lineParts = line.substring(1).trim().split(" ");
let lineVertices;
const lineUVs = [];
if (line.indexOf("/") === -1) {
lineVertices = lineParts;
} else {
lineVertices = [];
for (let li = 0, llen = lineParts.length; li < llen; li++) {
const parts = lineParts[li].split("/");
if (parts[0] !== "")
lineVertices.push(parts[0]);
if (parts[1] !== "")
lineUVs.push(parts[1]);
}
}
state.addLineGeometry(lineVertices, lineUVs);
} else if (lineFirstChar === "p") {
const lineData = line.substr(1).trim();
const pointData = lineData.split(" ");
state.addPointGeometry(pointData);
} else if ((result = OBJECT_RE.exec(line)) !== null) {
const name = (" " + result[0].substr(1).trim()).substr(1);
state.startObject(name);
} else if (MATERIAL_USE_RE.test(line)) {
state.object.startMaterial(line.substring(7).trim(), state.materialLibraries);
} else if (MATERIAL_RE.test(line)) {
state.materialLibraries.push(line.substring(7).trim());
} else if (lineFirstChar === "s") {
result = line.split(" ");
if (result.length > 1) {
const value = result[1].trim().toLowerCase();
state.object.smooth = value !== "0" && value !== "off";
} else {
state.object.smooth = true;
}
const material = state.object.currentMaterial();
if (material)
material.smooth = state.object.smooth;
} else {
if (line === "\0")
continue;
throw new Error(`Unexpected line: "${line}"`);
}
}
state.finalize();
const meshes = [];
const materials = [];
for (const object of state.objects) {
const { geometry } = object;
if (geometry.vertices.length === 0)
continue;
const mesh = {
header: {
vertexCount: geometry.vertices.length / 3
},
attributes: {}
};
switch (geometry.type) {
case "Points":
mesh.mode = 0;
break;
case "Line":
mesh.mode = 1;
break;
default:
mesh.mode = 4;
break;
}
mesh.attributes.POSITION = { value: new Float32Array(geometry.vertices), size: 3 };
if (geometry.normals.length > 0) {
mesh.attributes.NORMAL = { value: new Float32Array(geometry.normals), size: 3 };
}
if (geometry.colors.length > 0) {
mesh.attributes.COLOR_0 = { value: new Float32Array(geometry.colors), size: 3 };
}
if (geometry.uvs.length > 0) {
mesh.attributes.TEXCOORD_0 = { value: new Float32Array(geometry.uvs), size: 2 };
}
mesh.materials = [];
for (const sourceMaterial of object.materials) {
const _material = {
name: sourceMaterial.name,
flatShading: !sourceMaterial.smooth
};
mesh.materials.push(_material);
materials.push(_material);
}
mesh.name = object.name;
meshes.push(mesh);
}
return { meshes, materials };
}
// src/lib/get-obj-schema.ts
function getOBJSchema(attributes, metadata = {}) {
const stringMetadata = {};
for (const key in metadata) {
if (key !== "value") {
stringMetadata[key] = JSON.stringify(metadata[key]);
}
}
const fields = [];
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
const field = getFieldFromAttribute(attributeName, attribute);
fields.push(field);
}
return { fields, metadata: stringMetadata };
}
function getFieldFromAttribute(name, attribute) {
const metadata = {};
for (const key in attribute) {
if (key !== "value") {
metadata[key] = JSON.stringify(attribute[key]);
}
}
let { type } = getDataTypeFromArray(attribute.value);
const isSingleValue = attribute.size === 1 || attribute.size === void 0;
if (!isSingleValue) {
type = { type: "fixed-size-list", listSize: attribute.size, children: [{ name: "values", type }] };
}
return { name, type, nullable: false, metadata };
}
// src/lib/parse-obj.ts
function parseOBJ(text, options) {
const { meshes } = parseOBJMeshes(text);
const vertexCount = meshes.reduce((s, mesh) => s + mesh.header.vertexCount, 0);
const attributes = mergeAttributes(meshes, vertexCount);
const header = {
vertexCount,
// @ts-ignore Need to export Attributes type
boundingBox: getMeshBoundingBox(attributes)
};
const schema = getOBJSchema(attributes, {
mode: 4,
boundingBox: header.boundingBox
});
return {
// Data return by this loader implementation
loaderData: {
header: {}
},
// Normalised data
schema,
header,
mode: 4,
// TRIANGLES
topology: "point-list",
attributes
};
}
function mergeAttributes(meshes, vertexCount) {
const positions = new Float32Array(vertexCount * 3);
let normals;
let colors;
let uvs;
let i = 0;
for (const mesh of meshes) {
const { POSITION, NORMAL, COLOR_0, TEXCOORD_0 } = mesh.attributes;
positions.set(POSITION.value, i * 3);
if (NORMAL) {
normals = normals || new Float32Array(vertexCount * 3);
normals.set(NORMAL.value, i * 3);
}
if (COLOR_0) {
colors = colors || new Float32Array(vertexCount * 3);
colors.set(COLOR_0.value, i * 3);
}
if (TEXCOORD_0) {
uvs = uvs || new Float32Array(vertexCount * 2);
uvs.set(TEXCOORD_0.value, i * 2);
}
i += POSITION.value.length / 3;
}
const attributes = {};
attributes.POSITION = { value: positions, size: 3 };
if (normals) {
attributes.NORMAL = { value: normals, size: 3 };
}
if (colors) {
attributes.COLOR_0 = { value: colors, size: 3 };
}
if (uvs) {
attributes.TEXCOORD_0 = { value: uvs, size: 2 };
}
return attributes;
}
// src/obj-loader.ts
var VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "latest";
var OBJLoader = {
dataType: null,
batchType: null,
name: "OBJ",
id: "obj",
module: "obj",
version: VERSION,
worker: true,
extensions: ["obj"],
mimeTypes: ["text/plain"],
testText: testOBJFile,
options: {
obj: {}
}
};
function testOBJFile(text) {
return text[0] === "v";
}
// src/lib/parse-mtl.ts
var DELIMITER_PATTERN = /\s+/;
function parseMTL(text, options) {
const materials = [];
let currentMaterial = { name: "placeholder" };
const lines = text.split("\n");
for (let line of lines) {
line = line.trim();
if (line.length === 0 || line.charAt(0) === "#") {
continue;
}
const pos = line.indexOf(" ");
let key = pos >= 0 ? line.substring(0, pos) : line;
key = key.toLowerCase();
let value = pos >= 0 ? line.substring(pos + 1) : "";
value = value.trim();
switch (key) {
case "newmtl":
currentMaterial = { name: value };
materials.push(currentMaterial);
break;
case "ka":
currentMaterial.ambientColor = parseColor(value);
break;
case "kd":
currentMaterial.diffuseColor = parseColor(value);
break;
case "map_kd":
currentMaterial.diffuseTextureUrl = value;
break;
case "ks":
currentMaterial.specularColor = parseColor(value);
break;
case "map_ks":
currentMaterial.specularTextureUrl = value;
break;
case "ke":
currentMaterial.emissiveColor = parseColor(value);
break;
case "map_ke":
currentMaterial.emissiveTextureUrl = value;
break;
case "ns":
currentMaterial.shininess = parseFloat(value);
break;
case "map_ns":
break;
case "ni":
currentMaterial.refraction = parseFloat(value);
break;
case "illum":
currentMaterial.illumination = parseFloat(value);
break;
default:
break;
}
}
return materials;
}
function parseColor(value, options) {
const rgb = value.split(DELIMITER_PATTERN, 3);
const color = [
parseFloat(rgb[0]),
parseFloat(rgb[1]),
parseFloat(rgb[2])
];
return color;
}
// src/mtl-loader.ts
var VERSION2 = typeof __VERSION__ !== "undefined" ? __VERSION__ : "latest";
var MTLLoader = {
dataType: null,
batchType: null,
name: "MTL",
id: "mtl",
module: "mtl",
version: VERSION2,
worker: true,
extensions: ["mtl"],
mimeTypes: ["text/plain"],
testText: (text) => text.includes("newmtl"),
options: {
mtl: {}
}
};
// src/index.ts
var OBJLoader2 = {
...OBJLoader,
parse: async (arrayBuffer, options) => parseOBJ(new TextDecoder().decode(arrayBuffer), options),
parseTextSync: (text, options) => parseOBJ(text, options)
};
var MTLLoader2 = {
...MTLLoader,
parse: async (arrayBuffer, options) => parseMTL(new TextDecoder().decode(arrayBuffer), options?.mtl),
parseTextSync: (text, options) => parseMTL(text, options?.mtl)
};
return __toCommonJS(bundle_exports);
})();
return __exports__;
});