Genji Crowd API

v1
EN 中文

快速开始 — 只需两步:

1. API 密钥:YOUR_API_KEY

2. curl -H "Authorization: Bearer YOUR_API_KEY" https://www.genjicrowd.com/v1/crowd/cr_sample01

Genji 人群 API — v1

合作伙伴集成参考文档(适用于 Tuanjie / Unity 原生渲染)。


版本说明

本文档描述 API v1,版本号体现在 URL 前缀中(/v1/)。 破坏性变更(字段移除、重命名或语义变更)将使版本升级至 /v2/, 届时 v1 将提前公告废弃,不会立即下线。 非破坏性变更(新增可选响应字段)不会更改版本号。


概述

Genji 人群 API 允许引擎加载已保存的人群配方(recipe)——即在 Genji 编辑器中创作的人群所需的完整配置——以及预先解析好的每个角色的部件分配和直接资源 URL。

典型集成流程:

GET /v1/crowd/{crowd_id}
  → recipe(人群配置)
  → characters[](体型 / 面部 / 发型 ID,位置,动画状态)
  → assets(GLB URL,动画 GLB URL)

引擎随后:

  1. 下载一次 assets.skeleton_glb(包含共享骨架上的所有部件网格)。
  2. characters[] 中的每个条目,选择对应网格节点并应用每个角色的颜色、缩放和动画相位。
  3. 使用配方中的边界 / 碰撞参数运行自己的智能体模拟。

免密钥试用

以下接口无需账号、无需 API 密钥即可调用——复制、粘贴、运行:

# API 能构建什么——形状、部件目录、动画片段(无需密钥)
curl -s https://www.genjicrowd.com/v1/capabilities

# 一个完整的真实人群数据——配方 + 每个角色的部件 + 资源 URL(无需密钥)
curl -s https://www.genjicrowd.com/v1/crowd/cr_sample01

cr_sample01 是唯一无需密钥即可读取的人群,方便 AI 助手或初次接入的开发者在注册前 就获取一份完整、真实的响应。机器可读的接口规范 https://www.genjicrowd.com/openapi.json 同样公开。

其余接口——列出或获取你自己的人群、创建人群,以及 MCP 端点——都需要密钥 (见下方鉴权)。


颜色与贴图设置

⚠️ 如果角色显得过暗,问题出在你的渲染器色彩空间设置,而**不是**资源本身。

请先检查 `outputColorSpace` 与贴图的 `colorSpace`。

这是我们见到的最常见的集成错误,表现上很像资源损坏或灯光有问题,但两者都不是。

我们提供的资源: 角色的基础色贴图均以 sRGB 制作。没有法线贴图,也没有粗糙度/金属度贴图, 因此不存在被误标为颜色数据的贴图 —— GLB 中唯一的贴图就是基础色。

问题成因: Three.js 内部在线性空间下运算,并在输出时转换。若渲染器的输出色彩空间保持默认值, 线性结果就会未经转换直接输出,画面会明显偏暗。反过来把颜色贴图标记为线性,同样会导致偏暗。

必需的设置(Three.js r152 及以上)

// 1. 渲染器必须在输出时把线性转换为 sRGB。绝大多数"角色太暗"的问题,这一行即可解决。
renderer.outputColorSpace = THREE.SRGBColorSpace;

// 2. GLTFLoader 会自动为基础色贴图打标,因此加载我们的 GLB 无需额外处理。
//    但你自行加载的任何颜色贴图都必须标记:
texture.colorSpace = THREE.SRGBColorSpace;
//    数据类贴图(bone_tex.bin、遮罩、粗糙度)**不可**标记为 sRGB —— 它们存的是数值而非颜色,
//    保持默认即可。

较旧版本的 Three.js(r152 之前)

同样的两项设置名称不同。如果你锁定在旧版本:

renderer.outputEncoding = THREE.sRGBEncoding;   // 对应 outputColorSpace
texture.encoding        = THREE.sRGBEncoding;   // 对应 texture.colorSpace

我们的 SDK 与本文档中的所有示例均基于 Three.js r169three@^0.169.0),使用 colorSpace 写法。

使用 SDK?这一切已为你处理好

/sdk/genji-bbm.js 导出了 configureRenderer(renderer),并且在你把渲染器传给 buildBbmCrowd(...) 时会自动调用:

import { configureRenderer } from 'https://www.genjicrowd.com/sdk/genji-bbm.js';
configureRenderer(renderer);   // 自动匹配你所用 Three 版本的对应字段

资源托管

所有 GLB 和动画文件由响应请求的服务器提供。 资源 URL 已完整解析并包含在每次 API 响应中——您无需自行构建。

