curl --request POST \
--url https://apis.viggle.ai/v1/scenes/custom-pano \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"sessionId": "living-room-01",
"panoramaUri": "<string>",
"panoramaC2w": [
[
1,
0,
0,
0
],
[
0,
1,
0,
0
],
[
0,
0,
1,
0
],
[
0,
0,
0,
1
]
],
"metersPerUnit": 1,
"spzOutputUrl": "<string>",
"outputUris": {
"spz": "<string>",
"preview": "<string>",
"panorama": "<string>"
}
}
'import requests
url = "https://apis.viggle.ai/v1/scenes/custom-pano"
payload = {
"sessionId": "living-room-01",
"panoramaUri": "<string>",
"panoramaC2w": [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]],
"metersPerUnit": 1,
"spzOutputUrl": "<string>",
"outputUris": {
"spz": "<string>",
"preview": "<string>",
"panorama": "<string>"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
sessionId: 'living-room-01',
panoramaUri: '<string>',
panoramaC2w: [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]],
metersPerUnit: 1,
spzOutputUrl: '<string>',
outputUris: {spz: '<string>', preview: '<string>', panorama: '<string>'}
})
};
fetch('https://apis.viggle.ai/v1/scenes/custom-pano', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://apis.viggle.ai/v1/scenes/custom-pano",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'sessionId' => 'living-room-01',
'panoramaUri' => '<string>',
'panoramaC2w' => [
[
1,
0,
0,
0
],
[
0,
1,
0,
0
],
[
0,
0,
1,
0
],
[
0,
0,
0,
1
]
],
'metersPerUnit' => 1,
'spzOutputUrl' => '<string>',
'outputUris' => [
'spz' => '<string>',
'preview' => '<string>',
'panorama' => '<string>'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://apis.viggle.ai/v1/scenes/custom-pano"
payload := strings.NewReader("{\n \"sessionId\": \"living-room-01\",\n \"panoramaUri\": \"<string>\",\n \"panoramaC2w\": [\n [\n 1,\n 0,\n 0,\n 0\n ],\n [\n 0,\n 1,\n 0,\n 0\n ],\n [\n 0,\n 0,\n 1,\n 0\n ],\n [\n 0,\n 0,\n 0,\n 1\n ]\n ],\n \"metersPerUnit\": 1,\n \"spzOutputUrl\": \"<string>\",\n \"outputUris\": {\n \"spz\": \"<string>\",\n \"preview\": \"<string>\",\n \"panorama\": \"<string>\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://apis.viggle.ai/v1/scenes/custom-pano")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"sessionId\": \"living-room-01\",\n \"panoramaUri\": \"<string>\",\n \"panoramaC2w\": [\n [\n 1,\n 0,\n 0,\n 0\n ],\n [\n 0,\n 1,\n 0,\n 0\n ],\n [\n 0,\n 0,\n 1,\n 0\n ],\n [\n 0,\n 0,\n 0,\n 1\n ]\n ],\n \"metersPerUnit\": 1,\n \"spzOutputUrl\": \"<string>\",\n \"outputUris\": {\n \"spz\": \"<string>\",\n \"preview\": \"<string>\",\n \"panorama\": \"<string>\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://apis.viggle.ai/v1/scenes/custom-pano")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"sessionId\": \"living-room-01\",\n \"panoramaUri\": \"<string>\",\n \"panoramaC2w\": [\n [\n 1,\n 0,\n 0,\n 0\n ],\n [\n 0,\n 1,\n 0,\n 0\n ],\n [\n 0,\n 0,\n 1,\n 0\n ],\n [\n 0,\n 0,\n 0,\n 1\n ]\n ],\n \"metersPerUnit\": 1,\n \"spzOutputUrl\": \"<string>\",\n \"outputUris\": {\n \"spz\": \"<string>\",\n \"preview\": \"<string>\",\n \"panorama\": \"<string>\"\n }\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"sessionId": "<string>",
"operation": "pano",
"status": "processing",
"createdAt": 123,
"completedAt": 123,
"result": {
"operation": "pano",
"sessionId": "<string>",
"spz": {
"url": "<string>"
},
"preview": {
"url": "<string>"
},
"panorama": {
"url": "<string>"
},
"outputBatch": {
"batchId": "<string>",
"createdAt": 123,
"manifest": {
"url": "<string>"
},
"kind": "initial",
"views": [
{
"index": 1,
"url": "<string>",
"c2w": [
[
1,
0,
0,
0
],
[
0,
1,
0,
0
],
[
0,
0,
1,
0
],
[
0,
0,
0,
1
]
],
"intrinsic": [
123
]
}
]
},
"referenceViewCount": 123,
"newViewCount": 123,
"seconds": {}
},
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}从自定义全景图创建场景
用你自己的 360° 全景图构建 3D 场景,并按指定位姿和比例放入你的世界。
curl --request POST \
--url https://apis.viggle.ai/v1/scenes/custom-pano \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"sessionId": "living-room-01",
"panoramaUri": "<string>",
"panoramaC2w": [
[
1,
0,
0,
0
],
[
0,
1,
0,
0
],
[
0,
0,
1,
0
],
[
0,
0,
0,
1
]
],
"metersPerUnit": 1,
"spzOutputUrl": "<string>",
"outputUris": {
"spz": "<string>",
"preview": "<string>",
"panorama": "<string>"
}
}
'import requests
url = "https://apis.viggle.ai/v1/scenes/custom-pano"
payload = {
"sessionId": "living-room-01",
"panoramaUri": "<string>",
"panoramaC2w": [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]],
"metersPerUnit": 1,
"spzOutputUrl": "<string>",
"outputUris": {
"spz": "<string>",
"preview": "<string>",
"panorama": "<string>"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
sessionId: 'living-room-01',
panoramaUri: '<string>',
panoramaC2w: [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]],
metersPerUnit: 1,
spzOutputUrl: '<string>',
outputUris: {spz: '<string>', preview: '<string>', panorama: '<string>'}
})
};
fetch('https://apis.viggle.ai/v1/scenes/custom-pano', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://apis.viggle.ai/v1/scenes/custom-pano",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'sessionId' => 'living-room-01',
'panoramaUri' => '<string>',
'panoramaC2w' => [
[
1,
0,
0,
0
],
[
0,
1,
0,
0
],
[
0,
0,
1,
0
],
[
0,
0,
0,
1
]
],
'metersPerUnit' => 1,
'spzOutputUrl' => '<string>',
'outputUris' => [
'spz' => '<string>',
'preview' => '<string>',
'panorama' => '<string>'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://apis.viggle.ai/v1/scenes/custom-pano"
payload := strings.NewReader("{\n \"sessionId\": \"living-room-01\",\n \"panoramaUri\": \"<string>\",\n \"panoramaC2w\": [\n [\n 1,\n 0,\n 0,\n 0\n ],\n [\n 0,\n 1,\n 0,\n 0\n ],\n [\n 0,\n 0,\n 1,\n 0\n ],\n [\n 0,\n 0,\n 0,\n 1\n ]\n ],\n \"metersPerUnit\": 1,\n \"spzOutputUrl\": \"<string>\",\n \"outputUris\": {\n \"spz\": \"<string>\",\n \"preview\": \"<string>\",\n \"panorama\": \"<string>\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://apis.viggle.ai/v1/scenes/custom-pano")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"sessionId\": \"living-room-01\",\n \"panoramaUri\": \"<string>\",\n \"panoramaC2w\": [\n [\n 1,\n 0,\n 0,\n 0\n ],\n [\n 0,\n 1,\n 0,\n 0\n ],\n [\n 0,\n 0,\n 1,\n 0\n ],\n [\n 0,\n 0,\n 0,\n 1\n ]\n ],\n \"metersPerUnit\": 1,\n \"spzOutputUrl\": \"<string>\",\n \"outputUris\": {\n \"spz\": \"<string>\",\n \"preview\": \"<string>\",\n \"panorama\": \"<string>\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://apis.viggle.ai/v1/scenes/custom-pano")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"sessionId\": \"living-room-01\",\n \"panoramaUri\": \"<string>\",\n \"panoramaC2w\": [\n [\n 1,\n 0,\n 0,\n 0\n ],\n [\n 0,\n 1,\n 0,\n 0\n ],\n [\n 0,\n 0,\n 1,\n 0\n ],\n [\n 0,\n 0,\n 0,\n 1\n ]\n ],\n \"metersPerUnit\": 1,\n \"spzOutputUrl\": \"<string>\",\n \"outputUris\": {\n \"spz\": \"<string>\",\n \"preview\": \"<string>\",\n \"panorama\": \"<string>\"\n }\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"sessionId": "<string>",
"operation": "pano",
"status": "processing",
"createdAt": 123,
"completedAt": 123,
"result": {
"operation": "pano",
"sessionId": "<string>",
"spz": {
"url": "<string>"
},
"preview": {
"url": "<string>"
},
"panorama": {
"url": "<string>"
},
"outputBatch": {
"batchId": "<string>",
"createdAt": 123,
"manifest": {
"url": "<string>"
},
"kind": "initial",
"views": [
{
"index": 1,
"url": "<string>",
"c2w": [
[
1,
0,
0,
0
],
[
0,
1,
0,
0
],
[
0,
0,
1,
0
],
[
0,
0,
0,
1
]
],
"intrinsic": [
123
]
}
]
},
"referenceViewCount": 123,
"newViewCount": 123,
"seconds": {}
},
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}panoramaUri 调用创建场景类似,但还要说明全景图在你自己世界中的拍摄位置:panoramaC2w 是它的相机位姿,metersPerUnit 是场景模型坐标系与你的坐标系之间的比例。结果中的所有相机——现在批次中的视图,以及之后生成的新视图——都以你的世界坐标系返回,因此场景能与你已有的内容对齐。
该任务属于 pano 任务:查询场景任务会返回 operation: "pano",结果也相同。
请求参数
使用application/json,最大 1 MiB。不接受未知字段。
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
sessionId | string | 是 | — | 构建场景所在的会话,见创建场景中的“会话”一节。 |
panoramaUri | string | 是 | — | 360° 全景图的 URL,须符合创建场景中的 URL 规则。 |
panoramaC2w | number[4][4] | 是 | — | 全景图在你的世界中的相机到世界矩阵(c2w),见下方“坐标系”一节。 |
metersPerUnit | number | 否 | 1 | 在 panoramaC2w 把场景模型的坐标变换到你的世界之前,先对其应用的比例,见下方“坐标系”一节。不小于 0.05。 |
spzOutputUrl | string | 否 | — | 你自己的 .spz 场景上传地址,见创建场景中的“上传到自己的存储”一节。 |
outputUris | object | 否 | — | 按文件指定你自己的上传地址:spz、preview、panorama。不能与 spzOutputUrl 同时使用。 |
prompt、referenceImages 和 quality。
坐标系
panoramaC2w 是一个 4 × 4 的相机到世界矩阵,按四行书写:
- 最后一行必须为
[0, 0, 0, 1]。 - 左上角 3 × 3 旋转部分必须正交归一(无缩放、无切变),且为真旋转(行列式为
+1,无镜像),容差1e-4。 - 平移部分(第四列的前三个值)是全景图在你的世界中的位置,单位为你的单位。
- 场景必须在
.spz可存储的范围内:平移分量的最大绝对值加上25 × metersPerUnit(场景半径)必须小于4000。
p 在你的坐标系中位于 R · (metersPerUnit · p) + t,其中 R 和 t 是 panoramaC2w 的旋转和平移。结果中的相机也按同样方式转换。
响应参数
返回200 OK 和 processing 状态的场景任务,operation 为 pano:
{
"id": "scn_0199f0c2-6a1e-7c3d-9b2a-4f5e6d7c8b9a.atrium-01",
"sessionId": "atrium-01",
"operation": "pano",
"status": "processing",
"createdAt": 1791856740000,
"completedAt": null,
"result": null,
"error": null
}
常见错误
| 错误 | 原因 |
|---|---|
400 INVALID_REQUEST | 字段缺失、不合法或未知;panoramaC2w 不是有效位姿;metersPerUnit 小于 0.05;场景会超出坐标范围;或某个 URL 不符合创建场景中的 URL 规则。 |
401 UNAUTHENTICATED | 缺少或无效的 API 密钥。 |
409 INVALID_REQUEST | 该会话已有任务处于 processing,见 error.details.running_task_id。 |
503 SERVICE_BUSY | 场景模型暂时不可用,可重试。 |
示例
一张在原点上方 1.5 个单位处拍摄、绕 y 轴旋转 90° 的全景图,使用默认比例:curl -X POST https://apis.viggle.ai/v1/scenes/custom-pano \
-H "Authorization: Bearer $VIGGLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "atrium-01",
"panoramaUri": "https://example.com/atrium-360.jpg",
"panoramaC2w": [[0, 0, 1, 0], [0, 1, 0, 1.5], [-1, 0, 0, 0], [0, 0, 0, 1]]
}'
import os, requests
response = requests.post(
"https://apis.viggle.ai/v1/scenes/custom-pano",
headers={"Authorization": f"Bearer {os.environ['VIGGLE_API_KEY']}"},
json={
"sessionId": "atrium-01",
"panoramaUri": "https://example.com/atrium-360.jpg",
"panoramaC2w": [[0, 0, 1, 0], [0, 1, 0, 1.5], [-1, 0, 0, 0], [0, 0, 0, 1]],
},
)
response.raise_for_status()
task = response.json()
const response = await fetch("https://apis.viggle.ai/v1/scenes/custom-pano", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VIGGLE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
sessionId: "atrium-01",
panoramaUri: "https://example.com/atrium-360.jpg",
panoramaC2w: [[0, 0, 1, 0], [0, 1, 0, 1.5], [-1, 0, 0, 0], [0, 0, 0, 1]],
}),
});
const task = await response.json();
授权
服务端 SDK 使用项目 API 密钥,Remote MCP 使用 OAuth 访问令牌。不要在浏览器代码中暴露项目密钥。
请求头
可选的调用者自定义关联 ID,最长 128 字符。服务会在响应头返回实际使用的值,便于追踪和支持排查。
1 - 128可选来源标签,最长 128 字符,用于识别 SDK、集成、产品入口或内部工作流。
1 - 128请求体
POST /v1/scenes/custom-pano 的请求体。全景图放在调用方世界坐标系中的 panoramaC2w 处:模型坐标系中的点 p 对应 R · (metersPerUnit · p) + t。旋转部分必须正交且不含镜像(容差 1e-4),平移分量绝对值的最大值加上 25 × metersPerUnit 必须小于 4000。
调用方自定的会话 ID,按账户隔离。一个会话对应一个场景及其全部图像批次,同一时间只运行一个任务;已有任务 processing 时再提交会返回 409,error.details.running_task_id 为正在运行的任务。
1 - 128^[A-Za-z0-9_-]+$"living-room-01"
360° 全景图的 URL。
4 × 4 相机到世界(camera-to-world)矩阵,按行写成 4 行、每行 4 个有限数值;最后一行必须为 [0, 0, 0, 1]。
4 elements4 elements[ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]
在应用 panoramaC2w 之前作用于模型坐标的缩放系数。
x >= 0.05等同于 outputUris.spz。
调用方自己的上传地址,Viggle 完成后用 PUT 把对应文件上传过去;值为 null 的项会被忽略。上传到这里的 .spz Viggle 不再保留,结果中的 spz.url 即该地址;预览图和全景图上传后 Viggle 仍保留一份,结果中链接指向 Viggle 的副本。每个地址必须是 http(s)、不含账号密码、且主机只解析到公网地址。
Show child attributes
Show child attributes
响应
场景任务已启动。
场景任务(以 scn_ 开头),所有 /v1/scenes 提交接口和 GET /v1/scenes/{task_id} 都返回它。ready、failed、cancelled 为终态。任务结束后约 72 小时内可查询。
任务 ID,格式为 scn_{runId}.{sessionId}。
custom-pano 任务也返回 pano。
pano, generate, reconstruct processing, ready, failed, cancelled 开始时间,Unix 毫秒。
结束时间,Unix 毫秒;processing 时为 null。
只在 ready 时有值。
Show child attributes
Show child attributes
只在 failed 时有值。code 为 INVALID_REQUEST(不可重试,模型拒绝了输入,例如引用了其他会话的批次)或 PROCESSING_FAILED(可重试)。
Show child attributes
Show child attributes

