Skip to content

3D 场景 · 物理 ​

除了图表和图,同一个画布还可以容纳一个 Three.js 风格的小型 3D 场景:基本几何体和自定义三角形网格,支持变换、父子层级、光照、PBR 风格材质、刚体物理和射线拾取。适用于空间视图、简单仿真,或为图提供 3D 背景。

在线示例

基本用法 ​

ts
import { Charturbo } from 'charturbo';

const canvas = document.querySelector('canvas')!;
const sdk = await Charturbo.create(canvas, { nodeCount: 0, edgeCount: 0 });

sdk.setGraphVisible(false);                // 只显示场景
sdk.setRigidPhysics(true, [0, -40, 0]);    // 重力
sdk.setHelpers({ showGrid: true });

sdk.addPlane({ position: [0, -1, 0], scale: [160, 2, 160] });
sdk.addBox({ position: [0, 40, 0], scale: [10, 10, 10], bodyType: 'dynamic' });
sdk.addSphere({ position: [5, 70, 0], scale: [8, 8, 8], bodyType: 'dynamic', metallic: 0.6 });
sdk.setCameraPose([0, 55, 115], [0, 10, 0]); // eye, target

// 轨道旋转 / 平移 / 缩放;没有图节点,因此关闭节点提示框
const detach = sdk.enableNodeInteractions(canvas, { builtInTooltip: false, focusOnDoubleClick: false });

const tick = () => {
  sdk.stepPhysics(1 / 60); // 推进刚体(图可见时也推进图布局)
  sdk.render();
  requestAnimationFrame(tick);
};
tick();

所有 add* 方法都返回新对象的数字 ID。

对象 ​

方法说明
addBox(desc?)立方体
addSphere(desc?)球体
addCylinder(desc?)圆柱体
addPlane(desc?)薄板;默认 scale: [200, 2, 200]、color: [0.16, 0.21, 0.31, 1]
addPrimitive(desc)通过 shape: 'box' | 'sphere' | 'plane' | 'cylinder' 创建上述任一几何体
addCustomMesh(desc)带索引的三角形网格

基本几何体为单位尺寸,因此 scale 就是对象在世界单位下的尺寸。

基本几何体选项(PrimitiveObjectDescriptor) ​

选项类型默认值说明
namestring'<SHAPE>_<id>'保存在对象记录中的名称
shapePrimitiveShape仅 addPrimitive 必填
position[x, y, z][0, 0, 0]位置
rotation[rx, ry, rz][0, 0, 0]欧拉角,弧度
scale[x, y, z][10, 10, 10]尺寸
colorRgbaTuple[0.35, 0.68, 0.98, 1][0, 1] 范围的 RGBA
metallicnumber0.15金属度
roughnessnumber0.45粗糙度
emissivenumber0自发光
bodyType'static' | 'dynamic' | 'kinematic''static'刚体类型
velocity[x, y, z][0, 0, 0]初始线速度
angularVelocity[wx, wy, wz][0, 0, 0]初始世界空间角速度,弧度每秒
massnumbermax(0.2, abs(sx × sy × sz) × 0.01)质量
restitutionnumber0.55弹性系数
parentIdnumber | nullnull层级变换的父对象
map / mapRepeat / mapOffset / alphaTest纹理映射,见纹理
castShadow / receiveShadowbooleantrue是否参与阴影,见阴影

自定义网格(CustomMeshObjectDescriptor) ​

ts
const torus = sdk.addCustomMesh({
  name: 'torus',
  ...Charturbo.createTorusMesh(8, 2.6, 24, 48), // { positions, normals, uvs, indices }
  position: [0, 40, -10],
  color: [0.62, 0.5, 0.92, 1],
  metallic: 0.8,
  roughness: 0.2,
});
选项类型默认值说明
positionsFloat32Array | number[](必填)[x0, y0, z0, ...] 顶点位置
normalsFloat32Array | number[]顶点法线
indicesUint32Array | number[](必填)三角形顶点索引
uvsFloat32Array | number[][u0, v0, ...] 纹理坐标,每个顶点 2 个(见纹理)
scale[x, y, z][1, 1, 1]缩放
colorRgbaTuple[0.85, 0.45, 0.95, 1]RGBA
metallic / roughnessnumber0.35 / 0.3材质
mass / restitutionnumber2.0 / 0.6物理
map / mapRepeat / mapOffset / alphaTest纹理映射,见纹理
castShadow / receiveShadowbooleantrue是否参与阴影,见阴影

其余字段(name 默认 'MESH_<id>'、position、rotation、emissive、bodyType、velocity、angularVelocity、parentId)与基本几何体相同。Charturbo.createTorusMesh(majorRadius = 8, minorRadius = 2.6, radialSegments = 18, tubularSegments = 32) 用于生成测试网格。

