Flutter 全新真 3D 实现,用 flutter_scene 能开发一个「我的世界」

最近一个第三方实现的 bdero/flutter_scene 惊艳到我了,比如下面这个类似「我的世界」的游戏效果,就是作者用 Flutter Scene 做的,全部基于 Flutter GPU/Impeller 内容实现,没有引入任何其他外部引擎,没有任何 PlatfromView

Flutter Scene 整个实现里,都是自己管理场景图、材质、动画、光照、阴影、RenderGraph、GPU 资源、绘制排序、空间裁剪和 glTF 导入 ,底层依赖原生的 flutter_gpu ,同时支持 Flutter 全平台,除了 Web 平台用了一套 API 兼容的 WebGL2 后端,而且支持 Custom embedders ,也就是甚至可以支持鸿蒙。

从实现上看它已经远超官方的任何 GPU Demo 效果,比如项目有 PBR、IBL、阴影、SSAO、SSR、景深、后处理、动画混合、LOD、实例化、RenderTexture、3D Gaussian Splatting、物理、3D 场景中的 Flutter Widget、甚至支持了场景编辑器和 MCP Agent 操作接口。

简单看的话呀,整个项目可以分成六层:

层级 主要模块 作用
Flutter 展示层 SceneViewSemanticsPointerWidgetTexture 帧循环、Flutter 合成、输入、无障碍
场景逻辑层 SceneNodeComponent、Camera 场景图、行为、动画、物理生命周期
渲染数据层 RenderSceneRenderItem、BVH 扁平绘制列表和空间索引
渲染调度层 RenderGraph、各种 RenderPass 阴影、深度、SSAO、SSR、PBR、后处理
绘制编码层 SceneEncoder、Material、Geometry Pipeline、排序、绑定、Draw Call
GPU 后端层 Flutter GPU/Impeller、WebGL2 shim 真正提交 GPU 命令

除此之外还有三套外围系统支持:

  • glTF、.fsceneb.fmat 资产构建系统
  • Rapier、Box3D 物理后端
  • Editor Core、编辑器 UI 和 MCP Agent 接口

而且不只有 flutter_scene,项目还包含 flutter_scene_rapierflutter_scene_box3d、编辑器、MCP、和独立编辑器 App,完善程度是真的让人意外。

而且使用方式也很方便,通过 SceneView 入口就可以接入SceneView 内部拥有一个 Flutter Ticker,但每帧不会重建整个 Widget 子树,而是通过一个 ChangeNotifier 只触发绘制层重绘:

  • 驱动每帧 tick
  • 构造或取得 Camera
  • 调用 Scene.render
  • 管理加载占位和 Pipeline 预热
  • 处理场景里的 Widget 输入
  • 将 3D 节点投影进 Flutter Semantics 树

也就是 SceneView 不是 PlatformView,也不是 Flutter 上方盖一层原生 SurfaceSceneView 的结果实际是会参与 Flutter 的 Canvas 合成,可以和普通 Flutter UI 叠加、裁剪和布局

scss 复制代码
final scene = Scene();
​
class SpinComponent extends Component {
  SpinComponent(this.speed);
​
  final double speed;
​
  @override
  void update(double deltaSeconds) {
    node.localTransform *=
        vm.Matrix4.rotationY(speed * deltaSeconds);
  }
}
​
void buildScene() {
  final mesh = Mesh(
    CuboidGeometry(vm.Vector3.all(1)),
    PhysicallyBasedMaterial(),
  );
​
  scene.add(
    Node(mesh: mesh)..addComponent(SpinComponent(1.2)),
  );
}
​
Widget buildView() {
  return SceneView(
    scene,
    camera: PerspectiveCamera(
      position: vm.Vector3(0, 2, -5),
      target: vm.Vector3.zero(),
    ),
    warmUp: true,
  );
}

然后对于每个 RenderView 都会获得自己的离屏纹理 ,纹理尺寸按根据实际情况缩放成功后,再合成到 Flutter Canvas,3D 场景完成 GPU 渲染后,纹理通过 asImage() 转成 Flutter 可绘制图像,再由 Canvas.drawImageRect 合成到目标区域。

所以他有个很明显的超级优势,它天然可以支持:

  • Flutter 布局中的局部 3D 区域
  • 画中画
  • 分屏多 Camera
  • 不同 View 使用不同 LayerMask
  • RenderTexture
  • 3D 和 2D UI 混合

