跳到主内容
快讯直播
AI智模界
教程

从零用 Spark-X2.6 在国产算力上生成可交互 3D 故宫

适用场景

这套方案面向想把“大模型生成能力”落成一个能点、能转、能看的可视化页面的人:手里有国产算力机器(或只有一台普通笔记本),拿到 Spark-X2.6 的接口权限,希望走通“模型出场景数据 → 程序化建模 → 浏览器实时渲染 → 静态托管上线”这条完整链路。产出的东西不是一张渲染图,而是一个可交互的 3D 故宫场景:鼠标拖拽环视、滚轮缩放、点击建筑弹出名称说明。整个流程不依赖任何商业建模软件,全部由代码生成,改一行提示词就能换一套布局。

环境与前置条件

一个关键认知先讲清楚:这类“文生 3D 场景”的常见做法,是让模型输出结构化的场景描述(JSON),再由代码把 JSON 变成几何体。模型本身不直接吐 .obj/.glb 文件。所以算力消耗分两块——模型推理(走云端 API 时可以忽略)和渲染(在浏览器端跑,靠本机显卡)。下面两条路径都覆盖。

项目建议
操作系统Linux(Ubuntu 22.04 及以上等主流发行版)或 macOS;Windows 建议用 WSL2
Python3.10 及以上
浏览器支持 WebGL2 的现代浏览器(Chrome / Edge / Firefox 的较新版本)
内存调用云端 API 时 4 GB 起;本地跑推理建议 32 GB 起
显存 / NPU 内存本地推理按官方模型卡要求准备,以官方文档当前版本为准
磁盘20 GB 空闲(存放模型权重、前端依赖、日志)
网络能访问模型服务的 API 域名与前端 CDN;内网环境需提前放通

国产算力的驱动检查(本地推理才需要;纯 API 调用可跳过)。以昇腾为例,安装官方驱动与 CANN 工具包后,验证:

```bash

npu-smi info

python -c "import torch, torch_npu; print(torch.npu.is_available(), torch.npu.device_count())"

```

预期输出 True 1(或更多卡数)。驱动、CANN、torch_npu 三者版本必须配套,具体对应关系以官方文档当前版本为准,装错版本是最常见的坑。其他国产卡(海光 DCU、寒武纪等)同理,装对应厂商的 torch 插件,验证脚本换成厂商提供的设备检测命令即可。

分步骤部署

第 1 步:领取免费额度并创建 API Key

登录 Spark-X2.6 的官方平台,完成注册与实名认证,在控制台找到“体验额度 / 免费额度”入口领取(多数平台对新账号有一次性赠送额度,具体额度与有效期以官方页面为准)。随后进入“API Key”页面新建一个密钥,复制后立刻存进密码管理器——多数平台的密钥只在创建时完整显示一次。

在控制台里顺手确认三件事:可用的模型名称、API 的 Base URL、以及是否兼容 OpenAI 的 /chat/completions 协议。这三项都记下来,下一步要用。

第 2 步:配置本地环境变量

密钥不要写进代码。新建一个 .env 文件(记得加进 .gitignore):

```bash

export SPARK_BASE_URL="<控制台给出的 API 地址,不要带结尾斜杠>"

export SPARK_API_KEY="<你的 API Key>"

export SPARK_MODEL="<控制台给出的模型名称,以官方文档当前版本为准>"

```

加载并验证:

```bash

source .env

echo "${SPARK_BASE_URL:?未设置}" && echo "${SPARK_API_KEY:0:6}******"

```

看到 Base URL 和打码后的 key 前缀,说明变量已生效。

第 3 步:建项目骨架并装依赖

```bash

mkdir -p gugong/{scripts,web}

cd gugong

python -m venv .venv && source .venv/bin/activate

pip install requests

```

目录约定:scripts/ 放生成脚本,web/ 放前端静态文件,最终只要把 web/ 整个目录丢到服务器上就能跑。

第 4 步:写场景生成的提示词与调用脚本

