From game-creator
Finds, downloads, and integrates GLB/GLTF 3D models from Meshy AI, Sketchfab, Poly Haven into Three.js browser games, replacing primitive BoxGeometry/SphereGeometry shapes.
npx claudepluginhub opusgamelabs/game-creator --plugin game-creatorThis skill uses the workspace's default tool permissions.
You are an expert 3D game artist and integrator. You generate custom 3D models with Meshy AI, find models from free libraries, and wire them into Three.js games — replacing primitive geometry with recognizable 3D models.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
You are an expert 3D game artist and integrator. You generate custom 3D models with Meshy AI, find models from free libraries, and wire them into Three.js games — replacing primitive geometry with recognizable 3D models.
Primitive cubes and spheres are fast to scaffold, but players can't tell a house from a tree. Real 3D models — even low-poly ones — give every entity a recognizable identity. Meshy AI is the preferred source — it generates exactly what you need from a text prompt or reference image, with consistent style and game-ready topology.
| Tier | Source | Auth | Best for |
|---|---|---|---|
| 1. Meshy AI (preferred) | meshy.ai | MESHY_API_KEY | Custom characters, props, and scenery from text/image — exact match to game theme |
| 2. Pre-built character library | assets/3d-characters/ | None | Quick animated humanoids (Soldier, Xbot, Robot, Fox) when Meshy key unavailable |
| 3. Sketchfab | sketchfab.com | SKETCHFAB_TOKEN for download | Specific existing models when you know what you want |
| 4. Poly Haven | polyhaven.com | None | CC0 environment props |
| 5. Poly.pizza | poly.pizza | POLY_PIZZA_API_KEY | 10K+ low-poly CC-BY models |
| 6. Procedural geometry (last resort) | Code | N/A | BoxGeometry/SphereGeometry |
Meshy AI is the preferred source for all 3D assets. Before prompting the user, check if the key already exists:
test -f .env && grep -q '^MESHY_API_KEY=.' .env && echo "found"
If found, export it with set -a; . .env; set +a and skip the prompt.
If MESHY_API_KEY is not set, ask the user before falling back to other tiers:
I'd like to generate custom 3D models with Meshy AI for the best results. You can get a free API key:
- Sign up at https://app.meshy.ai
- Go to Settings → API Keys
- Create a new API key
Paste your key below like:
MESHY_API_KEY=your-key-here(It will be saved to .env and redacted from this conversation automatically.)Or type "skip" to use free model libraries instead.
If the user provides a key, use it for all meshy-generate.mjs calls. If they skip, fall through to Tier 2+.
These GLB files from the Three.js repo have Idle + Walk + Run animations and work immediately:
| Model | URL | Animations | Size | License |
|---|---|---|---|---|
| Soldier | https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/Soldier.glb | Idle, Walk, Run, TPose | 2.2 MB | MIT |
| Xbot | https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/Xbot.glb | idle, walk, run + additive poses | 2.9 MB | MIT |
| RobotExpressive | https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/RobotExpressive/RobotExpressive.glb | Idle, Walking, Running, Dance, Jump + 8 more | 464 KB | MIT |
| Fox | https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/Fox/glTF-Binary/Fox.glb | Survey (idle), Walk, Run | 163 KB | CC0/CC-BY 4.0 |
assets/3d-characters/)These characters have gesture/performance animations instead of walk/run. Best for standing-position games (debate, dance-off, boxing, rhythm):
| Model | Animations | Faces | Size | License |
|---|---|---|---|---|
| Trump | StillLook (idle), Clap, Dance, Point, Talk, Twist | 1,266 | 1.2 MB | CC-BY (Sketchfab) |
| Biden | 1 Mixamo idle/sway | 50,000 | 3.3 MB | CC-BY (Sketchfab) |
Copy from the character library (no auth needed):
cp <plugin>/assets/3d-characters/models/trump.glb public/assets/models/
cp <plugin>/assets/3d-characters/models/biden.glb public/assets/models/
Trump clipMap:
{
idle: 'root|TrumpStillLook_BipTrump',
clap: 'root|TrumpClap1_BipTrump',
dance: 'root|Trumpdance1_BipTrump',
point: 'root|TrumpPoint_BipTrump',
talk: 'root|TrumpTalk1_BipTrump',
twist: 'root|TrumpTwist_BipTrump'
}
Biden clipMap:
{ idle: 'mixamo.com' }
Game design for gesture characters: Since these lack walk/run, design games where characters are stationary — debate battles, dance-offs, boxing rings, rhythm games, or turn-based encounters. Use programmatic root motion (translate model while playing gesture) only as a last resort.
Download with curl — no auth needed:
curl -L -o public/assets/models/Soldier.glb "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/Soldier.glb"
Clip name mapping varies per model. Always log clip names on load and define a clipMap per character:
// Soldier: { idle: 'Idle', walk: 'Walk', run: 'Run' }
// Xbot: { idle: 'idle', walk: 'walk', run: 'run' } (lowercase)
// Robot: { idle: 'Idle', walk: 'Walking', run: 'Running' }
// Fox: { idle: 'Survey', walk: 'Walk', run: 'Run' }
For EACH character in the game, try these tiers in order:
Tier 1 — Generate with Meshy AI (preferred): Generate a custom character model matching the game's art direction. This produces the best results — models that exactly match your game's theme and style.
# Generate character from text description
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode text-to-3d \
--prompt "a stylized <character description>, low poly game character, full body, t-pose" \
--polycount 15000 --pbr \
--output public/assets/models/ --slug <character-slug>
# Then rig for animation (humanoid characters)
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode rig --task-id <refine-task-id> \
--height 1.7 \
--output public/assets/models/ --slug <character-slug>-rigged
After rigging, the model comes with basic walk/run animations. Log clip names to build the clipMap.
For named personalities, be descriptive: "a cartoon caricature of <Name>, <hair/glasses/suit details>, low poly game character".
Tier 2 — Pre-built in assets/3d-characters/: If Meshy is unavailable, check manifest.json for a name/theme match. Copy the GLB. Done.
Tier 3 — Search Sketchfab for character-specific model: Use find-3d-asset.mjs:
node scripts/find-3d-asset.mjs --query "<character name> animated character" --max-faces 10000 --list-only
Tier 4 — Generic library fallback: Use the best thematic match from assets/3d-characters/:
Multi-character games: When using Meshy, generate each character with distinct descriptions for visual variety. When falling back to library models, assign different models to each character (e.g., Soldier for one, Xbot for another).
Only needed if falling back to Sketchfab. Search is free but download requires SKETCHFAB_TOKEN. Before prompting, check if the key already exists:
test -f .env && grep -q '^SKETCHFAB_TOKEN=.' .env && echo "found"
If found, export it with set -a; . .env; set +a and skip the prompt.
If needed and not set, ask the user:
I need a Sketchfab API token to download this model. You can get one for free:
- Sign in at https://sketchfab.com
- Go to https://sketchfab.com/settings/password → "API Token"
- Copy the token
Paste your token below like:
SKETCHFAB_TOKEN=your-token-here(It will be saved to .env and redacted from this conversation automatically.)
Then use it via: set -a; . .env; set +a && node scripts/find-3d-asset.mjs ...
Use scripts/find-3d-asset.mjs for both character searches AND non-character models (props, scenery, buildings):
node scripts/find-3d-asset.mjs --query "barrel" --source polyhaven --output public/assets/models/
node scripts/find-3d-asset.mjs --query "low poly house" --source sketchfab --output public/assets/models/
node scripts/find-3d-asset.mjs --query "coin" --list-only
Create src/level/AssetLoader.js. Critical: use SkeletonUtils.clone() for animated models — regular .clone() breaks skeleton bindings and causes T-pose.
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
const loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder); // required for meshopt-compressed GLBs
const cache = new Map();
/** Load a static (non-animated) model. Uses regular clone. */
export async function loadModel(path) {
const gltf = await _load(path);
const clone = gltf.scene.clone(true);
clone.traverse((c) => {
if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; }
});
return clone;
}
/** Load an animated (skeletal) model. Uses SkeletonUtils.clone to preserve bone bindings. */
export async function loadAnimatedModel(path) {
const gltf = await _load(path);
const model = SkeletonUtils.clone(gltf.scene);
model.traverse((c) => {
if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; }
});
return { model, clips: gltf.animations };
}
export function disposeAll() {
cache.forEach((p) => p.then((gltf) => {
gltf.scene.traverse((c) => {
if (c.isMesh) {
c.geometry.dispose();
if (Array.isArray(c.material)) c.material.forEach(m => m.dispose());
else c.material.dispose();
}
});
}));
cache.clear();
}
function _load(path) {
if (!cache.has(path)) {
cache.set(path, new Promise((resolve, reject) => {
loader.load(path, resolve, undefined,
(err) => reject(new Error(`Failed to load: ${path} — ${err.message || err}`)));
}));
}
return cache.get(path);
}
GLBs optimized by scripts/optimize-glb.mjs (or meshy-generate.mjs / find-3d-asset.mjs which call it automatically) use meshopt compression. The MeshoptDecoder import + loader.setMeshoptDecoder() call above is required to load these compressed files. Without it, Three.js will fail to parse the geometry buffers.
| Method | Use for | What happens |
|---|---|---|
gltf.scene.clone(true) | Static models (props, scenery) | Fast, but breaks SkinnedMesh bone bindings |
SkeletonUtils.clone(gltf.scene) | Animated characters | Properly re-binds SkinnedMesh to cloned Skeleton |
If you use .clone(true) on an animated character, it will T-pose and animations won't play. Always use SkeletonUtils.clone() for anything with skeletal animation.
The proven pattern from the official Three.js webgl_animation_walk example:
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// Setup
const orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.enablePan = false;
orbitControls.enableDamping = true;
orbitControls.maxPolarAngle = Math.PI / 2 - 0.05; // don't go underground
orbitControls.target.set(0, 1, 0);
// Each frame — move camera and target by same delta as player
const dx = player.position.x - oldX;
const dz = player.position.z - oldZ;
orbitControls.target.x += dx;
orbitControls.target.z += dz;
orbitControls.target.y = player.position.y + 1;
camera.position.x += dx;
camera.position.z += dz;
orbitControls.update();
const _v = new THREE.Vector3();
const _q = new THREE.Quaternion();
const _up = new THREE.Vector3(0, 1, 0);
// Get camera azimuth from OrbitControls
const azimuth = orbitControls.getAzimuthalAngle();
// Build input vector from WASD
let ix = 0, iz = 0;
if (keyW) iz -= 1;
if (keyS) iz += 1;
if (keyA) ix -= 1;
if (keyD) ix += 1;
// Rotate input by camera azimuth → world space movement
_v.set(ix, 0, iz).normalize();
_v.applyAxisAngle(_up, azimuth);
// Move player
player.position.addScaledVector(_v, speed * delta);
// Rotate model to face movement direction
// +PI offset because most GLB models face +Z but atan2 gives 0 for +Z
const angle = Math.atan2(_v.x, _v.z) + Math.PI;
_q.setFromAxisAngle(_up, angle);
model.quaternion.rotateTowards(_q, turnSpeed * delta);
fadeToAction(name, duration = 0.3) {
const next = actions[name];
if (!next || next === activeAction) return;
if (activeAction) activeAction.fadeOut(duration);
next.reset().setEffectiveTimeScale(1).setEffectiveWeight(1).fadeIn(duration).play();
activeAction = next;
}
// In update loop:
if (isMoving) {
fadeToAction(shiftHeld ? 'run' : 'walk');
} else {
fadeToAction('idle');
}
if (mixer) mixer.update(delta);
After loading ANY 3D model (Meshy-generated, library, or Sketchfab), always verify orientation and scale. Skipping this leads to backwards characters and models that overflow their containers.
rotationY per model in Constants.js. Start with Math.PI for Meshy models.position.y = -box.min.y to plant feet on groundSee the meshyai skill's "Post-Generation Verification" section for detailed code patterns.
.clone() instead of SkeletonUtils.clone(). The skeleton binding is broken.rotationY: Math.PI in Constants.js. Always verify with a screenshot.mixer.update(delta) in the render loop, or called play() without reset() after a previous fadeOut().camera.lookAt() when using OrbitControls. It manages lookAt internally.THREE.GridHelper) and place props near spawn so movement is visible.clips.map(c => c.name) on load and define a clipMap per character. Never hardcode clip names.Before starting, check if MESHY_API_KEY is available. If not, ask the user for one (see "Meshy API Key" section above). If the user skips, proceed with Tier 2+ fallbacks.
package.json to confirm Three.jsBoxGeometry, SphereGeometry, etc.| Entity | Model Source | Type | Notes |
|---|---|---|---|
| Player | Meshy text-to-3d → rig | Animated character | Custom generated + rigged |
| Enemy | Meshy text-to-3d → rig | Animated character | Custom generated + rigged |
| Tree | Meshy text-to-3d | Static prop | "a low poly stylized tree, game asset" |
| Barrel | Meshy text-to-3d | Static prop | "a wooden barrel, low poly game asset" |
If Meshy unavailable, fall back to library characters + find-3d-asset.mjs for props.
# With Meshy (preferred) — generate each entity
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode text-to-3d \
--prompt "a heroic knight, low poly game character, full body" \
--polycount 15000 --pbr \
--output public/assets/models/ --slug player
# Rig humanoid characters for animation
MESHY_API_KEY=<key> node scripts/meshy-generate.mjs \
--mode rig --task-id <refine-task-id> --height 1.7 \
--output public/assets/models/ --slug player-rigged
# Generate static props
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
# Fallback: library characters
cp <plugin-root>/assets/3d-characters/models/Soldier.glb public/assets/models/
# Fallback: search free libraries for props
node scripts/find-3d-asset.mjs --query "barrel" --source polyhaven --output public/assets/models/
src/level/AssetLoader.js with SkeletonUtils.clone() for animated modelsclipMap per modelOrbitControls camera with target-follow patternfadeToAction() for animation crossfadingapplyAxisAngle(_up, azimuth)Math.PI offset to model facing rotationTHREE.GridHelper to ground for visible movement referencenpm run dev and walk around with WASDnpm run build to confirm no errorsCause: Using .clone(true) instead of SkeletonUtils.clone() breaks skeleton bindings on animated GLB models.
Fix: Always use SkeletonUtils.clone() from three/addons/utils/SkeletonUtils.js for any model with animations. Regular .clone() copies the mesh but not the skeleton bindings.
Cause: The Sketchfab API requires authentication for model downloads, or the model license doesn't permit downloading.
Fix: Ensure SKETCHFAB_TOKEN is set in environment. Check the model's license on Sketchfab — only CC-licensed models can be downloaded via API. Try alternative sources (Poly Haven, Poly.pizza) which don't require auth for free models.
Cause: Some GLB files use meshopt compression which requires a decoder not loaded by default in Three.js.
Fix: Add the meshopt decoder before loading: import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'; loader.setMeshoptDecoder(MeshoptDecoder);
Cause: Animation clips not connected to the model's AnimationMixer, or clip names don't match expected values.
Fix: Create an AnimationMixer for the model, then use mixer.clipAction(clip).play(). Log gltf.animations.map(a => a.name) to see available clip names — they vary by model source. Define a clipMap per character to map generic names (idle, walk, run) to actual clip names.
Cause: Manual camera position updates conflict with OrbitControls trying to maintain its own camera state.
Fix: Don't set camera.position directly when using OrbitControls. Instead, update controls.target to follow the player, and let OrbitControls manage the camera position relative to the target. Call controls.update() once per frame in the animation loop.
.env file and passed only to the Meshy API. Generated models are downloaded as GLB binaries.MESHY_API_KEY, SKETCHFAB_TOKEN) are stored in the project's .env file (which should be gitignored) and loaded via environment variables. Keys are never embedded in game source code or deployed artifacts.AssetLoader.js uses SkeletonUtils.clone() for animated modelsclipMap defined per character model (clip names vary)OrbitControls with target-follow (not manual camera.lookAt)applyAxisAngle(_up, azimuth)+ Math.PI offset in atan2fadeToAction() pattern with reset() before fadeIn().play()mixer.update(delta) called every framedestroy() disposes geometry + materials + stops mixernpm run build succeeds