资讯详情

资讯详情

Filament 绘制立方体:从零到一,避开黑屏与面片闪烁的实战指南

简介这份资源围绕「Filament绘制立方体」展开面向具备一定Android与图形学基础、希望入门实时3D渲染的开发者。Filament是Google开源维护的跨平台实时渲染引擎资源以绘制一个基础立方体为主线串联起渲染器、场景、相机与光照等核心组件并涉及几何体顶点数组、材质实例与Material Blueprint、透视相机配置及点光源、聚光灯、环境光等知识点同时覆盖gradlew构建脚本与依赖管理流程。压缩包共655个文件以xml布局与配置、flat与filamat材质、json数据、so动态库、jar与aar依赖、dex与class字节码、java源码及gradle构建脚本为主整体约100.05MB结构完整可直接导入Android工程。已有175人学习下载适合作为理解3D图形编程与实时渲染原理的实践参考帮助读者掌握从几何体创建、材质光照设置到渲染循环呈现的完整链路。1. Filament 绘制立方体从零到一为什么你的第一个 3D 物体总是“翻车”很多刚接触 Filament 的开发者第一次尝试绘制立方体时都会遇到一个反直觉的现象代码明明编译通过了屏幕却一片漆黑或者只有一个诡异的三角形在闪烁。这其实不是你的代码写错了而是 Filament 作为基于物理渲染PBR的引擎对坐标系、材质和光照的要求比传统 OpenGL 严格得多。Filament 绘制立方体这件事本质上是在验证你是否理解了它的渲染管线——从顶点数据到材质系统再到光照环境缺一环都不行。这篇文章就是写给那些想用 Filament 做 3D 可视化、但又不想被官方文档里那些晦涩术语劝退的一线开发者。我会带你从最基础的顶点缓冲区开始一步步把立方体画出来再把那些年我踩过的坑一个个填平。不管你是刚入行的新手还是从 Unity 转过来的老手只要跟着走就能在本地跑通一个带光照、能旋转的立方体。2. Filament 渲染立方体的底层逻辑与最小工程搭建2.1 为什么 Filament 不能像 OpenGL 那样直接画三角形Filament 的核心设计目标是移动端的高效 PBR 渲染这意味着它把很多传统图形 API 的细节封装了起来。在 OpenGL 里你写个glDrawArrays就能把顶点数据扔给 GPU但在 Filament 里你必须先构建一个VertexBuffer和IndexBuffer再通过RenderableManager把几何体注册到实体上。这听起来麻烦但好处是 Filament 会自动处理视锥剔除、材质排序和实例化渲染。如果你跳过这些步骤直接调底层 APIFilament 的渲染循环根本不会理你。常见做法是先用VertexBuffer::Builder定义顶点属性位置、法线、UV再用IndexBuffer::Builder定义三角形索引最后通过RenderableManager::Builder把两者绑到一个Entity上。这个过程就像给 Filament 交一份“简历”它审核通过了才会把你的立方体放进渲染队列。2.2 用 VertexBuffer 和 IndexBuffer 构建一个立方体立方体有 8 个顶点但每个面需要独立法线所以实际要写 24 个顶点每个面 4 个。下面是我常用的顶点数据结构位置和法线交错存储UV 暂时用不上可以留空。struct Vertex { filament::math::float3 position; filament::math::float3 normal; }; // 24 个顶点每个面 4 个法线朝外 static const Vertex CUBE_VERTICES[] { // 前面 (Z) {{-1, -1, 1}, { 0, 0, 1}}, {{ 1, -1, 1}, { 0, 0, 1}}, {{ 1, 1, 1}, { 0, 0, 1}}, {{-1, 1, 1}, { 0, 0, 1}}, // 后面 (-Z) {{ 1, -1, -1}, { 0, 0, -1}}, {{-1, -1, -1}, { 0, 0, -1}}, {{-1, 1, -1}, { 0, 0, -1}}, {{ 1, 1, -1}, { 0, 0, -1}}, // 上面 (Y) {{-1, 1, 1}, { 0, 1, 0}}, {{ 1, 1, 1}, { 0, 1, 0}}, {{ 1, 1, -1}, { 0, 1, 0}}, {{-1, 1, -1}, { 0, 1, 0}}, // 下面 (-Y) {{-1, -1, -1}, { 0, -1, 0}}, {{ 1, -1, -1}, { 0, -1, 0}}, {{ 1, -1, 1}, { 0, -1, 0}}, {{-1, -1, 1}, { 0, -1, 0}}, // 右面 (X) {{ 1, -1, 1}, { 1, 0, 0}}, {{ 1, -1, -1}, { 1, 0, 0}}, {{ 1, 1, -1}, { 1, 0, 0}}, {{ 1, 1, 1}, { 1, 0, 0}}, // 左面 (-X) {{-1, -1, -1}, {-1, 0, 0}}, {{-1, -1, 1}, {-1, 0, 0}}, {{-1, 1, 1}, {-1, 0, 0}}, {{-1, 1, -1}, {-1, 0, 0}}, }; // 36 个索引每面两个三角形 static const uint16_t CUBE_INDICES[] { 0, 1, 2, 0, 2, 3, // 前 4, 5, 6, 4, 6, 7, // 后 8, 9, 10, 8, 10, 11, // 上 12, 13, 14, 12, 14, 15, // 下 16, 17, 18, 16, 18, 19, // 右 20, 21, 22, 20, 22, 23 // 左 };这段代码里顶点顺序和索引顺序必须严格对应否则会出现面片撕裂。参数说明position用float3表示范围是 -1 到 1这样立方体边长为 2normal必须归一化否则光照计算会出错。索引用uint16_t就够了因为顶点数远小于 65535。构建VertexBuffer时需要指定AttributeType::POSITION和AttributeType::NORMAL并设置attributeCount为 2。2.3 把几何体挂到 Entity 上并配置渲染管线有了顶点和索引下一步是创建VertexBuffer和IndexBuffer对象然后通过RenderableManager绑定到实体。// 创建 VertexBuffer filament::VertexBuffer* vb filament::VertexBuffer::Builder() .vertexCount(24) .bufferCount(1) .attribute(filament::VertexAttribute::POSITION, 0, filament::VertexBuffer::AttributeType::FLOAT3, offsetof(Vertex, position), sizeof(Vertex)) .attribute(filament::VertexAttribute::NORMAL, 0, filament::VertexBuffer::AttributeType::FLOAT3, offsetof(Vertex, normal), sizeof(Vertex)) .build(engine); vb-setBufferAt(engine, 0, filament::VertexBuffer::BufferDescriptor( CUBE_VERTICES, sizeof(CUBE_VERTICES), nullptr)); // 创建 IndexBuffer filament::IndexBuffer* ib filament::IndexBuffer::Builder() .indexCount(36) .bufferType(filament::IndexBuffer::IndexType::USHORT) .build(engine); ib-setBuffer(engine, filament::IndexBuffer::BufferDescriptor( CUBE_INDICES, sizeof(CUBE_INDICES), nullptr)); // 创建 Entity 并绑定 Renderable filament::Entity cube entities::create(engine); filament::RenderableManager::Builder(1) .boundingBox({{-1, -1, -1}, {1, 1, 1}}) .material(0, material-getDefaultInstance()) .geometry(0, filament::RenderableManager::PrimitiveType::TRIANGLES, vb, ib) .culling(false) .receiveShadows(true) .castShadows(true) .build(engine, cube);这里的关键参数boundingBox必须准确否则视锥剔除会把立方体裁掉material需要提前加载好可以用 Filament 自带的defaultMaterialculling(false)在调试阶段建议关掉避免因为绕序问题看不到面。receiveShadows和castShadows按需开启如果场景里没有光源开了也没用。3. 材质、光照与相机让立方体真正“看得见”3.1 用 .mat 文件定义 PBR 材质并编译成 .filamatFilament 的材质系统是它的灵魂也是最容易让人懵的地方。你不能直接在 C 里写 shader而是要先写.mat文件然后用matc工具编译成.filamat二进制。下面是一个最简的材质定义只包含基础颜色和光照响应。material { name : cubeMaterial, shadingModel : lit, parameters : [ { type : float4, name : baseColor } ] } fragment { void material(inout MaterialInputs material) { prepareMaterial(material); material.baseColor materialParams.baseColor; } }shadingModel : lit表示这个材质会参与 PBR 光照计算baseColor是外部传入的参数可以在运行时修改。编译命令是matc -o cube.filamat cube.mat注意matc的路径通常在 Filament 的bin目录下。编译成功后用Material::Builder加载.filamat文件再通过getDefaultInstance()获取实例。如果编译报错多半是语法问题比如漏了分号或者参数类型写错。3.2 添加平行光、点光源和间接光没有光源立方体就是一团黑。Filament 支持多种光源最常用的是平行光模拟太阳和点光源。下面代码创建一个平行光和一个点光源。// 平行光 filament::Entity sun entities::create(engine); filament::LightManager::Builder(filament::LightManager::Type::DIRECTIONAL) .color({1.0, 0.98, 0.95}) .intensity(100000.0f) .direction({-0.5, -1.0, -0.3}) .castShadows(true) .build(engine, sun); // 点光源 filament::Entity pointLight entities::create(engine); filament::LightManager::Builder(filament::LightManager::Type::POINT) .color({0.2, 0.5, 1.0}) .intensity(50000.0f) .position({2.0, 2.0, 2.0}) .falloff(10.0f) .build(engine, pointLight);参数说明intensity单位是 lux平行光或 lumen点光源数值给太小会看不见给太大直接过曝。我一般先用 100000 试平行光点光源用 50000 左右。direction需要归一化falloff控制点光源的衰减半径。另外别忘了设置IndirectLight否则背光面会死黑。可以用 Filament 自带的defaultIbl或者自己生成一张环境贴图。3.3 相机设置与视图投影矩阵相机决定了你从哪个角度看立方体。Filament 的相机需要设置曝光、投影方式和位置。filament::Camera* camera engine-createCamera(filament::Entity()); camera-setExposure(16.0f, 1.0f / 125.0f, 100.0f); camera-setProjection(filament::Camera::Projection::PERSPECTIVE, 45.0f, 16.0f / 9.0f, 0.1f, 100.0f); camera-lookAt({3.0, 3.0, 3.0}, {0.0, 0.0, 0.0}, {0.0, 1.0, 0.0});setExposure的三个参数分别是光圈、快门速度和 ISO这里用 16、1/125、100 是室外晴天的典型值。lookAt的 eye 位置设在 (3,3,3)中心在原点这样立方体刚好在视野中央。如果画面太暗先调曝光如果立方体变形检查宽高比是否和视口一致。4. 避坑指南Filament 画立方体时最容易翻车的五个地方4.1 现象屏幕全黑日志无报错原因最常见的是没有设置View的setViewport和setCamera或者Renderer没有调用render。另一个可能是材质没有正确加载Material::Builder返回了空指针。解决检查View::Builder是否绑定了相机和场景确认renderer-render(view)在每一帧都被调用。用material-getDefaultInstance()前先判断material是否为空。4.2 现象立方体只有一半或者面片闪烁原因索引缓冲区里的三角形绕序winding order反了导致背面剔除把正面剔掉了。Filament 默认逆时针为正面。解决检查CUBE_INDICES里的顶点顺序确保每个三角形从外面看是逆时针。临时方案是关掉culling但正式项目必须修正绕序。4.3 现象光照计算异常面与面之间亮度突变原因法线没有归一化或者顶点法线方向反了。比如左面的法线写成了{1,0,0}。解决在顶点数据里逐面检查法线方向确保指向立方体外部。可以用normalize()在 shader 里再处理一次但最好在 CPU 端就保证正确。4.4 现象立方体边缘有锯齿或者纹理拉伸原因没有开启抗锯齿MSAA或者 UV 坐标缺失导致默认采样出错。解决在Engine::Builder里设置msaaSampleCount(4)。如果不需要纹理可以在材质里忽略 UV但顶点属性里最好保留避免驱动兼容性问题。4.5 现象旋转立方体时背面突然消失原因boundingBox设置得太小视锥剔除误判。或者相机的near和far平面范围不合理。解决把boundingBox稍微放大一点比如{{-1.1,-1.1,-1.1},{1.1,1.1,1.1}}。同时检查相机的near不要小于 0.01far不要超过 1000。5. 进阶技巧用 TransformManager 做旋转动画与性能验证5.1 用 TransformManager 驱动立方体自转静态立方体画出来只是第一步让它转起来才能验证渲染循环是否正常。Filament 的TransformManager可以给实体添加变换组件然后在每帧更新旋转矩阵。filament::TransformManager tcm engine-getTransformManager(); tcm.create(cube); filament::math::mat4f transform filament::math::mat4f::rotation( filament::math::radians(angle), filament::math::float3{0, 1, 0}); tcm.setTransform(tcm.getInstance(cube), transform);angle每帧递增比如angle 0.5f。注意setTransform会覆盖之前的变换所以如果你还想平移需要先乘平移矩阵。这个技巧我常用在调试阶段转起来就能一眼看出法线对不对、光照有没有跟着动。5.2 用 FrameGraph 和 GPU 计时器验证渲染开销Filament 的FrameGraph是个黑匣子但你可以通过Renderer::getFrameInfo拿到一些统计信息。更直接的办法是用Engine::getTimerQuery或者平台原生的 GPU 计时器。下面是一个简单的帧时间打印。filament::Renderer::FrameInfo info renderer-getFrameInfo(); utils::slog.i Frame time: info.frameTime ms utils::io::endl;如果帧时间超过 16ms说明有性能问题。常见原因是材质太复杂、光源太多或者分辨率太高。对于单个立方体正常应该在 1ms 以内。如果发现异常先检查是不是开了 MSAA 8x 或者阴影贴图分辨率过大。5.3 一个我坚持了三年的调试习惯每次新建一个 Filament 项目我都会先画一个立方体然后依次检查五件事顶点法线是否朝外、索引绕序是否逆时针、材质是否编译成功、相机曝光是否合理、TransformManager 是否更新。这五步走完基本能排除 90% 的“黑屏”问题。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →