课程 39 - 3D Mesh 渲染
在 课程 30 - 后处理与渲染图 之前,画布上的图形都走 2D 管线:正交相机、SDF、路径三角化填充等。本节在不替换这套 2D 渲染的前提下,增加一条 3D Mesh 分支:顶点带法线、透视/正交投影、深度测试,并与 2D 图层合成到同一块画布上。
Framer、Spline、Rive 和 Bevy
Framer 和 Spline 实现 2D/3D 融合的核心思路可以归纳为两种技术路径,它们根据产品定位选择了不同的架构策略。
Framer 的 3D 能力本质上是 CSS 3D Transforms 的增强封装,而非真正的 3D 渲染管线。因此无法导入外部 3D 模型(GLB/OBJ),无真实光照、阴影、材质系统,3D 效果仅限于"卡片翻转"、"视差层"等简单场景。详见:How to Turn 2D Elements into Interactive 3D Objects in Framer

Spline 是真正的 3D 编辑器,其架构更接近游戏引擎。Spline 允许创建独立的 UI Scenes(2D 画布),然后将其作为纹理贴到 3D 场景中的 UI Frame 对象上。Working with 2D and 3D objects

Rive 目前本质上仍是 2D 工具,它的"3D 感"来自 2.5D 变换、网格变形等,而非完整的 3D 管线。
我们的思路和 2D Rendering in Bevy 接近,2D 图形可视为落在 z = 0 平面上的对象,3D 模型与之共处同一世界;平移/缩放 2D 相机时,可选让 3D 相机一起联动。实现见 PR #271。
架构概览
同一 WebGL/WebGPU 上下文
└─ MeshPipeline 渲染图
├─ Pass:3D(MeshPipeline3D.drawMeshes)
├─ Pass:3D Gizmo(RenderGizmo3D.drawGizmos,叠在 mesh 之后)
├─ Pass:网格 / 2D 矢量(原有 BatchManager)
└─ Pass:后处理 → 上屏Renderer3DPlugin:注册 3D 组件,调度MeshPipeline3D(GPU 缓存)、CameraSync(联动 2D 相机)、Pick3D(选中/拖拽)与RenderGizmo3D(变换把手绘制)。MeshPipeline3D:不负责单独 swapchain present;在MeshPipeline的主 pass 里 先画 3D、再画 2D,共用颜色与深度附件。- 现有
Mesh.ts仍是 2D 矢量填充,不要与 3D 的Mesh3D混淆。
插件组合方式:
import {
App,
DefaultPlugins,
DefaultRenderer3DPlugin,
} from '@infinite-canvas-tutorial/ecs';
const app = new App().addPlugins(...DefaultPlugins, DefaultRenderer3DPlugin);
app.run();核心组件
| 组件 | 作用 |
|---|---|
Camera3D | projection: 'perspective' | 'orthographic';linked 时由 CameraSync 跟随 2D 平移/缩放 |
Mesh3D | positions、normals、可选 indices(三角网格) |
Material3D | 金属度与粗糙度 PBR:baseColor、metallic、roughness 和环境光系数;旧字段保留兼容性 |
Light3D | ambient / directional / point / spot;见 场景光照示例 |
Transform3D | 平移、欧拉角旋转、缩放 |
Selected3D | 3D 选中态:当前 gizmo 模式、激活轴/平面、拖拽参考点等(见 3D 变换 Gizmo) |
Mat4 | 4×4 矩阵工具(perspective / ortho / lookAt) |
通过 ECS commands.spawn 创建实体,例如:
import {
Camera3D,
Mesh3D,
Material3D,
Transform3D,
} from '@infinite-canvas-tutorial/ecs';
commands.spawn(
new Camera3D({
eye: [3, 3, 5],
center: [0, 0, 0],
clearColor: true,
}),
);
commands.spawn(
new Mesh3D({ positions, normals, indices }),
new Material3D({
baseColor: [0.25, 0.55, 0.95, 1],
ambient: 0.45,
diffuse: 0.75,
specular: 0.4,
shininess: 48,
}),
new Transform3D({
translation: [0, 0, 0],
rotation: [0.3, 0.6, 0],
scale: [1, 1, 1],
}),
);统一三维空间(linked 相机)
若希望 拖动画布时 2D 与 3D 一起平移/缩放,为 Camera3D 设置 linked: true
CameraSync 每帧读取 2D ComputedCamera 的 (x, y, zoom),并写入 3D 相机的 eye / center / baseDistance:
- 缩放:
baseDistance = canvas.height / 2,eye.z = baseDistance / zoom,与 2Dmat3.projection在 z=0 处的可见高度一致 - 正交 linked(
projection: 'orthographic'):eye = [x, -y, distance],center = [x, -y, 0](2D 画布 Y 向下,3D 世界 Y 向上,经canvasWorldToWorld3D取反) - 透视 linked(
projection: 'perspective'):eye = [x, y, distance],center = [x, y, 0](与 2D 节点相同,画布坐标 Y 向下)
linked + 正交
MeshPipeline3D 直接使用 2D 的 viewProjectionMatrix(含 Y 翻转与平移缩放),适合 extrude3d 与 2D rect 严格对齐。
立方体(正交) 示例使用 linked + orthographic(translation: [200, 100, 40])。
linked + 透视
透视模式下,屏幕位置由 完整 2D VP 决定(平移、缩放、Y 翻转与 2D 一致),深度方向用透视矩阵按 Transform3D.translation.z 做近大远小;锚点为 translation 的 (x, y, z),使物体落点与 2D 的 (x, y) 对齐。
立方体(透视) 使用上述 linked + perspective 配置;拖动画布时 cube 应与 2D 图层同向平移、同向缩放。
基于 Raycast 的拾取
3D 选中与 gizmo 拖拽由 Pick3D 系统负责(选择工具下 Select 也会调用同一套逻辑)。指针按下时,对光标下的视口像素做 CPU 检测,依次测试 gizmo 把手与 Mesh3D 三角形,不走 GPU picking pass。实现集中在 ray-casting.ts 与 pick3d-probe.ts。
流程
视口坐标 (x, y)
└─ buildPickSceneForViewport(camera, …) → Mesh3DPickScene(与渲染同一套矩阵)
└─ probePick3DAtViewport(…)
├─ 1. 已 Selected3D 实体的 gizmo 部件(逐 part 检测)
└─ 2. 本 canvas 全部 Mesh3D → 取最近命中(最小 t)优先级:先测 已选中 实体上的 gizmo;未命中再测场景 mesh。多个 mesh 同时命中时,取沿射线 距离最近 的一个(linked 透视下为 深度最大/最靠前)。
标准 Raycast(正交 / 自由相机)
非 linked 透视模式时,拾取路径与常见 3D 编辑器一致:
screenToRay:视口像素 → NDC → 用inv(view × projection)反投影近/远点,得到世界空间射线。rayMeshIntersection:顶点经 model 矩阵变换后:- 粗测:射线 vs 变换后的 AABB(slab 法)。
- 细测:对每个三角形做 Möller–Trumbore 射线-三角形求交,保留最小
t。
// packages/ecs/src/utils/ray-casting.ts(示意)
const invVP = computeInvViewProjection(projMatrix, viewMatrix);
const ray = screenToRay(vx, vy, width, height, invVP);
const hit = rayMeshIntersection(ray, positions, indices, modelMatrix);
// hit: { t, point, triangleIndex } | nullLinked 正交(linked + orthographic)走同一路径:MeshPipeline3D 将 2D 的 viewProjectionMatrix 注入 pick scene,点击与 2D rect、extrude3d 严格对齐。
Linked 透视:屏幕空间三角形
Linked 透视对 mesh 不用世界射线。屏幕位置由完整 2D VP 决定,深度由 translation.z 上的透视矩阵单独处理。为与顶点着色器一致,pickMeshLinkedPerspective 用相同 uniform 把每个三角形投影到视口,再用 pointInTriangle2D 判断光标是否在 二维投影三角形 内,并取该像素处 最靠前 的深度。gizmo 部件复用同一 helper(把手可带 Z 向 screen bias)。
这样在无限画布上平移/缩放时,透视 cube 仍能做到「所见即所点」。
拖拽约束
按下 gizmo 后会创建 GizmoSession,保存初始姿态和固定的投影。普通相机通过射线与 约束平面求交;linked 相机反解实际显示的把手投影,包括 Z 轴的屏幕偏移。平移沿 选中的轴或平面,旋转沿局部圆环轴;退化或无法求解的采样不会改动预览。
拖动只更新运行时组件,松手时声明式源节点才提交一次文档改动和撤销记录。 Escape、指针取消或离开画布会恢复起始姿态,不产生文档改动。
拖动期间切换相机、改变相机投影/视图或画布尺寸,会取消当前手势。通过 API 或撤销 修改源节点的姿态、几何配置或祖先节点的变换,也会取消手势,并从当前文档恢复 运行时预览,避免旧的松手事件覆盖新状态。名称、颜色等无关编辑可与继续进行的拖动 合并;其他画布的修改不会中断当前手势。再次按下锚点即可在新视图中开始拖动。
计算布局也属于手势上下文。Yoga 可能只更新 ECS 中的位置或尺寸,而保留文档中的 同名字段;这类变化以及祖先变换变化也会取消预览。取消时保留新的布局结果,只撤销 仍由预览持有的坐标;布局未变时按 Escape,会回到拖动前的计算位置,即使它与文档 坐标不同。手势自身产生的预览写入不会触发取消。
GizmoSourceFrame 保存源节点和祖先的局部几何快照,避免世界矩阵延迟更新导致 误判。指针会话拥有源节点预览期间,伴生同步不再重复回写源节点;直接操作伴生网格 的集成仍保留原有的反向同步。
无变化帧不再重复写入伴生网格的变换和材质。位姿或材质变化复用参数化网格的 CPU 顶点数据,只有几何配置变化才使对应缓存失效。
MeshPipeline3D 在位姿和材质编辑时也会复用 GPU 顶点、法线、UV、索引和逐网格 uniform 缓冲。只有 Mesh3D 写入会使几何缓存失效;直接修改数组前,需要通过 entity.write(Mesh3D) 取得组件。缓存检查在伴生同步之后执行,未绘制帧会保留 更新标记,下一次绘制只上传一次新几何。每个画布独立拥有缓存和场景 uniform。 删除网格或跨画布移动会释放旧缓冲;画布销毁时,先释放这些资源再销毁设备。 分配失败会回收本次已分配的缓冲,保留旧的完整缓存项,允许下一次绘制重试。
模型和法线矩阵只在位姿变化时重新计算。无变化帧或仅相机变化时跳过模型 uniform 上传;材质编辑和纹理加载完成会更新 uniform,无需重算这两个矩阵。直接修改 变换或颜色数组时,缓存按数值比较,能够识别原地修改。
场景 uniform 按画布缓存。相机与联动二维视图变化时更新场景矩阵;灯光变化只 重新打包灯光数据,无需重算矩阵。无变化绘制复用场景数据和各网格的 draw binding。 纹理就绪或替换、几何重建及 GPU 资源更换会使相应绑定失效;释放时先销毁绑定, 再销毁其引用的缓冲和纹理。场景上传或绑定分配失败后,下一次绘制可以重试。
3D 图层列表根据 ECS 中几何、名称、归属和成员的变化更新,无变化帧不再扫描 全部网格。选中成员单独跟踪,悬停 gizmo 把手不会重建图层列表。图层保留稳定的 实体引用,后续从面板选择对象以及撤销重做仍可使用。
完整指针流程与把手含义见 3D 变换 Gizmo。
3D 变换 Gizmo
在 选择工具(penbarSelected === Pen.SELECT)下点击 3D 网格,会为实体挂上 Selected3D,并在物体中心绘制 平移(translate) 把手:红/绿/蓝箭头 + 半透明平面块。拖拽把手会写回 Transform3D.translation;与 2D 的 Selected + RenderTransformer 类似,但走独立 3D 拾取与绘制路径。
交互流程
pointer down(Select 工具)
└─ Pick3D:先测 gizmo,再测 Mesh3D
├─ 命中 gizmo → 保存姿态/投影,创建 GizmoSession
└─ 命中 mesh → 选中;未命中 → 清空选择
pointer move(按住)
└─ GizmoSession.update → 受约束的运行时预览
pointer up
└─ finish(true) → 提交一次源节点姿态,记录历史
Escape / pointer cancel / leave
└─ finish(false) → 恢复姿态,清空手势状态坐标与把手含义
与 统一三维空间 一致,gizmo 使用 画布坐标(Y 向下),不是 Blender 的 Y-up:
| 颜色 | 轴 | 方向(典型 linked 画布视图) | 拖拽改动的分量 |
|---|---|---|---|
| 红 | +X | 向右 | translation.x |
| 绿 | +Y | 向下 | translation.y |
| 蓝 | +Z | 深度(z+ 朝画面里) | translation.z |
箭头:沿单轴平移。
平面(
xy/xz/yz半透明方块):在该平面内同时改两个分量(例如xy= 只动 X、Y,不改 Z)。合并模式
transform(默认,接近 Spline):同时显示平移箭头/平面与旋转圆环;点哪类把手就执行哪类操作(activePartKind:translate|rotate),无需W/E切换。拖 箭头 / 平面 → 改
Transform3D.translation;拖 圆环 → 改Transform3D.rotation(局部欧拉角,与 mesh 一致)。圆环随物体当前朝向绘制;箭头仍保持画布世界轴向(X 右、Y 下、Z 深度)。
拾取按屏幕空间 最近 命中;重叠时箭头在上层,优先拖到平移。
scale仍预留。
编辑挤出矩形
设置了 extrude3d 的 rect 使用与声明式网格相同的 gizmo。序列化选项支持高度偏移和局部三维旋转:
const node: RectSerializedNode = {
id: 'extruded-card',
type: 'rect',
zIndex: 0,
x: 100,
y: 80,
width: 160,
height: 100,
extrude3d: { depth: 80, z: 20, rotation: [0.2, 0.3, 0] },
fills: [{ type: 'solid', value: '#3377dd' }],
};- XY 平移写回源矩形在父坐标系中的
x/y,支持父级旋转、缩放和翻转。父变换不可逆时拒绝预览。 - Z 平移改变
extrude3d.z,保留厚度depth。未施加三维旋转时,z表示前表面的高度偏移;伴生网格中心位于z - depth / 2。 - 圆环旋转写入
extrude3d.rotation,使用弧度制 XYZ 欧拉角,相对于源图形已有的世界空间二维旋转。源图形的二维rotation保持不变。普通旋转图形的网格使用Rz(sourceAngle) × Rx(x) × Ry(y) × Rz(z)组合姿态。
松手提交一次,取消恢复预览;撤销、重做和重新载入文档会恢复相同姿态。拖动中关闭 挤出或替换伴生网格会中断手势,并从当前文档恢复,不会重建过期的网格。
extrude3d: true 仍表示厚度 100,数值仍表示指定厚度。对象写法默认厚度 100、 高度偏移 0、旋转 [0, 0, 0]。更新采用整个选项值替换,省略的字段恢复上述默认值。 目前没有缩放把手,可通过矩形尺寸和 depth 改变大小。Rect 挤出会沿着局部 顶点应用旋转和缩放;圆角、翻转及父级变换形成的剪切会烘入网格,底面与可编辑的 二维源图形对齐。不可逆的父级变换不会生成新的三维网格。
挤出闭合 Path
只有一层纯色填充的闭合 Path 也可以设置 extrude3d。前后封盖遵循实际填充轮廓, 包括孔洞;修改 d 或 fillRule 会重建网格,平移会复用。父级变换产生的剪切会 烘入网格,线性变换改变时会重建网格。
const vector: PathSerializedNode = {
id: 'extruded-vector',
type: 'path',
zIndex: 0,
x: 100,
y: 80,
d: 'M100 80 L200 80 L200 180 L100 180 Z M125 105 L125 155 L175 155 L175 105 Z',
fillRule: 'evenodd',
fills: [{ type: 'solid', value: '#3377dd' }],
extrude3d: { depth: 40, z: 0, sideColor: '#facc15' },
};每条子路径都必须闭合。开放路径、含描边图层、多层或非纯色填充、非流形轮廓 以及不可逆的退化变换会继续以 2D 显示。原 Path 文档节点保持可编辑,也支持 gizmo、撤销和 重新载入。
sideColor 可选,用 CSS 颜色设置侧壁;前后封盖仍使用原图形的填充色。 省略时所有表面都使用原填充色。修改侧壁颜色只更新材质,不重建网格。
对于 SVG 的 <path>、<polygon>、<rect>、<circle>、<ellipse>,以及首尾点重合的 <polyline>,可以通过 svgElementsToExtrudedNodes 导入为可编辑的挤出图形;其他元素继续保持 2D。 该函数要求厚度为有限正数。
import { svgElementsToExtrudedNodes } from '@infinite-canvas-tutorial/ecs';
const svg = new DOMParser().parseFromString(source, 'image/svg+xml');
const nodes = svgElementsToExtrudedNodes(
svg.documentElement as unknown as SVGSVGElement,
40,
);
api.updateNodes(nodes);在在线 App 中,选中矩形或 Path 后可从顶部工具栏打开 3D 控件,开启挤出并 调整厚度与倒角。项目菜单中的 导入 SVG 为 3D 会按可见图形的实际范围居中, 即使图形只占 SVG 画布的一小部分也能落在视口中央;不支持挤出的图层仍保持 2D。 通过 导出为 → GLB(三维) 可下载 项目中已就绪的所有挤出网格。App 还可设置倒角轮廓的段数(1–8)和可选的十六进制 侧面颜色;侧面颜色留空时沿用图形的填充色。 如果 Path 无法生成网格,App 会禁用挤出操作,并提示闭合轮廓、填充和描边的要求。
只有单层纯色填充、没有可见描边的闭合轮廓会挤出;SVG 的 stroke="none" 占位层 会被忽略。导入会保留 fill-rule(包括 evenodd 孔洞),并继承 SVG 分组的填充色。 普通矩形保持可编辑的 Rect;圆角矩形、椭圆和闭合折线转换为可编辑的 Path。 内联 style 中的填充、描边、不透明度、填充规则及描边几何属性优先于同名 SVG 属性。 fill="currentColor" 和 stroke="currentColor" 会查找图形、分组、<use> 实例或 SVG 根节点上最近的 color;没有指定时使用黑色。通过 style 指定描边的图形仍保持 2D。目前不会解析外部 CSS 样式表。 未闭合的折线仍保持 2D。挤出导入时, 圆角矩形会保留 SVG 中不同的 rx 与 ry,生成椭圆圆角;只指定其中一个时, 另一个使用相同的值,两个半径分别限制在对应尺寸的一半以内。普通 2D Rect 导入器仍只存储一个圆角半径。开放路径、实际描边、渐变及 不支持的 SVG 元素继续以 2D 显示。 本地 <use href="#id"> 和旧式 xlink:href 可引用上述图形或分组,并生成独立实例, 包括前向引用和重复引用。实例的像素位移、变换及继承填充会在挤出前生效, 且各自拥有可编辑的 ID。 循环引用和无法解析的引用会被跳过。<use> 也可以引用带 viewBox 的 <symbol>, 实例或符号本身需要指定正数像素 width 和 height;实例尺寸优先。 preserveAspectRatio 支持 meet 的各项对齐方式、none,以及挤出轮廓的 slice 裁切; 最终尺寸缺失或无效时会跳过该实例;暂不导入外部引用。 带显式正数像素宽高和 viewBox 的内嵌 <svg> 也会生成可编辑的视口分组。 其 x/y、变换和 preserveAspectRatio 会作用于挤出后的子图形。 本地 <use> 可以引用这样的 <svg>,并独立覆盖视口尺寸。内嵌 SVG 视口默认会裁切 挤出轮廓(包括 slice);设置 overflow="visible" 可保留超出视口的几何。 完整位于视口内的图形保持分组关系,移动视口分组时其 3D 网格会一同移动; 跨过边缘的图形则转换为裁切后的 Path。 本地 clip-path="url(#id)" 和内联 CSS clip-path 也会裁切可挤出的轮廓。 <clipPath> 可以包含 Path、多边形、折线、矩形、圆形、椭圆、嵌套分组, 也可以通过本地 <use> 复用这些图形。 多个裁剪图形取并集,clip-rule="evenodd" 可以生成孔洞;分组和重复 <use> 实例上的裁剪会跟随各自的变换。多级裁剪彼此相交,也会与 SVG 视口裁切相交。 clipPathUnits="objectBoundingBox" 支持单个图形以及分组裁剪,分组的包围盒 会计入所支持子图形的变换。 被裁切的轮廓仍是可编辑的 Path;完整落在裁剪区域内的图形保持原节点及分组关系。 无法解析或尚未支持的裁剪定义保持 2D。遮罩、滤镜、裁剪路径中的文本及外部裁剪引用 暂不转换为 3D。 display="none" 或继承 visibility="hidden" 的元素不会生成 3D 网格; 子元素可以显式设置 visibility="visible"。 传入完整的 <svg> 根节点后,若有以像素指定的 width/height 和 viewBox, 导入会应用视口缩放及 preserveAspectRatio。可挤出的填充图形也会保留嵌套分组中的 skewX、skewY 和 matrix(...) 剪切变换;变换后的轮廓会采样为仍可编辑的折线路径。 只有合成变换包含剪切时才会采样;普通 viewBox 缩放仍保留矩形和曲线路径的原有形式。 没有显式像素视口尺寸时,继续使用 viewBox 的原坐标。未挤出的 2D 图形仍使用原导入器, 其源 SVG 剪切变换尚不能准确保留。 设置 preserveAspectRatio="slice" 后,可挤出的填充轮廓会按视口裁切, 包括孔洞及被裁成多个独立区域的图形。完整位于视口内的矩形或曲线路径仍保持原有形式。 不支持挤出的 2D 图形仍沿用普通导入器,此辅助函数不会裁切它们。
点击下方的 Repeated use sample 可以查看同一定义以不同填充色重复实例化, 以及中间的普通矩形。 点击 Symbol viewport sample 可以比较同一个带孔洞的符号在两个视口中的效果: 第一个使用符号自身的尺寸,第二个指定实例尺寸。 点击 Nested SVG viewport sample 可以比较直接绘制的内嵌 SVG 视口与 通过 <use> 引用的第二个实例;两者的挤出轮廓都会按各自视口裁切。 点击 Group bounds + clip use sample 可以查看变换分组上的包围盒裁剪, 以及通过本地 <use> 复用的两个裁剪窗口。 点击 Paint style + currentColor sample 可以比较继承颜色、实例独立颜色、 内联填充覆盖,以及因内联描边而保持 2D 的图形。 点击 Clip paths + repeated use sample 可以查看两个分别裁剪、带孔洞的实例, 以及通过对象包围盒中的多个图形共同裁剪出的形状。
设置 extrude3d.bevelSize(单位为画布单位)可为 Path 和 Rect 挤出的前后两端 添加倒角,示例中可用 Bevel 滑块调整。内缩后拓扑保持稳定时,也支持孔洞、 多个独立轮廓和凹角。若轮廓相交或狭窄区域塌陷,则保留直边。改变深度或尺寸会 重新生成伴生网格,原图形仍可编辑。 为限制网格生成开销,填充边界求解后总计超过 4096 个顶点时也会保留直边。
设置 extrude3d.bevelSegments 为 1(默认)可使用直线倒角;设置为 2–8 可生成圆弧倒角。段数越多,轮廓越平滑,但网格三角形也越多。示例中的 Bevel profile 控件可用于比较两种效果。
示例中的 Download GLB 按钮会将已生成的三维网格导出为独立的 glTF 二进制文件。 使用 api.export 可下载画布中所有已就绪的挤出图形;指定 nodes 可只导出选区, 包括选中分组的子图形:
import { ExportFormat } from '@infinite-canvas-tutorial/ecs';
api.export({ format: ExportFormat.GLB });
const glb = api.export({
format: ExportFormat.GLB,
nodes: [api.getNodeById('extruded-vector')!],
download: false,
}); // Uint8Array导出会把画布向下的 Y 轴转换为 glTF 向上的 Y 轴,并保留当前三维姿态、纯色填充及 可选的侧壁颜色。只有已就绪的伴生网格会写入文件;不支持的二维 SVG 元素和纹理不会导出。 如需直接导出网格组件,可以使用底层的 exportMeshesToGlb。
点击 ViewBox + shear sample 可以查看变换后的轮廓与偶奇填充孔洞,再关闭挤出对比 2D 轮廓。 点击 Slice clipping sample 可以查看视口对孔洞、分离区域和曲线边缘的裁切。
Loading canvas…
光照
在静止的立方体、球体和圆柱上对比环境光、平行光、点光和聚光灯。可切换光源、调整强度与范围、修改球体金属度与粗糙度、开启摄影棚环境光照和投射阴影,或播放聚光灯轨道动画。点光与聚光灯可使用原有的平滑范围衰减,也可设置参考距离,改用逆平方衰减。渲染器对直接光照使用 GGX,并在线性空间计算颜色、以胶片映射处理亮部。可选的 LDR 全景图或 Radiance HDR 图片提供球谐漫反射光照,以及结合 split-sum BRDF 查找表的预过滤反射。
最多两盏设置 castShadow: true 的平行光、点光或聚光灯可同时投射阴影,并通过 3 × 3 PCF 过滤。每盏灯可设置 shadowStrength(0–1)、shadowSoftness(0–4 个纹素)、shadowBiasScale(0–4)和 shadowNormalBias(0–10 个画布单位);默认值保持原有画面。每个 Mesh 可以用 material3d.castShadow 与 material3d.receiveShadow 分别关闭投影和接影(两者默认 true)。平行光阴影图按当前 Mesh 范围拟合,聚光灯阴影图使用光锥和照射范围,点光阴影图包含六个面。控件、阴影图尺寸与 API 说明见光照示例。
立方体是粗糙电介质,球体是金属,圆柱是光滑电介质。可以分别切换球体投影与地板接影;切换 HDR 与 LDR 全景图可比较球体反射。
glTF 模型
通过声明式 mesh3d 的 geometry: { type: 'gltf', url: '…' } 加载外部 glTF/GLB。Khronos Duck 示例使用 /data/ 下的 .gltf + .bin;网格会归一化到单位包围盒,再用 scale3d 缩放到画布尺寸。