新建 scripts/generate_scene.py。核心思路是把“故宫”这件事拆成模型能稳定输出的字段:建筑类型、位置、尺寸、屋顶形式、颜色。字段越具体,模型返回的 JSON 越不容易跑偏。

```python

import json, os, re, sys, time

import requests

BASE_URL = os.environ["SPARK_BASE_URL"].rstrip("/")

API_KEY = os.environ["SPARK_API_KEY"]

MODEL = os.environ["SPARK_MODEL"]

SYSTEM_PROMPT = "你是三维场景数据生成器。只输出 JSON 对象,不要解释,不要 Markdown 代码块。"

USER_PROMPT = """生成一个可用于实时渲染的“故宫太和殿广场”三维场景,输出严格符合以下 JSON 结构:

{

"name": "字符串",

"ground": {"size": [宽, 深], "color": "#RRGGBB"},

"buildings": [

{

"id": "唯一英文小写标识",

"label": "中文名称",

"type": "hall | gate | pavilion | wall",

"position": [x, 0, z],

"size": [宽, 高, 深],

"storeys": 1,

"wallColor": "#RRGGBB",

"roof": {"type": "hip | gable | flat", "color": "#RRGGBB", "height": 数值}

}

]

}

约束:

1. 单位统一为米,右手坐标系,Y 轴向上,地面为 XZ 平面,原点在广场中心。

2. 至少包含:1 座主殿、2 座东西配殿、1 座门楼、4 段围墙。

3. 所有建筑不得互相穿插,坐标范围控制在 -120 到 120 之间。

4. 屋顶用传统宫殿的黄琉璃瓦色系,墙体用朱红色系。

5. 只输出 JSON,不要任何额外文字。"""

def extract_json(text: str) -> dict:

text = text.strip()

fenced = re.search(r"``(?:json)?\s*(.*?)``", text, re.S)

if fenced:

text = fenced.group(1).strip()

start, end = text.find("{"), text.rfind("}")

if start == -1 or end == -1:

raise ValueError("响应里找不到 JSON 对象")

return json.loads(text[start:end + 1])

def call_model() -> str:

payload = {

"model": MODEL,

"messages": [

{"role": "system", "content": SYSTEM_PROMPT},

{"role": "user", "content": USER_PROMPT},

],

"temperature": 0.3,

}

headers = {

"Authorization": f"Bearer {API_KEY}",

"Content-Type": "application/json",

}

resp = requests.post(f"{BASE_URL}/chat/completions",

headers=headers, json=payload, timeout=180)

if resp.status_code == 429:

raise RuntimeError("RATE_LIMIT")

resp.raise_for_status()

data = resp.json()

return data["choices"][0]["message"]["content"]

def main():

for attempt in range(3):

try:

raw = call_model()

scene = extract_json(raw)

with open("web/scene.json", "w", encoding="utf-8") as f:

json.dump(scene, f, ensure_ascii=False, indent=2)

print(f"OK 生成建筑 {len(scene['buildings'])} 座 -> web/scene.json")

return

except RuntimeError as e:

if "RATE_LIMIT" in str(e):

wait = 5 * (2 ** attempt)

print(f"触发限流,{wait}s 后重试…", file=sys.stderr)

time.sleep(wait)

continue

raise

except (ValueError, json.JSONDecodeError) as e:

print(f"第 {attempt + 1} 次解析失败:{e}", file=sys.stderr)

time.sleep(2)

sys.exit("三次尝试均失败,请检查提示词或额度是否用尽")

if __name__ == "__main__":

main()

```

运行:

```bash

python scripts/generate_scene.py

```

成功标志是终端打印 OK 生成建筑 N 座 -> web/scene.json,且 web/scene.json 能被正常打开、字段齐全。如果某个字段模型总是写错,别急着改代码,先在提示词里补一条约束,通常比在代码里做兼容更省事。

第 5 步:写前端渲染页面

新建 web/index.html。几何体全部用代码拼:墙体是盒子,屋顶是四棱锥(圆锥体取四段并旋转 45°)。这样零外部模型文件,加载快、也方便程序化调参。

