← Natura·Docs/Modules/Source

creatures/export-obj.js

66 lines · in natura-src-1.0.0.zip as natura/creatures/export-obj.js · a node script (exports a creature model to a file) · its entry in the module reference

// export-obj.js -- bridge the JS "bkit" to the Blender .py bkit: build the unified elephant with buildElephantUnified
// and write a Wavefront OBJ + MTL (grouped by material) that Blender can import for a Cycles/Eevee hero render.
//   node export-obj.js [african|asian] [N] [outPrefix] [pose]
//   pose = a preset name (stand|drink|reach|walk0..walk3) or inline JSON; the RIG deforms the unified mesh so we
//   can export any posed frame (walk cycle, drinking) for a Blender animation sequence.
const C = require('./render3d.js');
const S = require('./sdfmesh.js');
const R = require('./rig.js');
const fs = require('fs');
const species = process.argv[2] || 'african';
const N = parseInt(process.argv[3] || '72', 10);
const out = process.argv[4] || ('elephant_' + species);   // default: the current directory
const poseArg = process.argv[5] || 'stand';

const PRESETS = {
  stand: { trunkCurl: 0.12, earFlare: 0.10 },
  drink: { trunkReach: { x: 2.05, y: 0.12, z: 0.10 }, headDown: 0.16, earFlare: 0.05 },
  reach: { trunkReach: { x: 2.25, y: 0.95, z: 0.15 }, earFlare: 0.2 },
  walk0: { gait: 0.0, trunkCurl: 0.12, earFlare: 0.14, headDown: 0.04 },
  walk1: { gait: 0.25, trunkCurl: 0.10, earFlare: 0.10, headDown: 0.04 },
  walk2: { gait: 0.5, trunkCurl: 0.12, earFlare: 0.14, headDown: 0.04 },
  walk3: { gait: 0.75, trunkCurl: 0.10, earFlare: 0.10, headDown: 0.04 },
};
const pose = PRESETS[poseArg] || (poseArg.charAt(0) === '{' ? JSON.parse(poseArg) : PRESETS.stand);
const rig = R.buildElephantRigged(C.prims, { species, detail: 0.9, N });
rig.setPose(pose);
const faces = rig.faces;

// material by colour signature
const MAT = { '138,133,126': 'skin', '150,145,137': 'skin', '150,132,128': 'innerear',
  '232,226,208': 'ivory', '92,63,38': 'eye', '22,17,13': 'pupil', '40,37,34': 'tuft' };
const matName = (c) => MAT[c.join(',')] || ('c' + c.join('_'));

// dedup vertices by identity (faces share V objects) then by rounded position
const vIndex = new Map(), verts = [], norms = [];
const key = (p) => (Math.round(p.x * 1e4) / 1e4) + ',' + (Math.round(p.y * 1e4) / 1e4) + ',' + (Math.round(p.z * 1e4) / 1e4);
function vid(p, n) { const k = key(p); let i = vIndex.get(k); if (i == null) { i = verts.length; vIndex.set(k, i); verts.push(p); norms.push(n || { x: 0, y: 1, z: 0 }); } return i; }

const byMat = {};
for (const f of faces) {
  const m = matName(f.c); (byMat[m] || (byMat[m] = [])).push(f);
}
let objL = ['# elephant (JS bkit -> OBJ)', 'mtllib ' + out.split('/').pop() + '.mtl'];
// first pass: emit all verts (with normals) in face order, tracking indices
const faceLines = [];
for (const m of Object.keys(byMat)) {
  faceLines.push('usemtl ' + m); faceLines.push('g ' + m);
  for (const f of byMat[m]) {
    const idx = f.v.map((p, i) => vid(p, f.n && f.n[i]));
    // OBJ is 1-based; emit as a polygon (tri or quad)
    faceLines.push('f ' + idx.map((z) => (z + 1) + '//' + (z + 1)).join(' '));
  }
}
for (const p of verts) objL.push('v ' + p.x.toFixed(5) + ' ' + p.y.toFixed(5) + ' ' + p.z.toFixed(5));
for (const n of norms) objL.push('vn ' + n.x.toFixed(4) + ' ' + n.y.toFixed(4) + ' ' + n.z.toFixed(4));
objL = objL.concat(faceLines);
fs.writeFileSync(out + '.obj', objL.join('\n'));

// MTL: base colours (the Blender script overrides these with proper shaders, but names must exist)
const COL = { skin: [0.42, 0.40, 0.38], innerear: [0.52, 0.44, 0.44], ivory: [0.90, 0.87, 0.80],
  eye: [0.20, 0.12, 0.06], pupil: [0.04, 0.03, 0.02], tuft: [0.10, 0.09, 0.08] };
let mtl = [];
for (const m of Object.keys(byMat)) { const c = COL[m] || [0.5, 0.5, 0.5];
  mtl.push('newmtl ' + m, 'Kd ' + c.map((x) => x.toFixed(3)).join(' '), 'Ns 40', ''); }
fs.writeFileSync(out + '.mtl', mtl.join('\n'));
console.log('wrote', out + '.obj', 'verts', verts.length, 'faces', faces.length, 'mats', Object.keys(byMat).join(','));