环境 资源来源
生产环境 https://www.genjicrowd.com(资源可能由 Cloudflare R2 / CDN 分发)
本地开发 服务器自身主机(如 http://localhost:3001

请勿硬编码资源 URL。 始终从 response.assets.* 中读取。 服务器在请求时构建完整解析的 URL,反映当前运行位置。 将本文档中的示例 URL 复制到代码中将指向错误的主机,请直接使用 assets.skeleton_glbassets.animations.walk 等字段。


鉴权

每个请求必须在 Authorization 头中携带 Bearer 令牌:

Authorization: Bearer gk_live_…

密钥通过 Genji 控制台(设置 → API 密钥)或由部署团队签发。 原始密钥仅在创建时显示一次,不以明文形式存储。

错误响应:

状态码 响应体 原因
401 {"error":"Missing Authorization: Bearer <api_key>"} 缺少请求头
401 {"error":"Invalid or revoked api key"} 密钥错误或已吊销
404 {"error":"Crowd not found"} crowd_id 不存在

端点

`GET /v1/crowd/{crowd_id}`

返回一个已保存人群的完整配方、预解析角色列表和资源 URL。

路径参数

参数 类型 说明
crowd_id string 不透明 ID,格式为 cr_[a-zA-Z0-9]{8}

响应 `200 OK` — `application/json`

{
  "crowd_id":    "cr_sample01",          // string — 与请求路径匹配
  "name":        "Imperial Crowd — Tuanjie Sample",
  "collection":  "genji-v1",             // string — 集合标签(可为 "")
  "recipe":      { ... },                // object — 见下方配方结构
  "characters":  [ ... ],               // array  — 见下方角色对象
  "shared_parts": ["Eye_L","Eye_R","L_Hand","R_Hand","L_Foot","R_Foot"],
  "assets":      { ... },                // object — 见下方资源 URL
  "updated_at":  "2026-06-24 10:00:00"  // string — ISO 风格 UTC 时间戳
}

管理人群档案(`/v1/projects`)

使用与上面相同的 Bearer API key。这些端点可以让你以编程方式列出、创建和更新已保存的人群档案 ——与网站控制台读写的是同一张 projects 表,因此两边创建的人群档案可以互通。

`GET /v1/projects`

列出调用者已保存的人群档案。不计入用量。

[
  { "crowd_id": "cr_sample01", "name": "Imperial Crowd", "collection": "genji-v1",
    "thumbnail": null, "created_at": "...", "updated_at": "..." }
]

`POST /v1/projects`

根据配方创建并保存一个新的人群档案。计费方式与 GET /v1/crowd/{crowd_id} 及网站的保存操作相同。

// 请求体
{ "name": "My Crowd", "recipe_json": { "size": 500, "shape": "average", ... }, "collection": "" }
// 响应
{ "crowd_id": "cr_AbCdEf12", "name": "My Crowd" }

`PUT /v1/projects/{crowd_id}`

更新已有人群档案的配方/名称/分类。仅当 recipe_json 实际发生变化时才计费(仅重命名不计费, 与网站行为一致)。


MCP 服务器(面向 AI 编程助手)

Genji 还在 POST /mcp 提供了一个远程 MCP 服务器,使 Claude Code、Claude Desktop、Cursor 等工具 可以在对话中直接列出、获取、创建人群档案——用聊天描述一个场景,即可"vibe code"出来。

工具 用途
genji_get_capabilities 构建配方所需的有效形状、分类桶 ID、部件 ID/标签
genji_list_crowds 列出调用者已保存的人群档案
genji_get_crowd crowd_id 获取人群档案。可选 tier 参数(full / untextured / vat / bbm)决定返回内容的形态——full 包含逐角色列表,其余档位省略该列表(因为这些档位改为从骨骼纹理数据渲染)。无论哪种档位,返回的底层资源文件都是相同的(见下方"超级优化渲染档位"章节)。
genji_create_crowd 根据配方创建并保存一个新的人群档案——"用聊天生成场景"的核心工具

所有工具调用都经过与上方 REST 端点相同的用量/档位限制检查——MCP 只是接入同一套系统的另一个 鉴权入口,而非独立的另一套。


配方结构(Recipe Schema)

recipe 是一个包含以下字段的 JSON 对象。

核心字段

字段 类型 说明
v integer 配方版本,当前为 1
seed integer 主 RNG 种子,决定所有角色分配和初始位置(确定性)。
size integer 人群中的角色总数。
packs string[] 选用的文明包。缺省或为空时等同于 ["chinese"]。参见 文明包(Civilization packs)
weights object 桶权重映射 — { bucketId: number },控制服装原型的混合比例。桶 ID 见 能力目录(Capabilities)

行走 / 动画

字段 类型 说明
walkSpeedMin float 最小行走速度倍数(相对于动画片段速度),默认 0.6
walkSpeedMax float 最大行走速度倍数,默认 1.0
walkRatio float 旧版字段(已被 pace 取代)。
pace float 0–1 行走角色占总数的比例。0 = 全部待机,1 = 全部行走,默认 0.5
crowdRate float 流动 / 道路模式下每分钟进入的角色数。

布局

字段 类型 说明
boundaryShape string 人群边界形状:"circle""square""road""donut""spline"
boundaryRadius float 外边界半径(米)。road 模式下为半长。
spawnRadius float 内部生成半径(米)。road 模式下为半宽。
splinePoints [[x,z], …] | null spline 形状的控制点(世界空间 XZ)。其他形状为 null
facing float 0–1 0 = 随机朝向,1 = 全部朝同一方向。
distribution string "random""grid",控制边界内位置的分布方式。

地形

字段 类型 说明
terrain.active boolean true = 启用程序化地形。
terrain.amplitude float 地形高度振幅(米)。
terrain.frequency float 地形噪声频率。
terrain.noiseType string "fractal""simplex"

颜色变化

所有服装颜色均为桶特定的 HSL 值,并为每个角色随机抖动。

字段 类型 说明
hueShift float 应用于所有角色服装的全局色相旋转。
hueVar float 每角色色相抖动范围 ±hueVar
satVar float 每角色饱和度抖动范围 ±satVar
lightVar float 每角色亮度抖动范围 ±lightVar

碰撞 / 避让

这些参数输入到您自己的智能体模拟引擎中。

字段 类型 说明
collisionRadius float NPC 体积半径倍数(× 角色高度)。两个角色距离 = 2 × 半径 × 高度时视为"接触"。
personalSpace float 向心力 / 路径跟随力强度(01)。
anticipation float 0–1 NPC 开始躲避碰撞的提前程度。0 = 接触时才反应,1 = 提前转向。

障碍物

"obstacles": [
  { "type": "rock", "x": 20.0, "z": 15.0, "radius": 3.5 },
  ...
]

每个障碍物为世界空间 XZ 中的圆柱(Y 轴无限延伸)。角色会绕开以 (x, z) 为圆心、半径为 radius 米的圆盘区域。

效应器

字段 类型 说明
groupingMode boolean true 时,角色聚集成群组。
attractActive boolean 全局吸引器是否激活。
attractTarget {x, z} 吸引器的世界空间 XZ 位置。

渲染标志

这些是创作工具中的视口开关,供渲染器参考使用。

字段 类型 说明
textures boolean 保存时作者已开启纹理。
collisionEnabled boolean NPC 间避让已启用。
jogBlendEnabled boolean 行走→慢跑混合已启用。
lazyGPU boolean 内部 GPU 更新优化标志。
collection string 集合标签(如 "genji-v1")。

文明包(Civilization packs)

角色来自 13 个文明包。在配方中用 packs 选择:

"recipe": { "size": 200, "seed": 1, "packs": ["roman", "norse"] }
chinese 中华 african 非洲 arabian 阿拉伯 aztec 阿兹特克
egyptian 古埃及 european 欧洲 greek 古希腊 indian 印度
japanese 日本 norse 北欧 persian 波斯 roman 古罗马
western 西部

缺省 packs 只会得到中华包。 这是刻意保留的默认值,用于保证在文明包上线之前创建的人群仍能逐字节复现。但这也意味着:客户端若不显式设置该字段,无论用户要求什么,得到的都是中华人群。

桶会在读取权重之前先按启用的文明包过滤,因此若 packs 中没有 "roman",对罗马桶设置权重不会有任何效果。未知的文明包 ID 会在创建时返回 400 并附上合法 ID 列表。

GET /v1/capabilities 返回的每个桶和部件都带有 pack 标记。

加载文明包模型

中华身体、全部面部与发型、以及六个共享皮肤部件都在 skeleton_glb 内。其余每个文明包各自一个 GLB,列在 assets.pack_glbs 中,且只包含该人群实际用到的包:

"assets": {
  "skeleton_glb": "{base}/assets/Basemesh31.glb",
  "pack_glbs":    { "roman": "{base}/assets/packs/roman.glb" }
}

每个角色都带有 pack 字段,据此判断其 body_id 在哪个文件里。

所有文明包都绑定在同一套 72 根骨骼上,骨骼顺序完全一致。 因此 bone_tex_binbone_tex_manifest 以及所有动画片段对文明包模型完全适用,无需修改:GPU 实例化、VAT、BBM 路径均不受影响。新增一个文明包只多一次 GLB 请求。

每个文明包 GLB 约 7-11 MB。编辑器出于显存考虑最多同时加载 3 个;API 不做限制,因此可以请求全部 13 个,但那意味着约 96 MB 的模型数据。

目前所有文明包共用中华面部与发型。 现阶段只有身体和服装是分文明包的。在面部资产扩充之前,罗马军团兵会顶着一张中华面孔。

角色对象(Character Object)

characters[] 中的每个条目为一个已预解析部件 ID 的 NPC。

{
  "index":        0,                              // integer — 0 起的角色索引
  "bucket":       "soldiers",                     // string  — 原型桶 ID
  "pack":         "chinese",                      // string  — body_id 所在的文明包
  "body_id":      "M_Terracotta_Infantry_Soldier",// string  — skeleton_glb 中的 SkinnedMesh 节点名
  "face_id":      "M_Chinese_Face_03",            // string  — skeleton_glb 中的 SkinnedMesh 节点名
  "hair_id":      "Ming_Warrior_Crown",           // string  — skeleton_glb 中的 SkinnedMesh 节点名
  "x":            12.3400,                        // float   — 初始 X 坐标(米,世界空间)
  "z":            -5.7100,                        // float   — 初始 Z 坐标(米,世界空间)
  "rot_y":        1.2340,                         // float   — Y 轴初始旋转(弧度)
  "scale":        1.0230,                         // float   — 统一缩放倍数
  "phase":        0.6710,                         // float 0–1 — 动画相位偏移(避免集体同步)
  "anim_clip":    "walk",                         // string  — 生成时的动画:"idle" 或 "walk"
  "idle_variant": 2,                              // integer 0–7 — 待机动画变体编号
  "garment_hsl":  [0.0521, 0.5182, 0.4830],      // [H, S, L] 归一化 0–1 — 每角色服装色调
  "skin_hex":     "#ddb892",                      // string  — 皮肤颜色十六进制
  "hair_hex":     "#1a1410"                       // string  — 发色十六进制
}

共享部件

body_idface_idhair_id 外,每个角色还渲染六个共享网格节点(眼睛、手、脚)。 这些对所有角色相同,列于顶层 shared_parts 数组中:

["Eye_L", "Eye_R", "L_Hand", "R_Hand", "L_Foot", "R_Foot"]

坐标系

API 响应中的所有位置、距离和旋转均使用以下约定:

属性
手性 右手坐标系
向上轴 Y
单位
地面平面 XZ(Y = 地形高度,平地上为 0)
rot_y = 0 角色朝向 +Z
rot_y 从上方(+Y)俯视逆时针旋转

角色位置以 (x, z) 对给出(Y 始终为地面高度)。 将角色放置于世界坐标 (x, groundHeight, z),并应用 Y 轴旋转 rot_y 弧度。

角色朝向方向向量:

forward_x = sin(rot_y)
forward_z = cos(rot_y)

与 GLB 资源一致:skeleton_glb 和所有动画 GLB 均以 glTF 约定导出(右手系,Y 向上,米)。加载时无需进行轴交换。

开箱即用即为米制。 API 响应中的 x/z(角色位置)以米为单位,GLB 场景加载后同样以米为单位: 源骨架以 Maya 厘米制作,但导出时已将 0.01 缩放烘焙进 GLB 根节点。 请勿再做任何单位换算 —— 加载后的角色身高约 1.7 米。 加载后自检:约 1.7 = 正确;约 170 = 你的导入器丢弃了根节点缩放(需自行在根节点应用 0.01); 约 0.017 = 你额外应用了一次 0.01(必须移除)。

Unity / 左手引擎: Unity 使用左手 Y 向上坐标系。 转换方式:取反 zposition.z = -C.z,取反 rot_yrotation.y = -C.rot_y。 转换方向向量时同样取反 forward_z

rot_y 是出生时刻的朝向快照,不是运动方向向量。 它根据配方的出生区域形状 一次性生成(例如圆形边界时基本是随机值)。如果你自行实现了角色运动逻辑 (自定义路径、让角色沿街道行走、转向/避让等)——请每帧根据自己的速度矢量 重新计算朝向,不要对自行移动的角色继续使用 API 提供的静态 rot_yrot_y = atan2(velocity.x, velocity.z)。仅当角色为 idle(始终静止)或你在 原样复现配方描述的出生布局、未额外添加自定义运动时,才可直接使用 API 返回的 rot_y


资源 URL

// ⚠️  这些 URL 由服务器返回——不由客户端构建。
// 主机名随环境变化(本地开发 / 生产 CDN)。
// 始终使用响应中的值,切勿硬编码基础 URL。
"assets": {
  "unit":            "m",
  "unit_scale":      1.0,
  "skeleton_glb":    "{base}/assets/Basemesh31.glb",
  "lod_glb":         "{base}/assets/Basemesh31_LOD1.glb",
  // 仅包含本人群实际用到的文明包。纯中华人群不会出现该字段,
  // 因为中华模型就在 skeleton_glb 里。
  "pack_glbs":       { "roman": "{base}/assets/packs/roman.glb" },
  "capabilities_json": "{base}/v1/capabilities",
  "animations": {
    "idle":              "{base}/assets/A_Idle.glb",
    "idle_arms_crossed": "{base}/assets/A_Idle_ArmsCrossed.glb",
    "idle_hands_hip":    "{base}/assets/A_Idle_HandsHip.glb",
    "idle_clasp":        "{base}/assets/A_Idle_Clasp.glb",
    "idle_shift":        "{base}/assets/A_Idle_Shift.glb",
    "idle_look_around":  "{base}/assets/A_Idle_LookAround.glb",
    "idle_scratch":      "{base}/assets/A_Idle_Scratch.glb",
    "idle_tired":        "{base}/assets/A_Idle_Tired.glb",
    "walk":              "{base}/assets/A_WalkFwd.glb",
    "jog":               "{base}/assets/A_JogFwd.glb",
    "walk_stop":         "{base}/assets/A_WalkFwd_Stop.glb"
  }
}

部件 ID 与资源的映射关系

每个文明包加载一个 GLB,而不是每个角色一个。

skeleton_glb 包含全部中华身体、全部面部与发型,以及六个共享皮肤部件。其余每个文明包各自一个 GLB,列在 assets.pack_glbs 中。

每个部件 ID(如 M_Terracotta_Infantry_Soldier)是其中某个文件内 SkinnedMesh 节点的名称。具体在哪个文件:

字段 节点所在文件
body_id packchinese 时在 skeleton_glb,否则在 pack_glbs[character.pack]
face_idhair_id 始终在 skeleton_glb
shared_parts 始终在 skeleton_glb

渲染角色的步骤:

  1. 加载一次 skeleton_glb,以及 pack_glbs 中的每个 GLB 各一次。
  2. characters[] 中的每个角色,将以下网格节点设为可见
    • body_id 节点
    • face_id 节点
    • hair_id 节点
    • 全部 6 个 shared_parts 节点(眼睛、手、脚)
    • 所有其他网格节点:隐藏
  3. 实例化或克隆可见集,应用角色的 scalerot_y(x, z) 位置。
  4. 应用每角色颜色(见下方颜色应用)。
  5. 播放 anim_clip 指定的动画片段,偏移量为 phase × clip_duration

lod_glb 是一个独立的第二份 GLB 文件——节点名称、共享骨架、缩放/原点行为均与 skeleton_glb 相同,仅使用更低面数的网格(文件大小约为原版的 40%)。它不是 skeleton_glb 内部的网格变体,需要单独加载并以相同方式克隆。对于此规模的人群, 一个合理的默认策略是:距相机约 15–20 米内使用高精度 skeleton_glb,超出此距离 切换为 lod_glb

每个角色任意时刻只需保留一份激活的克隆,不需要同时维护两份。 切换成本很低: 当角色越过 LOD 距离阈值时,替换其克隆体(高精度 ↔ LOD),但将当前动画片段和 播放时间带到新克隆的 mixer 上newAction.time = oldAction.time),而不是从 0 重新开始。"不要在动画播放过程中热切换" 说的正是这一点——切换时保留播放进度。 这并不意味着要让两种精度的角色同时并行运行;那样会让每个角色始终占用双倍 内存和双倍动画计算,而实际上任意时刻只有一部分角色需要做距离判断。