```html

<!DOCTYPE html>

<html lang="zh-CN">

<head>

<meta charset="utf-8" />

<meta name="viewport" content="width=device-width, initial-scale=1" />

<title>可交互 3D 故宫</title>

<style>

html, body { margin: 0; height: 100%; overflow: hidden; background: #0e1116; }

#app { width: 100%; height: 100%; display: block; }

#info {

position: fixed; left: 16px; top: 16px; padding: 10px 14px;

background: rgba(20,24,32,.78); color: #e8eaf0; border-radius: 8px;

font: 14px/1.6 system-ui, sans-serif; pointer-events: none;

}

</style>

<script type="importmap">

{

"imports": {

"three": "https://unpkg.com/three@<VERSION>/build/three.module.js",

"three/addons/": "https://unpkg.com/three@<VERSION>/examples/jsm/"

}

}

</script>

</head>

<body>

<canvas id="app"></canvas>

<div id="info">拖拽旋转 · 滚轮缩放 · 点击建筑查看名称</div>

<script type="module" src="./app.js"></script>

</body>

</html>

```

<VERSION> 替换成官网当前推荐的版本号,以官方文档当前版本为准;内网环境把这两个文件下载到本地 vendor/ 目录再改 importmap 指向本地路径。

新建 web/app.js:

```js

import * as THREE from 'three';

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const canvas = document.getElementById('app');

const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });

renderer.setPixelRatio(Math.min(devicePixelRatio, 2));

renderer.setSize(innerWidth, innerHeight);

const scene = new THREE.Scene();

scene.background = new THREE.Color('#9fb6cf');

scene.fog = new THREE.Fog('#9fb6cf', 260, 620);

const camera = new THREE.PerspectiveCamera(55, innerWidth / innerHeight, 0.5, 2000);

camera.position.set(90, 70, 130);

const controls = new OrbitControls(camera, canvas);

controls.target.set(0, 10, 0);

controls.maxPolarAngle = Math.PI / 2.05;

controls.enableDamping = true;

scene.add(new THREE.HemisphereLight('#ffffff', '#6b6252', 1.1));

const sun = new THREE.DirectionalLight('#fff2d8', 1.6);

sun.position.set(120, 180, 90);

scene.add(sun);

function makeHipRoof(w, d, h, color) {

const r = w / Math.SQRT2;

const geo = new THREE.ConeGeometry(r, h, 4, 1);

geo.rotateY(Math.PI / 4);

geo.scale(1, 1, d / w);

const mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color, roughness: .85 }));

mesh.position.y = h / 2;

return mesh;

}

function makeBuilding(b) {

const [w, hgt, d] = b.size;

const group = new THREE.Group();

const body = new THREE.Mesh(

new THREE.BoxGeometry(w, hgt, d),

new THREE.MeshStandardMaterial({ color: b.wallColor, roughness: .9 })

);

body.position.y = hgt / 2;

group.add(body);

if (b.roof && b.roof.type !== 'flat') {

const roof = makeHipRoof(w * 1.12, d * 1.12, b.roof.height ?? hgt * 0.4, b.roof.color);

roof.position.y = hgt;

group.add(roof);

}

group.position.set(b.position[0], 0, b.position[2]);

group.userData = { label: b.label, id: b.id };

return group;

}

const res = await fetch('./scene.json');

const data = await res.json();

const [gw, gd] = data.ground.size;

const ground = new THREE.Mesh(

new THREE.PlaneGeometry(gw, gd),

new THREE.MeshStandardMaterial({ color: data.ground.color, roughness: 1 })

);

ground.rotation.x = -Math.PI / 2;

scene.add(ground);

const pickables = [];

for (const b of data.buildings) {

const g = makeBuilding(b);

g.traverse(o => { if (o.isMesh) { o.userData.root = g; pickables.push(o); } });

scene.add(g);

}

const raycaster = new THREE.Raycaster();

const pointer = new THREE.Vector2();

let downAt = null;

canvas.addEventListener('pointerdown', e => { downAt = { x: e.clientX, y: e.clientY }; });

canvas.addEventListener('pointerup', e => {

if (!downAt) return;

const moved = Math.hypot(e.clientX - downAt.x, e.clientY - downAt.y);

downAt = null;

if (moved > 5) return; // 拖拽不算点击

pointer.x = (e.clientX / innerWidth) * 2 - 1;

pointer.y = -(e.clientY / innerHeight) * 2 + 1;

raycaster.setFromCamera(pointer, camera);

const hit = raycaster.intersectObjects(pickables, false)[0];

document.getElementById('info').textContent =

hit ? 当前选中:${hit.object.userData.root.userData.label}

: '拖拽旋转 · 滚轮缩放 · 点击建筑查看名称';

});

addEventListener('resize', () => {

camera.aspect = innerWidth / innerHeight;

camera.updateProjectionMatrix();

renderer.setSize(innerWidth, innerHeight);

});

renderer.setAnimationLoop(() => { controls.update(); renderer.render(scene, camera); });

```

