← Natura·Docs/Modules/Source

creatures/export-meshdata.js

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

// export-meshdata.js -- export the elephant geometry as compact JSON per-material submeshes
// (local-indexed) for the WorldBridge module to encode into an OpenSim llmesh asset server-side.
//   node export-meshdata.js [african|asian] [N] [outFile]
const C = require('./render3d.js');
const R = require('./rig.js');
const fs = require('fs');
const path = require('path');

const species = process.argv[2] || 'african';
const N = parseInt(process.argv[3] || '64', 10);
const outFile = process.argv[4] || ('great-grey-' + species + '.meshdata.json');   // default: the current directory

const rig = R.buildElephantRigged(C.prims, { species, detail: 0.9, N });
rig.setPose({ trunkCurl: 0.10, earFlare: 0.08 });
const faces = rig.faces;

// Keep the elephant's MOTTLED skin texture, but in shades of GREY (not the brown/black the old
// nearest-RGB palette misfiled). Skin faces are bucketed by LIGHTNESS into 4 warm-grey levels (a
// mottle within SL's 8-material cap); ivory/inner-ear/eye split off by hue.
const SKIN = [ [104,101,96], [126,122,115], [146,142,134], [168,163,154] ];   // 4 warm-grey lightness levels
const PALETTE = SKIN.concat([ [178,122,118], [232,226,208], [28,23,20] ]);    // + inner ear, ivory, eye
const classify = (c) => {
  const r = c[0], g = c[1], b = c[2], mx = Math.max(r, g, b), mn = Math.min(r, g, b), sat = mx - mn, L = (r + g + b) / 3;
  if (mx >= 195 && sat < 50) return 5;                       // ivory: very light, low saturation
  if (mx < 70) return 6;                                     // eye/pupil/tuft: very dark
  if (r > g + 16 && r > b + 24 && mx < 180) return 4;        // inner ear: warm/pink mid
  return L < 110 ? 0 : L < 132 ? 1 : L < 154 ? 2 : 3;        // skin: mottle in 4 grey lightness buckets
};

// per-material: local vertex list (pos+normal, deduped within the submesh) + triangles (local indices)
const sub = PALETTE.map(() => ({ vmap: new Map(), verts: [], tris: [], rgb: null }));
const key = (p) => (Math.round(p.x*1e4))+','+(Math.round(p.y*1e4))+','+(Math.round(p.z*1e4));
function localVid(s, p, n) { const k = key(p); let i = s.vmap.get(k); if (i == null) { i = s.verts.length; s.vmap.set(k, i); s.verts.push([p.x, p.y, p.z, n ? n.x : 0, n ? n.y : 1, n ? n.z : 0]); } return i; }

for (const f of faces) {
  const mi = classify(f.c), s = sub[mi];
  if (!s.rgb) s.rgb = PALETTE[mi];
  const vi = f.v.map((p, k) => localVid(s, p, f.n ? f.n[k] : null));
  for (let t = 1; t < vi.length - 1; t++) s.tris.push([vi[0], vi[t], vi[t + 1]]);
}

const submeshes = sub.filter((s) => s.tris.length).map((s) => ({ rgb: s.rgb, verts: s.verts, tris: s.tris }));
fs.mkdirSync(path.dirname(outFile), { recursive: true });
fs.writeFileSync(outFile, JSON.stringify({ species, submeshes }));
console.log('wrote', outFile);
console.log('  submeshes', submeshes.length, 'verts', submeshes.reduce((a, s) => a + s.verts.length, 0), 'tris', submeshes.reduce((a, s) => a + s.tris.length, 0));
console.log('  per-submesh verts (SL cap 65536/submesh):', submeshes.map((s) => s.verts.length).join(','));