
简介这份资源面向Android平台3D图形开发初学者与进阶者围绕开源实时渲染引擎Filament完整演示如何创建并显示一个基础立方体。内容涵盖Filament核心组件渲染器、场景、相机、光照的协作方式立方体几何体的顶点、法线与纹理坐标定义Material Instance与Material Blueprint的材质配置透视相机视角设置以及点光源、聚光灯、环境光等光照应用并延伸至Android项目构建流程与Gradle依赖管理。压缩包共655个文件约100.05MB以xml资源、flat与filamat材质文件、json配置、so动态库、dex与jar依赖、webp贴图、java源码及gradle构建脚本为主完整保留可编译的工程结构。已有175人学习下载适合希望理解实时渲染管线、掌握Filament工程搭建与排错思路的开发者参考实践。1. Filament 绘制立方体从零跑通第一个 3D 场景到底要几步很多人第一次接触 Filament是被它“移动端也能跑 PBR”这句话吸引进来的结果打开官方文档一看引擎初始化、渲染器、场景、视图、相机、材质、顶点缓冲、索引缓冲一大堆概念砸过来反而不知道从哪下手。其实最有效的入门方式不是读文档而是直接用 Filament 画一个立方体——这一个动作就能把渲染管线里最核心的链路全部串起来引擎怎么起、场景怎么挂、相机怎么摆、网格数据怎么传、材质怎么编译、每帧怎么驱动。立方体是最小的完整样本六个面、八个顶点、十二个三角形不多不少刚好够验证整条链路是否通畅。这篇文章面向的是想用 Filament 做 Android 或桌面端 3D 渲染的开发者不管你是刚接触图形 API 的新手还是从 OpenGL ES 转过来的老手下面这套流程和参数都能直接抄。我踩过的坑、调过的参数、翻过的车都会在对应位置写清楚。2. Filament 渲染立方体的最小工程骨架2.1 为什么选 Filament 而不是直接写 OpenGL ESFilament 是 Google 开源的实时物理渲染引擎核心价值在于它把 PBR 材质、基于物理的光照、色调映射、抗锯齿这些在 OpenGL ES 里要写几千行才能搞定的东西封装成了声明式的接口。你只需要告诉它“这里有个立方体材质是金属度 0.8 粗糙度 0.3”它就能在移动端 GPU 上跑出接近离线渲染的效果。对于立方体这种基础几何体用 Filament 的收益不是“画得快”而是“画完之后换任何模型、任何材质、任何光照条件代码结构都不用大改”。这是选它的核心理由一次搭好管线后面只换数据。Filament 的 API 分两层底层是Engine、Renderer、SwapChain这些直接对应 GPU 资源的对象上层是Scene、View、Camera、MaterialInstance这些场景级抽象。画立方体需要两层都用上——底层负责把顶点数据传到 GPU上层负责告诉渲染器“用哪个相机、看哪个场景、光照怎么算”。2.2 环境准备与依赖引入Android 端最省事的做法是通过 Gradle 引入 Filament 的预编译包。在app/build.gradle里加dependencies { implementation com.google.android.filament:filament-android:1.51.0 implementation com.google.android.filament:filament-utils-android:1.51.0 implementation com.google.android.filament:gltfio-android:1.51.0 }filament-android是核心运行时filament-utils-android提供ModelViewer等辅助类gltfio-android用于加载 glTF 模型——画立方体其实用不到 gltfio但后面换模型时一定会用到建议一起加上。桌面端Linux/macOS/Windows则从 GitHub Releases 下载对应平台的预编译库CMake 里find_package(filament REQUIRED)即可。注意Filament 对 NDK 版本有要求Android 端建议 NDK r25 以上否则libfilament-jni.so可能加载失败。2.3 初始化 Engine、Renderer 和 SwapChainFilament 的启动顺序是固定的先建Engine再建Renderer然后创建SwapChain绑定到 Android 的SurfaceView。这三步的顺序不能乱因为Renderer依赖Engine的后端选择SwapChain又依赖Renderer的上下文。// 1. 创建 Engine指定后端为 OpenGL ES移动端默认 Engine engine Engine.create(); // 2. 创建 Renderer负责实际的绘制命令提交 Renderer renderer engine.createRenderer(); // 3. 创建 SwapChain绑定到 SurfaceView 的 Surface // 注意Surface 必须已经 valid否则 createSwapChain 返回 null SwapChain swapChain engine.createSwapChain(surface); // 4. 创建 Scene 和 View Scene scene engine.createScene(); View view engine.createView(); view.setScene(scene); // 5. 设置视口和相机 view.setViewport(new Viewport(0, 0, width, height)); Camera camera engine.createCamera(EntityManager.get().create()); view.setCamera(camera);Engine.create()默认会尝试 Vulkan失败后回退到 OpenGL ES。移动端如果想强制用 OpenGL ES可以传Engine.Backend.OPENGL。SwapChain的创建时机很关键——必须在SurfaceHolder.surfaceCreated回调之后否则surface无效createSwapChain会直接返回 null后面所有绘制都不会显示。这是新手最容易翻车的地方代码不报错但屏幕全黑。2.4 构建立方体的顶点与索引数据Filament 的VertexBuffer需要你明确指定每个顶点的属性布局。画立方体最少需要位置和法线两个属性如果要做 PBR 材质还需要切线tangent来计算各向异性但入门阶段位置法线就够了。// 立方体 8 个顶点每个顶点 3 个 float 位置 3 个 float 法线 float[] vertices { // 前面 (z 0.5) -0.5f, -0.5f, 0.5f, 0, 0, 1, 0.5f, -0.5f, 0.5f, 0, 0, 1, 0.5f, 0.5f, 0.5f, 0, 0, 1, -0.5f, 0.5f, 0.5f, 0, 0, 1, // 后面 (z -0.5) -0.5f, -0.5f, -0.5f, 0, 0, -1, 0.5f, -0.5f, -0.5f, 0, 0, -1, 0.5f, 0.5f, -0.5f, 0, 0, -1, -0.5f, 0.5f, -0.5f, 0, 0, -1, // 上面 (y 0.5) -0.5f, 0.5f, 0.5f, 0, 1, 0, 0.5f, 0.5f, 0.5f, 0, 1, 0, 0.5f, 0.5f, -0.5f, 0, 1, 0, -0.5f, 0.5f, -0.5f, 0, 1, 0, // 下面 (y -0.5) -0.5f, -0.5f, 0.5f, 0, -1, 0, 0.5f, -0.5f, 0.5f, 0, -1, 0, 0.5f, -0.5f, -0.5f, 0, -1, 0, -0.5f, -0.5f, -0.5f, 0, -1, 0, // 左面 (x -0.5) -0.5f, -0.5f, -0.5f, -1, 0, 0, -0.5f, -0.5f, 0.5f, -1, 0, 0, -0.5f, 0.5f, 0.5f, -1, 0, 0, -0.5f, 0.5f, -0.5f, -1, 0, 0, // 右面 (x 0.5) 0.5f, -0.5f, -0.5f, 1, 0, 0, 0.5f, -0.5f, 0.5f, 1, 0, 0, 0.5f, 0.5f, 0.5f, 1, 0, 0, 0.5f, 0.5f, -0.5f, 1, 0, 0, }; // 索引每个面 2 个三角形共 36 个索引 short[] indices { 0, 1, 2, 0, 2, 3, // 前面 4, 6, 5, 4, 7, 6, // 后面 8, 9, 10, 8, 10, 11, // 上面 12, 14, 13, 12, 15, 14, // 下面 16, 17, 18, 16, 18, 19, // 左面 20, 21, 22, 20, 22, 23, // 右面 };这里用了 24 个顶点而不是 8 个原因是每个面的法线方向不同如果共用顶点法线会被插值成错误的方向光照就会“糊”在一起。这是画立方体最经典的坑用 8 个顶点省内存结果光照全乱。24 个顶点换来正确的面法线对立方体这种小网格来说完全值得。2.5 创建 VertexBuffer 和 IndexBuffer// 顶点缓冲声明属性布局 VertexBuffer.Builder vbBuilder new VertexBuffer.Builder() .vertexCount(24) .bufferCount(1) .attribute(VertexAttribute.POSITION, 0, VertexBuffer.AttributeType.FLOAT3, 0, 24) // 偏移 0步长 24 字节 .attribute(VertexAttribute.NORMAL, 0, VertexBuffer.AttributeType.FLOAT3, 12, 24); // 偏移 12步长 24 字节 VertexBuffer vertexBuffer vbBuilder.build(engine); vertexBuffer.setBufferAt(engine, 0, ByteBuffer.allocateDirect(vertices.length * 4) .order(ByteOrder.nativeOrder()) .asFloatBuffer() .put(vertices)); // 索引缓冲 IndexBuffer indexBuffer new IndexBuffer.Builder() .indexCount(36) .bufferType(IndexBuffer.Builder.IndexType.USHORT) .build(engine); indexBuffer.setBuffer(engine, ByteBuffer.allocateDirect(indices.length * 2) .order(ByteOrder.nativeOrder()) .asShortBuffer() .put(indices));attribute方法的参数依次是属性类型、缓冲索引、数据类型、字节偏移、字节步长。步长 24 是因为每个顶点有 6 个 float位置 3 法线 3每个 float 4 字节。偏移 0 是位置偏移 12 是法线。这两个参数写错顶点数据就会错位表现为模型扭曲或全黑。2.6 编译材质并绑定到 RenderableFilament 的材质系统基于.mat文件用matc工具编译成.filamat。入门阶段可以直接用 Filament 内置的defaultMaterial但为了理解流程建议自己写一个最简材质// simple.mat material { name : simple, shadingModel : unlit, parameters : [ { type : float4, name : color } ] } fragment { void material(inout MaterialInputs material) { prepareMaterial(material); material.baseColor materialParams.color; } }用matc -a opengl -p mobile simple.mat -o simple.filamat编译。然后在 Java 侧加载Material material Material.Builder() .payload(loadAsset(simple.filamat)) .build(engine); MaterialInstance mi material.createInstance(); mi.setParameter(color, MaterialInstance.FloatElement.FLOAT4, new float[]{0.8f, 0.2f, 0.2f, 1.0f}, 0, 4); // 创建 Renderable 实体 Entity entity EntityManager.get().create(); RenderableManager.Builder(1) .boundingBox(new Box(0, 0, 0, 1, 1, 1)) .geometry(0, RenderableManager.PrimitiveType.TRIANGLES, vertexBuffer, indexBuffer) .material(0, mi) .build(engine, entity); scene.addEntity(entity);shadingModel : unlit表示不参与光照计算直接输出颜色。如果要用 PBR改成lit并加上roughness、metallic参数。boundingBox用于视锥剔除写错会导致模型被错误裁剪。2.7 每帧驱动渲染循环// 在 Choreographer 或 RenderThread 中每帧调用 void onFrame() { // 更新相机变换这里用固定视角 camera.setExposure(16.0f, 1.0f / 125.0f, 100.0f); camera.lookAt( new double[]{3, 3, 3}, // 相机位置 new double[]{0, 0, 0}, // 看向原点 new double[]{0, 1, 0} // 上方向 ); // 投影矩阵45 度 FOV宽高比近裁面 0.1远裁面 100 camera.setProjection(45.0, aspectRatio, 0.1, 100.0, Camera.Fov.VERTICAL); // 开始帧 if (renderer.beginFrame(swapChain, frameTimeNanos)) { renderer.render(view); renderer.endFrame(); } }beginFrame返回 false 表示当前帧应该跳过比如 Surface 还没准备好这时候不能调用render否则会崩。frameTimeNanos用System.nanoTime()即可Filament 用它做动画插值和时间相关效果。3. 相机、光照与材质参数怎么调才不出黑屏3.1 相机参数FOV、近远裁面和曝光相机是立方体能不能出现在屏幕上的第一道关卡。Filament 的Camera.setProjection四个参数分别是视场角度、宽高比、近裁面距离、远裁面距离。新手最常犯的错误是近裁面设成 0——数学上会导致投影矩阵除零实际表现是模型完全消失或闪烁。近裁面最小建议 0.01常规场景 0.1 就够。曝光参数setExposure有三个值光圈、快门时间、ISO。默认值16.0f, 1.0f/125.0f, 100.0f对应室外日光。如果场景偏暗优先调 ISO 而不是光圈因为光圈会影响景深虽然 Filament 默认不开启景深。曝光不对的表现是模型在但全黑或全白这时候先检查曝光再检查光照。3.2 光照配置定向光、点光源和环境光用lit材质时没有光照就是全黑。最简配置是一盏定向光加环境光// 定向光方向从右上到左下颜色暖白强度 10000 lux LightManager.Builder(LightManager.Type.DIRECTIONAL) .color(1.0f, 0.95f, 0.9f) .intensity(10000.0f) .direction(-1, -1, -1) .castShadows(true) .build(engine, lightEntity); scene.addEntity(lightEntity); // 间接光用 IBL 或简单的环境光 IndirectLight.Builder() .intensity(30000.0f) .build(engine);定向光的intensity单位是 lux室内场景 3000-5000 就够室外 10000-100000。castShadows开启后需要额外配置阴影参数入门阶段可以先关掉避免阴影贴图分辨率不够导致锯齿。3.3 材质参数baseColor、roughness、metallic 的取值边界PBR 材质三个核心参数baseColor基础色float4、roughness粗糙度0-1、metallic金属度0-1。立方体做演示时建议先用roughness 0.5, metallic 0.0这种中间值能同时看到漫反射和高光。metallic 1.0时漫反射为零只有环境反射如果没配 IBL 就会看起来像黑塑料。参数推荐范围典型值调错的表现baseColor0-1 每通道(0.8, 0.2, 0.2, 1.0)超过 1 会过曝roughness0.05-1.00.50 像镜面1 像粉笔metallic0 或 10.00.5 会看起来像脏金属3.4 视口与分辨率setViewport 和 setViewportScaling 的区别view.setViewport设置的是渲染目标区域view.setViewportScaling设置的是缩放比例。如果只设了 viewport 没设 scalingFilament 会按 1:1 渲染在高 DPI 屏幕上会模糊。正确做法是view.setViewport(new Viewport(0, 0, width, height)); view.setViewportScaling(1.0f); // 或根据 DPI 动态计算width和height应该用 Surface 的实际像素尺寸不是 dp 尺寸。用 dp 会导致渲染分辨率不足立方体边缘出现锯齿。4. Filament 画立方体避坑排查黑屏、闪烁、光照错乱4.1 黑屏但日志无报错现象应用启动后屏幕全黑Logcat 没有异常beginFrame返回 true。原因最常见的是SwapChain创建时 Surface 还没 valid或者相机位置在立方体内部/背面。其次是材质编译失败但被静默忽略。解决在surfaceCreated回调里再创建 SwapChain把相机位置改到(0, 0, 5)看向原点用matc编译材质时加-v查看详细输出确认.filamat文件生成成功。4.2 立方体闪烁或部分面消失现象旋转视角时某些面突然消失或者整个立方体闪烁。原因索引缓冲的三角形绕序winding order不一致导致背面剔除把正面剔掉了。Filament 默认逆时针为正面如果某个面的索引顺序写反那个面就会消失。解决检查每个面的索引三元组确保从外部看是逆时针。前面(0,1,2)和(0,2,3)是逆时针后面(4,6,5)和(4,7,6)也是逆时针——注意后面的顶点顺序要反过来因为从后面看时左右颠倒。4.3 光照全黑或全白现象用lit材质时模型全黑或者过曝成白色。原因全黑通常是没加光源或光源方向不对全白是曝光过高或baseColor超过 1。解决先加一盏定向光intensity设 10000方向(-1,-1,-1)曝光用默认值baseColor确保每个通道在 0-1 之间。如果还是黑检查IndirectLight是否缺失——PBR 材质需要间接光来补环境反射。4.4 顶点数据错位导致模型扭曲现象立方体看起来像被“捏扁”或顶点乱飞。原因VertexBuffer.Builder的attribute方法里偏移和步长写错。比如位置偏移写成 12法线偏移写成 0数据就完全错位。解决记住公式偏移 前面所有属性的字节数之和步长 所有属性字节数之和。位置 3 float 12 字节法线 3 float 12 字节所以位置偏移 0法线偏移 12步长 24。4.5 在 Android 上切换后台再回来崩溃现象应用切到后台再切回来Filament 报SwapChain相关异常或直接闪退。原因Surface 被系统回收后旧的 SwapChain 失效但代码还在用旧的引用。解决在surfaceDestroyed回调里销毁 SwapChain 和 View在surfaceCreated里重建。Engine 和 Scene 可以保留但 SwapChain 必须跟着 Surface 生命周期走。5. 从立方体到任意模型用 gltfio 加载外部网格的进阶技巧画完立方体之后下一步自然是加载真实模型。Filament 提供了gltfio库来加载 glTF 2.0 格式这是目前 3D 模型分发最通用的格式。但直接从立方体跳到 glTF 加载中间有几个容易忽略的细节。第一个是坐标系差异。Filament 用右手坐标系Y 轴向上相机默认看向 -Z。而很多建模工具导出 glTF 时可能用 Z 轴向上加载后模型会“躺倒”。解决办法是在AssetLoader里配置MaterialProvider时指定变换或者直接在建模工具里导出前调整轴向。第二个是材质映射。glTF 里的 PBR 材质参数和 Filament 的MaterialInstance不是一一对应的gltfio会自动生成适配的材质但如果你自定义了.mat文件需要确保参数名匹配。常见做法是先用gltfio的默认材质跑通再逐步替换成自定义材质。第三个是性能。立方体只有 36 个索引随便怎么画都不卡。但真实模型动辄几万到几十万三角形这时候必须开RenderableManager的视锥剔除和 LOD。Filament 的View默认开启视锥剔除但需要你正确设置boundingBox——如果 boundingBox 比实际模型小模型会被错误剔除如果太大剔除效果打折。我自己的习惯是每接入一个新模型先用ModelViewer工具在桌面端打开看一眼确认模型本身没问题再写代码加载。这样能把“模型问题”和“代码问题”分开省掉大量排查时间。另外gltfio加载是异步的AssetLoader.createAsset返回后模型不一定立即可用需要等ResourceLoader的回调或轮询asset.isReady()。这个异步特性在 Android 上尤其要注意因为主线程不能阻塞等待。最后一个技巧是关于材质实例的复用。如果场景里有多个相同材质的立方体不要每个都createInstance而是共用一个MaterialInstance只改Transform。Filament 的材质实例创建有开销复用能显著降低 draw call 之外的 CPU 消耗。这个习惯从画立方体的时候就应该养成——哪怕只有一个立方体也按“材质和变换分离”的方式组织代码后面扩展成场景时不用重构。希望帮到你。本文还有配套的精品资源点击获取