capabilities_jsonGET /v1/capabilities)列出可用的形状、可加权的桶 id/标签,以及每个部件的 id、标签、组别和性别,适合用于构建 UI 或验证工具。

颜色应用

每个角色有三个颜色输入:

输入 来源 应用目标
garment_hsl 每角色 HSL(0–1 归一化) 身体网格 —— 直接设置:material.color.setHSL(h, s, l)
skin_hex 十六进制字符串 面部 + 共享皮肤网格
hair_hex 十六进制字符串 发型网格

garment_hsl完整的最终服装颜色,由服务端计算。请直接用 material.color.setHSL(h, s, l) 应用 —— 切勿从其他来源推导或叠加。 另外请先为每个角色克隆材质再着色,否则所有角色会共享同一颜色。


GPU 实例化渲染(骨骼纹理)

上文描述的默认渲染路径(SkeletonUtils.clone() + 每角色一个 AnimationMixer)实现 简单,但扩展性有限 —— 根据硬件不同,大约在 150–300 个角色规模就会跌出流畅帧率 (实测数据:150–5000 个独立克隆角色时帧率为 17–24 fps)。

对于 1000+ 角色的人群,请改用 assets.bone_tex_bin + assets.bone_tex_manifest: 一份共享的 GPU 纹理,编码了每个动画片段的骨骼矩阵,在自定义蒙皮着色器中逐顶点采样, 通过每个身体/面部/发型部件一个 THREE.InstancedMesh(而非每角色一个)渲染 —— 无论人群规模多大,整个人群通常只需 ≤36 次绘制调用。这与 Genji 人群创作工具自身 用于以流畅帧率渲染数千角色的技术完全相同。

