mcpField
W
worldlabs

worldlabs

github.com/playableintelligence创意与媒体350832 次安装22 周前更新
代码仓库

关于 worldlabs

worldlabs 是一个调用 World Labs Marble API 的 3D 环境生成技能,可从文本或参考图创建写实场景,面向制作游戏环境的开发者。它输出 Gaussian Splat 视觉场景及用于物理碰撞的网格等素材。

主要功能

  1. 01从文本或图像生成 3D 环境
  2. 02输出多档 SPZ 场景文件
  3. 03生成 GLB 碰撞网格与全景图
  4. 04通过 SparkJS 集成 Three.js
  5. 05提供可运行的街机示例

适用场景

  1. 01依据概念图创建游戏关卡环境
  2. 02为场景生成可用于物理碰撞的网格
  3. 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 能提供更逼真的画质

输入优先级

图像优先 — 始终优先使用图像输入,而不是文本:

  1. 图像模式(默认) — 如果用户有参考图像、概念艺术图、屏幕截图或照片,请使用 --mode image。这种方式生成的结果最忠实,因为 AI 可以匹配精确的视觉风格、布局、光照和氛围。
  2. 文本模式(后备方案) — 仅在没有可用参考图像时使用 --mode text。API 会自动将简短提示词扩展为丰富的场景描述,但结果不如图像驱动的生成可预测。

运行游戏创建工作流时,先向用户询问参考图像:

我可以使用 World Labs 为你的游戏生成逼真的 3D 环境。 你有该环境的参考图像(照片、概念艺术图、屏幕截图)吗?

  • 有 → 请提供文件路径或 URL
  • 没有 → 我会改用文本描述生成

技术栈

组件技术
APIWorld 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 密钥:

  1. 在 https://platform.worldlabs.ai 注册
  2. 前往 API Keys
  3. 创建一个新密钥

请在下方粘贴密钥,格式如下: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 游戏,请结合使用两者:

  1. World Labs → 生成环境(房间、景观、竞技场)
  2. Meshy AI → 生成角色和道具(玩家、敌人、物品)
  3. 集成 → 角色在 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

评论

支持 Markdown