glTF Prop

Generating a BookNest Prop as a glTF Model

BookNest's display for The Clockmaker's Paradox needs a mantel clock. This script spends 30 credits on a preview remeshed to about 20,000 faces and sized to real-world height (auto_size), a PBR refine and the GLB download:

meshy-prop.mjs: preview, refine and download a GLBJavaScript
// meshy-prop.mjs: text to 3D in two paid stages, preview (mesh) then refine (texture)
import fs from 'node:fs';
const API = 'https://api.meshy.ai/openapi/v2/text-to-3d';
const headers = { Authorization: `Bearer ${process.env.MESHY_API_KEY}`,
  'Content-Type': 'application/json' };
async function run(body) {                        // create a task, then poll it
  const res = await fetch(API, { method: 'POST', headers, body: JSON.stringify(body) });
  if (!res.ok) throw new Error(`${body.mode}: ${res.status} ${(await res.json()).message}`);
  const { result: id } = await res.json();
  for (;;) {
    const task = await (await fetch(`${API}/${id}`, { headers })).json();
    if (task.status === 'SUCCEEDED') return task;
    if (task.status === 'FAILED') throw new Error(task.task_error.message);
    await new Promise((resolve) => setTimeout(resolve, 5000));
  }
}
try {
  const preview = await run({ mode: 'preview', ai_model: 'latest', target_formats: ['glb'],
    prompt: 'An antique walnut mantel clock with a brass bezel, for a bookshop shelf',
    should_remesh: true, target_polycount: 20000, auto_size: true });     // 20 credits
  const refine = await run({ mode: 'refine', preview_task_id: preview.id,
    enable_pbr: true, target_formats: ['glb'] });                          // 10 credits
  const glb = await fetch(refine.model_urls.glb);  // signed URL; assets kept 3 days
  fs.writeFileSync('clock.glb', Buffer.from(await glb.arrayBuffer()));
  console.log('clock.glb', preview.consumed_credits + refine.consumed_credits, 'credits');
} catch (err) {
  console.error(err.message);
}

It is shown not run here past its first request: this book buys no Meshy 20,231 credits. Run with the retired test key and with none, it printed the API's real answers:

Output of 24
preview: 401 Invalid API key
preview: 401 Missing API key

A DELETE on the task URL cancels and refunds only a PENDING task; once a task is IN_PROGRESS, its credits are spent, so set a monthly credit limit on each key.

The prop pipeline end to end without a network: a procedural clock packed into a .glb, then read back like a downloadHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; background: #fafaf7; font: 12px system-ui, sans-serif; color: #263238; }
  canvas { display: block; max-width: 100%; }
  pre { margin: 4px 0; font-size: 11px; background: #1d2327; color: #d7f0dd; padding: 6px; max-width: 580px; white-space: pre-wrap; }
</style>
<script type="importmap">
{ "imports": {
  "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<pre id="log"></pre>
<script type="module">
import * as THREE from 'three';
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const log = (s) => { document.getElementById('log').textContent += s + '\n'; };
// What meshy-prop.mjs would do: preview (20 credits) → refine (10 credits) → fetch model_urls.glb.
// Here the "service" is a function that builds the clock and packs it as binary glTF in memory.
function fakeMeshyTask() {
  const clock = new THREE.Group();
  const wood = new THREE.MeshStandardMaterial({ color: '#5a3522', roughness: 0.45, name: 'walnut' });
  const brass = new THREE.MeshStandardMaterial({ color: '#c9a44c', metalness: 1, roughness: 0.3, name: 'brass' });
  const face = new THREE.MeshStandardMaterial({ color: '#fff8e8', roughness: 0.6, name: 'dial' });
  const body = new THREE.Mesh(new THREE.BoxGeometry(0.24, 0.16, 0.09), wood);          // metres: auto_size to real height
  const dome = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.09, 40, 1, false, Math.PI / 2, Math.PI), wood);
  dome.rotation.x = Math.PI / 2;  dome.position.y = 0.08;
  const bezel = new THREE.Mesh(new THREE.TorusGeometry(0.068, 0.008, 12, 40), brass);  bezel.position.set(0, 0.07, 0.046);
  const dial = new THREE.Mesh(new THREE.CircleGeometry(0.066, 40), face);  dial.position.set(0, 0.07, 0.0455);
  const base = new THREE.Mesh(new THREE.BoxGeometry(0.27, 0.02, 0.11), wood);  base.position.y = -0.09;
  clock.add(body, dome, bezel, dial, base);
  clock.name = 'mantel-clock';
  return new GLTFExporter().parseAsync(clock, { binary: true });
}

log('POST /openapi/v2/text-to-3d {mode: "preview", should_remesh: true, target_polycount: 20000, auto_size: true}');
log('  … SUCCEEDED (20 credits)');
log('POST {mode: "refine", preview_task_id, enable_pbr: true}  … SUCCEEDED (10 credits)');
const glb = await fakeMeshyTask();
log(`GET model_urls.glb → clock.glb, ${(glb.byteLength / 1024).toFixed(1)} KB (an ordinary glTF 2.0 binary)`);
const gltf = await new GLTFLoader().parseAsync(glb, '');
let vertices = 0;
gltf.scene.traverse((o) => { if (o.isMesh) vertices += o.geometry.attributes.position.count; });
const names = new Set();
gltf.scene.traverse((o) => { if (o.isMesh) names.add(o.material.name); });
log(`GLTFLoader: ${gltf.scene.children[0].children.length} meshes, ${vertices} vertices, materials ${[...names].join(', ')}`);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 220);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#f3efe6');
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.4));
const sun = new THREE.DirectionalLight('#ffffff', 2.5);  sun.position.set(1, 2, 3);  scene.add(sun);
const camera = new THREE.PerspectiveCamera(30, 600 / 220, 0.01, 10);
camera.position.set(0, 0.06, 0.75);
scene.add(gltf.scene);                                           // the "download", drop-in
renderer.setAnimationLoop((t) => { gltf.scene.rotation.y = t / 1500;  renderer.render(scene, camera); });
</script>