重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
worldlabs by opusgamelabs/game-creator
npx skills add https://github.com/opusgamelabs/game-creator --skill worldlabs使用 World Labs Marble API,通过文本提示或图像生成逼真的 3D 环境。输出通过 SparkJS 在 Three.js 中渲染的高斯泼溅场景(SPZ),以及用于物理的碰撞体网格(GLB)。
图像优先 — 始终优先使用图像输入而非文本:
--mode image。这能产生最忠实的结果,因为 AI 可以匹配确切的视觉风格、布局、光照和氛围。--mode text。API 会自动将简短提示扩展为丰富的场景描述,但结果比图像驱动的生成更具不可预测性。当游戏创建者流水线运行时,首先向用户询问参考图像:
我可以使用 World Labs 为你的游戏生成逼真的 3D 环境。你有环境的参考图像(照片、概念图、截图)吗?
- 有 → 提供文件路径或 URL
- 没有 → 我将根据文本描述生成
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 组件 | 技术 |
|---|
| API | World Labs Marble API (https://api.worldlabs.ai/marble/v1) |
| 认证 | WLT-Api-Key 请求头 |
| 输出:视觉 | 高斯泼溅 (.spz) — 100k、500k、全分辨率层级 |
| 输出:物理 | 碰撞体网格 (.glb) — 用于碰撞检测 |
| 输出:天空盒 | 全景图像 (.jpg/.png) |
| 浏览器渲染器 | SparkJS (@worldlabs/spark) — 兼容 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 密钥
- 创建一个新密钥
在下方粘贴你的密钥,格式如:
WORLDLABS_API_KEY=your-key-here(它将自动保存到 .env 文件并从本次对话中隐去。)或者输入 "skip" 以使用基础几何体代替。
# 文本转 3D 世界
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
# 图像转 3D 世界(本地文件或 URL)
WORLDLABS_API_KEY=<key> node scripts/worldlabs-generate.mjs \
--mode image --image ./reference-photo.jpg \
--output public/assets/worlds/ --slug my-world
# 检查生成状态
WORLDLABS_API_KEY=<key> node scripts/worldlabs-generate.mjs \
--mode status --operation-id <op-id>
# 从现有世界下载资源
WORLDLABS_API_KEY=<key> node scripts/worldlabs-generate.mjs \
--mode get --world-id <id> --output public/assets/worlds/ --slug my-world
# 列出你的世界
WORLDLABS_API_KEY=<key> node scripts/worldlabs-generate.mjs --mode list
public/assets/worlds/
tavern.spz # 高斯泼溅(全分辨率)
tavern-500k.spz # 高斯泼溅(500k,中等质量)
tavern-100k.spz # 高斯泼溅(100k,轻量级/移动端)
tavern-collider.glb # 用于物理的碰撞体网格(GLB)
tavern-pano.jpg # 全景图像(天空盒)
tavern.meta.json # 元数据:世界 ID、提示、时间戳、资源 URL
已测试并正常工作 — 查看 examples/worldlabs-arcade/ 获取完整可运行的演示。
npm install @sparkjsdev/spark
包:@sparkjsdev/spark — 用于 Three.js 的高性能高斯泼溅渲染器。支持 SPZ、PLY、SOGS、KSPLAT、SPLAT 格式。
export const WORLD = {
splatPath: 'assets/worlds/tavern-500k.spz', // 500k 是良好的桌面默认值
colliderPath: 'assets/worlds/tavern-collider.glb',
panoPath: 'assets/worlds/tavern-pano.png',
scale: 1,
position: { x: 0, y: 0, z: 0 },
};
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { SplatMesh } from '@sparkjsdev/spark';
import { WORLD } from '../core/Constants.js';
let _colliderMesh = null;
let _splatMesh = null;
export async function loadWorld(scene, renderer, camera) {
const promises = [];
// 1. 通过 SparkJS 加载高斯泼溅 — SplatMesh 像任何 Three.js 对象一样工作
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);
_splatMesh = splat;
})());
}
// 2. 碰撞体网格(GLB)— 不可见,仅用于物理射线检测
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; });
scene.add(_colliderMesh);
})());
}
// 3. 全景图作为等距柱状投影天空盒 + 环境光照
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.background = panoTex;
scene.environment = panoTex;
})());
}
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; }
SplatMesh 作为普通 Three.js 场景的一部分渲染 — 无需单独的渲染通道。只需 scene.add(splat) 并照常调用 renderer.render(scene, camera)。
import { loadWorld, getGroundHeight } from '../level/WorldLoader.js';
// 在 init() 中:
await loadWorld(scene, renderer, camera);
// 在渲染循环中 — 标准的 Three.js,无需额外的泼溅通道:
function animate() {
requestAnimationFrame(animate);
player.update(delta, input, azimuth);
// 将玩家 Y 轴位置对齐到碰撞体地面
const groundY = getGroundHeight(player.mesh.position.x, player.mesh.position.z, 0);
player.mesh.position.y = groundY;
// 单次渲染调用同时处理网格和泼溅
renderer.render(scene, camera);
}
| 层级 | 文件 | 质量 | 使用场景 |
|---|---|---|---|
100k | {slug}-100k.spz | 低 | 移动端、快速加载、预览 |
500k | {slug}-500k.spz | 中 | 桌面游戏,良好平衡 |
full_res | {slug}.spz | 高 | 高端、主要环境 |
根据目标平台选择。无论泼溅分辨率如何,碰撞体网格(GLB)都是相同的。
对于完整的 3D 游戏,结合两者:
┌─────────────────────────────────────────────────┐
│ 完整的 3D 场景 │
├─────────────────────────────────────────────────┤
│ World Labs (环境) │
│ └─ 高斯泼溅 (视觉) │
│ └─ 碰撞体网格 (物理) │
│ └─ 全景图 (天空盒) │
│ │
│ Meshy AI (实体) │
│ └─ 玩家角色 (绑定骨骼、动画 GLB) │
│ └─ 敌人 (绑定骨骼、动画 GLB) │
│ └─ 道具/物品 (静态 GLB) │
│ │
│ Three.js (引擎) │
│ └─ SparkJS 渲染泼溅 │
│ └─ GLTFLoader 渲染角色/道具 │
│ └─ Raycaster 使用碰撞体检测地面/墙壁 │
└─────────────────────────────────────────────────┘
查看 examples/worldlabs-arcade/ — 一个完整、经过测试的演示,包含:
原因: World Labs SPZ 文件使用与 Three.js 约定相反的 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 分钟仍无进展,创建新的生成请求 — 不要重试同一操作。
原因: 包名是 @sparkjsdev/spark,而不是 @worldlabs/spark 或其他变体。修复: 使用 npm install @sparkjsdev/spark 安装。从此包导入 SplatMesh。它直接集成到 Three.js 场景中 — 无需单独的渲染通道。
原因: 使用了错误的端点进行媒体上传准备。修复: 使用 POST /media-assets:prepare_upload(不是 /media-assets)。这将返回一个用于 PUT 上传的签名 URL。冒号语法是故意的 — 它是资源上的自定义操作。
原因: 使用了错误的认证请求头格式。修复: 使用 WLT-Api-Key: <your-key> 请求头,不是 Authorization: Bearer <your-key>。World Labs API 使用自定义请求头格式。
WORLDLABS_API_KEY 环境变量已设置scripts/worldlabs-generate.mjs 生成世界(约 3-8 分钟)public/assets/worlds/@sparkjsdev/spark 已安装(npm install @sparkjsdev/spark)src/level/ 中创建了 WorldLoader.js(SplatMesh + GLTFLoader + TextureLoader)Constants.js 已更新 WORLD 配置(splatPath、colliderPath、panoPath)Game.js 在 init 中调用 loadWorld(),使用 getGroundHeight() 获取玩家 Y 轴位置renderer.render(scene, camera) 同时处理泼溅和网格 — 无需额外通道每周安装次数
57
代码仓库
GitHub 星标数
32
首次出现
2026年3月10日
安全审计
安装于
claude-code50
opencode25
gemini-cli25
github-copilot25
amp25
cline25
Generate photorealistic 3D environments from text prompts or images using the World Labs Marble API. Outputs Gaussian Splat scenes (SPZ) rendered via SparkJS in Three.js, plus collider meshes (GLB) for physics.
Image-first — always prefer image input over text:
--mode image. This produces the most faithful results because the AI can match the exact visual style, layout, lighting, and mood.--mode text when no reference image is available. The API auto-expands short prompts into rich scene descriptions, but results are less predictable than image-driven generation.When the game-creator pipeline runs, ask the user for a reference image first :
I can generate a photorealistic 3D environment for your game using World Labs. Do you have a reference image (photo, concept art, screenshot) for the environment?
- Yes → provide the file path or URL
- No → I'll generate from a text description instead
| Component | Technology |
|---|---|
| API | World Labs Marble API (https://api.worldlabs.ai/marble/v1) |
| Auth | WLT-Api-Key header |
| Output: Visual | Gaussian Splat (.spz) — 100k, 500k, full resolution tiers |
| Output: Physics | Collider mesh (.glb) — for collision detection |
| Output: Skybox | Panorama image (.jpg/.png) |
| Browser Renderer |
Before prompting the user, check if the key already exists: test -f .env && grep -q '^WORLDLABS_API_KEY=.' .env && echo "found" If found, export it with set -a; . .env; set +a and skip the prompt.
If not set, ask the user:
I'll generate a photorealistic 3D environment with World Labs. You can get a free API key:
- Sign up at https://platform.worldlabs.ai
- Go to API Keys
- Create a new key
Paste your key below like:
WORLDLABS_API_KEY=your-key-here(It will be saved to .env and redacted from this conversation automatically.)Or type "skip" to use basic geometry instead.
# 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
Tested & working — see examples/worldlabs-arcade/ for a complete runnable demo.
npm install @sparkjsdev/spark
Package : @sparkjsdev/spark — high-performance Gaussian Splat renderer for Three.js. Supports SPZ, PLY, SOGS, KSPLAT, SPLAT formats.
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 },
};
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { SplatMesh } from '@sparkjsdev/spark';
import { WORLD } from '../core/Constants.js';
let _colliderMesh = null;
let _splatMesh = null;
export async function loadWorld(scene, renderer, camera) {
const promises = [];
// 1. Gaussian Splat via SparkJS — SplatMesh works like any Three.js object
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);
_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; });
scene.add(_colliderMesh);
})());
}
// 3. Panorama as equirectangular skybox + environment lighting
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.background = panoTex;
scene.environment = panoTex;
})());
}
await Promise.all(promises);
return { splat: _splatMesh, collider: _colliderMesh };
}
// Raycast down to find ground height on collider mesh
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; }
SplatMesh renders as part of the normal Three.js scene — no separate render pass needed. Just scene.add(splat) and call renderer.render(scene, camera) as usual.
import { loadWorld, getGroundHeight } from '../level/WorldLoader.js';
// In init():
await loadWorld(scene, renderer, camera);
// In the render loop — standard Three.js, no extra splat pass:
function animate() {
requestAnimationFrame(animate);
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);
}
| Tier | File | Quality | Use Case |
|---|---|---|---|
100k | {slug}-100k.spz | Low | Mobile, fast loading, previews |
500k | {slug}-500k.spz | Medium | Desktop games, good balance |
full_res | {slug}.spz | High |
Choose based on target platform. The collider mesh (GLB) is the same regardless of splat resolution.
For a complete 3D game, combine both:
┌─────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────┘
See examples/worldlabs-arcade/ — a complete, tested demo with:
Cause: World Labs SPZ files use Y-inverted coordinates compared to Three.js convention. Fix: Apply rotation.x = Math.PI to both the splat mesh and collider mesh. Then adjust position.z to compensate: position.z += (minZ + maxZ). Do NOT use scale.y = -1 on a parent group — SparkJS breaks with negative parent scale.
Cause: After Y-flip, the coordinate system is inverted. Downward raycasts hit what was originally the floor (now the ceiling in flipped space). Fix: Raycast UPWARD from Y=-50 with direction (0, 1, 0) to hit the visual floor first. The floor is the lowest surface after the flip.
Cause: The collider mesh's world matrix hasn't been updated after setting rotation/position, especially before the first render frame. Fix: Call _colliderMesh.updateMatrixWorld(true) immediately after setting rotation and position, before any raycast operations.
Cause: Using the World Labs panorama as scene.background shows the same environment as a giant sphere surrounding the 3D scene. Fix: Don't use the panorama as scene background. Use a solid color (scene.background = new THREE.Color(0x87CEEB)) or a custom skybox instead.
Cause: World Labs generation typically takes 3-8 minutes. Complex scenes or high server load can extend this. Fix: Poll the operation status endpoint every 10-15 seconds. Check progress.status for "IN_PROGRESS" vs "COMPLETE". If stuck beyond 15 minutes, create a new generation request — don't retry the same operation.
Cause: The package name is @sparkjsdev/spark, not @worldlabs/spark or other variations. Fix: Install with npm install @sparkjsdev/spark. Import SplatMesh from this package. It integrates directly into the Three.js scene — no separate render pass needed.
Cause: Using the wrong endpoint for media upload preparation. Fix: Use POST /media-assets:prepare_upload (NOT /media-assets). This returns a signed URL for PUT upload. The colon syntax is intentional — it's a custom action on the resource.
Cause: Using the wrong authentication header format. Fix: Use WLT-Api-Key: <your-key> header, NOT Authorization: Bearer <your-key>. The World Labs API uses a custom header format.
WORLDLABS_API_KEY environment variable is setscripts/worldlabs-generate.mjs to generate world (~3-8 min)public/assets/worlds/@sparkjsdev/spark installed (npm install @sparkjsdev/spark)WorldLoader.js created in src/level/ (SplatMesh + GLTFLoader + TextureLoader)Constants.js updated with WORLD config (splatPath, colliderPath, panoPath)Weekly Installs
57
Repository
GitHub Stars
32
First Seen
Mar 10, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
claude-code50
opencode25
gemini-cli25
github-copilot25
amp25
cline25
超能力技能使用指南:AI助手技能调用优先级与工作流程详解
55,300 周安装
NSFC标书质量控制工具nsfc-qc - LaTeX科研文档自动检查与引用验证
67 周安装
iOS MapKit API 参考指南:SwiftUI Map (iOS 17+) 与 MKMapView 完整对比与使用教程
72 周安装
Telos:个人与项目上下文管理系统 - 智能分析、目标追踪与自动化报告生成
57 周安装
Azure Developer CLI (azd) 容器应用部署指南:使用Bicep和远程构建
67 周安装
Python ECharts 数据可视化指南:pyecharts 图表制作与 HTML 报告生成
80 周安装
Astro 开发者技能指南 - 单体仓库开发、架构与测试最佳实践
74 周安装
SparkJS (@worldlabs/spark) — Three.js compatible |
| CLI Script | scripts/worldlabs-generate.mjs (zero dependencies) |
| High-end, hero environments |
Game.js calls loadWorld() in init, uses getGroundHeight() for player Yrenderer.render(scene, camera) handles both splats and meshes — no extra pass