纹理 ​

ts
const wood = await sdk.loadTexture('/img/wood.jpg');            // 获取并解码
const checker = sdk.createDataTexture(2, 2, new Uint8Array([    // 原始 RGBA8 纹素
  255, 255, 255, 255,  60, 60, 60, 255,
   60, 60, 60, 255,  255, 255, 255, 255,
]));

const crate = sdk.addBox({ map: wood, mapRepeat: [2, 2] });
sdk.addPlane({ map: checker, mapRepeat: [16, 16] });
sdk.setObjectMaterial(crate, { map: null });                     // 恢复为纯色
sdk.disposeTexture(wood);
方法返回值说明
loadTexture(source, { mipmaps?, signal? })Promise<number>解码图片并上传,完成后返回纹理 ID。source 可以是 URL 字符串、URL、Blob、ImageBitmap、HTMLImageElement、HTMLCanvasElement、OffscreenCanvas 或 ImageData
createDataTexture(width, height, rgba, { mipmaps? })number上传原始 RGBA8 纹素(直通 Alpha,首行在上);rgba.length 必须等于 width × height × 4
disposeTexture(id)boolean释放纹理;使用它的对象将不带纹理绘制
getTextures()TextureRecord[]通过此 SDK 创建的纹理的 { id, width, height, src }(仅字符串 / URL 来源有 src)
maxTextureSizenumber纹理单边的最大纹素数(最大 4096);引擎不支持纹理时为 0

映射字段可用于 addBox / addSphere / addCylinder / addPlane / addPrimitive / addCustomMesh 以及 setObjectMaterial:

选项类型默认值说明
mapnumber | nullnull纹理 ID;null(或 0)表示移除纹理
mapRepeat[u, v][1, 1]平铺;纹理会重复
mapOffset[u, v][0, 0]UV 偏移
alphaTestnumber有纹理时为 0.5,否则为 0Alpha(纹理 Alpha × 颜色 Alpha)低于该值的片元会被丢弃,用于镂空
uvsFloat32Array | number[]仅自定义网格:每个顶点两个值 u, v。缺省时沿最长的两个轴做平面投影
  • UV 约定。 u 从左到右、v 从上到下递增(v = 0 是图片最上面一行),因此无需翻转图片。盒体和平面的每个面都显示整张图片,球体环绕一周,圆柱体环绕侧面并平面投影到端盖,createTorusMesh 会同时返回 uvs(展开传给 addCustomMesh 即可)。
  • 颜色。 纹理会乘以 color,所以默认白色按原样显示图片,其他颜色则为其着色。光照、metallic 和 roughness 照常生效。透明度仍由 color[3] 决定,纹理 Alpha 用于镂空(alphaTest):有纹理时 Alpha 低于 alphaTest(默认 0.5)的片元会被丢弃,因此 color[3] < 0.5 的带纹理对象请设置 alphaTest: 0。
  • 色彩空间。 纹理在渲染器的伽马空间中按 RGBA8 直接采样,mip 层级也在该空间求平均,因此缩小后的纹理会比线性光过滤略暗一点。
  • 限制。 超过 maxTextureSize 的图片会保持宽高比缩小;ImageData 和 createDataTexture 的数据必须在限制之内。每个引擎的纹理总量上限为 256 MiB(texture memory budget exceeded)。纹理 ID 不会被复用。
  • 错误。 被污染的图片(无 CORS 的跨源图片)会以 loadTexture: cross-origin image 拒绝,被中止的 signal 以 AbortError 拒绝,不带纹理支持构建的引擎会抛出 ... engine has no texture support。
  • GPU 设备丢失不会自动恢复:请刷新页面,或重新创建 SDK 和纹理。拾取、物理、SVG 导出和图表不受纹理影响。

glTF 模型 ​

ts
const house = await sdk.loadGltf('/models/house.glb', { rotation: [0, Math.PI / 4, 0], scale: 10 });
console.log(house.objects, house.bounds);   // 对象 ID、世界空间包围盒
sdk.disposeGltf(house);                      // 移除其对象和纹理
方法返回值说明
loadGltf(source, options?)Promise<GltfLoadResult>加载 glTF 2.0 模型(.gltf 或 .glb)。source 为 URL 字符串、URL、ArrayBuffer、Uint8Array 或 Blob
disposeGltf(result)移除此次加载创建的所有对象和纹理
选项类型默认值说明
position[x, y, z][0, 0, 0]所有创建的网格的对象位置;模型原点落在此处
rotation[rx, ry, rz][0, 0, 0]对象旋转(欧拉角,弧度,与基本几何体相同)
scalenumber | [x, y, z]1烘焙进顶点(glTF 单位为米)
scenenumber文件的 scene,否则为 0场景索引
baseUrlstring源 URL,否则为页面 URL相对缓冲区和图片 URI 的基准
signalAbortSignal中止获取和解码;Promise 以 AbortError 拒绝

