// Copyright (C) 2017 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause var fs = require('fs'); var metadata = { vertexCount: 0, aabb: [[null, null], [null, null], [null, null]], emitVertex: function(v) { ++metadata.vertexCount; var aabb = metadata.aabb; if (aabb[0][0] === null || v[0] < aabb[0][0]) // min x aabb[0][0] = v[0]; if (aabb[0][1] === null || v[0] > aabb[0][1]) // max x aabb[0][1] = v[0]; if (aabb[1][0] === null || v[1] < aabb[1][0]) // min y aabb[1][0] = v[1]; if (aabb[1][1] === null || v[1] > aabb[1][1]) // max y aabb[1][1] = v[1]; if (aabb[2][0] === null || v[2] < aabb[2][0]) // min z aabb[2][0] = v[2]; if (aabb[2][1] === null || v[2] > aabb[2][1]) // max z aabb[2][1] = v[2]; }, getBuffer: function() { var aabb = metadata.aabb; console.log(metadata.vertexCount + " vertices"); console.log("AABB: " + aabb[0][0] + ".." + aabb[0][1] + ", " + aabb[1][0] + ".." + aabb[1][1] + ", " + aabb[2][0] + ".." + aabb[2][1]); var buf = new Buffer((2 + 6) * 4); var format = 1, p = 0; buf.writeUInt32LE(format, p++); buf.writeUInt32LE(metadata.vertexCount, p++ * 4); for (var i = 0; i < 3; ++i) { buf.writeFloatLE(aabb[i][0], p++ * 4); buf.writeFloatLE(aabb[i][1], p++ * 4); } return buf; } }; function makeVec(s, n) { var v = []; s.split(' ').forEach(function (coordStr) { var coord = parseFloat(coordStr); if (!isNaN(coord)) v.push(coord); }); if (v.length != n) { console.error("Wrong vector size, expected " + n + ", got " + v.length); process.exit(); } return v; } function parseObj(filename, callback) { fs.readFile(filename, "ascii", function (err, data) { if (err) throw err; var groupCount = 0; var parsed = { 'vertices': [], 'normals': [], 'texcoords': [], 'links': [] }; var missingTexCount = 0, missingNormCount = 0; data.split('\n').forEach(function (line) { var s = line.trim(); if (!s.length || groupCount > 1) return; if (s[0] === '#') return; if (s[0] === 'g') { ++groupCount; } else if (s.substr(0, 2) === "v ") { parsed.vertices.push(makeVec(s, 3)); } else if (s.substr(0, 3) === "vn ") { parsed.normals.push(makeVec(s, 3)); } else if (s.substr(0, 3) === "vt ") { parsed.texcoords.push(makeVec(s, 2)); } else if (s.substr(0, 2) === "f ") { var refs = s.split(' '); var vertCount = refs.length - 1; if (vertCount != 3) console.warn("Face " + parsed.links.length / 3 + " has " + vertCount + " vertices! (not triangulated?)"); for (var i = 1, ie = Math.min(4, refs.length); i < ie; ++i) { var refComps = refs[i].split('/'); var vertIndex = parseInt(refComps[0]) - 1; var texIndex = -1; if (refComps.length >= 2 && refComps[1].length) texIndex = parseInt(refComps[1]) - 1; var normIndex = -1; if (refComps.length >= 3 && refComps[2].length) normIndex = parseInt(refComps[2]) - 1; parsed.links.push([vertIndex, texIndex, normIndex]); if (texIndex == -1) ++missingTexCount; if (normIndex == -1) ++missingNormCount; } } }); console.log(missingTexCount + " missing texture coordinates, " + missingNormCount + " missing normals"); callback(parsed); }); } function fillVert(src, index, dst, elemCount, isVertexCoord) { var vertex = []; if (index >= 0) { for (var i = 0; i < elemCount; ++i) { var elem = src[index][i]; if (isVertexCoord) vertex.push(elem); dst.buf.writeFloatLE(elem, dst.bufptr++ * 4); } if (vertex.length == 3) metadata.emitVertex(vertex); } else { if (isVertexCoord) { console.error("Missing vertex"); process.exit(); } for (var i = 0; i < elemCount; ++i) dst.buf.writeFloatLE(0, dst.bufptr++ * 4); } return vertex; } function normalize(v) { var len = v[0] * v[0] + v[1] * v[1] + v[2] * v[2]; if (len == 0.0 || len == 1.0) return; len = Math.sqrt(len); return [ v[0] / len, v[1] / len, v[2] / len ]; } function surfaceNormal(a, b, c) { var u = [ b[0] - a[0], b[1] - a[1], b[2] - a[2] ]; var v = [ c[0] - a[0], c[1] - a[1], c[2] - a[2] ]; var result = [ u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0] ]; return normalize(result); } function objDataToBuf(parsed) { var floatCount = parsed.links.length * (3 + 2 + 3); var buf = new Buffer(floatCount * 4); var dst = { 'buf': buf, 'bufptr': 0 }; var tri = []; var genNormals = false; var genNormCount = 0; for (var i = 0; i < parsed.links.length; ++i) { var link = parsed.links[i]; var vertIndex = link[0], texIndex = link[1], normIndex = link[2]; tri.push(fillVert(parsed.vertices, vertIndex, dst, 3, true)); fillVert(parsed.texcoords, texIndex, dst, 2); fillVert(parsed.normals, normIndex, dst, 3); if (normIndex == -1) genNormals = true; if (tri.length == 3) { if (genNormals) { var norm = surfaceNormal(tri[0], tri[1], tri[2]); for (var nvIdx = 0; nvIdx < 3; ++nvIdx) { dst.buf.writeFloatLE(norm[0], (dst.bufptr - 3 - nvIdx * 8) * 4); dst.buf.writeFloatLE(norm[1], (dst.bufptr - 2 - nvIdx * 8) * 4); dst.buf.writeFloatLE(norm[2], (dst.bufptr - 1 - nvIdx * 8) * 4); } genNormCount += 3; } tri = []; } } if (genNormCount) console.log("Generated " + genNormCount + " normals"); return buf; } var inFilename = process.argv[2]; var outFilename = process.argv[3]; if (process.argv.length < 4) { console.log("Usage: objconvert file.obj file.buf"); process.exit(); } parseObj(inFilename, function (parsed) { var buf = objDataToBuf(parsed); var f = fs.createWriteStream(outFilename); f.on("error", function (e) { console.error(e); }); f.write(metadata.getBuffer()); f.write(buf); f.end(); console.log("Written to " + outFilename + ", format is:"); console.log(" uint32 version, uint32 vertex_count, float32 aabb[6], vertex_count * (float32 vertex[3], float32 texcoord[2], float32 normal[3])"); });