"assets": {
  "bone_tex_bin":      "{base}/assets/bone_tex.bin",
  "bone_tex_manifest":  "{base}/assets/bone_tex_manifest.json"
}

可直接使用的参考实现位于 /sdk/genji-crowd-instanced.js(独立 ES 模块,唯一的 对等依赖是 three),并附带一个可运行示例 /sdk/genji-crowd-instanced-demo.html (追加 ?size=2000 可控制人群规模)。

工作原理

bone_tex_bin 是一份原始 Float32Array 二进制数据(无文件头、无压缩),按 RGBA32F 纹理布局排列:width = bone_tex_manifest.texWidthheight = bone_tex_manifest.texHeight。 每个动画片段占据一段连续的纹理列(每帧一列,30 fps 烘焙);每根骨骼占据 4 行纹理 (一个 mat4,存为 4 个 vec4 行)。你的着色器逐顶点采样该纹理,按 skinWeight 混合 4 根影响骨骼,重建出与 SkeletonUtils.clone() 在 CPU 上计算结果完全一致的蒙皮 位置 —— 只是这次是在 GPU 上为一个部件的所有实例统一计算一次,而不是每个克隆角色 在 CPU 上各算一次。

使用时无需理解这些细节 —— 参考 SDK 中的 loadBoneTexture() 会为你构建 THREE.DataTexture,内置着色器已完成采样逻辑。

bone_tex_manifest 字段