几个要点:拾取时用 pointerdown/pointerup 的位移差区分“点击”和“拖动”,否则一转镜头就误触发弹窗;每个 mesh 上挂 root 引用,点击到屋顶也能取到整体建筑的名称。

第 6 步:本地起服务预览

浏览器对 file:// 下的 fetch 有限制,必须走 HTTP:

```bash

cd web && python -m http.server 8080

```

浏览器打开 http://127.0.0.1:8080/。

第 7 步:用 Nginx 托管并反代 API

生产环境不要让浏览器直接持有 API Key。把生成脚本包成一个内部接口(示例用 FastAPI 或任何你顺手的小框架),再由 Nginx 反代;前端只请求同域的 /api/。同时给 .glb/.json 明确 MIME,避免加载被拦。

```nginx

server {

listen 80;

server_name _;

root /var/www/gugong;

index index.html;

location / {

try_files $uri $uri/ /index.html;

}

location /api/ {

proxy_pass http://127.0.0.1:8000/;

proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

proxy_read_timeout 300s;

}

location ~* \.(glb|gltf)$ {

default_type application/octet-stream;

}

location ~* \.(json|js|css)$ {

add_header Cache-Control "public, max-age=300";

}

gzip on;

gzip_types application/json application/javascript text/css;

}

```

```bash

sudo mkdir -p /var/www/gugong && sudo cp -r web/* /var/www/gugong/

sudo nginx -t && sudo systemctl reload nginx

```

nginx -t 输出 syntax is ok 与 test is successful 即配置通过。

验证部署是否成功

接口连通性

```bash

curl -s -X POST "$SPARK_BASE_URL/chat/completions" \

-H "Authorization: Bearer $SPARK_API_KEY" \

-H "Content-Type: application/json" \

-d "{\"model\":\"$SPARK_MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"回复两个字:收到\"}]}" \

| head -c 300

```

预期返回含 choices 数组的 JSON。返回 401/403 说明密钥或额度有问题,先查这一步再看后面。

产物结构完整性

```bash

python - <<'PY'

import json

d = json.load(open("web/scene.json"))

assert "buildings" in d and len(d["buildings"]) >= 6, "建筑数量不足"

need = {"id", "label", "type", "position", "size", "wallColor", "roof"}

for b in d["buildings"]:

missing = need - set(b)

assert not missing, f"{b.get('id')} 缺字段 {missing}"

print("scene.json 校验通过,共", len(d["buildings"]), "座建筑")

PY

```

预期打印 scene.json 校验通过,共 N 座建筑。

页面与渲染

```bash

curl -sI http://127.0.0.1:8080/scene.json | head -n 1

curl -sI http://127.0.0.1:8080/app.js | head -n 1

```

预期两行都是 HTTP/1.1 200 OK。再用浏览器打开页面,按 F12 看 Console 应为空(无红色报错),拖动画面能转、滚轮能缩放、点主殿左上角信息条变成“当前选中:太和殿”。在 Console 里执行 renderer 不可见时,改为观察 Network 面板:scene.json 状态 200、大小与文件一致即可。

常见报错与解决

