Skip to content

课程 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

source: https://framer.university/blog/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

source: https://docs.spline.design/designing-in-3-d/working-with-2d-and-3d-objects

Rive 目前本质上仍是 2D 工具,它的"3D 感"来自 2.5D 变换、网格变形等,而非完整的 3D 管线。

我们的思路和 2D Rendering in Bevy 接近,2D 图形可视为落在 z = 0 平面上的对象,3D 模型与之共处同一世界;平移/缩放 2D 相机时,可选让 3D 相机一起联动。实现见 PR #271。

架构概览 ​

plaintext
同一 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 混淆。

插件组合方式:

ts
import {
    App,
    DefaultPlugins,
    DefaultRenderer3DPlugin,
} from '@infinite-canvas-tutorial/ecs';

const app = new App().addPlugins(...DefaultPlugins, DefaultRenderer3DPlugin);
app.run();

核心组件 ​

组件作用
Camera3Dprojection: 'perspective' | 'orthographic';linked 时由 CameraSync 跟随 2D 平移/缩放
Mesh3Dpositions、normals、可选 indices(三角网格)
Material3D金属度与粗糙度 PBR:baseColor、metallic、roughness 和环境光系数;旧字段保留兼容性
Light3Dambient / directional / point / spot;见 场景光照示例
Transform3D平移、欧拉角旋转、缩放
Selected3D3D 选中态:当前 gizmo 模式、激活轴/平面、拖拽参考点等(见 3D 变换 Gizmo)
Mat44×4 矩阵工具(perspective / ortho / lookAt)

通过 ECS commands.spawn 创建实体,例如:

ts
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,与 2D mat3.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。

流程 ​

plaintext
视口坐标 (x, y)
  └─ buildPickSceneForViewport(camera, …)  →  Mesh3DPickScene(与渲染同一套矩阵)
       └─ probePick3DAtViewport(…)
            ├─ 1. 已 Selected3D 实体的 gizmo 部件(逐 part 检测)
            └─ 2. 本 canvas 全部 Mesh3D → 取最近命中(最小 t)

优先级:先测 已选中 实体上的 gizmo;未命中再测场景 mesh。多个 mesh 同时命中时,取沿射线 距离最近 的一个(linked 透视下为 深度最大/最靠前)。

标准 Raycast(正交 / 自由相机) ​

非 linked 透视模式时,拾取路径与常见 3D 编辑器一致:

  1. screenToRay:视口像素 → NDC → 用 inv(view × projection) 反投影近/远点,得到世界空间射线。
  2. rayMeshIntersection:顶点经 model 矩阵变换后:
    • 粗测:射线 vs 变换后的 AABB(slab 法)。
    • 细测:对每个三角形做 Möller–Trumbore 射线-三角形求交,保留最小 t。
ts
// 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 } | null

Linked 正交(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 拾取与绘制路径。

交互流程 ​

plaintext
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。序列化选项支持高度偏移和局部三维旋转:

ts
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 会重建网格,平移会复用。父级变换产生的剪切会 烘入网格,线性变换改变时会重建网格。

ts
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。 该函数要求厚度为有限正数。

ts
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 可只导出选区, 包括选中分组的子图形:

ts
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 缩放到画布尺寸。

扩展阅读 ​

Released under the MIT License.