字段 类型 含义
texWidth / texHeight int bone_tex_bin 的 RGBA32F 纹理尺寸。
nBones int 骨骼数量(与 skeleton_glb 的骨架一致)。
bakeFps int 动画烘焙时使用的帧率(30)。
format string 固定为 "RGBA32F" —— 每纹素 4 个浮点数,无压缩。
totalBakedFrames int 所有已烘焙片段的总帧数。
numIdle int 待机变体数量(索引 0..numIdle-1,与 idle_variant 对应)。
clipWalk int anim_clip === "walk" 时应使用的片段索引。
clipInfo object 按片段名称索引的 {start, frames, duration}
modelScale number 仅供参考 —— 已烘焙进纹理数据,请勿重复应用。

最简集成示例

import { loadParts, loadBoneTexture, buildCrowdInstances, updateCrowdTime }
  from '/sdk/genji-crowd-instanced.js';

const parts   = await loadParts(response.assets.skeleton_glb);
const boneTex = await loadBoneTexture(response.assets.bone_tex_bin, response.assets.bone_tex_manifest);
const crowd   = buildCrowdInstances(scene, parts, boneTex, response.characters, response.shared_parts);

function animate(dt) {
  updateCrowdTime(crowd, dt);
  renderer.render(scene, camera);
}

适用范围:不支持每角色速度差异

该渲染路径严格实现公开 Character 数据结构中已有的字段 (anim_clip/idle_variant/phase/garment_hsl/skin_hex/hair_hex/x/z/rot_y/scale) —— API 中没有速度或动量字段,因此 walk 始终以其原始烘焙速度播放。角色之间仅通过 phase(相位错开)产生差异,而非播放节奏。这是人群规模渲染下已知且可接受的取舍, 并非缺陷:2000 名步行者组成的人墙,观感会比手工调校、带每角色速度差异的模拟略显 整齐划一。

常见集成故障(骨骼纹理路径)

症状 原因 解决方法
所有角色共享同一姿势 / T-pose loadBoneTexture() 完成前就开始渲染 调用 buildCrowdInstances 前先 await
角色完全不动画(静止不动) uTime uniform 从未更新 每帧调用 updateCrowdTime
所有角色播放同一片段/姿势 未正确写入每实例的 aClip/aPhase 使用 writeCrowdInstances,不要手写实例缓冲区
请求了 jog/run 动画但不播放 不可用 —— 公开 API 仅提供 idle/walk 属预期行为 —— jog 仅供内部使用,不在此 API 范围内
绘制调用数超过约 36 次 按每角色而非每部件构建了 InstancedMesh 应按每个唯一的 body_id/face_id/hair_id/共享部件值各建一个,而非每角色一个

超级优化渲染档位(无纹理 / VAT / BBM)

上述骨骼纹理路径已能以约 36 次绘制调用满帧渲染整个人群。对于超大规模(数万角色) 或显存 / GPU 受限的目标平台(移动端、VR、集成显卡),还有三个档位可以用视觉精度 换取纯吞吐量。这些正是 Genji 工具内部使用的同一批优化。

这三者消耗的都正是 API 已返回的数据 —— assets.skeleton_glb 以及 assets.bone_tex_bin + assets.bone_tex_manifest没有额外接口,也没有额外下载; 每个档位都是你在客户端做出的渲染选择。它们还可以作为 LOD 分带混用(近处用完整网格, 远处用 BBM),而且由于每个档位读取的都是同一份骨骼纹理,角色在切换档位时动画始终保持同步。

一览

档位 绘制调用 逐顶点计算 额外显存 适用场景
完整蒙皮(骨骼纹理) 约 36 4 骨骼采样 + 加权混合 部件纹理 默认;近处相机
无纹理 约 36 4 骨骼混合,无贴图采样 (释放漫反射贴图) 显存受限;风格化;远处分带
VAT 约 36 2–8 次采样,无混合 大(整套约 2.4 GB) ALU / 蒙皮混合受限
BBM 骨骼立方体代理 1 1 次采样 + 1 次矩阵乘法 超大规模;远处 LOD

1. 无纹理(扁平)模式

完全不加载部件的漫反射 / 法线 / 粗糙度贴图,将每个部件渲染为一个受光的纯色(白色, 或每部件一个纯色调)。这会释放全部纹理显存,并从片段着色器中移除每一次纹理采样 —— 是这里最轻量的改动,也很适合远处 LOD 分带或内存有限的设备。

2. VAT —— 顶点动画纹理(Vertex Animation Texture)

蒙皮后的顶点位置(即骨骼纹理着色器计算出的完全相同的结果)预烘焙到一张按 [绝对帧, 顶点ID] 索引的纹理中。运行时顶点着色器只做一次查表并在两帧之间插值,而不再 混合四根骨骼矩阵:

// f0,f1 = 夹住当前时间的两个帧列;fr = 混合系数
vec3 pos = mix( texelFetch(uVAT, ivec2(f0, gl_VertexID), 0).xyz,
                texelFetch(uVAT, ivec2(f1, gl_VertexID), 0).xyz, fr );

3. BBM —— 骨骼立方体代理(超大规模 LOD)

约 17 个刚性基本体替换每个角色的蒙皮网格 —— 每根骨骼一个立方体 / 圆柱体(四肢圆柱、 两个躯干块、一个头部、双手),每个都固定到单根骨骼上。没有 4 骨骼混合:每顶点只做一次 骨骼矩阵采样。整个人群合并为一个实例化网格 → 1 次绘制调用,是所有档位中最廉价的动画 顶点着色器。

从骨架绑定姿势(skeleton_glb)一次性构建它: 对每根四肢骨骼,构建一个从该骨骼延伸到 其子骨骼的圆柱;躯干和头部则根据髋部 / 肩部 / 头部骨骼的位置拟合。在每个基本体所属骨骼的 局部绑定空间中构建它,然后给它的每个顶点打上那一根骨骼的索引(aBone)。

运行时:同一份骨骼纹理采样那一根骨骼的动画矩阵,并刚性地变换顶点 —— 这是父级 约束,而非蒙皮:

mat4 b = boneMatrix(aBone);        // 从 bone_tex(共享纹理)采样
vec3 world = (b * vec4(pos, 1.0)).xyz * UNIT_SCALE;   // 见下方陷阱

关键的顺序陷阱(踩坑换来的经验)。 骨骼纹理把厘米→米的 0.01 模型缩放烘焙进了 每个矩阵(bone_tex_manifest.modelScale),而立方体几何体以米为单位,因此你要用一个 ×100 来补偿。×100 施加在变换后的点上,而不是输入上: (b * p) * 100绝不能写成 b * (p * 100)。均匀缩放与矩阵的旋转部分可交换,但与其平移部分 不可交换 —— 放错一边,旋转看起来仍然正确,但每根骨骼的姿势偏移被压缩到 1%,于是 任何远离绑定姿势的骨骼(迈步中的小腿、摆动的手臂)的立方体都会脱离身体并漂浮。在 绑定姿势的 T-pose 下一切正常,只有动起来才会崩,因此这个 bug 极易被漏掉又难以定位。