当然,相对的成本就是每个屏幕 View 都有独立离屏目标和 RenderGraph,多个 Camera 不能一次绘制自动复用,不同 View 不共享帧内 Render Target

然后项目里的场景图主要靠 Node 和 Component,其中 Node 只负责层级和变换Node 是典型的保留式场景图节点,包含:

  • 本地变换和缓存的世界变换
  • 父子层级
  • Mesh
  • Component
  • 可见性
  • Render Layer
  • Raycast 开关
  • 阴影静态标记
  • LOD、动画、Skin 等附加数据

变换采用脏标记机制,修改父节点时,子 tree 的世界变换会被标记失效,然后真正访问或渲染时才重新计算,世界 AABB 也有单独缓存,同时向祖先传播失效。

简单来说,比如你直接原地修改 Matrix4,引擎不知道矩阵变了,必须手工调用 markTransformDirty()

ini 复制代码
node.localTransform.translate(...);
node.markTransformDirty();

然后 Component 是行为和能力插件Component 类似轻量 ECS 中的组件,但需要依附在传统 Node ,有完整生命周期:

sql 复制代码
onAttach
  ↓
onMount
  ↓
onLoad(异步)
  ↓
fixedUpdate / update
  ↓
onUnmount
  ↓
onDetach

比如 onLoad 没结束前,update 不会执行,物理系统还可以用 fixedUpdate 运行固定时间步。

目前引擎内置组件包括:

  • Mesh、Instanced Mesh
  • Camera
  • Directional、Point、Spot Light
  • LOD
  • Semantics
  • WidgetComponent
  • SplatComponent
  • Environment Volume
  • 各类物理组件

这个设计的优势在于, Flutter 开发者不用接受完整 ECS 心智模型,自定义行为只需要继承 Component

当然,整个项目最最最重要的是,场景树不会直接拿来绘制,Node Graph 和 RenderScene 是两套结构

Node 用来表达业务层级,但 GPU 绘制时不适合每帧递归整棵树,所以 MeshComponent 挂载到场景时,会把每个可绘制 Primitive 注册成一个 RenderItem

也是很贴合 Flutter 多颗 Tree 的设计。

RenderItem 主要保存:

  • Geometry 和 Material
  • 当前世界变换
  • 可见性
  • Layer
  • 世界 AABB
  • LOD
  • Instance Transform
  • 对象对应的局部光源列表
  • 阴影和高亮状态

渲染 Pass 遍历的是扁平的 RenderScene.items

然后就是 BVH 用于裁剪和灯光分配,有边界的 RenderItem 会放进 BVH,没有边界或关闭裁剪的对象进入 always-visible 列表。

静态结构变化会重建 BVH,仅仅是对象移动时可以走 refit,而不是完整 rebuild。

BVH 不只用于视锥裁剪,还会用在局部灯光分配,而 Point 和 Spot Light 每帧通过空间结构找到可能受影响的对象,然后给每个 RenderItem 生成灯光索引切片,Fragment Shader 不需要循环整个场景的灯光。

所以本质上是一个带 Cluster 思路但粒度在对象级的 Forward Renderer,不是传统 Deferred Renderer

整个完整渲染流程大概如下:

更详细的渲染流程会是如下所示,每帧大体走下面这条路径:

这里这些 Pass 不是每帧全部运行,会根据实际情况按能力动态启用,例如:

  • SSAO、SSR、景深或自定义 Pass 请求深度时,才生成 Depth Prepass
  • 材质声明 scene_color 时,才把主 Scene Pass 拆成不透明和透明两个阶段
  • 没有阴影灯就不创建 Shadow Pass
  • 没启用后处理就没有额外成本

另外这里面的 RenderGraph 并不是复杂的图编译器,我感觉这个名字很有误解,更准确地说应该是:

  • 一个有顺序的 Pass 列表
  • 一个 Pass 间共享数据的 Blackboard
  • 一个跨帧复用纹理的 TransientTexturePool

Pass 按插入顺序执行,不做自动依赖分析、自动重排和无效 Pass 裁剪,每个 Pass 还会创建自己的 CommandBuffer。

这种实现的好处是简单、可调试、可以适合现在还在演进的 Flutter GPU API,不过缺点也和明显,如果 Pass 越来越多,资源复用和调度效率会落后于成熟引擎的 RenderGraph。