结果为 { objects, textures, nodes, bounds }:nodes 以深度优先顺序列出场景节点 { index, name, objectIds },bounds 是世界空间的 { min, max }。每个网格图元成为一个自定义网格对象,以其节点(或网格)命名;网格有多个图元时追加 #<n>。

  • 支持。 使用外部或 data: 缓冲区和图片的 .gltf,带二进制块的 .glb;所有访问器类型,包括交错(byteStride)、归一化、稀疏以及没有缓冲视图的访问器;三角形、三角形带和扇形(点和线会发出警告并跳过)。节点层级(TRS 或 matrix)烘焙进顶点,法线用逆转置矩阵变换,镜像节点翻转绕序;缺少法线时计算平滑法线。扩展 KHR_texture_transform、KHR_mesh_quantization 和 KHR_materials_emissive_strength。
  • 材质。 baseColorFactor 从线性空间转换到渲染器的伽马空间;baseColorTexture 成为 map(仅 TEXCOORD_0;KHR_texture_transform 的 offset 和 scale 成为 mapOffset / mapRepeat);metallicFactor 和 roughnessFactor 与 glTF 一样默认为 1;emissiveFactor(乘以自发光强度)成为 emissiveColor,其最大分量作为经典着色用的 emissive;金属度-粗糙度、法线、遮蔽和自发光纹理成为材质贴图(仅 TEXCOORD_0;同一材质的纹理共用一个 uv 变换,其余的发出警告并忽略)。alphaMode: 'MASK' 将 alphaTest 设为 alphaCutoff(默认 0.5),'BLEND' 保留颜色 alpha,'OPAQUE' 将颜色 alpha 固定为 1。多个材质共用的图片只解码一次。
  • 忽略。 采样器(纹理始终重复并线性过滤)。以下会发出警告并忽略:纹理变换的旋转,TEXCOORD_1 及以上的纹理,蒙皮(按绑定姿势渲染),变形目标,动画,相机以及其他扩展。若 extensionsRequired 中列出了不支持的扩展,则以 loadGltf: extension X is required but not supported 拒绝。
  • 限制。 单次加载最多创建 GLTF_MAX_VERTICES(8,388,608)个顶点和 GLTF_MAX_INDICES(67,108,864)个索引。所有对象共用一个几何缓冲区,因此多个大模型仍可能超出 GPU 缓冲区上限。纹理遵循上文的纹理限制。
  • 错误。 错误信息以 loadGltf: 开头。任何失败(包括中止)都会移除已创建的对象和纹理。
  • 同一模型的所有对象都以模型原点为枢轴,因此对 result.objects 中的每个对象用相同的 position / rotation 调用 setObjectTransform 即可整体移动模型。

编辑对象 ​

方法返回值说明
setObjectTransform(id, { position?, rotation?, scale?, parentId? })boolean部分更新变换
getObjectTransform(id){ position, rotation, scale } | null当前变换(包含物理运动结果)
setObjectMaterial(id, { color?, metallic?, roughness?, emissive?, map?, mapRepeat?, mapOffset?, alphaTest?, ...material maps })boolean部分更新材质
setObjectPhysics(id, { bodyType?, velocity?, angularVelocity?, mass?, restitution? })boolean部分更新物理属性
setObjectParent(id, parentId | null)boolean连接到父对象 / 解除连接
removeObject(id)boolean删除一个对象
clearScene()删除所有对象和当前图表
objectCount()number对象数量
getObjects() / getObjectRecord(id)SceneObjectRecord[] / SceneObjectRecord | null包含当前变换的完整记录

接收 ID 的方法在 ID 未知时返回 false(或 null)。

光照与辅助显示 ​

ts
sdk.setLighting({ ambientIntensity: 0.3, directionalIntensity: 1, pointPosition: [0, 45, 25] });
sdk.setHelpers({ showGrid: true, gridSize: 160, gridDivisions: 16, showAxes: true });

setLighting 和 setHelpers 合并到当前值;getLighting() / getHelpers() 返回完整设置。

光照选项类型默认值
ambientIntensitynumber0.32
directionalDirection[x, y, z][0.45, 0.85, 0.55]
directionalIntensitynumber0.95
pointPosition[x, y, z][0, 45, 25]
pointIntensitynumber0.65
辅助显示选项类型默认值
showGridbooleanfalse
gridSizenumber200
gridDivisionsnumber20
showAxesbooleanfalse
axesLengthnumber60

基于物理的着色 ​

