playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
162 lines (147 loc) • 6.24 kB
JavaScript
import { Geometry, GraphNode, Http, Mesh, MeshInstance, Model, StandardMaterial } from 'playcanvas';
// Sample Obj model parser. This is not built into the engine library by default.
//
// To use, first register the parser:
//
// import { Asset } from 'playcanvas';
// import { ObjModelParser } from 'playcanvas/scripts/esm/parsers/obj-model.mjs';
//
// // add parser to model resource handler
// const objParser = new ObjModelParser(this.app.graphicsDevice);
// this.app.loader.getHandler("model").addParser(objParser);
//
// Then load obj as a model asset:
//
// const asset = new Asset("MyObj", "model", {
// url: "model.obj"
// });
// this.app.assets.add(asset);
// this.app.assets.load(asset);
// First draft obj parser
// probably doesn't handle a lot of the obj spec
// Known issues:
// - can't handle meshes larger than 65535 verts
// - assigns default material to all meshes
// - doesn't created indexed geometry
class ObjModelParser {
constructor(device) {
this._device = device;
this._defaultMaterial = new StandardMaterial();
}
canParse(context) {
return context.ext === 'obj';
}
load(url, callback, asset) {
this.handler.fetch(url, Http.ResponseType.TEXT, (err, response) => {
if (err) {
callback(err);
} else {
this.parse(response, callback);
}
}, asset);
}
parse(input, callback) {
// expanded vert, uv and normal values from face indices
const parsed = {
default: {
verts: [],
normals: [],
uvs: [],
indices: []
}
};
let group = 'default'; // current group
const lines = input.split('\n');
const verts = [], normals = [], uvs = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const parts = line.split(/\s+/);
if (line[0] === 'v') {
if (parts[0] === 'v') {
verts.push(parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3]));
} else if (parts[0] === 'vn') {
normals.push(parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3]));
} else if (parts[0] === 'vt') {
uvs.push(parseFloat(parts[1]), parseFloat(parts[2]));
}
} else if (line[0] === 'g' || line[0] === 'o' || line[0] === 'u') {
// split into groups for 'g' 'o' and 'usemtl' elements
group = parts[1]; // only first value for name for now
if (!parsed[group]) {
parsed[group] = {
verts: [],
normals: [],
uvs: []
};
}
} else if (line[0] === 'f') {
if (parts.length === 4) {
// triangles
for (let p = 1; p < parts.length; p++) {
const r = this._parseIndices(parts[p]);
parsed[group].verts.push(verts[r[0] * 3], verts[r[0] * 3 + 1], verts[r[0] * 3 + 2]); // expand uvs from indices
if (r[1] * 2 < uvs.length) {
parsed[group].uvs.push(uvs[r[1] * 2], uvs[r[1] * 2 + 1]);
} // expand uvs from indices
if (r[2] * 3 < normals.length) {
parsed[group].normals.push(normals[r[2] * 3], normals[r[2] * 3 + 1], normals[r[2] * 3 + 2]);
} // expand normals from indices
}
} else if (parts.length === 5) {
// quads
const order = [1, 2, 3, 3, 4, 1]; // split quad into to triangles;
for (let o = 0; o < order.length; o++) {
const p = order[o];
const r = this._parseIndices(parts[p]);
parsed[group].verts.push(verts[r[0] * 3], verts[r[0] * 3 + 1], verts[r[0] * 3 + 2]); // expand uvs from indices
if (r[1] * 2 < uvs.length) {
parsed[group].uvs.push(uvs[r[1] * 2], uvs[r[1] * 2 + 1]);
} // expand uvs from indices
if (r[2] * 3 < normals.length) {
parsed[group].normals.push(normals[r[2] * 3], normals[r[2] * 3 + 1], normals[r[2] * 3 + 2]);
} // expand normals from indices
}
} else {
console.error(`OBJ uses unsupported ${parts.length - 1}-gons`);
}
}
}
const model = new Model();
const groupNames = Object.keys(parsed);
const root = new GraphNode();
// create a new mesh instance for each "group"
for (let i = 0; i < groupNames.length; i++) {
const currentGroup = parsed[groupNames[i]];
if (!currentGroup.verts.length) continue;
if (currentGroup.verts.length > 65535) {
console.warn('Warning: mesh with more than 65535 vertices');
}
const geom = new Geometry();
geom.positions = currentGroup.verts;
if (currentGroup.normals.length > 0) {
geom.normals = currentGroup.normals;
}
if (currentGroup.uvs.length > 0) {
geom.uvs = currentGroup.uvs;
}
const mesh = Mesh.fromGeometry(this._device, geom);
const mi = new MeshInstance(mesh, this._defaultMaterial, new GraphNode());
model.meshInstances.push(mi);
root.addChild(mi.node);
}
model.graph = root;
model.getGraph().syncHierarchy();
callback(null, model);
}
_parseIndices(str) {
const result = [];
const indices = str.split('/');
for (let i = 0; i < 3; i++) {
if (indices[i]) {
result[i] = parseInt(indices[i], 10) - 1; // convert to 0-indexed
}
}
return result;
}
}
export { ObjModelParser };