3D 场景 · 物理
除了图表和图,同一个画布还可以容纳一个 Three.js 风格的小型 3D 场景:基本几何体和自定义三角形网格,支持变换、父子层级、光照、PBR 风格材质、刚体物理和射线拾取。适用于空间视图、简单仿真,或为图提供 3D 背景。
基本用法
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)
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name | string | '<SHAPE>_<id>' | 保存在对象记录中的名称 |
shape | PrimitiveShape | 仅 addPrimitive 必填 | |
position | [x, y, z] | [0, 0, 0] | 位置 |
rotation | [rx, ry, rz] | [0, 0, 0] | 欧拉角,弧度 |
scale | [x, y, z] | [10, 10, 10] | 尺寸 |
color | RgbaTuple | [0.35, 0.68, 0.98, 1] | [0, 1] 范围的 RGBA |
metallic | number | 0.15 | 金属度 |
roughness | number | 0.45 | 粗糙度 |
emissive | number | 0 | 自发光 |
bodyType | 'static' | 'dynamic' | 'kinematic' | 'static' | 刚体类型 |
velocity | [x, y, z] | [0, 0, 0] | 初始线速度 |
angularVelocity | [wx, wy, wz] | [0, 0, 0] | 初始世界空间角速度,弧度每秒 |
mass | number | max(0.2, abs(sx × sy × sz) × 0.01) | 质量 |
restitution | number | 0.55 | 弹性系数 |
parentId | number | null | null | 层级变换的父对象 |
map / mapRepeat / mapOffset / alphaTest | 纹理映射,见纹理 | ||
castShadow / receiveShadow | boolean | true | 是否参与阴影,见阴影 |
自定义网格(CustomMeshObjectDescriptor)
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,
});| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
positions | Float32Array | number[] | (必填) | [x0, y0, z0, ...] 顶点位置 |
normals | Float32Array | number[] | 顶点法线 | |
indices | Uint32Array | number[] | (必填) | 三角形顶点索引 |
uvs | Float32Array | number[] | [u0, v0, ...] 纹理坐标,每个顶点 2 个(见纹理) | |
scale | [x, y, z] | [1, 1, 1] | 缩放 |
color | RgbaTuple | [0.85, 0.45, 0.95, 1] | RGBA |
metallic / roughness | number | 0.35 / 0.3 | 材质 |
mass / restitution | number | 2.0 / 0.6 | 物理 |
map / mapRepeat / mapOffset / alphaTest | 纹理映射,见纹理 | ||
castShadow / receiveShadow | boolean | true | 是否参与阴影,见阴影 |
其余字段(name 默认 'MESH_<id>'、position、rotation、emissive、bodyType、velocity、angularVelocity、parentId)与基本几何体相同。Charturbo.createTorusMesh(majorRadius = 8, minorRadius = 2.6, radialSegments = 18, tubularSegments = 32) 用于生成测试网格。
纹理
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) |
maxTextureSize | number | 纹理单边的最大纹素数(最大 4096);引擎不支持纹理时为 0 |
映射字段可用于 addBox / addSphere / addCylinder / addPlane / addPrimitive / addCustomMesh 以及 setObjectMaterial:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
map | number | null | null | 纹理 ID;null(或 0)表示移除纹理 |
mapRepeat | [u, v] | [1, 1] | 平铺;纹理会重复 |
mapOffset | [u, v] | [0, 0] | UV 偏移 |
alphaTest | number | 有纹理时为 0.5,否则为 0 | Alpha(纹理 Alpha × 颜色 Alpha)低于该值的片元会被丢弃,用于镂空 |
uvs | Float32Array | 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 模型
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] | 对象旋转(欧拉角,弧度,与基本几何体相同) |
scale | number | [x, y, z] | 1 | 烘焙进顶点(glTF 单位为米) |
scene | number | 文件的 scene,否则为 0 | 场景索引 |
baseUrl | string | 源 URL,否则为页面 URL | 相对缓冲区和图片 URI 的基准 |
signal | AbortSignal | 中止获取和解码;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)。
光照与辅助显示
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() 返回完整设置。
| 光照选项 | 类型 | 默认值 |
|---|---|---|
ambientIntensity | number | 0.32 |
directionalDirection | [x, y, z] | [0.45, 0.85, 0.55] |
directionalIntensity | number | 0.95 |
pointPosition | [x, y, z] | [0, 45, 25] |
pointIntensity | number | 0.65 |
| 辅助显示选项 | 类型 | 默认值 |
|---|---|---|
showGrid | boolean | false |
gridSize | number | 200 |
gridDivisions | number | 20 |
showAxes | boolean | false |
axesLength | number | 60 |
基于物理的着色
场景网格默认使用经典 Blinn-Phong(classic)外观。setShading({ model: 'pbr' }) 可切换为可选的基于物理的着色(图表、科学 3D 和 3D 图不受影响):
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' |
exposure | number (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.intensity | number (0..16) | 1 |
environment.map | number | null | null |
environment.rotation | number(绕 +y 轴的弧度) | 0 |
HDR 环境贴图
高动态范围环境以真实辐亮度照亮 PBR 网格:值为 50 的太阳不会在 1 处截断,而是在光滑金属上留下清晰明亮的高光。
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' 下使用(经典着色忽略它们),没有贴图的对象渲染结果与之前完全相同:
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],
});| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
metallicRoughnessMap | number | null | null | 蓝色通道乘以 metallic,绿色通道乘以 roughness(线性数据) |
normalMap | number | null | null | 切线空间法线贴图,OpenGL / glTF 约定(绿色 = 图片上方) |
normalScale | number | 1 | 缩放贴图的 x / y(限制在 −16..16) |
aoMap | number | null | null | 红色通道的环境光遮蔽(线性);只压暗环境光 |
aoStrength | number | 1 | 0..1:1 + aoStrength × (ao − 1) |
emissiveColor | [r, g, b] | [0, 0, 0] | 线性自发光颜色(glTF emissiveFactor,0..1000)。非黑色时替代 PBR 的 color × emissive 发光 |
emissiveMap | number | null | null | 乘以 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 图从不投射或接收阴影):
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。
| 阴影选项 | 类型 | 默认值 |
|---|---|---|
enabled | boolean | false |
mapSize | 512 | 1024 | 2048 | 4096 | 2048 |
bias | number(0..16 纹素) | 0.5 |
normalBias | number(0..16 纹素) | 1 |
softness | number(0..8 纹素) | 1.5 |
darkness | number(0..1) | 1 |
bounds | 'auto' | { center: [x, y, z]; radius } | 'auto' |
point | ScenePointShadowsDescriptor | 关闭,见点光源阴影 |
点光源阴影
setShadows({ point: { enabled: true } }) 为 setLighting 的点光源(pointPosition)开启全方向阴影。默认关闭,且与方向光设置相互独立,可以只开其中一种或同时开启:
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的场景保留当前点光源设置)。
| 点光源阴影选项 | 类型 | 默认值 |
|---|---|---|
enabled | boolean | false |
mapSize | 256 | 512 | 1024 | 512 |
bias | number(0..16 纹素) | 1 |
normalBias | number(0..16 纹素) | 1 |
softness | number(0..8 纹素) | 1 |
darkness | number(0..1) | 1 |
near | number(0.01..1e6) | 0.5 |
far | number | 'auto' | 'auto' |
抗锯齿
多重采样抗锯齿(MSAA)默认关闭,可在创建时或运行时开启:
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 效果,默认均关闭:
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 }- 开启任一效果后,网格不经色调映射渲染到
Rgba16FloatHDR 目标(开启 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会恢复它们。
| 泛光选项 | 类型 | 默认值 |
|---|---|---|
enabled | boolean | false |
intensity | number (0..8) | 0.6 |
threshold | number (0..64,曝光后的线性亮度) | 1 |
knee | number (0..1,相对 threshold 的比例) | 0.5 |
radius | number (0..1) | 0.7 |
| 自动曝光选项 | 类型 | 默认值 |
|---|---|---|
enabled | boolean | false |
minEv / maxEv | number (-16..16,maxEv >= minEv) | -4 / 4 |
speedUp / speedDown | number (每秒 0..100) | 3 / 1 |
compensation | number (-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)跟随父对象,不单独仿真。
拾取
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)无法恢复,其映射会被丢弃。
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)会使画布进入图表模式;内置相机操作只在没有图表时生效。