场景网格默认使用经典 Blinn-Phong(classic)外观。setShading({ model: 'pbr' }) 可切换为可选的基于物理的着色(图表、科学 3D 和 3D 图不受影响):

ts
const sky = await sdk.loadTexture('/env/sky-equirect.jpg'); // 2:1 等距柱状投影,带 mipmap
sdk.setShading({
  model: 'pbr',
  toneMapping: 'aces',
  exposure: 1.2,
  environment: { map: sky, intensity: 1 },
});
sdk.addSphere({ position: [0, 10, 0], scale: [20, 20, 20], color: [1, 0.78, 0.35, 1], metallic: 1, roughness: 0.2 });
  • 金属度-粗糙度模型:GGX 分布、高度相关 Smith 可见性、F0 = mix(0.04, color, metallic) 的 Schlick 菲涅耳以及 Lambert 漫反射项。roughness 最小限制为 0.045。setLighting 的方向光、点光源和 emissive 照常生效;ambientIntensity 仅用于 classic(PBR 的环境光来自环境)。
  • 颜色与其他地方一样使用 sRGB。着色器将颜色和纹理解码为线性值,在线性空间中计算光照,乘以 exposure,进行色调映射('aces'、'reinhard' 或 'none'),再编码回 sRGB。
  • 环境:没有 map 时由天空 / 地面半球(sky、ground,0..1 的 sRGB [r, g, b])照亮场景。有 map(来自 loadTexture / createDataTexture 的等距柱状纹理 ID,首行 = 上方,中间列朝向 −z)时,镜面反射按粗糙度采样逐渐增大的 mip 级别,漫反射使用模糊的 mip,并以解析的 split-sum 近似代替预过滤贴图。请保持默认的 mipmaps: true。HDR 贴图则会预过滤,见 HDR 环境贴图。未知或已释放的 ID 回退到半球;disposeTexture 会清除该引用。
  • setShading 合并到当前设置,值无效时抛出 setShading: ...;getShading() 返回副本。exportScene() 将设置保存在 shading 中(环境纹理保存在 textures 中),loadScene 会恢复设置,loadSceneWithTextures 会重新映射环境纹理 ID。
  • PBR 管线在首次使用时编译。同时支持 WebGPU 和 WebGL2。
着色选项类型默认值
model'classic' | 'pbr''classic'
toneMapping'aces' | 'reinhard' | 'none''aces'
exposurenumber (0..16)1
environment.sky[r, g, b][0.6, 0.66, 0.76]
environment.ground[r, g, b][0.24, 0.22, 0.2]
environment.intensitynumber (0..16)1
environment.mapnumber | nullnull
environment.rotationnumber(绕 +y 轴的弧度)0

HDR 环境贴图 ​

高动态范围环境以真实辐亮度照亮 PBR 网格:值为 50 的太阳不会在 1 处截断,而是在光滑金属上留下清晰明亮的高光。

ts
const studio = await sdk.loadEnvironmentHdr('/env/studio.hdr'); // Radiance RGBE,平铺或 RLE
sdk.setShading({ model: 'pbr', exposure: 0.8, environment: { map: studio, rotation: Math.PI / 2 } });

