关于 game-3d-assets
game-3d-assets 是用于在 Three.js 游戏中生成、查找和集成 3D 资产的 Agent 技能。它提供分层模型来源流程,并说明动画模型、加载和验证的实用方法。
主要功能
- 01分层选择 3D 模型来源
- 02使用 Meshy AI 从文本或图像生成模型
- 03搜索并下载模型库资源
- 04动画 GLB 加载与克隆指南
- 05验证模型朝向、尺寸和地面贴合
适用场景
- 01在 Three.js 游戏中替换占位几何体
- 02查找角色、道具和场景模型
- 03正确处理骨骼以加入动画角色
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ playableintelligence/ game-creator --skill game-3d-assets安装后在 Claude Code 中输入/game-3d-assets确认已加载。
详细介绍
游戏 3D 资产工程师(模型流程)
你是一名资深 3D 游戏美术与集成工程师。你使用 Meshy AI 生成自定义 3D 模型,从免费素材库中查找模型,并将它们接入 Three.js 游戏,以可识别的 3D 模型替换基础几何体。
理念
基础立方体和球体能快速搭建场景,但玩家分不清房子和树。真实的 3D 模型——即使是低多边形模型——也能让每个实体拥有鲜明、可识别的特征。Meshy AI 是首选来源——它可以根据文本提示词或参考图片生成你所需的模型,风格一致,拓扑结构适合游戏使用。
资产等级
| 等级 | 来源 | 认证 | 适用场景 |
|---|---|---|---|
| 1. Meshy AI(首选) | meshy.ai | MESHY_API_KEY | 根据文本/图片生成自定义角色、道具和场景,精准匹配游戏主题 |
| 2. 预制角色库 | assets/3d-characters/ | 无 | Meshy 密钥不可用时,快速使用动画人形角色(Soldier、Xbot、Robot、Fox) |
| 3. Sketchfab | sketchfab.com | 下载需使用 SKETCHFAB_TOKEN | 已知所需模型时,查找特定的现有模型 |
| 4. Poly Haven | polyhaven.com | 无 | CC0 环境道具 |
| 5. Poly.pizza | poly.pizza | POLY_PIZZA_API_KEY | 10K+ 低多边形 CC-BY 模型 |
| 6. 程序化几何体(最后手段) | 代码 | 不适用 | BoxGeometry/SphereGeometry |
Meshy API 密钥
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 文件包含 Idle + Walk + Run 动画,可立即使用:
| 模型 | URL | 动画 | 大小 | 许可证 |
|---|---|---|---|---|
| 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/)
这些角色使用手势/表演动画,而非走路/跑步动画。最适合站立位置游戏(辩论、舞蹈对决、拳击、节奏游戏):
| 模型 | 动画 | 面部 | 大小 | 许可证 |
|---|---|---|---|---|
| Trump | StillLook(待机)、Clap、Dance、Point、Talk、Twist | 1,266 | 1.2 MB | CC-BY(Sketchfab) |
| Biden | 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/
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' }
**姿势角色的游戏设计:**由于这些角色没有走路/跑步动画,应设计角色保持静止的游戏,例如辩论对决、舞蹈对决、拳击场、节奏游戏或回合制遭遇战。仅在万不得已时,才在播放手势动画的同时以程序方式移动根节点。
使用 curl 下载——无需身份验证:
curl -L -o public/assets/models/Soldier.glb "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/Soldier.glb"
**每个模型的动画片段名称各不相同。**加载时务必记录动画片段名称,并为每个角色定义一个 clipMap:
// 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' }
角色选择 — 分层回退
对于游戏中的每个角色,请按顺序尝试以下层级:
第 1 层 — 使用 Meshy AI 生成(首选):生成符合游戏美术风格的自定义角色模型。这样效果最好——模型能与游戏的主题和风格完全一致。
# 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
绑定骨骼后,模型会附带基础的走路/跑步动画。记录动画片段名称,以构建 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/ 中选择最符合主题的模型:
- Soldier — 动作/军事/通用人类(默认)
- Xbot — 科幻/科技/未来风格
- RobotExpressive — 卡通/休闲风格(动画最多,共 13 个片段)
- Fox — 自然/动物
多角色游戏:使用 Meshy 时,为每个角色生成不同的描述,以丰富视觉效果。改用库中的模型时,为每个角色分配不同的模型(例如,一个使用 Soldier,另一个使用 Xbot)。
Sketchfab 令牌(第 3 层备用方案)
仅在改用 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
AssetLoader 工具
创建 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); // 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);
}
压缩 GLB 支持
由 scripts/optimize-glb.mjs 优化的 GLB(或由自动调用它的 meshy-generate.mjs / find-3d-asset.mjs 优化的 GLB)使用 meshopt 压缩。必须导入 MeshoptDecoder 并调用上述 loader.setMeshoptDecoder(),才能加载这些压缩文件。否则,Three.js 将无法解析几何缓冲区。
严重注意:SkeletonUtils.clone 与 .clone()
| 方法 | 适用场景 | 效果 |
|---|---|---|
gltf.scene.clone(true) | 静态模型(道具、场景) | 速度快,但会破坏 SkinnedMesh 骨骼绑定 |
SkeletonUtils.clone(gltf.scene) | 动画角色 | 正确地将 SkinnedMesh 重新绑定到克隆后的 Skeleton |
如果对动画角色使用 .clone(true),角色会摆出 T 姿势,动画也无法播放。任何带有骨骼动画的对象都应始终使用 SkeletonUtils.clone()。
第三人称角色控制器
官方 Three.js webgl_animation_walk 示例中的成熟模式:
摄像机:使用目标跟随的 OrbitControls
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();
移动:相对于摄像机的 WASD 控制
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 模式
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);
加载后验证(必须)
加载任何 3D 模型(Meshy 生成的模型、素材库模型或 Sketchfab 模型)后,始终验证朝向和缩放。跳过此步骤会导致角色朝向错误,以及模型超出容器范围。
- 加载后立即记录包围盒 — 尺寸和中心点
- 检查朝向 — 大多数 Meshy 模型面朝 +Z,大多数素材库模型则各不相同。为每个模型在 Constants.js 中设置
rotationY。Meshy 模型可先从Math.PI开始。 - 检查缩放 — 计算自动缩放值,使模型适合目标高度。确保模型能适应所在环境(例如,角色位于圆环内、玩家位于平台上)。
- 与地面齐平 — 设置
position.y = -box.min.y,使角色双脚落地 - 截取 Playwright 屏幕截图,直观确认朝向和适配情况。不要跳过这一步。
详细代码模式请参阅 meshyai 技能中的“生成后验证”部分。
常见问题
- 动画角色摆出 T 姿势 — 你使用了
.clone(),而不是SkeletonUtils.clone()。骨骼绑定已损坏。 - 模型朝向错误 — Meshy 模型通常面朝 +Z。在 Constants.js 中添加
rotationY: Math.PI。始终通过屏幕截图验证。 - 模型太大/超出容器范围 — 根据包围盒计算自动缩放值,使模型适配目标高度和容器边界。不要想当然地认为缩放值 1.0 就正确。
- 动画不播放 — 忘记在渲染循环中调用
mixer.update(delta),或者上一次fadeOut()后调用play(),却没有先调用reset()。 - 摄像机与 OrbitControls 相互干扰 — 使用 OrbitControls 时,绝不要调用
camera.lookAt()。OrbitControls 会在内部管理 lookAt。 - 感觉像“自由漂浮” — 摄像机完美跟随玩家,没有环境参照。添加网格(
THREE.GridHelper),并在出生点附近放置道具,以便看出移动效果。 - 不同模型的片段名称各不相同 — 加载时始终记录
clips.map(c => c.name),并为每个角色定义clipMap。绝不要硬编码片段名称。 - 跳过人形角色的绑定 — 静态模型需要用取巧的程序化动画。始终通过 Meshy 的绑定 API 为人形角色绑定骨骼,以实现正确的骨骼动画。
流程
步骤 0:检查 Meshy API 密钥
开始前,检查是否提供了 MESHY_API_KEY。如果没有,请向用户索取(参见上文“Meshy API Key”部分)。如果用户跳过,则使用 Tier 2 及更高层级的回退方案。
步骤 1:审查
- 阅读
package.json,确认使用了 Three.js - 检查实体文件中的
BoxGeometry、SphereGeometry等 - 列出所有使用几何形状的实体
步骤 2:规划
| 实体 | 模型来源 | 类型 | 备注 |
|---|---|---|---|
| 玩家 | Meshy 文本转 3D → 绑定骨骼 | 动画角色 | 自定义生成并绑定骨骼 |
| 敌人 | Meshy 文本转 3D → 绑定骨骼 | 动画角色 | 自定义生成并绑定骨骼 |
| 树木 | Meshy 文本转 3D | 静态道具 | “低多边形风格化树木,游戏素材” |
| 木桶 | Meshy 文本转 3D | 静态道具 | “木制木桶,低多边形游戏素材” |
如果无法使用 Meshy,则改用素材库角色,并使用 find-3d-asset.mjs 查找道具。
步骤 3:生成/下载
# 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/
步骤 4:集成
- 创建
src/level/AssetLoader.js,使用SkeletonUtils.clone()克隆动画模型 - 在 Constants.js 中为每个模型添加角色定义和
clipMap - 设置带目标跟随模式的
OrbitControls摄像机 - 实现用于动画交叉淡入淡出的
fadeToAction() - 使用基于摄像机方向的 WASD 移动,并通过
applyAxisAngle(_up, azimuth)旋转 - 在模型朝向旋转中增加
Math.PI偏移 - 在地面添加
THREE.GridHelper,作为可视化移动参照
步骤 5:验证
- 运行
npm run dev,使用 WASD 四处移动 - 确认角色会播放动画(停止时播放 Idle,移动时播放 Walk,按住 Shift 时播放 Run)
- 确认角色朝向移动方向
- 使用鼠标拖动旋转摄像机,使用滚轮缩放
- 运行
npm run build,确认没有错误
故障排除
模型卡在 T 姿势(不播放动画)
原因: 对动画 GLB 模型使用 .clone(true),而不是 SkeletonUtils.clone(),会破坏骨架绑定。
解决方法: 对任何带有动画的模型,始终使用 three/addons/utils/SkeletonUtils.js 中的 SkeletonUtils.clone()。普通的 .clone() 会复制网格,但不会复制骨架绑定。
Sketchfab 下载返回 403 Forbidden
原因: Sketchfab API 要求进行身份验证才能下载模型,或者模型许可证不允许下载。
解决方法: 确保已在环境中设置 SKETCHFAB_TOKEN。检查模型在 Sketchfab 上的许可证——只有采用 CC 许可证的模型才能通过 API 下载。也可以尝试其他来源(Poly Haven、Poly.pizza),它们提供的免费模型无需身份验证即可下载。
加载模型时出现 meshopt 解码器错误
原因: 某些 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,将通用名称(idle、walk、run)映射到实际剪辑名称。
摄像机与 OrbitControls 冲突(抖动)
原因: 手动更新摄像机位置会与 OrbitControls 维护自身摄像机状态的操作冲突。
解决方法: 使用 OrbitControls 时,不要直接设置 camera.position。应改为更新 controls.target 以跟随玩家,并让 OrbitControls 管理相对于目标的摄像机位置。在动画循环中每帧调用一次 controls.update()。
安全说明
- 第三方模型下载: 从 Sketchfab、Poly Haven 和 Poly.pizza 下载的模型是二进制 GLB/GLTF 文件(包含 3D 几何体、纹理和动画)。它们不是可执行代码,无法执行任意操作。Three.js 的 GLTFLoader 只会解析有效的 glTF 数据结构。
- Meshy AI API: Meshy 会根据文本/图像提示词在服务器端生成模型。API 密钥存储在本地
.env文件中,并且只会传递给 Meshy API。生成的模型以 GLB 二进制文件的形式下载。 - API 密钥存储: 所有 API 密钥(
MESHY_API_KEY、SKETCHFAB_TOKEN)都存储在项目的.env文件中(该文件应加入 gitignore),并通过环境变量加载。密钥不会嵌入游戏源代码或部署产物中。 - 内容范围: 下载的模型仅用作由 Three.js 渲染的视觉资源。不会将第三方来源中的文本内容解释为指令或命令。
检查清单
-
AssetLoader.js对动画模型使用SkeletonUtils.clone() - 为每个角色模型单独定义
clipMap(剪辑名称各不相同) - 使用带目标跟随的
OrbitControls(而非手动调用 camera.lookAt) - 使用
applyAxisAngle(_up, azimuth)实现相机相对的 WASD 移动 - 模型朝向旋转在
atan2中使用+ Math.PI偏移量 -
fadeToAction()模式中,在fadeIn().play()前调用reset() - 每帧调用
mixer.update(delta) - 使用地面网格或参考物体,确保移动效果可见
-
destroy()会释放几何体和材质,并停止 mixer -
npm run build执行成功