然后在编码上,项目 SceneEncoder 将可见对象分成两类

  • 不透明对象:先按 Render Pipeline 分组,减少 Shader、Vertex Layout 等状态切换,然后同 Pipeline 内再按距离从近到远绘制,利用深度测试尽早杀掉后面的 Fragment
  • 透明对象:按 Camera 距离从远到近绘制,使用 Premultiplied Alpha 混合,透明的 Instanced Mesh 会被拆成每个 Instance 一个排序记录

然后整个 Pipeline 使用进程级缓存,Key 是:

复制代码
Vertex Shader + Fragment Shader + Vertex Layout

项目还针对 Pipeline 创建、Uniform 临时内存和重复绑定做了大量优化,比如:

一个 100 Tile 的流式场景在 Metal 上,平均帧构建时间 5.7ms

然后 Flutter Scene 支持两种导入模式

1、运行时导入

ini 复制代码
final model = await Node.fromGlbAsset('assets/model.glb');
scene.add(model);

2、外部获得 bytes:

ini 复制代码
final model = await Node.fromGlbBytes(bytes);

然后在构建期预处理,通过 Build Hook 将 glTF 转成 .fsceneb

less 复制代码
buildScenes(
  buildInput: input,
  buildOutput: output,
  inputFilePaths: ['assets_src/model.glb'],
);

也就是实际读取的是构建时生成的 .fsceneb,避免在终端设备上重新解析 glTF,同时它还支持 Scene Subtree 流式加载和 Prefab 复用

然后对于材质系统,内置材质主要有

  • PhysicallyBasedMaterial
  • UnlitMaterial
  • SpriteMaterial
  • ShaderMaterial
  • PreprocessedMaterial

PBR 使用 Metallic-Roughness 工作流,支持 Base Color、Normal、Metallic/Roughness、AO、Emissive 和环境反射。
默认 Scene 会懒加载一个程序化 Studio Environment,所以创建 PBR 材质后不额外配置灯光也能看到结果。

另外可以通哟 .fmat 自定义材质语言.fmat 的定位类似 Filament .mat、Godot Shader 和一种简化的 Shader DSL :

scss 复制代码
material {
  name: "Toon",
  shading_model: lit,
  blending: opaque,
​
  parameters: [
    { type: vec4, name: base_color,
      hint: source_color, default: [1, 1, 1, 1] },
    { type: float, name: bands,
      hint: range(1, 8, 1), default: 4 },
  ],
}
​
fragment {
  void Surface(inout MaterialInputs material) {
    float lighting = max(dot(
      GetWorldNormal(),
      normalize(vec3(0.4, 0.8, 0.2))
    ), 0.0);
​
    material.base_color =
        material_params.base_color *
        floor(lighting * material_params.bands) /
        material_params.bands;
​
    PrepareMaterial(material);
  }
}

之后 Build Hook 会编译 Shader,同时生成参数 Metadata,然后 Dart 侧通过名称设置类型安全参数:

csharp 复制代码
final material = await loadFmatMaterial('assets/toon.fmat');
​
material.parameters
  ..setColor('base_color', const Color(0xFFE0A030))
  ..setFloat('bands', 4);

而相比原始的 ShaderMaterial,开发者不需要手工处理 std140 Packing、Sampler 和 Uniform Slot,甚至它还支持材质主动声明:

复制代码
scene_color
scene_depth

只有可见材质请求时,引擎才创建对应资源。

不过也不是没有限制,项目距离完整 glTF/PBR 还有明显差距:

  • 没有自定义 per-light BRDF Hook
  • mat3 因 GLES std140 问题暂不支持
  • lit Shader 已接近 Metal 最低 16 Sampler 限制
  • 第二 UV、glTF Tangent、Texture Transform 尚未完成
  • Clearcoat、Sheen、Transmission 等 glTF 扩展尚未完成

因此普通 Metallic-Roughness 模型基本可用,但汽车漆、玻璃、镜片、布料、复杂 UV 资源和高保真工业模型可能不能正确还原。

之后导入的 glTF Animation 会保存在模型根节点上,通过以下代码执行:

ini 复制代码
final walk = model.createAnimationClip(
  model.findAnimationByName('Walk')!,
)
  ..loop = true
  ..play();

每个 Clip 有独立播放状态、速度、时间、循环和 Weight,多个 Clip 可以同时运行,内部 AnimationPlayer 会按权重混合,适合 Idle、Walk、Run Cross Fade。