// 也可以用线性浮点数创建:width * height * 3,首行 = 上方
const env = sdk.createEnvironmentFromFloat(256, 128, rgb);
  • loadEnvironmentHdr(source, { signal }) 接受包含 Radiance .hdr 文件(#?RADIANCE / #?RGBE、FORMAT=32-bit_rle_rgbe、-Y h +X w 或 +Y h +X w)的 URL 字符串、URL、Blob、ArrayBuffer 或类型化数组。createEnvironmentFromFloat(width, height, rgb) 接受线性 RGB 浮点数。两者都返回可用于 environment.map 的纹理 ID,最大支持 4096x2048;错误信息以方法名为前缀,已中止的 signal 以 AbortError 拒绝。
  • 创建时等距柱状图像被缩小到最多 1024 texel 宽,以 Rgba16Float 存储,并在 GPU 上预过滤一次:n 个 mip 中的第 i 级保存粗糙度 i / (n - 1) 的 GGX 卷积(每 texel 128 个重要性采样,最小 mip 高 8 texel),因此粗糙反射按 BRDF 的方式模糊。漫反射来自贴图的 9 系数球谐投影。大于 1 的值保留到 exposure 和色调映射(超过 65504 的值被截断,负值和 NaN texel 变为 0)。
  • 1024x512 的贴图约占 5.6 MB(getTextures() 中带 hdr: true,计入纹理预算和 getRenderStats().gpuBytes)。用 disposeTexture 释放。预过滤在 WebGPU 上需数毫秒,在 WebGL2 上需数十毫秒。无法渲染到半精度浮点的 WebGL2 设备改用简单的盒式滤波 mip(粗糙反射更模糊,API 相同)。
  • 预过滤时发生 GPU 错误(验证、内存不足)会再次释放贴图。loadEnvironmentHdr 以该错误拒绝,WebGL2 上 createEnvironmentFromFloat 直接抛出。WebGPU 异步报告这些错误,因此 createEnvironmentFromFloat 先返回 ID,之后释放贴图并输出控制台警告;await sdk.environmentReady(env) 在确认贴图有效后兑现,失败时以该错误拒绝。
  • environment.rotation 让任意环境贴图(HDR 或 LDR)绕 +y 轴旋转。HDR 纹理仅用于 environment.map:作为对象 map 时渲染为白色。exportScene 用 hdr: true 标记它们,loadSceneWithTextures 通过 loadEnvironmentHdr 重新加载带 src 的项。

材质贴图 ​

glTF 风格的纹理贴图按纹素细化 PBR 材质。仅在 model: 'pbr' 下使用(经典着色忽略它们),没有贴图的对象渲染结果与之前完全相同:

ts
const normal = await sdk.loadTexture('/tex/bricks-normal.png');
const orm = await sdk.loadTexture('/tex/bricks-mr.png');
sdk.addBox({
  map: bricks,
  metallicRoughnessMap: orm,  // B = 金属度,G = 粗糙度
  normalMap: normal,
  normalScale: 1,
  aoMap: orm,                 // R = 遮蔽(打包的 ORM 纹理可同时用于两者)
  emissiveMap: windows,
  emissiveColor: [1, 0.6, 0.2],
});
选项类型默认值说明
metallicRoughnessMapnumber | nullnull蓝色通道乘以 metallic,绿色通道乘以 roughness(线性数据)
normalMapnumber | nullnull切线空间法线贴图,OpenGL / glTF 约定(绿色 = 图片上方)
normalScalenumber1缩放贴图的 x / y(限制在 −16..16)
aoMapnumber | nullnull红色通道的环境光遮蔽(线性);只压暗环境光
aoStrengthnumber10..1:1 + aoStrength × (ao − 1)
emissiveColor[r, g, b][0, 0, 0]线性自发光颜色(glTF emissiveFactor,0..1000)。非黑色时替代 PBR 的 color × emissive 发光
emissiveMapnumber | nullnull乘以 emissiveColor 的 sRGB 纹理(emissiveColor 为黑色时无效果)
  • 这些字段适用于所有 add* 方法、addCustomMesh 和 setObjectMaterial(只修改给出的字段;null 或 0 清空槽位)。getObjectRecord 返回它们,disposeTexture 清空所有使用该纹理的槽位,exportScene 仅为带贴图的对象写出这些字段并把纹理列入 textures,loadScene 丢弃未知 ID 并发出警告,loadSceneWithTextures 会重新映射 ID。
  • 无论是否有基础 map,所有贴图都使用对象的 uv 和 mapRepeat / mapOffset。
  • 法线贴图的切线坐标系由位置和 uv 的屏幕空间导数求得(无需顶点切线),因此适用于所有网格,包括镜像 uv;不使用 glTF 的 TANGENT 属性。阴影仍使用几何法线。
  • 图片与其他纹理一样以 RGBA8 上传,因此一张打包的 ORM(遮蔽-粗糙度-金属度)纹理可同时用于 aoMap 和 metallicRoughnessMap。
  • loadGltf 读取 metallicRoughnessTexture、normalTexture(scale)、occlusionTexture(strength)和 emissiveTexture(emissiveFactor × 自发光强度作为 emissiveColor)。

阴影 ​

setShadows({ enabled: true }) 为 setLighting 的方向光开启阴影贴图(默认关闭;图表、科学 3D 和 3D 图从不投射或接收阴影):

ts
sdk.setLighting({ directionalDirection: [0.6, 1, 0.3] }); // 指向光源
sdk.setShadows({ enabled: true, mapSize: 2048, softness: 1.5 });
sdk.addSphere({ position: [0, 30, 0], castShadow: false }); // 不投射阴影
sdk.addBox({ position: [20, 5, 0], receiveShadow: false }); // 不会变暗
  • 在主通道之前,一个从方向光出发的仅深度通道把所有可见且 castShadow 的不透明对象绘制到 Depth32Float 贴图中;光源的正交相机适配所有投射和接收阴影的对象(bounds: 'auto',按整纹素对齐,相机移动时阴影不会闪烁),或固定球体 bounds: { center, radius }。自动适配的场景越大,贴图覆盖的区域越大,因此大场景请在关注区域周围使用固定边界。
  • 在 classic 和 PBR 模型中,阴影都只按 darkness 削弱方向光(环境光、点光源、自发光和 PBR 环境光不变)。
  • 边缘使用硬件比较的 PCF,在跨越 softness 个纹素的 4 × 4 网格上过滤(softness: 0 = 单次采样的硬阴影)。bias(常量,随表面倾斜增大)和 normalBias(接收面沿法线的偏移)以阴影贴图纹素为单位;出现阴影粉刺(acne)时调大,阴影与物体脱离时调小。
  • 半透明对象(color 的 alpha 小于 1)不投射阴影;alpha 测试纹理(alphaTest)会在阴影中镂出相同的孔。castShadow / receiveShadow 在基本几何体、自定义网格和 glTF 对象上默认为 true,之后可用 setObjectMaterial 修改。
  • setShadows 合并到当前设置,值无效时抛出 setShadows: ...;getShadows() 返回副本,其中 mapSize 已按设备纹理上限截断。exportScene() 将设置保存在 shadows 中并保存每个对象的标志,loadScene 会恢复它们。
  • 贴图在开启阴影时分配(2048² = 16 MiB,计入 getRenderStats().gpuBytes),mapSize 改变时重建,关闭时释放。每帧额外增加一个针对投射阴影对象的深度通道。同时支持 WebGPU 和 WebGL2。
阴影选项类型默认值
enabledbooleanfalse
mapSize512 | 1024 | 2048 | 40962048
biasnumber(0..16 纹素)0.5
normalBiasnumber(0..16 纹素)1
softnessnumber(0..8 纹素)1.5
darknessnumber(0..1)1
bounds'auto' | { center: [x, y, z]; radius }'auto'
pointScenePointShadowsDescriptor关闭,见点光源阴影

点光源阴影 ​

setShadows({ point: { enabled: true } }) 为 setLighting 的点光源(pointPosition)开启全方向阴影。默认关闭,且与方向光设置相互独立,可以只开其中一种或同时开启:

ts
sdk.setLighting({ pointPosition: [0, 26, 0], pointIntensity: 1.6 });
sdk.setShadows({ point: { enabled: true, mapSize: 512, softness: 1 } });
sdk.addSphere({ position: [0, 26, 0], scale: [2, 2, 2], emissive: 1, castShadow: false }); // 灯泡
  • 在主通道之前,从光源位置出发的 6 个仅深度通道(每个 90° 立方体面一个)把 castShadow 的不透明对象绘制到 Depth32Float 立方体贴图中。每个面存储 near 与 far 之间的反向 Z 透视深度;接收面为自身位置重新计算相同的深度并由硬件比较,因此立方体的各面和接缝都能无缝过滤。
  • 在 classic 和 PBR 模型中,阴影都只按 darkness 削弱点光源。边缘使用 3 × 3 网格的双线性比较采样过滤,范围为接收面距离处的 softness 个纹素(softness: 0 = 单次硬件过滤采样)。bias(随表面倾斜增大)和 normalBias 以接收面距离处的纹素为单位。
  • 距离光源比 near(世界单位,默认 0.5)更近的投射对象(例如灯泡)会被忽略;far: 'auto'(默认)覆盖所有投射和接收阴影对象的远端,传入数字则固定(至少为 2 * near)。
  • 立方体贴图在开启时分配(6 × mapSize² × 4 字节:256 为 1.5 MiB,512 为 6 MiB,1024 为 24 MiB,计入 getRenderStats().gpuBytes),mapSize 改变时重建,关闭时释放。关闭时没有额外通道,着色器也不变。开启后每帧要把投射对象绘制 6 次,建议使用 mapSize: 512 并控制投射对象数量。同时支持 WebGPU 和 WebGL2。
  • getShadows().point 返回 mapSize 已按设备上限截断的设置;exportScene() / loadScene 与 shadows 的其余部分一起保存和恢复它(未保存 point 的场景保留当前点光源设置)。
点光源阴影选项类型默认值
enabledbooleanfalse
mapSize256 | 512 | 1024512
biasnumber(0..16 纹素)1
normalBiasnumber(0..16 纹素)1
softnessnumber(0..8 纹素)1
darknessnumber(0..1)1
nearnumber(0.01..1e6)0.5
farnumber | 'auto''auto'

抗锯齿 ​

多重采样抗锯齿(MSAA)默认关闭,可在创建时或运行时开启:

ts
const sdk = await Charturbo.create(canvas, { antialias: true }); // 或 4
sdk.setAntialias(1); // 关闭
sdk.setAntialias(4); // MSAA x4
sdk.getAntialias(); // 实际采样数:1 或 4
  • 使用 4 个采样时,主通道渲染到多重采样的颜色与深度目标,再 resolve 到画布。网格轮廓与相交处的锯齿(细杆、盒体与板的边缘)变得平滑。纹理内部、阴影贴图、离屏通道和 DOM 文本不受影响。2D 图表的线条和标记已由着色器抗锯齿,几乎没有变化。
  • antialias 接受 true / 4(4 个采样)或 false / 1(关闭)。setAntialias 只接受 1 或 4,其他值抛出 setAntialias: ...。若设备无法以 4 个采样渲染画布格式,则保持 1 并输出控制台警告;getAntialias() 返回实际生效的值。
  • 开销:多重采样目标每个画布像素占用 4 × (颜色 + 深度) 字节(480 × 300 ≈ 4.4 MiB,计入 getRenderStats().gpuBytes),主通道的填充开销也增加到 4 倍。开启 MSAA 时创建,尺寸变化时重建,setAntialias(1) 时释放。关闭时不分配任何资源,管线也保持不变。
  • 画布导出(toImage、toDataURL)得到的是 resolve 后的图像。WebGPU 与 WebGL2 均受支持。该设置属于画布而非场景,exportScene 不会保存它。

后期处理 ​

泛光(bloom)与自动曝光(人眼适应)是作用于 PBR 场景网格的可选 HDR 效果,默认均关闭:

ts
sdk.setShading({ model: 'pbr', toneMapping: 'aces' });
sdk.setPostProcessing({
  bloom: { enabled: true, intensity: 0.8, threshold: 1, knee: 0.5, radius: 0.7 },
  autoExposure: { enabled: true, minEv: -4, maxEv: 4, speedUp: 3, speedDown: 1, compensation: 0 },
});
sdk.addSphere({ position: [0, 10, 0], emissiveColor: [8, 2.4, 0.5] }); // HDR 自发光产生光晕
sdk.getPostProcessing(); // { bloom, autoExposure, supported, active }
  • 开启任一效果后,网格不经色调映射渲染到 Rgba16Float HDR 目标(开启 MSAA 时先渲染到多重采样目标再 resolve 到它)。随后全屏合成通道应用曝光、叠加泛光、进行色调映射(setShading({ toneMapping }))、与清除色混合并以 sRGB 写入画布。2D 图表、节点、边和辅助线照常绘制在合成结果之上,因此也会绘制在半透明网格之上。
  • 泛光:预过滤保留曝光后高于 threshold 的亮度,并带有柔和的 knee(对亮像素取平均,避免单个高光闪烁);最多 6 级半尺寸 mip 链用 13 采样滤波逐级缩小,再用帐篷滤波逐级上采样叠加回来。radius 控制光晕范围,intensity 控制强度。大于 1 的自发光颜色、强烈的太阳和 HDR 环境贴图泛光最明显。
  • 自动曝光:GPU 每帧测量网格的平均对数亮度(64 × 64 逐级缩小到 1 × 1,忽略背景),并将曝光调向 EV log2(平均值 / 0.18),限制在 minEv..maxEv 内并加上 compensation。该值只保存在 GPU 上(无回读)。setShading({ exposure }) 仍会与结果相乘。adaptation: 'smooth'(默认)按已渲染帧的时间以每秒 speedUp / speedDown 的速度变化,因此需要持续渲染(动画循环)才能看到收敛过程;adaptation: 'instant' 每帧直接测光。开启后的第一帧总是直接到位。
  • 需要 PBR 模型(经典着色以显示为参照,保持不变,active 为 false)。不支持浮点渲染目标的设备(没有 EXT_color_buffer_float 的 WebGL2)会跳过后期处理:场景照常渲染,getPostProcessing().supported 为 false。WebGPU 与带该扩展的 WebGL2 均受支持。
  • 开销:两个效果都关闭时没有任何开销(无目标、无额外通道、管线不变)。开启后 HDR 目标每像素 8 字节(MSAA x4 时为 32 字节加 8 字节 resolve 目标),泛光 mip 约为其三分之一,曝光链 34 KiB,均计入 getRenderStats().gpuBytes。目标在开启时创建,尺寸或 MSAA 变化时重建,两个效果都关闭时释放。在无头 Chrome(Apple M1,1280 × 720,41 个 PBR 网格)上,每帧耗时 WebGPU 为关闭 0.72 ms、泛光 1.18 ms、泛光 + 自动曝光 1.36 ms,WebGL2 为 1.09 / 1.72 / 1.82 ms。
  • setPostProcessing 合并到当前设置,无效值抛出 setPostProcessing: ...。exportScene() 将设置保存在 postProcessing 中,loadScene 会恢复它们。
泛光选项类型默认值
enabledbooleanfalse
intensitynumber (0..8)0.6
thresholdnumber (0..64,曝光后的线性亮度)1
kneenumber (0..1,相对 threshold 的比例)0.5
radiusnumber (0..1)0.7
自动曝光选项类型默认值
enabledbooleanfalse
minEv / maxEvnumber (-16..16,maxEv >= minEv)-4 / 4
speedUp / speedDownnumber (每秒 0..100)3 / 1
compensationnumber (-8..8 EV)0
adaptation'smooth' | 'instant''smooth'

刚体物理 ​

setRigidPhysics(enabled, gravity?) 开启或关闭仿真;默认开启,重力为 [0, -40, 0]。getRigidPhysics() 返回 { enabled, gravity }。

  • stepPhysics(dt) 推进场景。dt 被限制在 0.001..0.05 秒,并拆分为不超过 1/120 秒的子步。返回动能。
  • 'dynamic' 刚体受重力下落,并与其他所有刚体碰撞。'kinematic' 刚体不受重力,按自身的 velocity 和 angularVelocity 运动。'static' 刚体从不移动。
  • 偏心碰撞会使物体旋转,摩擦使球体滚动。立方体使用有向接触流形;圆柱体按立方体碰撞;自定义网格使用其包围盒。
  • 子对象(parentId)跟随父对象,不单独仿真。

拾取 ​

ts
sdk.enableNodeInteractions(canvas, {
  builtInTooltip: false,
  focusOnDoubleClick: false,
  onNodeClick: (_node, e) => {
    const rect = canvas.getBoundingClientRect();
    const ndcX = ((e.clientX - rect.left) / rect.width) * 2 - 1;
    const ndcY = 1 - ((e.clientY - rect.top) / rect.height) * 2;
    const hit = sdk.raycastObjectAtNdc(ndcX, ndcY); // { objectId, point } | null
    sdk.selectObject(hit?.objectId ?? null);         // 为命中的对象描边
  },
});
  • raycastObjectAtNdc(ndcX, ndcY) 返回最近的命中对象和世界空间中的交点。球体、平面和圆柱体(侧面与端面)用解析法求交,立方体按立方体求交,自定义网格先做包围盒检测再逐个三角形求交;64 个及以上三角形的网格使用首次拾取时构建的三角形 BVH,射线未穿过其世界包围盒的对象会被跳过。
  • pickObjectAtNdc(ndcX, ndcY) 只返回对象 ID(没有时为 null)。
  • selectObject(id | null) 高亮对象(大型静态平面只显示描边)。

保存与加载 ​

exportScene() 返回可序列化的 SceneDescriptor(包含变换、材质和物理属性的对象,以及光照、着色、辅助显示、刚体物理和图的可见性)。loadScene(descriptor) 清空场景、应用设置并重新创建对象,返回新的 ID 列表。

clearScene() / loadScene() 不会清除纹理。exportScene() 会在 textures 中列出对象用到的纹理({ id, src, width, height },不含像素数据),对象上保留 map ID。loadScene 是同步的:仍然存活的纹理的 map ID 会保留,未知 ID 会被丢弃并输出 console.warn。要恢复在其他会话中保存的场景,请调用 await sdk.loadSceneWithTextures(descriptor):它会重新加载带 src 的纹理(相同 src 的存活纹理会被复用),把对象的 map ID 重映射为新 ID,然后调用 loadScene。没有 src 的纹理(createDataTexture、画布、ImageData)无法恢复,其映射会被丢弃。

ts
sdk.loadScene({
  lighting: { ambientIntensity: 0.3, directionalDirection: [0.4, 0.9, 0.5], directionalIntensity: 1, pointIntensity: 0.5 },
  helpers: { showGrid: true, gridSize: 160, gridDivisions: 16 },
  rigidPhysics: { enabled: true, gravity: [0, -40, 0] },
  graphVisible: false,
  objects: [
    { kind: 'primitive', name: 'floor', shape: 'plane', position: [0, -1, 0], scale: [160, 2, 160], roughness: 0.9 },
    { kind: 'primitive', name: 'step', shape: 'box', position: [-25, 5, 0], scale: [30, 10, 30] },
  ],
});

const saved = JSON.stringify(sdk.exportScene());

相机 ​

场景使用与 3D 网络图相同的轨道相机:鼠标和触摸操作、setCameraPose(eye, target)、rotateCamera、panCamera、zoomCamera 和 getCameraState()。第一人称移动请使用 moveCameraLocal(forward, right, up) 和 rotateCameraLook(deltaYaw, deltaPitch)。

说明 ​

  • 不传 nodeCount: 0, edgeCount: 0 时,Charturbo.create 会生成一个 1000 节点的图。画布只显示场景时请调用 setGraphVisible(false);也可以让可见的图与场景共用同一画布。
  • 渲染图表(render*Chart)会使画布进入图表模式;内置相机操作只在没有图表时生效。

另请参阅 ​

MIT License