跨平台前端移动开发OpenHarmony【免费下载链接】flutter_flutter本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。项目地址https://gitcode.com/CPF-Flutter/flutter_flutter点击查看免费下载本指南以 Flutter SDK 的 Impeller 渲染引擎位于本仓库engine/src/flutter/impeller为背景完整讲解如何从渲染栈的最底层——硬件抽象层HAL与着色器编译体系——入手绘制一个最简单的红色三角形。读完本文你将掌握 Impeller 中着色器编写、管线Pipeline构建、顶点缓冲Vertex Buffer上传与绘制调用的完整闭环并进一步学会用 varying 实现逐顶点颜色插值、用 uniform 实现随时间变化的动画着色为理解 Flutter 上层渲染工作打下基础。为什么从三角形开始Impeller 的 HAL 与着色器编译机制Impeller 是 Flutter 新一代渲染引擎负责把 Flutter 的绘制指令DrawCommand翻译成 GPU 可执行的渲染工作。它的渲染栈可以粗略分为实体层Entity、渲染器Renderer与后端Backend。本文刻意绕开上层抽象直接使用renderer层提供的 HAL 接口原因正如文档所说——三角形是最小的可验证单元它能让我们看到 Impeller 管线构建与着色器编译的全貌而 Flutter 上层的一切绘制在概念上并不比这更复杂。Impeller 的着色器编译体系位于engine/src/flutter/impeller/compiler其 README 明确说明这套宿主侧工具链消费 GLSL 着色器生成可供各后端OpenGL ES / Metal / Vulkan消费的着色器库同时由 Reflector 生成用于在运行时构建渲染与计算管线的代码和元数据。这正是本文所有“编译器替我们做了苦力活”说法的来源。本文对应的完整可运行代码示例位于engine/src/flutter/impeller/renderer/renderer_unittests.cc的RendererTest.BabysFirstTriangle测试用例中约 第 123 行起文中所有 C 代码片段均可在其中找到完整上下文。第一步理解管线Pipeline任何渲染工作开始前都必须先创建一条管线。管线的职责是描述一次光栅化作业中的固定功能阶段fixed function stages如深度测试、模板测试、混合、采样等由 GPU 硬件规定好行为的部分与可编程阶段programmable stages即着色器。管线对象在 GPU 上对应一次较重的状态编译因此创建成本高昂——最佳实践是在初始化阶段一次性创建好并长期复用绝不要在逐帧渲染路径中创建管线否则会造成明显的掉帧jank。编写着色器从输入规划开始文档提示当你想好要渲染什么时通常应该先写着色器。着色器定义了你需要向 GPU 提供哪些输入编译器会根据着色器生成对应的接口C 头文件之后你只需在代码里按图索骥地接线还能享受代码补全的便利。Impeller 正是希望你以“着色器驱动”的方式工作编译器会主动帮助你。着色器定义了管线的可编程阶段。绘制三角形需要两段着色器顶点着色器Vertex Shader对每个顶点执行一次职责是把顶点坐标变换到归一化设备坐标NDC。之后光栅化器把这些坐标转换成帧缓冲中的 2D 像素坐标。片段着色器Fragment Shader对三角形覆盖的每个纹理元素texel即像素执行一次职责是为像素着色。顶点着色器baby.vertin vec2 position; void main() { gl_Position vec4(position, 0.0, 1.0); }该着色器接受一个vec2坐标直接打包成vec4(position, 0.0, 1.0)输出。注意这里没有做任何坐标转换——因为我们的顶点将直接以 NDC 给出。在 Impeller 坐标系中NDC 范围为(-1,-1)左下到(1,1)右上Y向上详见engine/src/flutter/impeller/docs/coordinate_system.md的坐标系对照表这也是 Metal 的约定。绘制三角形只需三个顶点具体数值稍后在顶点缓冲中给出。片段着色器baby.fragout vec4 frag_color; void main() { frag_color vec4(1.0, 0.0, 1.0, 1.0); }该着色器对三角形覆盖的每个像素执行一次固定输出纯红色R1.0, G0.0, B0.0, A1.0。这两份着色器在仓库中的真实文件为engine/src/flutter/impeller/fixtures/baby.vert与engine/src/flutter/impeller/fixtures/baby.frag并被登记在engine/src/flutter/impeller/fixtures/BUILD.gn的shaders列表中第 42、43 行由构建系统负责把 GLSL 编译为各后端可用的着色器产物。管线描述符Pipeline Descriptor调用着色器编译器后除了生成各后端专属的着色器代码OpenGL ES 对应 GLSL ES、Metal 对应 MSL、Vulkan 对应 SPIR-V编译器还会额外生成两个 C 头文件baby.vert.h与baby.frag.h。它们包含你在运行时组装管线所需的全部接口与元数据把它们#include进你的翻译单元即可。以baby.vert.h为例编译器为我们产出了一个非常关键的 structstruct PerVertexData { Point position; // (offset 0, size 8) }; // struct PerVertexData (size 8)编译器从着色器中“反射”出每个顶点需要提供一个position字段偏移 0、大小 8 字节渲染时由我们负责填充。这个 struct 的价值在于当你修改着色器时编译器会自动增删、重排字段并依据 GPU 对齐要求自动插入填充字节padding你只需要关心业务数据本身。虽然你完全可以不用这个 struct但信任编译器能极大简化你的工作。所有这类接口与元数据统一封装在BabyVertexShaderstruct 中baby.frag.h里则对应有BabyFragmentShader。以这两个类型为模板参数即可拼装管线// Declare a shorthand for the shaders we are going to use. using VS BabyVertexShader; using FS BabyFragmentShader; // Create a pipeline descriptor that uses the shaders together and default // initializes the fixed function state. // // If the vertex shader outputs disagree with the fragment shader inputs, this // will be a compile time error. auto desc PipelineBuilderVS, FS::MakeDefaultPipelineDescriptor(*context);MakeDefaultPipelineDescriptor会为管线的固定功能阶段填入默认值。从 pipeline_builder.hInitializePipelineDescriptorDefaults实现可以看到这些默认值包括从context-GetShaderLibrary()解析顶点/片段的入口函数kEntrypointName依据VertexShader::kAllShaderStageInputs与kInterleavedBufferLayout反射信息自动搭建顶点描述符Vertex Descriptor并注册两阶段各自的描述符集布局为 0 号颜色附件配置默认像素格式GetDefaultColorFormat()并开启混合配置默认深度附件CompareFunction::kAlways与模板附件CompareFunction::kEqual及其像素格式。注意测试用例RendererTest.BabysFirstTriangle中还会针对测试环境对描述符做两处微调desc-SetSampleCount(SampleCount::kCount4)4x MSAA 多重采样和desc-SetStencilAttachmentDescriptors(std::nullopt)关闭模板附件。这说明默认描述符并非在所有环境下一成不变根据目标环境做少量调整是正常的。细心的读者会发现这是第一次把顶点与片段两个阶段“融合”在一起。如果二者不兼容会怎样例如片段阶段声明了一个顶点阶段没有提供的输入——编译器早已替你考虑了这一点PipelineBuilderVS, FS的实例化会在编译期静态检查两阶段是否兼容不兼容则 C 代码直接拒绝编译。最后创建管线对象auto pipeline context-GetPipelineLibrary()-GetPipeline(desc).Get();这里的调用链值得说明GetPipeline返回的是一个PipelineFuture见 pipeline_library.h 的声明管线创建在后台线程执行Get()负责同步等待结果。再次强调管线创建昂贵务必只在启动阶段创建一次并长期持有。第二步准备顶点数据Vertex Data还记得编译器生成的PerVertexData吗现在轮到我们填充顶点信息并把它以 GPU 缓冲的形式交给绘制调用。Impeller 为此提供了VertexBufferBuilderT工具类模板参数 T 即PerVertexData。回顾顶点着色器它是个“懒汉”不对手头坐标做任何 NDC 转换因此我们提供的坐标必须已经是 NDC 值VertexBufferBuilderVS::PerVertexData vertex_buffer_builder; vertex_buffer_builder.AddVertices({ {{-0.5, -0.5}}, {{0.0, 0.5}}, {{0.5, -0.5}}, });三个顶点(-0.5,-0.5)、(0,0.5)、(0.5,-0.5)围成一个等腰三角形底边在下、尖顶朝上全部位于 NDC 可视范围内。从 vertex_buffer_builder.h 的源码可以看到AddVertices把顶点逐个emplace_back进内部std::vectorVertexType索引缓冲indices_为空时GetIndexType()返回IndexType::kNone即无索引直连绘制。然后让构建器为我们创建设备缓冲auto vertex_buffer vertex_buffer_builder.CreateVertexBuffer( *context-GetResourceAllocator());CreateVertexBuffer(Allocator)的内部实现会把顶点数组一次性拷贝进设备缓冲CreateBufferWithCopy并生成包含vertex_count、index_type的VertexBuffer。由于每帧都不改顶点坐标这个缓冲可以只创建一次在后续所有帧的绘制调用中反复引用。这里还有一个非常实用的设计如果你改了着色器里的顶点数据定义PerVertexData会随之变化那么你设置顶点缓冲处的 C 代码将立即出现编译错误。这样重构着色器时所有“给着色器喂顶点数据”的调用点都会被立刻标记出来杜绝了静默的数据不匹配。第三步发起绘制Draw重活已经干完了。每帧只需要在渲染通道RenderPass上设置好我们提前存放的管线和顶点缓冲然后发起一次绘制调用pass.SetPipeline(pipeline); pass.SetVertexBuffer(vertex_buffer); pass.Draw();就这样大约 10 行 C 和几句简单的 GLSL一个鲜红的三角形就出现在屏幕上了——编译器替我们把 GLSL 翻译成了各后端的着色器语言如 Metal Shading Language并推导出组装管线所需的全部元数据。进阶一Varyings——让顶点与片段“对话”上一节的三角形虽然能渲染但顶点着色器与片段着色器之间毫无交流。现在演示如何让数据在两阶段间流动除了每个顶点的坐标再为每个顶点附带一个颜色片段着色器根据像素在三角形内的位置混合三个顶点的颜色贡献。第一步给baby.vert增加输入与输出in vec4 color; out vec4 v_color;在顶点着色器主函数中直接把输入透传给输出v_color color;第二步在baby.frag中声明来自上一阶段的输入in vec4 v_color;并在主函数中把片段颜色设为该输入frag_color v_color;这里我们没有做任何颜色混合计算——混合由光栅化器自动完成它在两阶段之间对输出值按像素位置做插值。由于这种值随像素而变Impeller 称之为varying并以v_前缀命名这类变量。着色器改完了但编译器立刻发出警告顶点缓冲构建器无法再构建顶点缓冲了它是对的——现在每个顶点还多了一个颜色需要提供。补齐数据vertex_buffer_builder.AddVertices({ {{-0.5, -0.5}, Color::Red()}, {{0.0, 0.5}, Color::Green()}, {{0.5, -0.5}, Color::Blue()}, });三个顶点分别取红、绿、蓝光栅化器在三角形内部对三色做重心插值你将看到每个像素颜色各不相同的渐变三角形。在真实测试用例中每个顶点甚至配了两个颜色{position, Color::Red(), Color::Green()}等见 renderer_unittests.cc为后面的动画示例做准备。进阶二Uniforms——向着色器提交全局常量你已经会画三角形了。还有一个被广泛使用的重要概念uniform 数据。此前提交给着色器的数据要么作为顶点输入要么经 varying 固定功能单元变换。但有时你需要向某个阶段的所有调用提交一份完全相同的常量数据——这类常量信息就叫 uniform 数据。设想的演示方案为每个顶点提供两个颜色并根据当前时间秒在二者之间做动画。时间对所有片段着色器调用都是常量正好作为 uniform 数据提交。至于“为每个顶点提供第二个颜色并经 varying 传给片段着色器”正是上一节讲过的内容留作读者练习。在片段着色器中声明 uniform 块uniform FragInfo { float time; } frag_info;编译器会“贴心地”生成一个结构相似的 C struct。和PerVertexData一样编译器深谙各字段的对齐规则会自动放置必要的填充字节你只需在 C 侧填充数据再以设备缓冲提交。先看编译器为上面这段 GLSL 生成的产物struct FragInfo { Scalar time; // (offset 0, size 4) }; // struct FragInfo (size 4)看起来直截了当。为了证明对齐处理的价值我们再往 GLSL uniform 块里加几个“需要填充”的字段uniform FragInfo { float time; vec2 bar; vec4 baz; } frag_info;这段 GLSL 生成的 C struct 如下——注意那些你完全不用操心的填充字节struct FragInfo { Scalar time; // (offset 0, size 4) Padding4 _PADDING_bar_; // (offset 4, size 4) Point bar; // (offset 8, size 8) Vector4 baz; // (offset 16, size 16) }; // struct FragInfo (size 32)float与vec2之间被自动插入了 4 字节Padding4vec2对齐到 8 字节边界、vec4对齐到 16 字节边界整个 struct 大小为 32 字节——这些 GPU 对齐细节全部由编译器代劳。在我们的“游乐场”里创建FragInfo并填入当前秒数FS::FragInfo frag_info; frag_info.time fml::TimePoint::Now().ToEpochDelta().ToSecondsF();接下来需要把这数据放进 GPU 可访问的内存。主机缓冲HostBuffer与“巨型缓冲”Jumbo Buffer与可用realloc调整大小的malloc缓冲不同Impeller 中的设备缓冲不可 resize同时创建大量零碎的小设备缓冲也是不好的实践。因此 Impeller 倾向于把所有数据uniform、顶点或其他都暂存进一个大分配中绘制时每个绘制调用引用这个大分配中指定偏移与长度的视图view。HostBuffer正是为此而生它是一块在堆上分配malloc或类似机制的缓冲核心诉求是尽可能快地增长。Impeller 把一帧内的所有数据都暂存在这样的缓冲里随着缓冲不断构建指向其中各段偏移长度的视图被记录进命令流在绘制调用提交前的最后一刻这些信息才上传到 GPU必要时缓冲会被复用。在 Impeller 代码库中这类缓冲被称为jumbo buffers巨型缓冲。我们从engine/src/flutter/impeller/core/host_buffer.h的源码可以看到它的设计HostBuffer内部维护一个大小为kHostBufferArenaSize 4u的 arena约等于最大在途帧数每帧调用Reset()复用内部设备缓冲块EmplaceUniform()在放置数据时取max(alignof(UniformType), GetMinimumUniformAlignment())作为对齐确保满足后端特定的 uniform 对齐要求顶点数据则可通过Emplace()以alignof(VertexType)对齐放入同一缓冲。我们的演示数据没那么“巨型”但思路一致把FragInfo放进每帧新建的主机缓冲再绑定到渲染通道auto host_buffer HostBuffer::Create(context-GetResourceAllocator()); FS::BindFragInfo(pass, host_buffer-EmplaceUniform(frag_info));BindFragInfo从哪来的它是编译器根据生成的元数据自动产生的绑定函数——编译器知道如何把缓冲绑定到特定阶段。如果你改了着色器所有调用点会报编译错误直到你全部修正这大大降低了着色器重构的出错率。让三角形动起来最后改写片段着色器把时间因素纳入两色混合void main() { float floor floor(frag_info.time); float fract frag_info.time - floor; if (mod(int(floor), 2) 0) { fract 1.0 - fract; } frag_color mix(v_color, v_color2, fract); }思路是取当前时间的整数部分与小数部分当秒数为偶数时把小数部分翻转1.0 - fract使颜色在v_color与v_color2之间来回摆动mix(v_color, v_color2, fract)按比例混合两色。配合每帧更新的frag_info.time你将看到三角形着色不断来回渐变——这正是仓库中baby.frag的真实实现。小结与延伸至此你已经走完了 Impeller 最低层渲染的完整闭环编写着色器顶点着色器定义顶点变换与 varying 输出片段着色器定义像素着色与 uniform 输入编译器据此生成 C 反射头文件构建管线用PipelineBuilderVS, FS::MakeDefaultPipelineDescriptor得到默认描述符按环境微调后经GetPipelineLibrary()-GetPipeline(desc)一次性创建并复用填充顶点用VertexBufferBuilderVS::PerVertexData组装 NDC 顶点CreateVertexBuffer生成设备缓冲发起绘制pass.SetPipelinepass.SetVertexBufferpass.Draw进阶玩法varying 实现逐顶点颜色插值uniform经HostBuffer::EmplaceUniform与编译器生成的BindFragInfo实现逐帧动画。你可能会觉得“不能把颜色涂到三角形外面”有点不尽兴——那就画更多、更复杂的形状吧。正如文档结论所言Flutter 的上层绘制在概念上并不比这更复杂。理解了这条最底层的渲染链路你就拿到了理解 Impeller 乃至 Flutter 全部渲染机制的关键钥匙。若想继续深入建议接着阅读 Impeller 坐标系说明理解各后端坐标系差异与 着色器编译与反射器理解反射产物生成机制并在RendererTest.BabysFirstTriangle中亲手运行、修改、验证上述每一步。赞分享跨平台前端移动开发OpenHarmony【免费下载链接】flutter_flutter本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。项目地址https://gitcode.com/CPF-Flutter/flutter_flutter点击查看免费下载相关推荐安全新闻写作教程如何把Exploitarium式PoC写成传播性强的技术文章安全新闻写作教程如何把Exploitarium式PoC写成传播性强的技术文章 Exploitarium 是一个集中收录公开漏洞 PoCProof of Co跨平台前端移动开发OpenHarmonyImpeller 入门实战用 HAL 与着色器编译管线绘制你的第一个三角形Impeller 入门实战用 HAL 与着色器编译管线绘制你的第一个三角形 导读 本文基于 Flutter 引擎中 Impeller 渲染器的官方教程文档跨平台移动开发前端图形学Flutter Impeller“Babys First Triangle”实战从 GLSL 着色器到底层渲染管线的完整走读Flutter Impeller“Babys First Triangle”实战从 GLSL 着色器到底层渲染管线的完整走读 本篇指南基于 Impeller跨平台移动开发前端UI组件桌面应用上一篇B站大会员4K视频下载终极指南解锁离线观看新体验下一篇3分钟搞定Windows和Office激活这个智能脚本让你彻底告别弹窗烦恼创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考