然后针对光照、阴影和后处理,目前已经有:

  • HDR/EXR/equirectangular Environment Ma
  • Diffuse SH 和 Prefiltered Specular IBL
  • Procedural Gradient/Physical Sky
  • 自定义 .fmat Sky
  • Directional、Point、Spot Light
  • Cascaded Directional Shadow
  • Spot Shadow Atlas
  • 静态阴影缓存
  • Environment Volume 混合
  • Exposure 和 Tone Mapping
  • SSAO、SSR、Fog、God Rays
  • Bloom、Color Grading、Vignette、Film Grain
  • Chromatic Aberration、DoF、FXAA、MSAA
  • 自定义 Render Pass 和 Post Effect

其中静态阴影缓存最值得单独说,只需要给静态物体设置:

ini 复制代码
node.shadowStatic = true;

之后它们的 Cascade Shadow Tile 就可以跨帧复用,只把动态物体叠加进去,这在大规模静态世界里,比每帧每个 Cascade 重绘所有建筑和地面便宜很多。

然后最重磅的就是,Flutter Widget 可以放进 3D 场景,这是项目最有 Flutter 特色的功能:

less 复制代码
scene.add(
  Node()
    ..addComponent(
      WidgetComponent(
        size: const Size(480, 300),
        pixelRatio: 2,
        worldHeight: 1.5,
        child: const MyControlPanel(),
      ),
    ),
);

甚至可以把 Widget 绑定到 glTF 模型已有的显示屏材质,不额外创建 Quad。

不过因为它目前的工作方式是:

复制代码
Flutter Raster
→ GPU/CPU Readback
→ CPU
→ 再上传到 3D Texture

也就是有一次 CPU 往返开销,比如典型 Panel 每次捕获可能花费数毫秒,而动态 Widget 默认每帧捕获,所以大量高分辨率 Widget Surface 会很增加不少开销。

另外其他限制还有:

  • MouseRegion Hover 不能转发
  • 键盘和 Focus 不支持
  • TextField 还不可用
  • WidgetComponent 不进入 .fscene 序列化

然后在物理架构上,核心包只定义抽象物理接口:

  • PhysicsWorld
  • RigidBody
  • Collider
  • Joint
  • Raycast、ShapeCast、Overlap
  • Collision Event

而 Rapier 3D 是通过 flutter_scene_rapier ,利用 Dart FFI 调用 Rust Rapier ,它支持刚体、碰撞器、关节、电机、查询和 Character Controller,并提供大部分平台的预编译二进制,物理由固定时间步推进,渲染 Transform 使用插值。

而 Box3D 是通过 flutter_scene_box3d ,利用 Dart box3d 包接入,支持常用 Shape、查询和碰撞事件,但关节和显式质量/惯性覆盖貌似还没接好。

所以可以看出来,这是一整套完善的 Flutter 3D 引擎,而且还是可用的成套支持,还支持 Flutter Widget 融合使用,比如在游戏之外的支持,例如:

  • 商品和设备 3D 展示
  • App 里的可交互 3D 数字形象
  • 数据可视化
  • 3D 编辑工具
  • ···

这个项目算是直接补全了 Flutter 官方一直没时间支持的 3D 场景,同时作者也是前 Impeller 工程师,貌似离开谷歌之后做的这套项目,想想有趣,在谷歌没做成的事情,离开谷歌反而做成了

链接

fscene.dev/

相关推荐
东方小月1 小时前
从0开发一个 Coding Agent(一):前言
前端·人工智能·typescript
ji_shuke2 小时前
远程排查 Web 系统问题:如何导出 HAR 文件协助定位
前端·问题排查
程序员爱钓鱼2 小时前
Rust String 与 &str 详解:字符串所有权、借用与转换
前端·后端·rust
woshihuanglaoshi2 小时前
数据迁移与版本管理 - Flutter在鸿蒙平台实现数据库升级策略
数据库·学习·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
码农学院2 小时前
GEO与SEO协同:从传统搜索到生成式搜索的平滑迁移路径
服务器·前端·python
咩咩啃树皮10 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
阳光是sunny10 小时前
LangGraph中的Reducer是什么
前端·人工智能·后端
触底反弹10 小时前
一文搞懂 Tailwind CSS 弹性布局:从原理到实战
前端·css·html
阳光是sunny11 小时前
从链到图:LangGraph 入门基础全解析
前端·人工智能·后端