动画片段

共有 8 个待机变体(索引 0–7,anim_clip = "idle")和 1 个行走片段anim_clip = "walk")。使用 idle_variant 选择对应待机 GLB。

anim_clip idle_variant GLB 键
"idle" 0 animations.idle
"idle" 1 animations.idle_arms_crossed
"idle" 2 animations.idle_hands_hip
"idle" 3 animations.idle_clasp
"idle" 4 animations.idle_shift
"idle" 5 animations.idle_look_around
"idle" 6 animations.idle_scratch
"idle" 7 animations.idle_tired
"walk" (任意) animations.walk

所有片段共享 skeleton_glb 中的同一骨架,加载并重定向一次即可。

⚠️ 只应用旋转轨道,必须丢弃所有位移轨道

动画片段是为 rest_reference_glb 骨架制作的,除旋转外还带有位移轨道(walk 片段有 60 条位移通道,包括 rootpelvis 和整条脊椎)。skeleton_glb 中的 Genji 骨架 骨骼名称相同但绑定姿势不同,因此这些位移无法直接迁移。

一旦应用这些位移轨道,角色会在动画开始播放的瞬间沉入地面 —— 骨盆被拉到参考骨架的 胯部高度。播放前的 A-pose 完全正常,所以很容易被误判为缩放或地面偏移问题,但两者都不是。

不要试图用 Y 轴偏移来补偿。 骨盆位移每一帧都会重新应用,静态偏移会与动画值相互冲突, 结果是角色悬空、断裂,或每帧下沉的幅度都不一样。

正确做法是只保留旋转轨道。骨骼位置来自 Genji 骨架自身的绑定姿势,动画只提供旋转:

// three.js —— 完整修复只需这一行
const tracks = clip.tracks.filter((t) => t.name.endsWith('.quaternion'));

然后对保留下来的轨道套用下面的骨骼四元数差值重定向。/demo.html 中的参考实现正是这么做的。

预期的副作用: 丢弃 root 轨道后,walk 片段会原地播放。这是有意为之 —— Genji 提供 人群的构成与位置,移动由你的引擎负责。请自行驱动每个角色的位置(参考 recipe 中的边界/ 碰撞字段),并让行走循环原地播放。

animations.walk 在根骨骼上烘焙了根运动位移,方向严格沿局部 +Z 轴(已验证: 整个片段中根节点位移从 [0,0,0] 变化到 [0,0,1.7])——与 rot_y = 0 时角色的 朝向完全一致。如果角色看起来在倒着走,问题不在动画片段本身——请检查你自己的 位置/旋转代码(很可能是 atan2 参数顺序颠倒,或从运动方向推导 rot_y 时符号 搞反了)。在用条件性的 + Math.PI 翻转来打补丁之前,请直接从速度矢量重新推导 rot_yrot_y = atan2(velocity.x, velocity.z),并移除所有按分支添加的符号 补丁——在错误公式上叠加条件翻转,通常表现为"修好一半",因为它补偿的是错误的 那个 bug。

phase 字段(0–1)为生成时片段的归一化时间偏移:

startTime = phase × clipDuration   // 单位:秒

这样可避免整个人群动画同步。


能力目录(Capabilities)

GET /v1/capabilities(也在每个人群响应中通过 assets.capabilities_json 提供)是您用于构建请求和 UI 的目录。它列出可用的边界形状、可在 weights 中引用的桶 id/标签,以及每个部件的 id、标签、组别和性别。结构如下:

{
  "shapes": ["circle", "square", "road", "donut", "spline", "polygon"],
  "buckets": [
    { "id": "soldiers", "label": "Soldiers" },
    { "id": "laborers", "label": "Laborers" }
    // ... 每个可加权的原型一个条目
  ],
  "parts": [
    { "id": "M_Terracotta_Infantry_Soldier", "label": "Terracotta Infantry Soldier", "group": "body", "gender": "m" },
    { "id": "M_Chinese_Face_01",             "label": "Male Face 01",                 "group": "face", "gender": "m" },
    { "id": "Ming_Warrior_Crown",            "label": "Ming Warrior Crown",           "group": "hair", "gender": "m" }
    // ... 所有身体、面部和发型部件
  ]
}

完整示例

请求

cURL

curl -s \
  -H "Authorization: Bearer YOUR_API_KEY" \
  https://www.genjicrowd.com/v1/crowd/cr_sample01

JavaScript

