关于 worldlabs
worldlabs 是一个调用 World Labs Marble API 的 3D 环境生成技能,可从文本或参考图创建写实场景,面向制作游戏环境的开发者。它输出 Gaussian Splat 视觉场景及用于物理碰撞的网格等素材。
主要功能
- 01从文本或图像生成 3D 环境
- 02输出多档 SPZ 场景文件
- 03生成 GLB 碰撞网格与全景图
- 04通过 SparkJS 集成 Three.js
- 05提供可运行的街机示例
适用场景
- 01依据概念图创建游戏关卡环境
- 02为场景生成可用于物理碰撞的网格
- 03在 Three.js 游戏中加载高斯泼溅场景
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ playableintelligence/ game-creator --skill worldlabs安装后在 Claude Code 中输入/worldlabs确认已加载。
详细介绍
World Labs — 3D 世界/环境生成
使用 World Labs Marble API,根据文本提示词或图像生成逼真的 3D 环境。输出通过 SparkJS 在 Three.js 中渲染的 Gaussian Splat 场景(SPZ),以及用于物理模拟的碰撞网格(GLB)。
适用场景
- 环境/关卡生成 — 根据参考图像或文本创建完整的 3D 世界(房间、风景、建筑)
- 与 Meshy AI 互补 — Meshy 用于生成单独的模型/角色;World Labs 用于生成它们所处的环境
- 逼真场景 — 与基于网格的环境相比,Gaussian Splat 能提供更逼真的画质
输入优先级
图像优先 — 始终优先使用图像输入,而不是文本:
- 图像模式(默认) — 如果用户有参考图像、概念艺术图、屏幕截图或照片,请使用
--mode image。这种方式生成的结果最忠实,因为 AI 可以匹配精确的视觉风格、布局、光照和氛围。 - 文本模式(后备方案) — 仅在没有可用参考图像时使用
--mode text。API 会自动将简短提示词扩展为丰富的场景描述,但结果不如图像驱动的生成可预测。
运行游戏创建工作流时,先向用户询问参考图像:
我可以使用 World Labs 为你的游戏生成逼真的 3D 环境。 你有该环境的参考图像(照片、概念艺术图、屏幕截图)吗?
- 有 → 请提供文件路径或 URL
- 没有 → 我会改用文本描述生成
技术栈
| 组件 | 技术 |
|---|---|
| API | World Labs Marble API(https://api.worldlabs.ai/marble/v1) |
| 身份验证 | WLT-Api-Key 标头 |
| 输出:视觉 | Gaussian Splat(.spz)— 100k、500k、全分辨率档位 |
| 输出:物理 | 碰撞网格(.glb)— 用于碰撞检测 |
| 输出:天空盒 | 全景图像(.jpg/.png) |
| 浏览器渲染器 | SparkJS 2.x(@sparkjsdev/spark)— SparkRenderer + SplatMesh,兼容 Three.js |
| CLI 脚本 | scripts/worldlabs-generate.mjs(零依赖) |
环境变量
向用户询问之前,先检查密钥是否已存在:
test -f .env && grep -q '^WORLDLABS_API_KEY=.' .env && echo "found"
如果已找到,则使用 set -a; . .env; set +a 导出,并跳过询问。
如果未设置,则询问用户:
我将使用 World Labs 生成逼真的 3D 环境。你可以免费获取 API 密钥:
- 在 https://platform.worldlabs.ai 注册
- 前往 API Keys
- 创建一个新密钥
请在下方粘贴密钥,格式如下:
WORLDLABS_API_KEY=your-key-here(密钥会保存到 .env,并自动从本次对话中隐藏。)或输入“skip”改用基础几何体。
CLI 脚本用法
# Text to 3D world
WORLDLABS_API_KEY=<key> node scripts/worldlabs-generate.mjs \
--mode text --prompt "a medieval tavern with wooden beams and a roaring fireplace" \
--output public/assets/worlds/ --slug tavern
# Image to 3D world (local file or URL)
WORLDLABS_API_KEY=<key> node scripts/worldlabs-generate.mjs \
--mode image --image ./reference-photo.jpg \
--output public/assets/worlds/ --slug my-world
# Check generation status
WORLDLABS_API_KEY=<key> node scripts/worldlabs-generate.mjs \
--mode status --operation-id <op-id>
# Download assets from existing world
WORLDLABS_API_KEY=<key> node scripts/worldlabs-generate.mjs \
--mode get --world-id <id> --output public/assets/worlds/ --slug my-world
# List your worlds
WORLDLABS_API_KEY=<key> node scripts/worldlabs-generate.mjs --mode list
输出文件
public/assets/worlds/
tavern.spz # Gaussian Splat (full resolution)
tavern-500k.spz # Gaussian Splat (500k, medium quality)
tavern-100k.spz # Gaussian Splat (100k, lightweight/mobile)
tavern-collider.glb # Collider mesh for physics (GLB)
tavern-pano.jpg # Panorama image (skybox)
tavern.meta.json # Metadata: world ID, prompt, timestamps, asset URLs
集成到 Three.js 游戏
已测试并可正常运行 — 请参阅 examples/worldlabs-arcade/,其中包含一个完整且可运行的演示。
安装 SparkJS 2.0
npm install @sparkjsdev/spark@^2.0.0
包:@sparkjsdev/spark — 面向 Three.js 的高性能 Gaussian Splat 渲染器。请固定为 ^2.0.0;0.x 系列的 API 不同(没有 SparkRenderer),与下方代码片段不兼容。支持 SPZ、PLY、SOGS、KSPLAT、SPLAT 格式(根据扩展名自动检测)。
Three.js 对等依赖版本:Spark 2.0 将 three@^0.180.0 声明为对等依赖。Three 将每个 0.x 次要版本都视为可能包含破坏性变更的版本,因此 three@^0.181 或更高版本会在 npm install 时触发 ERESOLVE 对等依赖错误。请在项目的 package.json 中将 three 固定为 ^0.180.0 — threejs-3d 模板和 worldlabs-arcade 示例都使用此版本。
没有打包器? 使用 CDN importmap 代替 npm:
<script type="importmap">{
"imports": {
"three": "https://cdnjs.cloudflare.com/ajax/libs/three.js/0.180.0/three.module.js",
"@sparkjsdev/spark": "https://sparkjs.dev/releases/spark/2.0.0/spark.module.js"
}
}</script>
WebGLRenderer 配置:创建时设置 antialias: false。Spark 的 splat 着色器自带抗锯齿,在此基础上启用 MSAA 会浪费 GPU 资源,却不会提升画面效果:
const renderer = new THREE.WebGLRenderer({ antialias: false });
Constants.js — 世界配置
export const WORLD = {
splatPath: 'assets/worlds/tavern-500k.spz', // 500k is a good desktop default
colliderPath: 'assets/worlds/tavern-collider.glb',
panoPath: 'assets/worlds/tavern-pano.png',
scale: 1,
position: { x: 0, y: 0, z: 0 },
};
WorldLoader.js — 加载高斯泼溅 + 碰撞体
Spark 2.0 引入了 SparkRenderer——一个场景级对象,可接入 Three.js 的渲染管线,并控制泼溅的质量、排序和 LOD。实例化一次并将其添加到场景中,然后像添加普通 Three.js 对象一样添加 SplatMesh 实例。WebGLRenderer 的 render() 调用会透明地处理所有工作。
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { SparkRenderer, SplatMesh } from '@sparkjsdev/spark';
import { WORLD } from '../core/Constants.js';
let _colliderMesh = null;
let _splatMesh = null;
export async function loadWorld(scene, renderer, camera) {
// 0. SparkRenderer — quality + LOD controls for splat rendering.
// Must be added to the scene BEFORE any SplatMesh. One per scene.
const spark = new SparkRenderer({
renderer,
maxPixelRadius: 512, // bigger max → sharper close-up splats
sortRadial: true, // eliminates black-bar artifacts on camera rotation
enableLod: true, // auto-reduce splat count at distance
lodSplatScale: 1.0, // >1.0 = more splats (higher quality), <1.0 = fewer
});
scene.add(spark);
const promises = [];
// 1. Gaussian Splat via SparkJS — SplatMesh works like any Three.js object.
// `splat.initialized` is a Promise that resolves once the file is parsed
// and GPU buffers are uploaded.
if (WORLD.splatPath) {
promises.push((async () => {
const splat = new SplatMesh({ url: WORLD.splatPath });
splat.scale.setScalar(WORLD.scale);
splat.position.set(WORLD.position.x, WORLD.position.y, WORLD.position.z);
scene.add(splat);
if (splat.initialized) await splat.initialized;
_splatMesh = splat;
})());
}
// 2. Collider mesh (GLB) — invisible, for physics raycasting only
if (WORLD.colliderPath) {
promises.push((async () => {
const loader = new GLTFLoader();
const gltf = await loader.loadAsync(WORLD.colliderPath);
_colliderMesh = gltf.scene;
_colliderMesh.visible = false;
_colliderMesh.scale.setScalar(WORLD.scale);
_colliderMesh.position.set(WORLD.position.x, WORLD.position.y, WORLD.position.z);
_colliderMesh.traverse(c => { if (c.isMesh) c.material.side = THREE.DoubleSide; });
_colliderMesh.updateMatrixWorld(true); // needed before the first raycast
scene.add(_colliderMesh);
})());
}
// 3. Panorama as equirectangular skybox + environment lighting.
// NOTE: leave this OFF when using a World Labs splat scene — the panorama
// depicts the same environment as the splat and you get a "world inside a
// world" doubling effect. For IBL on Meshy characters, prefer
// `spark.renderEnvMap({ scene, worldCenter })` to light from the splat itself.
if (WORLD.panoPath) {
promises.push((async () => {
const texLoader = new THREE.TextureLoader();
const panoTex = await texLoader.loadAsync(WORLD.panoPath);
panoTex.mapping = THREE.EquirectangularReflectionMapping;
panoTex.colorSpace = THREE.SRGBColorSpace;
scene.environment = panoTex; // lighting only — NOT scene.background
})());
}
await Promise.all(promises);
return { splat: _splatMesh, collider: _colliderMesh };
}
const _raycaster = new THREE.Raycaster();
const _downDir = new THREE.Vector3(0, -1, 0);
const _rayOrigin = new THREE.Vector3();
export function getGroundHeight(x, z, fallback = 0) {
if (!_colliderMesh) return fallback;
_rayOrigin.set(x, 50, z);
_raycaster.set(_rayOrigin, _downDir);
const hits = _raycaster.intersectObject(_colliderMesh, true);
return hits.length > 0 ? hits[0].point.y : fallback;
}
export function getCollider() { return _colliderMesh; }
Game.js — 渲染循环集成
将 SparkRenderer 添加到场景后,泼溅渲染会透明地进行。SparkRenderer.autoUpdate 默认为 true,因此 renderer.render(scene, camera) 会处理场景中每个 SplatMesh 的排序和绘制,无需额外调用 .render() 或 .update()。使用 renderer.setAnimationLoop(Spark 2.0 推荐的循环形式,也兼容 WebXR)。
import { loadWorld, getGroundHeight } from '../level/WorldLoader.js';
// In init():
await loadWorld(scene, renderer, camera);
// Render loop — standard Three.js, no extra splat pass:
renderer.setAnimationLoop((time) => {
const delta = clock.getDelta();
player.update(delta, input, azimuth);
// Snap player Y to collider ground
const groundY = getGroundHeight(player.mesh.position.x, player.mesh.position.z, 0);
player.mesh.position.y = groundY;
// Single render call handles both meshes AND splats
renderer.render(scene, camera);
});
分辨率档位
| 档位 | 文件 | 质量 | 用途 |
|---|---|---|---|
100k | {slug}-100k.spz | 低 | 移动设备、快速加载、预览 |
500k | {slug}-500k.spz | 中 | 桌面游戏、良好平衡 |
full_res | {slug}.spz | 高 | 高端设备、主打环境 |
根据目标平台进行选择。无论泼溅分辨率如何,碰撞体网格(GLB)都相同。
Spark 2.0 质量调优
默认的 SparkRenderer({ renderer }) 对大多数场景来说效果不错。如果效果不理想,以下四个参数可以显著改善效果——在 SparkRenderer 构造函数中传入:
| 现象 | 参数 | 调整方向 |
|---|---|---|
| 近距离泼溅显得粗糙/分辨率低 | lodSplatScale | 调高至接近 2.0(渲染的泼溅数量为 2 倍) |
| 泼溅在远处明显地出现/消失 | lodRenderScale | 调高至接近 2.0,让远处的泼溅在屏幕上保持更大 |
| 摄像机旋转时出现黑边或画面撕裂 | sortRadial | 保持为 true(默认值)。仅在合成时需要严格的 Z 深度时设为 false。 |
| 低端 GPU/移动设备性能不佳 | lodSplatScale + maxPixelRadius | 将 lodSplatScale 降至 0.5,maxPixelRadius 降至 256 |
来自泼溅场景的 IBL——Spark 2.0 新增了 spark.renderEnvMap({ scene, worldCenter }),可将泼溅场景渲染为 THREE.Texture,用于 scene.environment 或 MeshStandardMaterial.envMap。这是让 Meshy 角色捕捉其所处 World Labs 环境光照的正确方式。完整选项请参阅 SparkRenderer 文档。
工作流:World Labs + Meshy AI
若要创建完整的 3D 游戏,请结合使用两者:
- World Labs → 生成环境(房间、景观、竞技场)
- Meshy AI → 生成角色和道具(玩家、敌人、物品)
- 集成 → 角色在 World Labs 碰撞体网格上行走,并在高斯泼溅场景中渲染
┌─────────────────────────────────────────────────┐
│ Complete 3D Scene │
├─────────────────────────────────────────────────┤
│ World Labs (environment) │
│ └─ Gaussian Splat (visual) │
│ └─ Collider mesh (physics) │
│ └─ Panorama (skybox) │
│ │
│ Meshy AI (entities) │
│ └─ Player character (rigged, animated GLB) │
│ └─ Enemies (rigged, animated GLB) │
│ └─ Props/items (static GLB) │
│ │
│ Three.js (engine) │
│ └─ SparkJS renders splats │
│ └─ GLTFLoader renders characters/props │
│ └─ Raycaster uses collider for ground/walls │
└─────────────────────────────────────────────────┘
参考实现
参见 examples/worldlabs-arcade/——一个完整且经过测试的演示,包含:
- World Labs 高斯泼溅环境(复古街机)
- 动画士兵角色(行走/奔跑/待机)
- OrbitControls 第三人称摄像机
- 碰撞体网格地面射线检测
- 全景天空盒
故障排除
场景上下颠倒(Y 轴翻转)
**原因:**与 Three.js 的坐标约定相比,World Labs SPZ 文件使用 Y 轴反转的坐标。
**修复:**对泼溅网格和碰撞体网格都应用 rotation.x = Math.PI。然后调整 position.z 进行补偿:position.z += (minZ + maxZ)。不要在父级组上使用 scale.y = -1——SparkJS 无法处理负的父级缩放。
射线检测命中天花板而不是地板
**原因:**Y 轴翻转后,坐标系反转。向下发射的射线会命中原本的地板(翻转后的空间中变成了天花板)。
**修复:**从 Y=-50 处沿 (0, 1, 0) 方向向上发射射线,优先命中视觉上的地板。翻转后,地板是最低的表面。
碰撞体网格射线检测未命中
**原因:**设置旋转/位置后,碰撞体网格的世界矩阵尚未更新,尤其是在第一帧渲染之前。
**修复:**设置旋转和位置后、执行任何射线检测操作前,立即调用 _colliderMesh.updateMatrixWorld(true)。
场景看起来重影/世界中还有一个世界
原因: 将 World Labs 全景图用作 scene.background,会让同一个环境以巨大球体的形式包围 3D 场景。
解决方法: 不要将全景图用作场景背景。改用纯色(scene.background = new THREE.Color(0x87CEEB))或自定义天空盒。
生成超时或耗时过长
原因: World Labs 生成通常需要 3-8 分钟。复杂场景或服务器负载较高时,耗时可能更长。
解决方法: 每隔 10-15 秒轮询一次操作状态端点。检查 progress.status 是 "IN_PROGRESS" 还是 "COMPLETE"。如果超过 15 分钟仍无进展,请创建新的生成请求,不要重试同一操作。
Splat 文件已加载,但没有任何内容渲染(场景为黑屏)
原因: 在 Spark 2.0 中,实际绘制 splat 的是 SparkRenderer。如果将 SplatMesh 添加到场景中,却没有同时将 SparkRenderer 添加到场景中,就不会有任何输出。
解决方法: 在添加任何 SplatMesh 之前,先实例化 new SparkRenderer({ renderer }) 并执行 scene.add(spark)。每个场景只需一个 SparkRenderer。
Spark API 错误/方法不存在
原因: 安装的是 0.x,而不是 2.x。0.x 系列早于 SparkRenderer、LOD 和 sortRadial。
解决方法: 固定使用 2.0 或更高版本:npm install @sparkjsdev/spark@^2.0.0。检查 node_modules/@sparkjsdev/spark/package.json 以确认版本。
摄像机移动时,Splat 忽隐忽现
原因: LOD 系统正在切换层级。对于大型世界来说这没问题,但在小场景中会很碍眼。
解决方法: 提高 lodRenderScale,让远处的 splat 在视觉上更大;或者对于可以在各处都以完整细节渲染的小场景,设置 enableLod: false。
角色看起来没有光照/与 splat 环境不协调
原因: Meshy(或其他 GLB)角色使用 MeshStandardMaterial,但没有环境贴图,因此在照片级写实的 splat 背景上会显得平淡。
解决方法: 使用 spark.renderEnvMap({ scene, worldCenter }) 将 splat 场景烘焙到 THREE.Texture 中并赋值:scene.environment = envMap。或者通过 spark.recurseSetEnvMap(character, envMap) 按材质赋值。
媒体上传返回 404
原因: 媒体上传准备使用了错误的端点。
解决方法: 使用 POST /media-assets:prepare_upload(而不是 /media-assets)。这会返回用于 PUT 上传的签名 URL。冒号语法是有意设计的,它表示对该资源执行自定义操作。
API 返回 401 Unauthorized
原因: 使用了错误的身份验证标头格式。
解决方法: 使用 WLT-Api-Key: <your-key> 标头,而不是 Authorization: Bearer <your-key>。World Labs API 使用自定义标头格式。
检查清单
- 已设置
WORLDLABS_API_KEY环境变量 - 先向用户索取参考图像(优先使用图像转世界)
- 运行
scripts/worldlabs-generate.mjs生成世界(约 3-8 分钟) - 已将 SPZ + collider GLB + 全景图下载到
public/assets/worlds/ - 已安装
@sparkjsdev/spark@^2.0.0(不是 0.x——API 不同) - 创建
WebGLRenderer时使用了antialias: false - 已在
src/level/中创建WorldLoader.js(SparkRenderer + SplatMesh + GLTFLoader) - 在添加任何
SplatMesh之前,已实例化SparkRenderer并将其添加到场景中 - 将 splat 视为已加载前,已使用
await splat.initialized - 已使用
WORLD配置(splatPath、colliderPath、panoPath)更新Constants.js -
Game.js在初始化时调用loadWorld(),并使用getGroundHeight()获取玩家的 Y 坐标 - 单次
renderer.render(scene, camera)同时处理 splat 和网格——无需额外渲染通道 - 物理系统使用不可见的碰撞网格进行地面/墙壁射线检测
- 测试:角色能在碰撞表面上行走,splat 在角色周围渲染
- 性能:桌面端使用 500k SPZ,移动端使用 100k;如有需要,调整
lodSplatScale