game-3d-assets by opusgamelabs/game-creator
npx skills add https://github.com/opusgamelabs/game-creator --skill game-3d-assets你是一位专业的 3D 游戏美术师和集成专家。你使用 Meshy AI 生成自定义 3D 模型,从免费库中寻找模型,并将它们集成到 Three.js 游戏中——用可识别的 3D 模型替换原始的几何体。
原始的立方体和球体可以快速搭建原型,但玩家无法区分房屋和树木。真实的 3D 模型——即使是低多边形模型——也能赋予每个实体可识别的身份。Meshy AI 是首选来源——它可以根据文本提示或参考图像生成你所需的内容,风格一致且拓扑结构适合游戏。
| 层级 | 来源 | 认证 | 最适合 |
|---|---|---|---|
| 1. Meshy AI (首选) | meshy.ai | MESHY_API_KEY | 根据文本/图像生成自定义角色、道具和场景——与游戏主题完全匹配 |
| 2. 预构建角色库 | assets/3d-characters/ | 无 | 当 Meshy 密钥不可用时,快速获取动画人形角色(士兵、Xbot、机器人、狐狸) |
| 3. Sketchfab | sketchfab.com |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
SKETCHFAB_TOKEN 用于下载 |
| 当你确切知道需要什么时,寻找特定的现有模型 |
| 4. Poly Haven | polyhaven.com | 无 | CC0 环境道具 |
| 5. Poly.pizza | poly.pizza | POLY_PIZZA_API_KEY | 10K+ 低多边形 CC-BY 模型 |
| 6. 程序化几何体 (最后手段) | 代码 | N/A | BoxGeometry/SphereGeometry |
Meshy AI 是所有 3D 资产的首选来源。在提示用户之前,检查密钥是否已存在:test -f .env && grep -q '^MESHY_API_KEY=.' .env && echo "found" 如果找到,使用 set -a; . .env; set +a 导出它并跳过提示。
如果 MESHY_API_KEY 未设置,在回退到其他层级之前询问用户:
我希望使用 Meshy AI 生成自定义 3D 模型以获得最佳效果。你可以获取一个免费的 API 密钥:
- 在 https://app.meshy.ai 注册
- 前往 Settings → API Keys
- 创建一个新的 API 密钥
将你的密钥粘贴在下方,格式如:
MESHY_API_KEY=your-key-here(它将自动保存到 .env 并从本次对话中隐藏。)或者输入 "skip" 以使用免费模型库。
如果用户提供了密钥,将其用于所有 meshy-generate.mjs 调用。如果他们跳过,则回退到第 2 层及以后。
这些来自 Three.js 仓库的 GLB 文件具有空闲 + 行走 + 奔跑动画,并且可以立即使用:
| 模型 | URL | 动画 | 大小 | 许可证 |
|---|---|---|---|---|
| 士兵 | https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/Soldier.glb | 空闲、行走、奔跑、T姿势 | 2.2 MB | MIT |
| Xbot | https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/Xbot.glb | 空闲、行走、奔跑 + 附加姿势 | 2.9 MB | MIT |
| RobotExpressive | https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/RobotExpressive/RobotExpressive.glb | 空闲、行走、奔跑、舞蹈、跳跃 + 8 个其他 | 464 KB | MIT |
| 狐狸 | https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/Fox/glTF-Binary/Fox.glb | 观察(空闲)、行走、奔跑 | 163 KB | CC0/CC-BY 4.0 |
assets/3d-characters/)这些角色具有手势/表演动画,而不是行走/奔跑。最适合站立位置游戏(辩论、舞蹈对决、拳击、节奏游戏):
| 模型 | 动画 | 面数 | 大小 | 许可证 |
|---|---|---|---|---|
| 特朗普 | StillLook (空闲)、鼓掌、跳舞、指向、说话、扭转 | 1,266 | 1.2 MB | CC-BY (Sketchfab) |
| 拜登 | 1 个 Mixamo 空闲/摇摆 | 50,000 | 3.3 MB | CC-BY (Sketchfab) |
从角色库复制(无需认证):
cp <plugin>/assets/3d-characters/models/trump.glb public/assets/models/
cp <plugin>/assets/3d-characters/models/biden.glb public/assets/models/
特朗普 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'
}
拜登 clipMap:
{ idle: 'mixamo.com' }
手势角色的游戏设计: 由于这些角色缺乏行走/奔跑动画,请设计角色静止的游戏——辩论战斗、舞蹈对决、拳击场、节奏游戏或回合制遭遇战。仅在最后手段时使用程序化的根骨骼运动(播放手势时平移模型)。
使用 curl 下载——无需认证:
curl -L -o public/assets/models/Soldier.glb "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/Soldier.glb"
剪辑名称映射因模型而异。 总是在加载时记录剪辑名称,并为每个角色定义一个 clipMap:
// 士兵: { idle: 'Idle', walk: 'Walk', run: 'Run' }
// Xbot: { idle: 'idle', walk: 'walk', run: 'run' } (小写)
// 机器人: { idle: 'Idle', walk: 'Walking', run: 'Running' }
// 狐狸: { idle: 'Survey', walk: 'Walk', run: 'Run' }
对于游戏中的每个角色,按顺序尝试这些层级:
层级 1 — 使用 Meshy AI 生成 (首选):生成与游戏美术方向匹配的自定义角色模型。这会产生最佳效果——完全匹配你游戏主题和风格的模型。
# 根据文本描述生成角色
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>
# 然后为动画绑定骨骼(人形角色)
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
绑定骨骼后,模型会附带基本的行走/奔跑动画。记录剪辑名称以构建 clipMap。
对于命名的人物,请使用描述性语言:"a cartoon caricature of <Name>, <hair/glasses/suit details>, low poly game character"。
层级 2 — 预构建在 assets/3d-characters/ 中:如果 Meshy 不可用,检查 manifest.json 以匹配名称/主题。复制 GLB 文件。完成。
层级 3 — 在 Sketchfab 中搜索特定角色模型:使用 find-3d-asset.mjs:
node scripts/find-3d-asset.mjs --query "<character name> animated character" --max-faces 10000 --list-only
层级 4 — 通用库回退:使用 assets/3d-characters/ 中最佳的主题匹配:
多角色游戏:当使用 Meshy 时,为每个角色生成具有不同描述的模型,以实现视觉多样性。当回退到库模型时,为每个角色分配不同的模型(例如,一个用士兵,另一个用 Xbot)。
仅在回退到 Sketchfab 时需要。搜索是免费的,但下载需要 SKETCHFAB_TOKEN。在提示之前,检查密钥是否已存在:test -f .env && grep -q '^SKETCHFAB_TOKEN=.' .env && echo "found" 如果找到,使用 set -a; . .env; set +a 导出它并跳过提示。
如果需要但未设置,询问用户:
我需要一个 Sketchfab API 令牌来下载此模型。你可以免费获取一个:
- 在 https://sketchfab.com 登录
- 前往 https://sketchfab.com/settings/password → "API Token"
- 复制令牌
将你的令牌粘贴在下方,格式如:
SKETCHFAB_TOKEN=your-token-here(它将自动保存到 .env 并从本次对话中隐藏。)
然后通过以下方式使用:set -a; . .env; set +a && node scripts/find-3d-asset.mjs ...
使用 scripts/find-3d-asset.mjs 进行角色搜索和非角色模型(道具、场景、建筑)搜索:
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
创建 src/level/AssetLoader.js。关键:对动画模型使用 SkeletonUtils.clone()——常规的 .clone() 会破坏骨骼绑定并导致 T 姿势。
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); // 加载 meshopt 压缩的 GLB 文件所必需
const cache = new Map();
/** 加载静态(非动画)模型。使用常规克隆。 */
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;
}
/** 加载动画(骨骼)模型。使用 SkeletonUtils.clone 以保留骨骼绑定。 */
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);
}
由 scripts/optimize-glb.mjs(或自动调用它的 meshy-generate.mjs / find-3d-asset.mjs)优化的 GLB 使用 meshopt 压缩。上面的 MeshoptDecoder 导入 + loader.setMeshoptDecoder() 调用是加载这些压缩文件所必需的。没有它,Three.js 将无法解析几何缓冲区。
| 方法 | 用于 | 会发生什么 |
|---|---|---|
gltf.scene.clone(true) | 静态模型(道具、场景) | 快速,但破坏 SkinnedMesh 骨骼绑定 |
SkeletonUtils.clone(gltf.scene) | 动画角色 | 正确地将 SkinnedMesh 重新绑定到克隆的骨骼 |
如果你在动画角色上使用 .clone(true),它将呈现T 姿势且动画不会播放。对于任何带有骨骼动画的内容,始终使用 SkeletonUtils.clone()。
来自官方 Three.js webgl_animation_walk 示例的成熟模式:
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 设置
const orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.enablePan = false;
orbitControls.enableDamping = true;
orbitControls.maxPolarAngle = Math.PI / 2 - 0.05; // 不要进入地下
orbitControls.target.set(0, 1, 0);
// 每帧——相机和目标随玩家移动相同的增量
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);
// 从 OrbitControls 获取相机方位角
const azimuth = orbitControls.getAzimuthalAngle();
// 根据 WASD 构建输入向量
let ix = 0, iz = 0;
if (keyW) iz -= 1;
if (keyS) iz += 1;
if (keyA) ix -= 1;
if (keyD) ix += 1;
// 将输入向量绕相机方位角旋转 → 世界空间移动
_v.set(ix, 0, iz).normalize();
_v.applyAxisAngle(_up, azimuth);
// 移动玩家
player.position.addScaledVector(_v, speed * delta);
// 旋转模型以面向移动方向
// +PI 偏移,因为大多数 GLB 模型面向 +Z,但 atan2 对于 +Z 给出 0
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;
}
// 在更新循环中:
if (isMoving) {
fadeToAction(shiftHeld ? 'run' : 'walk');
} else {
fadeToAction('idle');
}
if (mixer) mixer.update(delta);
加载任何 3D 模型(Meshy 生成、库或 Sketchfab)后,始终验证方向和缩放。跳过此步骤会导致角色朝向错误和模型溢出容器。
rotationY。对于 Meshy 模型,从 Math.PI 开始。position.y = -box.min.y 以使脚部着地有关详细代码模式,请参阅 meshyai 技能的“生成后验证”部分。
.clone() 而不是 SkeletonUtils.clone()。骨骼绑定被破坏。rotationY: Math.PI。始终用截图验证。mixer.update(delta),或者在之前的 fadeOut() 之后调用 play() 而没有调用 reset()。camera.lookAt()。它内部管理 lookAt。THREE.GridHelper)并在生成点附近放置道具,以便移动可见。clips.map(c => c.name) 并为每个角色定义 clipMap。切勿硬编码剪辑名称。开始之前,检查 MESHY_API_KEY 是否可用。如果不可用,向用户请求一个(参见上面的“Meshy API 密钥”部分)。如果用户跳过,则继续使用第 2 层及以上的回退方案。
package.json 以确认 Three.jsBoxGeometry、SphereGeometry 等| 实体 | 模型来源 | 类型 | 备注 |
|---|---|---|---|
| 玩家 | Meshy text-to-3d → rig | 动画角色 | 自定义生成 + 骨骼绑定 |
| 敌人 | Meshy text-to-3d → rig | 动画角色 | 自定义生成 + 骨骼绑定 |
| 树木 | Meshy text-to-3d | 静态道具 | "a low poly stylized tree, game asset" |
| 木桶 | Meshy text-to-3d | 静态道具 | "a wooden barrel, low poly game asset" |
如果 Meshy 不可用,则回退到库角色 + 使用 find-3d-asset.mjs 获取道具。
# 使用 Meshy (首选) —— 生成每个实体
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
# 为人形角色绑定骨骼以进行动画
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
# 生成静态道具
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
# 回退:库角色
cp <plugin-root>/assets/3d-characters/models/Soldier.glb public/assets/models/
# 回退:在免费库中搜索道具
node scripts/find-3d-asset.mjs --query "barrel" --source polyhaven --output public/assets/models/
src/level/AssetLoader.js,对动画模型使用 SkeletonUtils.clone()clipMapOrbitControls 相机fadeToAction() 用于动画交叉淡入淡出applyAxisAngle(_up, azimuth) 的相机相对 WASD 移动Math.PI 偏移THREE.GridHelper 作为可见的移动参考npm run dev 并使用 WASD 四处走动npm run build 以确认没有错误原因: 使用 .clone(true) 而不是 SkeletonUtils.clone() 会破坏动画 GLB 模型的骨骼绑定。修复: 对于任何带有动画的模型,始终使用来自 three/addons/utils/SkeletonUtils.js 的 SkeletonUtils.clone()。常规的 .clone() 会复制网格但不会复制骨骼绑定。
原因: Sketchfab API 需要身份验证才能下载模型,或者模型许可证不允许下载。修复: 确保环境中设置了 SKETCHFAB_TOKEN。检查 Sketchfab 上的模型许可证——只有 CC 许可的模型才能通过 API 下载。尝试其他不需要免费模型认证的来源(Poly Haven、Poly.pizza)。
原因: 一些 GLB 文件使用 meshopt 压缩,这需要 Three.js 默认未加载的解码器。修复: 在加载前添加 meshopt 解码器:import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'; loader.setMeshoptDecoder(MeshoptDecoder);
原因: 动画剪辑未连接到模型的 AnimationMixer,或者剪辑名称与预期值不匹配。修复: 为模型创建一个 AnimationMixer,然后使用 mixer.clipAction(clip).play()。记录 gltf.animations.map(a => a.name) 以查看可用的剪辑名称——它们因模型来源而异。为每个角色定义一个 clipMap,以将通用名称(空闲、行走、奔跑)映射到实际的剪辑名称。
原因: 手动更新相机位置与 OrbitControls 试图维护其自身相机状态冲突。修复: 使用 OrbitControls 时不要直接设置 camera.position。相反,更新 controls.target 以跟随玩家,并让 OrbitControls 管理相机相对于目标的位置。在动画循环中每帧调用一次 controls.update()。
AssetLoader.js 对动画模型使用 SkeletonUtils.clone()clipMap(剪辑名称各不相同)OrbitControls(而非手动 camera.lookAt)applyAxisAngle(_up, azimuth) 实现相机相对 WASDatan2 中使用 + Math.PI 偏移fadeToAction() 模式,在 fadeIn().play() 之前调用 reset()mixer.update(delta)destroy() 会释放几何体 + 材质 + 停止混合器npm run build 成功每周安装量
96
仓库
GitHub 星标数
28
首次出现
2026年2月26日
安全审计
安装于
claude-code79
opencode53
gemini-cli52
kimi-cli52
codex52
cursor52
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 |
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 inassets/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 requiresSKETCHFAB_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: useSkeletonUtils.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.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.
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()Weekly Installs
96
Repository
GitHub Stars
28
First Seen
Feb 26, 2026
Security Audits
Gen Agent Trust HubPassSocketWarnSnykFail
Installed on
claude-code79
opencode53
gemini-cli52
kimi-cli52
codex52
cursor52
AI 代码实施计划编写技能 | 自动化开发任务分解与 TDD 流程规划工具
50,900 周安装
| 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 |
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.mixer.update(delta) called every framedestroy() disposes geometry + materials + stops mixernpm run build succeeds