1. 401 Unauthorized / invalid api key

原因:环境变量没加载、密钥复制时带了空格或换行、密钥被删除或重置。

```bash

source .env

echo -n "$SPARK_API_KEY" | wc -c # 与平台显示的密钥长度比对

curl -s -o /dev/null -w "%{http_code}\n" "$SPARK_BASE_URL/models" -H "Authorization: Bearer $SPARK_API_KEY"

```

返回 200 说明密钥有效;仍是 401 就到控制台重新生成一个。

2. 429 Too Many Requests 或提示免费额度已用尽

原因:短时间请求过密,或赠送额度耗完。脚本里已经带了指数退避;额度问题只能去控制台查看剩余量并按其指引处理,具体规则以官方页面为准。

```bash

降低调用频率:把脚本中的 time.sleep 调大,或把 temperature 固定后复用同一份 scene.json

python scripts/generate_scene.py

```

3. json.decoder.JSONDecodeError: Expecting value

原因:模型在 JSON 前后加了说明文字,或字段里用了中文全角引号。extract_json 已处理代码块包裹,仍有问题时把提示词里的“只输出 JSON”再强调一次,并把 temperature 降到 0.1~0.3。

```bash

python -c "import json;json.load(open('web/scene.json'));print('ok')"

```

4. RuntimeError: No NPU device found / torch.npu.is_available() 为 False

原因:驱动、CANN、torch_npu 版本不配套,或当前用户没有设备访问权限。

```bash

npu-smi info # 驱动层是否识别到卡

ls -l /dev/davinci* # 设备节点是否存在

python -c "import torch, torch_npu; print(torch.__version__, torch_npu.__version__)"

```

版本对应关系以官方文档当前版本为准;把当前用户加入设备所属用户组后重新登录再试。

5. 页面空白,Console 报 Failed to load module script: MIME type

原因:Nginx 把 .js 当成了纯文本,或 importmap 里的 CDN 地址不可达。

```bash

curl -sI http://your-host/app.js | grep -i content-type # 应为 application/javascript

```

修正 Nginx 的 mime.types 引用并 systemctl reload nginx;内网环境改为本地 vendor/ 目录引入 Three.js。

6. blocked by CORS policy

原因:前端页面与 API 不同源,浏览器拦截。用第 7 步的 /api/ 反向代理走同源,或在 API 服务侧显式允许页面域名。

后续维护

备份:需要长期保留的只有三样——web/scene.json(生成结果)、scripts/(提示词与调用逻辑)、Nginx 站点配置。用 cron 每天打包一次、保留最近 14 天即可:

```bash

tar -czf /backup/gugong-$(date +%F).tar.gz web/scene.json scripts /etc/nginx/conf.d/gugong.conf

find /backup -name 'gugong-*.tar.gz' -mtime +14 -delete

```

升级:分三层看。模型升级时只改 SPARK_MODEL 环境变量,用同一份提示词重跑一次,对比新旧 scene.json 的建筑数量与坐标合理性;前端升级 Three.js 时先改 importmap 里的版本号,在测试环境跑一遍再切生产,注意新版 API 可能有破坏性变更;系统层升级驱动与推理框架时,务必三个组件成套升级并先在备用机器验证。

日志与监控:生成脚本把每次的请求耗时、状态码、重试次数写进结构化日志(JSON 行格式最省事),方便 grep 统计失败率;Nginx 打开 access_log 与 error_log,重点关注 5xx 与 404 比例。可用性监控建议至少覆盖两个探针:GET / 返回 200、GET /scene.json 能解析出 buildings 字段。额度侧也要盯——免费额度是会被耗尽的,在控制台设置余额提醒,或在自己服务里对每日调用次数设上限,避免某天页面上线了但场景刷不出来。

成本与体验的小优化:场景 JSON 生成一次就可以固定下来,日常访问不再消耗推理额度,把它当作“构建产物”纳入版本管理;只有在需要换布局、换季节配色时重新生成。前端可以顺手做个降级:WebGL2 不可用时给出静态预览图,比白屏友好得多。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。