Loading...
Loading...
Generate custom 3D models from text or images using Meshy AI, then auto-rig and animate them for Three.js games. The preferred source for all 3D game assets.
npx skill4agent add opusgamelabs/game-creator meshyaiMESHY_API_KEY| Fallback | Source | Best for |
|---|---|---|
| Pre-built GLBs | Quick animated humanoids (Soldier, Xbot, Robot, Fox) |
| Sketchfab, Poly Haven, Poly.pizza | Searching existing free model libraries |
| Procedural geometry | Code | BoxGeometry/SphereGeometry as last resort |
MESHY_API_KEYI'll generate custom 3D models with Meshy AI for the best results. You can get a free API key in 30 seconds:
- Sign up at https://app.meshy.ai
- Go to Settings → API Keys
- Create a new API key
What is your Meshy API key? (Or type "skip" to use free model libraries instead)
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs ...scripts/meshy-generate.mjs# Full pipeline: preview → refine → download
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode text-to-3d \
--prompt "a cartoon knight with sword and shield" \
--output public/assets/models/ \
--slug knight
# Preview only (faster, untextured — good for geometry check)
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode text-to-3d \
--prompt "a wooden barrel" \
--preview-only \
--output public/assets/models/ \
--slug barrel
# With PBR textures and specific polycount
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode text-to-3d \
--prompt "a sci-fi hover bike" \
--pbr \
--polycount 15000 \
--ai-model meshy-6 \
--output public/assets/models/ \
--slug hoverbike# From URL
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode image-to-3d \
--image "https://example.com/character-concept.png" \
--output public/assets/models/ \
--slug character
# From local file (auto-converts to base64)
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode image-to-3d \
--image "./concept-art/hero.png" \
--output public/assets/models/ \
--slug heroMESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode rig \
--task-id <meshy-task-id> \
--height 1.8 \
--output public/assets/models/ \
--slug herohero.glbhero-walk.glbhero-run.glbMESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode animate \
--task-id <rig-task-id> \
--action-id 1 \
--output public/assets/models/ \
--slug hero-walkMESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode status \
--task-id <task-id> \
--task-type text-to-3d # or: image-to-3d, rigging, animationsMESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode text-to-3d \
--prompt "a crystal sword" \
--no-poll
# Later:
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode status --task-id <id> --task-type text-to-3dscripts/optimize-glb.mjs--no-optimize--texture-size <n>npx@gltf-transform/cligltf-transformMeshoptDecoderAssetLoader.js# Skip optimization for a specific generation
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode text-to-3d --prompt "a barrel" --preview-only \
--no-optimize --output public/assets/models/ --slug barrel
# Custom texture size (e.g., 512 for mobile)
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode text-to-3d --prompt "a barrel" --preview-only \
--texture-size 512 --output public/assets/models/ --slug barrel
# Re-optimize an existing GLB directly
node scripts/optimize-glb.mjs public/assets/models/barrel.glb --texture-size 512https://api.meshy.ai/openapi/v2/text-to-3d{
"mode": "preview",
"prompt": "a cartoon knight with sword and shield",
"ai_model": "latest",
"topology": "triangle",
"target_polycount": 10000
}{
"mode": "refine",
"preview_task_id": "<preview-task-id>",
"enable_pbr": true,
"texture_prompt": "hand-painted fantasy style"
}/v2/text-to-3d/:id{
"id": "task-uuid",
"status": "SUCCEEDED",
"progress": 100,
"model_urls": {
"glb": "https://assets.meshy.ai/...",
"fbx": "https://assets.meshy.ai/...",
"obj": "https://assets.meshy.ai/...",
"usdz": "https://assets.meshy.ai/..."
},
"texture_urls": [
{ "base_color": "https://..." }
],
"thumbnail_url": "https://..."
}ai_modelmeshy-5meshy-6latestlatestmodel_typestandardlowpolytopologyquadtriangletriangletarget_polycountsymmetry_modeoffautoonautopose_modea-poset-poseenable_pbr/v1/image-to-3d{
"image_url": "https://example.com/photo.png",
"ai_model": "latest",
"enable_pbr": false,
"should_texture": true,
"topology": "triangle",
"target_polycount": 10000
}/v1/image-to-3d/:idimage_urldata:image/png;base64,.../v1/multi-image-to-3d/v1/rigging{
"input_task_id": "<text-to-3d or image-to-3d task id>",
"height_meters": 1.7
}/v1/rigging/:idrigged_character_glb_urlrigged_character_fbx_urlbasic_animations/v1/animations{
"rig_task_id": "<rigging-task-id>",
"action_id": 1
}/v1/animations/:idanimation_glb_urlanimation_fbx_urlPENDINGIN_PROGRESSSUCCEEDEDFAILEDCANCELED# Generate
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode text-to-3d --prompt "a wooden barrel, low poly game asset" \
--polycount 5000 --output public/assets/models/ --slug barrel
# Integrate with loadModel (regular clone, no SkeletonUtils)
const barrel = await loadModel('assets/models/barrel.glb');
scene.add(barrel);// Add this immediately after loading any GLB
model.updateMatrixWorld(true);
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
console.log(`[Model] ${slug} — size: ${size.x.toFixed(2)} x ${size.y.toFixed(2)} x ${size.z.toFixed(2)}`);
console.log(`[Model] ${slug} — center: ${center.x.toFixed(2)}, ${center.y.toFixed(2)}, ${center.z.toFixed(2)}`);rotationY: Math.PIrotationY: 0rotationY// Calculate scale to fit a target height
const box = new THREE.Box3().setFromObject(model);
const currentHeight = box.max.y - box.min.y;
const targetHeight = 2.0; // desired height in world units
const autoScale = targetHeight / currentHeight;
model.scale.setScalar(autoScale);// Ensure model fits within container bounds
const containerWidth = RING.PLATFORM_WIDTH * 0.8; // 80% of ring width
const modelWidth = box.max.x - box.min.x;
if (modelWidth * currentScale > containerWidth) {
const fitScale = containerWidth / modelWidth;
model.scale.setScalar(Math.min(currentScale, fitScale));
}const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
model.position.y = -box.min.y; // feet on ground
model.position.x = -center.x; // centered X
model.position.z = -center.z; // centered Z| Model type | Rig? | Why |
|---|---|---|
| Humanoid character (player, NPC, enemy) | YES — always | Skeletal animation for walk/run/idle/attack |
| Animal with legs | YES | Walk/run animations |
| Vehicle, prop, building | No | Static or simple rotation |
| Abstract shape, particle | No | Procedural animation |
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode text-to-3d \
--prompt "a stylized robot boxer, low poly game character, full body" \
--pbr --polycount 15000 \
--output public/assets/models/ --slug robotMESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode rig \
--task-id <refine-task-id-from-meta.json> \
--height 1.7 \
--output public/assets/models/ --slug robot-riggedrigged_character_glb_urlbasic_animations.walkingbasic_animations.running# Each action_id corresponds to a different animation
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode animate \
--task-id <rig-task-id> \
--action-id <id> \
--output public/assets/models/ --slug robot-punchloadAnimatedModel()AnimationMixerimport { loadAnimatedModel } from './level/AssetLoader.js';
import * as THREE from 'three';
// Load rigged model (SkeletonUtils.clone preserves bone bindings)
const { model, clips } = await loadAnimatedModel('assets/models/robot-rigged.glb');
const mixer = new THREE.AnimationMixer(model);
// Log clip names — they vary per model
console.log('Clips:', clips.map(c => c.name));
// Load additional animation GLBs and add their clips to the same mixer
const walkData = await loadAnimatedModel('assets/models/robot-walk.glb');
const walkClip = walkData.clips[0];
const walkAction = mixer.clipAction(walkClip);
// fadeToAction pattern for smooth transitions
function fadeToAction(nextAction, duration = 0.3) {
if (activeAction) activeAction.fadeOut(duration);
nextAction.reset().setEffectiveTimeScale(1).setEffectiveWeight(1).fadeIn(duration).play();
activeAction = nextAction;
}
// In update loop:
mixer.update(delta);| Goal | Prompt | Why |
|---|---|---|
| Game character | "a stylized goblin warrior, low poly game character, full body" | "low poly" + "game character" = game-ready topology |
| Prop | "a wooden treasure chest, stylized, closed" | Simple, specific, single object |
| Environment piece | "a fantasy stone archway, low poly, game asset" | "game asset" signals clean geometry |
| Vehicle | "a sci-fi hover bike, side view, clean topology" | "clean topology" = fewer artifacts |
┌─────────────────────────────────────────────────────┐
│ 3D Asset Sources │
├──────────────┬──────────────┬───────────────────────┤
│ Free Libraries│ Character Lib │ Meshy AI │
│ find-3d-asset │ 3d-char-lib/ │ meshy-generate.mjs │
│ .mjs │ │ text/image → 3D │
│ │ │ rig → animate │
├──────────────┴──────────────┴───────────────────────┤
│ AssetLoader.js │
│ loadModel() / loadAnimatedModel() │
├─────────────────────────────────────────────────────┤
│ Three.js Game │
└─────────────────────────────────────────────────────┘public/assets/models/AssetLoader.jsMESHY_API_KEYrotationYMath.PIloadModel()loadAnimatedModel()clipMap.meta.jsonnpm run build