const res = await fetch('https://www.genjicrowd.com/v1/crowd/cr_sample01', {
  headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.json()).error}`);

const crowd = await res.json();
// crowd.assets.*     — 完整解析的 GLB URL,直接传给加载器
// crowd.characters[] — 每 NPC 的部件 ID、位置、动画片段、颜色
console.log(crowd.characters.length + ' 个角色');
console.log('骨架 GLB:', crowd.assets.skeleton_glb);

Unity (C#)

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class GenjICrowdLoader : MonoBehaviour
{
    [SerializeField] string apiHost = "https://www.genjicrowd.com";
    [SerializeField] string crowdId = "cr_sample01";
    [SerializeField] string apiKey  = "YOUR_API_KEY";

    void Start() => StartCoroutine(LoadCrowd());

    IEnumerator LoadCrowd()
    {
        using var req = UnityWebRequest.Get($"{apiHost}/v1/crowd/{crowdId}");
        req.SetRequestHeader("Authorization", $"Bearer {apiKey}");
        yield return req.SendWebRequest();

        if (req.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError($"[Genji] 人群加载失败: {req.error}");
            yield break;
        }

        // crowd.assets.skeleton_glb  — 使用 GLTFast / Piglet / 自定义 GLB 加载器加载
        // crowd.characters[]         — 每 NPC 的部件 ID、位置、动画、颜色
        string json = req.downloadHandler.text;
        Debug.Log($"[Genji] 响应: {json.Length} 字节");

        // 下一步:将 json 解析为 CrowdResponse 类并下载资源
    }
}

响应 `200 OK`(缩略版——完整响应含 150 个角色)

{
  "crowd_id": "cr_sample01",
  "name": "Imperial Crowd — Tuanjie Sample",
  "collection": "genji-v1",
  "recipe": {
    "v": 1,
    "seed": 42,
    "size": 150,
    "weights": {
      "laborers": 40,
      "merchants": 10,
      "soldiers": 35,
      "court_officials": 5,
      "emperor": 0,
      "monks": 5,
      "sages": 5,
      "swordsmen": 0
    },
    "walkSpeedMin": 0.6,
    "walkSpeedMax": 1.0,
    "walkRatio": 0.65,
    "pace": 0.5,
    "crowdRate": 30,
    "scaleVariation": 0.06,
    "boundaryShape": "circle",
    "boundaryRadius": 80,
    "spawnRadius": 80,
    "splinePoints": null,
    "facing": 0,
    "distribution": "random",
    "terrain": { "active": false, "amplitude": 2.0, "frequency": 0.08, "noiseType": "fractal" },
    "hueShift": 0.0,
    "hueVar": 0.035,
    "satVar": 0.18,
    "lightVar": 0.12,
    "collisionRadius": 0.4,
    "personalSpace": 0.8,
    "anticipation": 0.3,
    "obstacles": [
      { "type": "rock", "x": 20, "z": 15, "radius": 3.5 },
      { "type": "rock", "x": -25, "z": 30, "radius": 2.8 }
    ],
    "groupingMode": false,
    "attractActive": false,
    "attractTarget": { "x": 0, "z": 0 },
    "textures": true,
    "collisionEnabled": true,
    "jogBlendEnabled": true,
    "lazyGPU": false,
    "collection": "genji-v1"
  },
  "characters": [
    {
      "index": 0,
      "bucket": "soldiers",
      "body_id": "M_Terracotta_Infantry_Soldier",
      "face_id": "M_Chinese_Face_02",
      "hair_id": "Ming_Warrior_Crown",
      "x": 14.2317,
      "z": -8.5901,
      "rot_y": 3.9821,
      "scale": 1.0184,
      "phase": 0.7341,
      "anim_clip": "walk",
      "idle_variant": 5,
      "garment_hsl": [0.0181, 0.5442, 0.4716],
      "skin_hex": "#e8c8a0",
      "hair_hex": "#1a1410"
    },
    {
      "index": 1,
      "bucket": "laborers",
      "body_id": "Conscript_Laborer_Plain_Belted_Tunic",
      "face_id": "M_Chinese_Face_04",
      "hair_id": "Bamboo_Hat",
      "x": -22.1083,
      "z": 11.4452,
      "rot_y": 1.2076,
      "scale": 0.9812,
      "phase": 0.3295,
      "anim_clip": "idle",
      "idle_variant": 2,
      "garment_hsl": [0.0049, 0.5214, 0.5123],
      "skin_hex": "#ddb892",
      "hair_hex": "#251c14"
    }
    // … 148 个角色
  ],
  "shared_parts": ["Eye_L", "Eye_R", "L_Hand", "R_Hand", "L_Foot", "R_Foot"],
  // 资源 URL 反映服务器当前运行位置——请从响应中读取
  "assets": {
    "skeleton_glb":    "https://www.genjicrowd.com/assets/Basemesh31.glb",
    "lod_glb":         "https://www.genjicrowd.com/assets/Basemesh31_LOD1.glb",
    "capabilities_json": "https://www.genjicrowd.com/v1/capabilities",
    "animations": {
      "idle":              "https://www.genjicrowd.com/assets/A_Idle.glb",
      "idle_arms_crossed": "https://www.genjicrowd.com/assets/A_Idle_ArmsCrossed.glb",
      "idle_hands_hip":    "https://www.genjicrowd.com/assets/A_Idle_HandsHip.glb",
      "idle_clasp":        "https://www.genjicrowd.com/assets/A_Idle_Clasp.glb",
      "idle_shift":        "https://www.genjicrowd.com/assets/A_Idle_Shift.glb",
      "idle_look_around":  "https://www.genjicrowd.com/assets/A_Idle_LookAround.glb",
      "idle_scratch":      "https://www.genjicrowd.com/assets/A_Idle_Scratch.glb",
      "idle_tired":        "https://www.genjicrowd.com/assets/A_Idle_Tired.glb",
      "walk":              "https://www.genjicrowd.com/assets/A_WalkFwd.glb",
      "jog":               "https://www.genjicrowd.com/assets/A_JogFwd.glb",
      "walk_stop":         "https://www.genjicrowd.com/assets/A_WalkFwd_Stop.glb"
    }
  },
  "updated_at": "2026-06-24 10:00:00"
}

如何在引擎中渲染

1.  GET /v1/crowd/{crowd_id}                  → 获取响应 R

2.  下载一次 R.assets.skeleton_glb。
    其中包含每个部件对应的 SkinnedMesh 节点,所有节点绑定到同一共享骨架。
    自检:加载后的角色身高约 1.7 米。请勿做任何单位换算(见上方"坐标系")。

3.  对 R.characters[] 中的每个角色 C:

    a.  使用支持骨架的克隆方法克隆整个已加载场景
        (three.js:THREE.SkeletonUtils.clone(gltf.scene))。
        普通的 .clone(true) 会让克隆体仍绑定到原始骨架 ——
        所有副本将同步变形或渲染在错误位置。

    b.  选择网格节点:C.body_id、C.face_id、C.hair_id
        + R.shared_parts 中的所有节点
        隐藏所有其他 SkinnedMesh 节点。

    c.  放置角色 —— 变换只应用在克隆的最外层包装节点上:
        position  = (C.x, 0, C.z)   ← Y 取 (x,z) 处的地形高度
        rotation  = Quaternion.fromAxisAngle(Y, C.rot_y)
        scale     = Vector3(C.scale, C.scale, C.scale)   ← 仅为体型差异,约 0.95–1.05

        注意:蒙皮网格跟随的是骨骼,不是自身节点变换。给单个 SkinnedMesh
        设置 position/scale 不会有任何效果。请变换克隆调用返回的场景包装
        节点,并且绝不要碰内部名为 "root" 的节点 —— 它携带烘焙的
        0.01 厘米→米缩放,覆盖它会破坏角色。

    d.  应用颜色(先为每个角色克隆材质,避免颜色互相串扰):
        身体网格  → C.garment_hsl 是完整颜色:
                    material.color.setHSL(h, s, l)。无需查询基础色调。
        面部 + 手 + 脚 → C.skin_hex
        发型网格  → C.hair_hex

    e.  选择动画:
        if C.anim_clip == "idle":
            clip = idle_animation[C.idle_variant]   ← 见动画表
        else:
            clip = walk_animation
        动画片段以 R.assets.rest_reference_glb 为基准制作 ——
        播放前需重定向到骨架(见"动画片段")。
        startTime = C.phase × clip.duration

    f.  运行模拟:
        使用 R.recipe 作为智能体系统的配置输入:
          - boundary shape/radius  → 限制区域
          - collisionRadius        → NPC 体积大小(用于避让)
          - personalSpace          → 向心力 / 路径跟随强度
          - anticipation           → 提前转向距离
          - obstacles[]            → 静态避让圆盘
          - pace                   → 行走与待机角色比例
          - walkSpeedMin/Max       → 每 NPC 速度范围(× 片段速度)

常见集成故障(调试前先查此表)

症状 原因 解决方法
角色缩小约 100 倍 / 不可见 额外应用了 0.01 单位换算 移除 —— GLB 已是米制
角色放大约 100 倍 导入器丢弃了 GLB 根节点缩放 在根节点应用一次 0.01
角色沉入地面 / 设置位置无效 变换设置在了 SkinnedMesh 节点上 改为变换克隆的包装节点
所有克隆同步变形 / 集中在原点 使用了普通 .clone(true) 改用 SkeletonUtils.clone
身体与帽子/头发比例不一致 内部 "root" 节点的缩放被覆盖 绝不修改内部 "root" 节点
只有头部/帽子可见,没有身体 同上,或身体节点被误隐藏 检查 "root" 节点缩放和可见性列表
所有服装颜色相同 对共享材质实例着色 先为每个角色克隆材质再 setHSL
服装颜色不对 把 garment_hsl 当作基础色调的偏移量 它是完整颜色 —— 直接应用
角色悬空或下沉 为掩盖缩放问题添加了 Y 偏移 移除偏移,修复根节点变换

数据量规模

v1 响应包含完整的 characters[] 数组,每个 NPC 对应一个条目。数据量随人群规模线性增长:

recipe.size 近似响应大小
150(示例) ~55 KB
1 000 ~360 KB
5 000 ~1.8 MB
10 000 ~3.6 MB

对于数千角色以内的人群,在局域网或快速连接上表现良好。对于超大人群(10,000+),首次加载和解析完整列表可能增加明显延迟。

确定性

相同的 seed 和相同的请求参数始终返回相同的人群——相同的角色、位置和颜色,顺序也相同。您可以据此进行缓存和复现:按 crowd_id + seed(+ size)缓存响应并随意重新请求。生成在服务端运行;您只需消费已解析的 characters[] 数组并直接渲染。


认证(Authentication)

每个 /v1 请求都需要在 Bearer 令牌中提供您的 API 密钥:

Authorization: Bearer YOUR_API_KEY

请在 Genji 账户 → 密钥(Account → Keys) 页面创建和管理密钥。请妥善保密 —— 切勿提交到代码库或嵌入客户端代码中。


本地运行

# 1. 启动 API 服务器
npm run server

# 2. 写入示例数据(运行一次)
node --env-file=.env server/seed-api.js

# 3. 测试——示例人群无需密钥,因此无需 Authorization 头
curl -s http://localhost:3001/v1/crowd/cr_sample01

本节仅用于本地开发localhostngrok URL 绝不会出现在生产响应中—— 线上 API 为 https://www.genjicrowd.com

如需将本地服务器对外暴露以供远程测试(可选,请勿自动运行):

ngrok http 3001
# ngrok 会打印一个临时隧道 URL,形如 https://<random>.ngrok.app——将其用作
# 你的基础 URL,并在启动服务器前把 ASSET_BASE_URL 设为同一值。

Unity / Tuanjie 集成

控制分为两个层级。

第一层:设计阶段(加载前)

人群在 Genji 编辑器中创作并保存为配方(recipe)。工程团队调用 API,获取配方和角色列表,然后在 Unity / Tuanjie 中构建人群。如需修改形状、颜色、行走比例或障碍物,只需在 Genji 中编辑配方后重新请求即可。

角色克隆:使用 SkeletonUtils,而非 scene.clone()

所有角色共享同一个 GLB 文件。要在场景中放置多个角色,必须为每个角色单独克隆已加载的场景图。请勿使用 scene.clone(true) — 该方法虽然复制了节点,但克隆出的 SkinnedMesh 实例仍绑定到原始骨架的骨骼上,导致所有副本同步变形或完全不可见。

请使用 THREE.SkeletonUtils.clone()(Three.js)或引擎对应的深度克隆方法,确保每个克隆独立绑定骨架:

import { clone } from 'three/examples/jsm/utils/SkeletonUtils.js';

// gltf = GLTFLoader.load(assets.skeleton_glb, ...) 的加载结果
for (const C of characters) {
  const root = clone(gltf.scene);   // 正确用法 — 每个克隆独立绑定骨骼
  // 隐藏所有网格,仅显示 C.body_id、C.face_id、C.hair_id 及 shared_parts
  root.position.set(C.x, 0, C.z);
  scene.add(root);
}

第二层:运行阶段(加载后)

API 提供的是初始状态。运行时,您的 Unity / Tuanjie 代码驱动整个模拟。配方字段作为配置参数输入到您自己的智能体系统中:

字段 控制内容
boundaryShape + boundaryRadius 将角色限制在此区域内
pace 行走与待机角色的比例
walkSpeedMin / walkSpeedMax 每个角色的速度范围
collisionRadius 角色间最小距离
personalSpace 拥挤时的排斥力强度
anticipation 提前转向的距离
obstacles[] 需要绕行的静态障碍物
attractTarget 角色聚集的目标点

您的引擎拥有完整的运行时控制权——可以逐帧覆盖上述任意参数、移动吸引点、动态添加障碍物或实时调整行走比例。API 提供的是初始设计值,运行时模拟完全由您的引擎驱动。

未来规划功能

  1. 运行时 API — 实时连接,支持在场景进行中更新人群配置,无需重新加载。
  2. 编辑器实时联动 — Genji 编辑器直接将配方变更推送到运行中的 Unity / Tuanjie 实例。
  3. 单角色覆盖 — 无需重新加载整个人群,即可替换单个角色的服装或位置。

当前模式:一次加载,自主模拟。