src/plugins/OBJLoaderPlugin/OBJSceneGraphLoader.js
import {Mesh} from "../../viewer/scene/mesh/Mesh.js";
import {ReadableGeometry} from "../../viewer/scene/geometry/ReadableGeometry.js";
import {PhongMaterial} from "../../viewer/scene/materials/PhongMaterial.js";
import {Texture} from "../../viewer/scene/materials/Texture.js";
import {core} from "../../viewer/scene/core.js";
import {worldToRTCPositions} from "../../viewer/scene/math/rtcCoords.js";
import {math} from "../../viewer/scene/math/math.js";
const tempVec3a = math.vec3();
/**
* @private
*/
class OBJSceneGraphLoader {
/**
* Loads OBJ and MTL from file(s) into a {@link Node}.
*
* @static
* @param {Node} modelNode Node to load into.
* @param {String} src Path to OBJ file.
* @param {Object} params Loading options.
*/
load(modelNode, src, params = {}) {
var spinner = modelNode.scene.canvas.spinner;
spinner.processes++;
loadOBJ(modelNode, src, function (state) {
loadMTLs(modelNode, state, function () {
createMeshes(modelNode, state);
spinner.processes--;
core.scheduleTask(function () {
modelNode.fire("loaded", true, false);
});
});
});
}
/**
* Parses OBJ and MTL text strings into a {@link Node}.
*
* @static
* @param {Node} modelNode Node to load into.
* @param {String} objText OBJ text string.
* @param {String} [mtlText] MTL text string.
* @param {String} [basePath] Base path for external resources.
*/
parse(modelNode, objText, mtlText, basePath) {
if (!objText) {
this.warn("load() param expected: objText");
return;
}
var state = parseOBJ(modelNode, objText, null);
if (mtlText) {
parseMTL(modelNode, mtlText, basePath);
}
createMeshes(modelNode, state);
modelNode.src = null;
modelNode.fire("loaded", true, false);
}
}
//--------------------------------------------------------------------------------------------
// Loads OBJ
//
// Parses OBJ into an intermediate state object. The object will contain geometry data
// and material IDs from which meshes can be created later. The object will also
// contain a list of filenames of the MTL files referenced by the OBJ, is any.
//
// Originally based on the THREE.js OBJ and MTL loaders:
//
// https://github.com/mrdoob/three.js/blob/dev/examples/js/loaders/OBJLoader.js
// https://github.com/mrdoob/three.js/blob/dev/examples/js/loaders/MTLLoader.js
//--------------------------------------------------------------------------------------------
var loadOBJ = function (modelNode, url, ok) {
loadFile(url, function (text) {
var state = parseOBJ(modelNode, text, url);
ok(state);
},
function (error) {
modelNode.error(error);
});
};
var parseOBJ = (function () {
const regexp = {
// v float float float
vertex_pattern: /^v\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/,
// vn float float float
normal_pattern: /^vn\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/,
// vt float float
uv_pattern: /^vt\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/,
// f vertex vertex vertex
face_vertex: /^f\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)(?:\s+(-?\d+))?/,
// f vertex/uv vertex/uv vertex/uv
face_vertex_uv: /^f\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+))?/,
// f vertex/uv/normal vertex/uv/normal vertex/uv/normal
face_vertex_uv_normal: /^f\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+)\/(-?\d+))?/,
// f vertex//normal vertex//normal vertex//normal
face_vertex_normal: /^f\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)(?:\s+(-?\d+)\/\/(-?\d+))?/,
// o object_name | g group_name
object_pattern: /^[og]\s*(.+)?/,
// s boolean
smoothing_pattern: /^s\s+(\d+|on|off)/,
// mtllib file_reference
material_library_pattern: /^mtllib /,
// usemtl material_name
material_use_pattern: /^usemtl /
};
return function (modelNode, text, url) {
url = url || "";
var state = {
src: url,
basePath: getBasePath(url),
objects: [],
object: {},
positions: [],
normals: [],
uv: [],
materialLibraries: {}
};
startObject(state, "", false);
// Parts of this parser logic are derived from the THREE.js OBJ loader:
// https://github.com/mrdoob/three.js/blob/dev/examples/js/loaders/OBJLoader.js
if (text.indexOf('\r\n') !== -1) {
// This is faster than String.split with regex that splits on both
text = text.replace('\r\n', '\n');
}
var lines = text.split('\n');
var line = '', lineFirstChar = '', lineSecondChar = '';
var lineLength = 0;
var result = [];
// Faster to just trim left side of the line. Use if available.
var trimLeft = (typeof ''.trimLeft === 'function');
for (var 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') {
lineSecondChar = line.charAt(1);
if (lineSecondChar === ' ' && (result = regexp.vertex_pattern.exec(line)) !== null) {
// 0 1 2 3
// ['v 1.0 2.0 3.0', '1.0', '2.0', '3.0']
state.positions.push(
parseFloat(result[1]),
parseFloat(result[2]),
parseFloat(result[3])
);
} else if (lineSecondChar === 'n' && (result = regexp.normal_pattern.exec(line)) !== null) {
// 0 1 2 3
// ['vn 1.0 2.0 3.0', '1.0', '2.0', '3.0']
state.normals.push(
parseFloat(result[1]),
parseFloat(result[2]),
parseFloat(result[3])
);
} else if (lineSecondChar === 't' && (result = regexp.uv_pattern.exec(line)) !== null) {
// 0 1 2
// ['vt 0.1 0.2', '0.1', '0.2']
state.uv.push(
parseFloat(result[1]),
parseFloat(result[2])
);
} else {
modelNode.error('Unexpected vertex/normal/uv line: \'' + line + '\'');
return;
}
} else if (lineFirstChar === 'f') {
if ((result = regexp.face_vertex_uv_normal.exec(line)) !== null) {
// f vertex/uv/normal vertex/uv/normal vertex/uv/normal
// 0 1 2 3 4 5 6 7 8 9 10 11 12
// ['f 1/1/1 2/2/2 3/3/3', '1', '1', '1', '2', '2', '2', '3', '3', '3', undefined, undefined, undefined]
addFace(state,
result[1], result[4], result[7], result[10],
result[2], result[5], result[8], result[11],
result[3], result[6], result[9], result[12]
);
} else if ((result = regexp.face_vertex_uv.exec(line)) !== null) {
// f vertex/uv vertex/uv vertex/uv
// 0 1 2 3 4 5 6 7 8
// ['f 1/1 2/2 3/3', '1', '1', '2', '2', '3', '3', undefined, undefined]
addFace(state,
result[1], result[3], result[5], result[7],
result[2], result[4], result[6], result[8]
);
} else if ((result = regexp.face_vertex_normal.exec(line)) !== null) {
// f vertex//normal vertex//normal vertex//normal
// 0 1 2 3 4 5 6 7 8
// ['f 1//1 2//2 3//3', '1', '1', '2', '2', '3', '3', undefined, undefined]
addFace(state,
result[1], result[3], result[5], result[7],
undefined, undefined, undefined, undefined,
result[2], result[4], result[6], result[8]
);
} else if ((result = regexp.face_vertex.exec(line)) !== null) {
// f vertex vertex vertex
// 0 1 2 3 4
// ['f 1 2 3', '1', '2', '3', undefined]
addFace(state, result[1], result[2], result[3], result[4]);
} else {
modelNode.error('Unexpected face line: \'' + line + '\'');
return;
}
} else if (lineFirstChar === 'l') {
var lineParts = line.substring(1).trim().split(' ');
var lineVertices = [], lineUVs = [];
if (line.indexOf('/') === -1) {
lineVertices = lineParts;
} else {
for (var li = 0, llen = lineParts.length; li < llen; li++) {
var parts = lineParts[li].split('/');
if (parts[0] !== '') {
lineVertices.push(parts[0]);
}
if (parts[1] !== '') {
lineUVs.push(parts[1]);
}
}
}
addLineGeometry(state, lineVertices, lineUVs);
} else if ((result = regexp.object_pattern.exec(line)) !== null) {
// o object_name
// or
// g group_name
var id = result[0].substr(1).trim();
startObject(state, id, true);
} else if (regexp.material_use_pattern.test(line)) {
// material
var id = line.substring(7).trim();
state.object.material.id = id;
} else if (regexp.material_library_pattern.test(line)) {
// mtl file
state.materialLibraries[line.substring(7).trim()] = true;
} else if ((result = regexp.smoothing_pattern.exec(line)) !== null) {
// smooth shading
var value = result[1].trim().toLowerCase();
state.object.material.smooth = (value === '1' || value === 'on');
} else {
// Handle null terminated files without exception
if (line === '\0') {
continue;
}
modelNode.error('Unexpected line: \'' + line + '\'');
return;
}
}
return state;
};
function getBasePath(src) {
var n = src.lastIndexOf('/');
return (n === -1) ? src : src.substring(0, n + 1);
}
function startObject(state, id, fromDeclaration) {
if (state.object && state.object.fromDeclaration === false) {
state.object.id = id;
state.object.fromDeclaration = (fromDeclaration !== false);
return;
}
state.object = {
id: id || '',
geometry: {
positions: [],
normals: [],
uv: []
},
material: {
id: '',
smooth: true
},
fromDeclaration: (fromDeclaration !== false)
};
state.objects.push(state.object);
}
function parseVertexIndex(value, len) {
var index = parseInt(value, 10);
return (index >= 0 ? index - 1 : index + len / 3) * 3;
}
function parseNormalIndex(value, len) {
var index = parseInt(value, 10);
return (index >= 0 ? index - 1 : index + len / 3) * 3;
}
function parseUVIndex(value, len) {
var index = parseInt(value, 10);
return (index >= 0 ? index - 1 : index + len / 2) * 2;
}
function addVertex(state, a, b, c) {
var src = state.positions;
var dst = state.object.geometry.positions;
dst.push(src[a + 0]);
dst.push(src[a + 1]);
dst.push(src[a + 2]);
dst.push(src[b + 0]);
dst.push(src[b + 1]);
dst.push(src[b + 2]);
dst.push(src[c + 0]);
dst.push(src[c + 1]);
dst.push(src[c + 2]);
}
function addVertexLine(state, a) {
var src = state.positions;
var dst = state.object.geometry.positions;
dst.push(src[a + 0]);
dst.push(src[a + 1]);
dst.push(src[a + 2]);
}
function addNormal(state, a, b, c) {
var src = state.normals;
var dst = state.object.geometry.normals;
dst.push(src[a + 0]);
dst.push(src[a + 1]);
dst.push(src[a + 2]);
dst.push(src[b + 0]);
dst.push(src[b + 1]);
dst.push(src[b + 2]);
dst.push(src[c + 0]);
dst.push(src[c + 1]);
dst.push(src[c + 2]);
}
function addUV(state, a, b, c) {
var src = state.uv;
var dst = state.object.geometry.uv;
dst.push(src[a + 0]);
dst.push(src[a + 1]);
dst.push(src[b + 0]);
dst.push(src[b + 1]);
dst.push(src[c + 0]);
dst.push(src[c + 1]);
}
function addUVLine(state, a) {
var src = state.uv;
var dst = state.object.geometry.uv;
dst.push(src[a + 0]);
dst.push(src[a + 1]);
}
function addFace(state, a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd) {
var vLen = state.positions.length;
var ia = parseVertexIndex(a, vLen);
var ib = parseVertexIndex(b, vLen);
var ic = parseVertexIndex(c, vLen);
var id;
if (d === undefined) {
addVertex(state, ia, ib, ic);
} else {
id = parseVertexIndex(d, vLen);
addVertex(state, ia, ib, id);
addVertex(state, ib, ic, id);
}
if (ua !== undefined) {
var uvLen = state.uv.length;
ia = parseUVIndex(ua, uvLen);
ib = parseUVIndex(ub, uvLen);
ic = parseUVIndex(uc, uvLen);
if (d === undefined) {
addUV(state, ia, ib, ic);
} else {
id = parseUVIndex(ud, uvLen);
addUV(state, ia, ib, id);
addUV(state, ib, ic, id);
}
}
if (na !== undefined) {
// Normals are many times the same. If so, skip function call and parseInt.
var nLen = state.normals.length;
ia = parseNormalIndex(na, nLen);
ib = na === nb ? ia : parseNormalIndex(nb, nLen);
ic = na === nc ? ia : parseNormalIndex(nc, nLen);
if (d === undefined) {
addNormal(state, ia, ib, ic);
} else {
id = parseNormalIndex(nd, nLen);
addNormal(state, ia, ib, id);
addNormal(state, ib, ic, id);
}
}
}
function addLineGeometry(state, positions, uv) {
state.object.geometry.type = 'Line';
var vLen = state.positions.length;
var uvLen = state.uv.length;
for (var vi = 0, l = positions.length; vi < l; vi++) {
addVertexLine(state, parseVertexIndex(positions[vi], vLen));
}
for (var uvi = 0, uvl = uv.length; uvi < uvl; uvi++) {
addUVLine(state, parseUVIndex(uv[uvi], uvLen));
}
}
})();
//--------------------------------------------------------------------------------------------
// Loads MTL files listed in parsed state
//--------------------------------------------------------------------------------------------
function loadMTLs(modelNode, state, ok) {
var basePath = state.basePath;
var srcList = Object.keys(state.materialLibraries);
var numToLoad = srcList.length;
for (var i = 0, len = numToLoad; i < len; i++) {
loadMTL(modelNode, basePath, basePath + srcList[i], function () {
if (--numToLoad === 0) {
ok();
}
});
}
}
//--------------------------------------------------------------------------------------------
// Loads an MTL file
//--------------------------------------------------------------------------------------------
var loadMTL = function (modelNode, basePath, src, ok) {
loadFile(src, function (text) {
parseMTL(modelNode, text, basePath);
ok();
},
function (error) {
modelNode.error(error);
ok();
});
};
var parseMTL = (function () {
var delimiter_pattern = /\s+/;
return function (modelNode, mtlText, basePath) {
var lines = mtlText.split('\n');
var materialCfg = {
id: "Default"
};
var needCreate = false;
var line;
var pos;
var key;
var value;
var alpha;
basePath = basePath || "";
for (var i = 0; i < lines.length; i++) {
line = lines[i].trim();
if (line.length === 0 || line.charAt(0) === '#') { // Blank line or comment ignore
continue;
}
pos = line.indexOf(' ');
key = (pos >= 0) ? line.substring(0, pos) : line;
key = key.toLowerCase();
value = (pos >= 0) ? line.substring(pos + 1) : '';
value = value.trim();
switch (key.toLowerCase()) {
case "newmtl": // New material
//if (needCreate) {
createMaterial(modelNode, materialCfg);
//}
materialCfg = {
id: value
};
needCreate = true;
break;
case 'ka':
materialCfg.ambient = parseRGB(value);
break;
case 'kd':
materialCfg.diffuse = parseRGB(value);
break;
case 'ks':
materialCfg.specular = parseRGB(value);
break;
case 'map_kd':
if (!materialCfg.diffuseMap) {
materialCfg.diffuseMap = createTexture(modelNode, basePath, value, "sRGB");
}
break;
case 'map_ks':
if (!materialCfg.specularMap) {
materialCfg.specularMap = createTexture(modelNode, basePath, value, "linear");
}
break;
case 'map_bump':
case 'bump':
if (!materialCfg.normalMap) {
materialCfg.normalMap = createTexture(modelNode, basePath, value);
}
break;
case 'ns':
materialCfg.shininess = parseFloat(value);
break;
case 'd':
alpha = parseFloat(value);
if (alpha < 1) {
materialCfg.alpha = alpha;
materialCfg.alphaMode = "blend";
}
break;
case 'tr':
alpha = parseFloat(value);
if (alpha > 0) {
materialCfg.alpha = 1 - alpha;
materialCfg.alphaMode = "blend";
}
break;
default:
// modelNode.error("Unrecognized token: " + key);
}
}
if (needCreate) {
createMaterial(modelNode, materialCfg);
}
};
function createTexture(modelNode, basePath, value, encoding) {
var textureCfg = {};
var items = value.split(/\s+/);
var pos = items.indexOf('-bm');
if (pos >= 0) {
//matParams.bumpScale = parseFloat(items[pos + 1]);
items.splice(pos, 2);
}
pos = items.indexOf('-s');
if (pos >= 0) {
textureCfg.scale = [parseFloat(items[pos + 1]), parseFloat(items[pos + 2])];
items.splice(pos, 4); // we expect 3 parameters here!
}
pos = items.indexOf('-o');
if (pos >= 0) {
textureCfg.translate = [parseFloat(items[pos + 1]), parseFloat(items[pos + 2])];
items.splice(pos, 4); // we expect 3 parameters here!
}
textureCfg.src = basePath + items.join(' ').trim();
textureCfg.flipY = true;
textureCfg.encoding = encoding || "linear";
//textureCfg.wrapS = self.wrap;
//textureCfg.wrapT = self.wrap;
var texture = new Texture(modelNode, textureCfg);
return texture.id;
}
function createMaterial(modelNode, materialCfg) {
new PhongMaterial(modelNode, materialCfg);
}
function parseRGB(value) {
var ss = value.split(delimiter_pattern, 3);
return [parseFloat(ss[0]), parseFloat(ss[1]), parseFloat(ss[2])];
}
})();
//--------------------------------------------------------------------------------------------
// Creates meshes from parsed state
//--------------------------------------------------------------------------------------------
function createMeshes(modelNode, state) {
for (var j = 0, k = state.objects.length; j < k; j++) {
var object = state.objects[j];
var geometry = object.geometry;
var isLine = (geometry.type === 'Line');
if (geometry.positions.length === 0) {
// Skip o/g line declarations that did not follow with any faces
continue;
}
var geometryCfg = {
primitive: "triangles",
compressGeometry: false
};
geometryCfg.positions = geometry.positions;
if (geometry.normals.length > 0) {
geometryCfg.normals = geometry.normals;
}
if (geometry.uv.length > 0) {
geometryCfg.uv = geometry.uv;
}
var indices = new Array(geometryCfg.positions.length / 3); // Triangle soup
for (var idx = 0; idx < indices.length; idx++) {
indices[idx] = idx;
}
geometryCfg.indices = indices;
const origin = tempVec3a;
worldToRTCPositions(geometry.positions, geometry.positions, origin);
var readableGeometry = new ReadableGeometry(modelNode, geometryCfg);
var materialId = object.material.id;
var material;
if (materialId && materialId !== "") {
material = modelNode.scene.components[materialId];
if (!material) {
modelNode.error("Material not found: " + materialId);
}
} else {
material = new PhongMaterial(modelNode, {
//emissive: [0.6, 0.6, 0.0],
diffuse: [0.6, 0.6, 0.6],
backfaces: true
});
}
// material.emissive = [Math.random(), Math.random(), Math.random()];
var mesh = new Mesh(modelNode, {
id: modelNode.id + "#" + object.id,
origin: (origin[0] !== 0 || origin[1] !== 0 || origin[2] !== 0) ? origin : null,
isObject: true,
geometry: readableGeometry,
material: material,
pickable: true
});
modelNode.addChild(mesh);
}
}
function loadFile(url, ok, err) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.addEventListener('load', function (event) {
var response = event.target.response;
if (this.status === 200) {
if (ok) {
ok(response);
}
} else if (this.status === 0) {
// Some browsers return HTTP Status 0 when using non-http protocol
// e.g. 'file://' or 'data://'. Handle as success.
console.warn('loadFile: HTTP Status 0 received.');
if (ok) {
ok(response);
}
} else {
if (err) {
err(event);
}
}
}, false);
request.addEventListener('error', function (event) {
if (err) {
err(event);
}
}, false);
request.send(null);
}
export {OBJSceneGraphLoader};