本文主要是介绍【Filament】加载obj和fbx模型,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
1 前言
3D 模型的常用格式主要有 obj、fbx、gltf 等,Filament 中的 filamesh.exe 工具可以将 obj、fbx 格式转换为 filamesh 格式,然后再加载显示。对于 gltf 格式模型,可以通过 ModelViewer 加载显示,这不在本文的讨论范围内。
1)filamesh 简介
filamesh 工具的官方介绍如下。
filamesh is a tool to convert meshes into an optimized binary formatCaution! filamesh was designed to operate on trusted inputs. To minimize the risk of
triggering memory corruption vulnerabilities, please make sure that the files passed
to filamesh come from a trusted source, or run filamesh in a sandboxed environment.Usage:filamesh [options] <source mesh> <destination file>Supported mesh formats:FBX, OBJInput meshes must have texture coordinates.Options:--help, -hprint this message--licensePrint copyright and license information--interleaved, -iinterleaves mesh attributes--compress, -cenable compression--ignore-uv1, -gIgnore the second set of UV coordinates
注意:原始 obj、fbx 模型一定要包含纹理坐标,否在会转换失败。
以下是一个简单的 filamesh 工具的使用案例。
filamesh cube.obj cube.filamesh
2)obj 模型
obj 模型主要由 v(顶点坐标)、vt(纹理坐标)、vn(法线向量)、f(三角面或四角面的顶点索引序列)组成,如下是一个三角形的 obj 文件。
# 三角形模型# 顶点位置
v 1.0 1.0 -1.0 # V1
v 1.0 -1.0 -1.0 # V2
v 1.0 1.0 1.0 # V3# 纹理坐标
vt 0.0 0.0 # VT1
vt 1.0 0.0 # VT2
vt 1.0 1.0 # VT3
vt 0.0 1.0 # VT4# 法线
vn 0.0 1.0 0.0 # VN1# 面(v/vt/vn)
f 1/1/1 2/2/1 3/3/1
对于非设计类人员,也可以使用记事本按照以上格式编辑一些简单的模型,然后再拖拽到 Unity(或 Blender、Maya、3DMax 等)软件中进行预览。
3)推荐阅读
读者如果对 Filament 不太熟悉,请回顾以下内容。
- Filament环境搭建
- 绘制三角形
- 绘制矩形
- 绘制圆形
- 绘制立方体
- 纹理贴图
- 立方体贴图(6张图)
2 立方体贴图
本文项目结构如下,完整代码资源 → Filament加载obj和fbx模型。
2.1 基础类
为方便读者将注意力聚焦在 Filament 的输入上,轻松配置复杂的环境依赖逻辑,笔者仿照 OpenGL ES 的写法,抽出了 FLSurfaceView、BaseModel、Mesh、MaterialUtils 和 MeshUtils 类。FLSurfaceView 与 GLSurfaceView 的功能类似,承载了渲染环境配置;BaseModel 用于管理模型的网格和材质;Mesh 用于管理模型的顶点属性;MaterialUtils 和 MeshUtils 中分别提供了一些材质和网格相关的工具。
build.gradle
...
android {...aaptOptions { // 在应用程序打包过程中不压缩的文件noCompress 'filamat', 'ktx'}
}dependencies {implementation fileTree(dir: '../libs', include: ['*.aar'])...
}
说明:在项目根目录下的 libs 目录中,需要放入以下 aar 文件,它们源自Filament环境搭建中编译生成的 aar。
FLSurfaceView.java
package com.zhyan8.loadmodel.filament.base;import android.content.Context;
import android.graphics.Point;
import android.view.Choreographer;
import android.view.Surface;
import android.view.SurfaceView;import com.google.android.filament.Camera;
import com.google.android.filament.Engine;
import com.google.android.filament.EntityManager;
import com.google.android.filament.Filament;
import com.google.android.filament.Renderer;
import com.google.android.filament.Scene;
import com.google.android.filament.Skybox;
import com.google.android.filament.SwapChain;
import com.google.android.filament.View;
import com.google.android.filament.Viewport;
import com.google.android.filament.android.DisplayHelper;
import com.google.android.filament.android.FilamentHelper;
import com.google.android.filament.android.UiHelper;import java.util.ArrayList;/*** Filament中待渲染的SurfaceView* 功能可以类比OpenGL ES中的GLSurfaceView* 用于创建Filament的渲染环境*/
public class FLSurfaceView extends SurfaceView {public static int RENDERMODE_WHEN_DIRTY = 0; // 用户请求渲染才渲染一帧public static int RENDERMODE_CONTINUOUSLY = 1; // 持续渲染protected int mRenderMode = RENDERMODE_CONTINUOUSLY; // 渲染模式protected Choreographer mChoreographer; // 消息控制protected DisplayHelper mDisplayHelper; // 管理Display(可以监听分辨率或刷新率的变化)protected UiHelper mUiHelper; // 管理SurfaceView、TextureView、SurfaceHolderprotected Engine mEngine; // 引擎(跟踪用户创建的资源, 管理渲染线程和硬件渲染器)protected Renderer mRenderer; // 渲染器(用于操作系统窗口, 生成绘制命令, 管理帧延时)protected Scene mScene; // 场景(管理渲染对象、灯光)protected View mView; // 存储渲染数据(View是Renderer操作的对象)protected Camera mCamera; // 相机(视角管理)protected Point mDesiredSize; // 渲染分辨率protected float[] mSkyboxColor; // 背景颜色protected SwapChain mSwapChain; // 操作系统的本地可渲染表面(native renderable surface, 通常是一个window或view)protected FrameCallback mFrameCallback = new FrameCallback(); // 帧回调protected ArrayList<RenderCallback> mRenderCallbacks; // 每一帧渲染前的回调(一般用于处理模型变换、相机变换等)static {Filament.init();}public FLSurfaceView(Context context) {super(context);mChoreographer = Choreographer.getInstance();mDisplayHelper = new DisplayHelper(context);mRenderCallbacks = new ArrayList<>();}public void init() { // 初始化setupSurfaceView();setupFilament();setupView();setupScene();}public void setRenderMode(int renderMode) { // 设置渲染模式mRenderMode = renderMode;}public void addRenderCallback(RenderCallback renderCallback) { // 添加渲染回调if (renderCallback != null) {mRenderCallbacks.add(renderCallback);}}public void requestRender() { // 请求渲染mChoreographer.postFrameCallback(mFrameCallback);}public void onResume() { // 恢复mChoreographer.postFrameCallback(mFrameCallback);}public void onPause() { // 暂停mChoreographer.removeFrameCallback(mFrameCallback);}public void onDestroy() { // 销毁Filament环境mChoreographer.removeFrameCallback(mFrameCallback);mRenderCallbacks.clear();mUiHelper.detach();mEngine.destroyRenderer(mRenderer);mEngine.destroyView(mView);mEngine.destroyScene(mScene);mEngine.destroyCameraComponent(mCamera.getEntity());EntityManager entityManager = EntityManager.get();entityManager.destroy(mCamera.getEntity());mEngine.destroy();}protected void setupScene() { // 设置Scene参数}protected void onResized(int width, int height) { // Surface尺寸变化时回调double zoom = 1;double aspect = (double) width / (double) height;mCamera.setProjection(Camera.Projection.ORTHO,-aspect * zoom, aspect * zoom, -zoom, zoom, 0, 1000);}private void setupSurfaceView() { // 设置SurfaceViewmUiHelper = new UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK);mUiHelper.setRenderCallback(new SurfaceCallback());if (mDesiredSize != null) {mUiHelper.setDesiredSize(mDesiredSize.x, mDesiredSize.y);}mUiHelper.attachTo(this);}private void setupFilament() { // 设置Filament参数mEngine = Engine.create();// mEngine = (new Engine.Builder()).featureLevel(Engine.FeatureLevel.FEATURE_LEVEL_0).build();mRenderer = mEngine.createRenderer();mScene = mEngine.createScene();mView = mEngine.createView();mCamera = mEngine.createCamera(mEngine.getEntityManager().create());}private void setupView() { // 设置View参数float[] color = mSkyboxColor != null ? mSkyboxColor : new float[] {0, 0, 0, 1};Skybox skybox = (new Skybox.Builder()).color(color).build(mEngine);mScene.setSkybox(skybox);if (mEngine.getActiveFeatureLevel() == Engine.FeatureLevel.FEATURE_LEVEL_0) {mView.setPostProcessingEnabled(false); // FEATURE_LEVEL_0不支持post-processing}mView.setCamera(mCamera);mView.setScene(mScene);View.DynamicResolutionOptions options = new View.DynamicResolutionOptions();options.enabled = true;mView.setDynamicResolutionOptions(options);}/*** 帧回调*/private class FrameCallback implements Choreographer.FrameCallback {@Overridepublic void doFrame(long frameTimeNanos) { // 渲染每帧数据if (mRenderMode == RENDERMODE_CONTINUOUSLY) {mChoreographer.postFrameCallback(this); // 请求下一帧}mRenderCallbacks.forEach(callback -> callback.onCall());if (mUiHelper.isReadyToRender()) {if (mRenderer.beginFrame(mSwapChain, frameTimeNanos)) {mRenderer.render(mView);mRenderer.endFrame();}}}}/*** Surface回调*/private class SurfaceCallback implements UiHelper.RendererCallback {@Overridepublic void onNativeWindowChanged(Surface surface) { // Native窗口改变时回调if (mSwapChain != null) {mEngine.destroySwapChain(mSwapChain);}long flags = mUiHelper.getSwapChainFlags();if (mEngine.getActiveFeatureLevel() == Engine.FeatureLevel.FEATURE_LEVEL_0) {if (SwapChain.isSRGBSwapChainSupported(mEngine)) {flags = flags | SwapChain.CONFIG_SRGB_COLORSPACE;}}mSwapChain = mEngine.createSwapChain(surface, flags);mDisplayHelper.attach(mRenderer, getDisplay());}@Overridepublic void onDetachedFromSurface() { // 解绑Surface时回调mDisplayHelper.detach();if (mSwapChain != null) {mEngine.destroySwapChain(mSwapChain);mEngine.flushAndWait();mSwapChain = null;}}@Overridepublic void onResized(int width, int height) { // Surface尺寸变化时回调mView.setViewport(new Viewport(0, 0, width, height));FilamentHelper.synchronizePendingFrames(mEngine);FLSurfaceView.this.onResized(width, height);}}/*** 每一帧渲染前的回调* 一般用于处理模型变换、相机变换等*/public interface RenderCallback {void onCall();}
}
BaseModel.java
package com.zhyan8.loadmodel.filament.base;import android.content.Context;import com.google.android.filament.Engine;
import com.google.android.filament.EntityManager;
import com.google.android.filament.Material;
import com.google.android.filament.MaterialInstance;
import com.google.android.filament.RenderableManager;
import com.google.android.filament.RenderableManager.PrimitiveType;
import com.google.android.filament.Texture;
import com.google.android.filament.TransformManager;
import com.zhyan8.loadmodel.filament.utils.MaterialUtils;
import com.zhyan8.loadmodel.filament.base.Mesh.Part;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 模型基类* 管理模型的网格、材质、渲染id*/
public class BaseModel {private static String TAG = "BaseModel";protected Context mContext; // 上下文protected Engine mEngine; // Filament引擎protected TransformManager mTransformManager; // 模型变换管理器protected Mesh mMesh; // 模型网格protected Material[] mMaterials; // 模型材质protected MaterialInstance[] mMaterialInstances; // 模型材质实例protected Map<String, MaterialInstance> mMaterialMap = new HashMap<>(); // 材质名->材质protected Texture[] mTextures; // 纹理protected int mRenderable; // 渲染idprotected int mTransformComponent; // 模型变换组件的idprotected FLSurfaceView.RenderCallback mRenderCallback; // 每一帧渲染前的回调(一般用于处理模型变换、相机变换等)public BaseModel(Context context, Engine engine) {mContext = context;mEngine = engine;mTransformManager = mEngine.getTransformManager();}public int getRenderable() { // 获取渲染idreturn mRenderable;}public FLSurfaceView.RenderCallback getRenderCallback() { // 获取渲染回调return mRenderCallback;}public void destroy() { // 销毁模型mMaterialMap.clear();mEngine.destroyEntity(mRenderable);if (mMesh != null) {mMesh.destroy();}if (mTextures != null) {for (int i = 0; i < mTextures.length; i++) {mEngine.destroyTexture(mTextures[i]);}}if (mMaterialInstances != null) {for (int i = 0; i < mMaterialInstances.length; i++) {mEngine.destroyMaterialInstance(mMaterialInstances[i]);}}if (mMaterials != null) {for (int i = 0; i < mMaterials.length; i++) {mEngine.destroyMaterial(mMaterials[i]);}}EntityManager entityManager = EntityManager.get();entityManager.destroy(mRenderable);}protected int getRenderable(PrimitiveType primitiveType) { // 获取渲染idint renderable = EntityManager.get().create();List<Part> parts = mMesh.getParts();List<String> materialNames = mMesh.getMaterialNames();RenderableManager.Builder builder = new RenderableManager.Builder(parts.size()).boundingBox(mMesh.getBox());for (int i = 0; i < parts.size(); i++) {Part part = parts.get(i);builder.geometry(i, primitiveType, mMesh.getVertexBuffer(), mMesh.getIndexBuffer(),part.offset, part.minIndex, part.maxIndex, part.indexCount);MaterialInstance material = getMaterialInstance(materialNames, part.materialID);builder.material(i, material);}builder.build(mEngine, renderable);return renderable;}protected Material[] loadMaterials(String materialPath) { // 加载材质Material material = MaterialUtils.loadMaterial(mContext, mEngine, materialPath);if (material != null) {return new Material[] {material};}return null;}protected Material[] loadMaterials(String[] materialPaths) { // 加载材质Material[] materials = new Material[materialPaths.length];for (int i = 0; i < materials.length; i++) {materials[i] = MaterialUtils.loadMaterial(mContext, mEngine, materialPaths[i]);}return materials;}protected MaterialInstance[] getMaterialInstance(Material[] materials) { // 获取材质实例MaterialInstance[] materialInstances = new MaterialInstance[materials.length];for (int i = 0; i < materials.length; i++) {materialInstances[i] = materials[i].createInstance();}return materialInstances;}protected MaterialInstance[] getMaterialInstance(Material material, int count) { // 获取材质实例MaterialInstance[] materialInstances = new MaterialInstance[count];for (int i = 0; i < count; i++) {materialInstances[i] = material.createInstance();}return materialInstances;}private MaterialInstance getMaterialInstance(List<String> materialNames, int materialID) { // 获取材质MaterialInstance material = null;if (materialNames != null && materialNames.size() > materialID && materialID >= 0) {String name = materialNames.get(materialID);if (mMaterialMap.containsKey(name)) {material = mMaterialMap.get(name);}}if (material == null && mMaterialMap.containsKey("DefaultMaterial")) {material = mMaterialMap.get("DefaultMaterial");}return material;}
}
Mesh.java
package com.zhyan8.loadmodel.filament.base;import com.google.android.filament.Box;
import com.google.android.filament.Engine;
import com.google.android.filament.IndexBuffer;
import com.google.android.filament.VertexBuffer;
import com.google.android.filament.VertexBuffer.AttributeType;
import com.google.android.filament.VertexBuffer.VertexAttribute;import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;/*** 网格* 用于管理模型的顶点属性和顶点索引*/
public class Mesh {private Engine mEngine; // Filament引擎private VertexBuffer mVertexBuffer; // 顶点属性缓存private IndexBuffer mIndexBuffer; // 顶点索引缓存private List<Part> mParts; // 子网格信息private Box mBox; // 渲染区域private List<String> mMaterialNames; // 材质名public Mesh(Engine engine) {mEngine = engine;}public Mesh(Engine engine, float[] vertices, short[] indices, List<Part> parts, Box box, List<String> materialNames) {mEngine = engine;setVertices(vertices);setIndices(indices);setParts(parts, indices.length);setBox(box);mMaterialNames = materialNames;}public Mesh(Engine engine, VertexBuffer vrtexBuffer, IndexBuffer indexBuffer, List<Part> parts, Box box, List<String> materialNames) {mEngine = engine;mVertexBuffer = vrtexBuffer;mIndexBuffer = indexBuffer;mParts = parts;setBox(box);mMaterialNames = materialNames;}public Mesh(Engine engine, VertexPosCol[] vertices, short[] indices, List<Part> parts, Box box, List<String> materialNames) {mEngine = engine;setVertices(vertices);setIndices(indices);setParts(parts, indices.length);setBox(box);mMaterialNames = materialNames;}public Mesh(Engine engine, VertexPosUV[] vertices, short[] indices, List<Part> parts, Box box, List<String> materialNames) {mEngine = engine;setVertices(vertices);setIndices(indices);setParts(parts, indices.length);setBox(box);mMaterialNames = materialNames;}public void setVertices(float[] vertices) { // 设置顶点属性mVertexBuffer = getVertexBuffer(vertices);}public void setVertices(VertexPosCol[] vertices) { // 设置顶点属性mVertexBuffer = getVertexBuffer(vertices);}public void setVertices(VertexPosUV[] vertices) { // 设置顶点属性mVertexBuffer = getVertexBuffer(vertices);}public void setIndices(short[] indices) { // 设置顶点索引mIndexBuffer = getIndexBuffer(indices);}public void setParts(List<Part> parts, int count) { // 设置顶点索引if (parts == null || parts.size() == 0) {mParts = new ArrayList<>();mParts.add(new Part(0, count, 0, count - 1));} else {mParts = parts;}}public void setBox(Box box) { // 渲染区域if (box == null) {mBox = new Box(0, 0, 0, 1, 1, 1);} else {mBox = box;}}public VertexBuffer getVertexBuffer() { // 获取顶点属性缓存return mVertexBuffer;}public IndexBuffer getIndexBuffer() { // 获取顶点索引缓存return mIndexBuffer;}public List<Part> getParts() { // 获取顶点索引缓存return mParts;}public Box getBox() {return mBox;}public List<String> getMaterialNames() {return mMaterialNames;}public void destroy() {mEngine.destroyVertexBuffer(mVertexBuffer);mEngine.destroyIndexBuffer(mIndexBuffer);if (mParts != null) {mParts.clear();}if (mMaterialNames != null) {mMaterialNames.clear();}}private VertexBuffer getVertexBuffer(float[] values) { // 获取顶点属性缓存ByteBuffer vertexData = getByteBuffer(values);int vertexCount = values.length / 3;int vertexSize = Float.BYTES * 3;VertexBuffer vertexBuffer = new VertexBuffer.Builder().bufferCount(1).vertexCount(vertexCount).attribute(VertexAttribute.POSITION, 0, AttributeType.FLOAT3, 0, vertexSize).build(mEngine);vertexBuffer.setBufferAt(mEngine, 0, vertexData);return vertexBuffer;}private VertexBuffer getVertexBuffer(VertexPosCol[] values) { // 获取顶点属性缓存ByteBuffer vertexData = getByteBuffer(values);int vertexCount = values.length;int vertexSize = VertexPosCol.BYTES;VertexBuffer vertexBuffer = new VertexBuffer.Builder().bufferCount(1).vertexCount(vertexCount).attribute(VertexAttribute.POSITION, 0, AttributeType.FLOAT3, 0, vertexSize).attribute(VertexAttribute.COLOR, 0, AttributeType.UBYTE4, 3 * Float.BYTES, vertexSize).normalized(VertexAttribute.COLOR).build(mEngine);vertexBuffer.setBufferAt(mEngine, 0, vertexData);return vertexBuffer;}private VertexBuffer getVertexBuffer(VertexPosUV[] values) { // 获取顶点属性缓存ByteBuffer vertexData = getByteBuffer(values);int vertexCount = values.length;int vertexSize = VertexPosUV.BYTES;VertexBuffer vertexBuffer = new VertexBuffer.Builder().bufferCount(1).vertexCount(vertexCount).attribute(VertexAttribute.POSITION, 0, AttributeType.FLOAT3, 0, vertexSize).attribute(VertexAttribute.UV0, 0, AttributeType.FLOAT2, 3 * Float.BYTES, vertexSize).build(mEngine);vertexBuffer.setBufferAt(mEngine, 0, vertexData);return vertexBuffer;}private IndexBuffer getIndexBuffer(short[] values) { // 获取顶点索引缓存ByteBuffer indexData = getByteBuffer(values);int indexCount = values.length;IndexBuffer indexBuffer = new IndexBuffer.Builder().indexCount(indexCount).bufferType(IndexBuffer.Builder.IndexType.USHORT).build(mEngine);indexBuffer.setBuffer(mEngine, indexData);return indexBuffer;}private ByteBuffer getByteBuffer(float[] values) { // float数组转换为ByteBufferByteBuffer byteBuffer = ByteBuffer.allocate(values.length * Float.BYTES);byteBuffer.order(ByteOrder.nativeOrder());for (int i = 0; i < values.length; i++) {byteBuffer.putFloat(values[i]);}byteBuffer.flip();return byteBuffer;}private ByteBuffer getByteBuffer(short[] values) { // short数组转换为ByteBufferByteBuffer byteBuffer = ByteBuffer.allocate(values.length * Short.BYTES);byteBuffer.order(ByteOrder.nativeOrder());for (int i = 0; i < values.length; i++) {byteBuffer.putShort(values[i]);}byteBuffer.flip();return byteBuffer;}private ByteBuffer getByteBuffer(VertexPosCol[] values) { // VertexPosCol数组转换为ByteBufferByteBuffer byteBuffer = ByteBuffer.allocate(values.length * VertexPosCol.BYTES);byteBuffer.order(ByteOrder.nativeOrder());for (int i = 0; i < values.length; i++) {values[i].put(byteBuffer);}byteBuffer.flip();return byteBuffer;}private ByteBuffer getByteBuffer(VertexPosUV[] values) { // VertexPosUV数组转换为ByteBufferByteBuffer byteBuffer = ByteBuffer.allocate(values.length * VertexPosUV.BYTES);byteBuffer.order(ByteOrder.nativeOrder());for (int i = 0; i < values.length; i++) {values[i].put(byteBuffer);}byteBuffer.flip();return byteBuffer;}/*** 子网格信息*/public static class Part {public int offset = 0;public int indexCount = 0;public int minIndex = 0;public int maxIndex = 0;public int materialID = -1;public Box aabb = new Box();public Part() {}public Part(int offset, int indexCount, int minIndex, int maxIndex) {this.offset = offset;this.indexCount = indexCount;this.minIndex = minIndex;this.maxIndex = maxIndex;}public Part(int offset, int indexCount, int minIndex, int maxIndex, int materialID, Box aabb) {this.offset = offset;this.indexCount = indexCount;this.minIndex = minIndex;this.maxIndex = maxIndex;this.materialID = materialID;this.aabb = aabb;}}/*** 顶点数据(位置+颜色)* 包含顶点位置和颜色*/public static class VertexPosCol {public static int BYTES = 16;public float x;public float y;public float z;public int color;public VertexPosCol() {}public VertexPosCol(float x, float y, float z, int color) {this.x = x;this.y = y;this.z = z;this.color = color;}public ByteBuffer put(ByteBuffer buffer) { // VertexPosCol转换为ByteBufferbuffer.putFloat(x);buffer.putFloat(y);buffer.putFloat(z);buffer.putInt(color);return buffer;}}/*** 顶点数据(位置+纹理坐标)* 包含顶点位置和纹理坐标*/public static class VertexPosUV {public static int BYTES = 20;public float x;public float y;public float z;public float u;public float v;public VertexPosUV() {}public VertexPosUV(float x, float y, float z, float u, float v) {this.x = x;this.y = y;this.z = z;this.u = u;this.v = v;}public ByteBuffer put(ByteBuffer buffer) { // VertexPosUV转换为ByteBufferbuffer.putFloat(x);buffer.putFloat(y);buffer.putFloat(z);buffer.putFloat(u);buffer.putFloat(v);return buffer;}}
}
MaterialUtils.java
package com.zhyan8.loadmodel.filament.utils;import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;import com.google.android.filament.Engine;
import com.google.android.filament.Material;import java.io.FileInputStream;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;/*** 材质工具类*/
public class MaterialUtils {private static String TAG = "MaterialUtils";public static Material loadMaterial(Context context, Engine engine, String materialPath) { // 加载材质Buffer buffer = readUncompressedAsset(context, materialPath);if (buffer != null) {Material material = (new Material.Builder()).payload(buffer, buffer.remaining()).build(engine);material.compile(Material.CompilerPriorityQueue.HIGH,Material.UserVariantFilterBit.ALL,new Handler(Looper.getMainLooper()),() -> Log.i(TAG, "Material " + material.getName() + " compiled."));engine.flush();return material;}return null;}private static Buffer readUncompressedAsset(Context context, String assetPath) { // 加载资源ByteBuffer dist = null;try {AssetFileDescriptor fd = context.getAssets().openFd(assetPath);try(FileInputStream fis = fd.createInputStream()) {dist = ByteBuffer.allocate((int) fd.getLength());try (ReadableByteChannel src = Channels.newChannel(fis)) {src.read(dist);}}} catch (IOException e) {e.printStackTrace();}if (dist != null) {return dist.rewind();}return null;}
}
MeshUtils.java
package com.zhyan8.loadmodel.filament.utils;import android.content.Context;
import android.util.Log;import com.google.android.filament.Box;
import com.google.android.filament.Engine;
import com.google.android.filament.IndexBuffer;
import com.google.android.filament.VertexBuffer;
import com.google.android.filament.VertexBuffer.AttributeType;
import com.zhyan8.loadmodel.filament.base.Mesh;
import com.zhyan8.loadmodel.filament.base.Mesh.Part;import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;/*** 材质工具类*/
public class MeshUtils {private static final String FILAMESH_FILE_IDENTIFIER = "FILAMESH";private static final long HEADER_FLAG_SNORM16_UV = 0x2L;private static final long MAX_UINT32 = 4294967295L;public static Mesh loadMesh(Context context, Engine engine, String meshPath) {try (InputStream inputStream = context.getAssets().open(meshPath)) {Header header = readHeader(inputStream);ReadableByteChannel channel = Channels.newChannel(inputStream);ByteBuffer vertexBufferData = readSizedData(channel, header.verticesSizeInBytes);ByteBuffer indexBufferData = readSizedData(channel, header.indicesSizeInBytes);List<Part> parts = readParts(header, inputStream);List<String> materialNames = readMaterials(inputStream);VertexBuffer vertexBuffer = createVertexBuffer(engine, header, vertexBufferData);IndexBuffer indexBuffer = createIndexBuffer(engine, header, indexBufferData);return new Mesh(engine, vertexBuffer, indexBuffer, parts, header.aabb, materialNames);} catch (IOException e) {e.printStackTrace();}return null;}private static Header readHeader(InputStream input) { // 读取文件头信息Header header = new Header();if (!readMagicNumber(input)) {Log.e("Filament", "Invalid filamesh file.");return header;}header.versionNumber = readUIntLE(input);header.parts = readUIntLE(input);header.aabb = new Box(readFloat32LE(input), readFloat32LE(input), readFloat32LE(input),readFloat32LE(input), readFloat32LE(input), readFloat32LE(input));header.flags = readUIntLE(input);header.posOffset = readUIntLE(input);header.positionStride = readUIntLE(input);header.tangentOffset = readUIntLE(input);header.tangentStride = readUIntLE(input);header.colorOffset = readUIntLE(input);header.colorStride = readUIntLE(input);header.uv0Offset = readUIntLE(input);header.uv0Stride = readUIntLE(input);header.uv1Offset = readUIntLE(input);header.uv1Stride = readUIntLE(input);header.totalVertices = readUIntLE(input);header.verticesSizeInBytes = readUIntLE(input);header.indices16Bit = readUIntLE(input);header.totalIndices = readUIntLE(input);header.indicesSizeInBytes = readUIntLE(input);header.valid = true;return header;}private static ByteBuffer readSizedData(ReadableByteChannel channel, int sizeInBytes) { // 读取模型顶点数据ByteBuffer buffer = ByteBuffer.allocateDirect(sizeInBytes);buffer.order(ByteOrder.LITTLE_ENDIAN);try {channel.read(buffer);} catch (IOException e) {e.printStackTrace();}buffer.flip();return buffer;}private static List<Part> readParts(Header header, InputStream input) { // 读取子网格属性List<Part> parts = new ArrayList<>(header.parts);for (int i = 0; i < header.parts; i++) {Part p = new Part();p.offset = readUIntLE(input);p.indexCount = readUIntLE(input);p.minIndex = readUIntLE(input);p.maxIndex = readUIntLE(input);p.materialID = readUIntLE(input);float minX = readFloat32LE(input);float minY = readFloat32LE(input);float minZ = readFloat32LE(input);float maxX = readFloat32LE(input);float maxY = readFloat32LE(input);float maxZ = readFloat32LE(input);p.aabb = new Box(minX, minY, minZ, maxX, maxY, maxZ);parts.add(p);}return parts;}private static boolean readMagicNumber(InputStream input) { // 读取魔法数字, 用于判断是否是有效的filamesh文件byte[] temp = new byte[FILAMESH_FILE_IDENTIFIER.length()];int bytesRead = 0;try {bytesRead = input.read(temp);} catch (IOException e) {throw new RuntimeException(e);}if (bytesRead != FILAMESH_FILE_IDENTIFIER.length()) {return false;}String tempS = new String(temp, Charset.forName("UTF-8"));return tempS.equals(FILAMESH_FILE_IDENTIFIER);}private static List<String> readMaterials(InputStream input) { // 读取材质int numMaterials = readUIntLE(input);List<String> materials = new ArrayList<>(numMaterials);for (int i = 0; i < numMaterials; i++) {int dataLength = readUIntLE(input);byte[] data = new byte[dataLength];try {input.read(data);} catch (IOException e) {e.printStackTrace();}try {input.skip(1);} catch (IOException e) {e.printStackTrace();}materials.add(new String(data, Charset.forName("UTF-8")));}return materials;}private static IndexBuffer createIndexBuffer(Engine engine, Header header, ByteBuffer data) { // 创建顶点索引缓冲IndexBuffer.Builder.IndexType indexType = (header.indices16Bit != 0) ?IndexBuffer.Builder.IndexType.USHORT : IndexBuffer.Builder.IndexType.UINT;IndexBuffer buffer = new IndexBuffer.Builder().bufferType(indexType).indexCount(header.totalIndices).build(engine);buffer.setBuffer(engine, data);return buffer;}private static VertexBuffer createVertexBuffer(Engine engine, Header header, ByteBuffer data) { // 创建顶点属性缓冲AttributeType uvType = uvType(header);VertexBuffer.Builder vertexBufferBuilder = new VertexBuffer.Builder().bufferCount(1).vertexCount(header.totalVertices).normalized(VertexBuffer.VertexAttribute.COLOR).normalized(VertexBuffer.VertexAttribute.TANGENTS).attribute(VertexBuffer.VertexAttribute.POSITION, 0, VertexBuffer.AttributeType.HALF4, header.posOffset, header.positionStride).attribute(VertexBuffer.VertexAttribute.TANGENTS, 0, AttributeType.SHORT4, header.tangentOffset, header.tangentStride).attribute(VertexBuffer.VertexAttribute.COLOR, 0, AttributeType.UBYTE4, header.colorOffset, header.colorStride).attribute(VertexBuffer.VertexAttribute.UV0, 0, uvType, header.uv0Offset, header.uv0Stride);if (header.uv1Offset != MAX_UINT32 && header.uv1Stride != MAX_UINT32) {vertexBufferBuilder.attribute(VertexBuffer.VertexAttribute.UV1, 0, uvType, header.uv1Offset, header.uv1Stride).normalized(VertexBuffer.VertexAttribute.UV1, true);}VertexBuffer buffer = vertexBufferBuilder.build(engine);buffer.setBufferAt(engine, 0, data);return buffer;}private static AttributeType uvType(Header header) { // UV坐标的精度类型if ((header.flags & HEADER_FLAG_SNORM16_UV) != 0L) {return AttributeType.SHORT2;}return AttributeType.HALF2;}private static int readIntLE(InputStream input) { // 获取输入流中Little Endian格式的整数try {return (input.read() & 0xff) |((input.read() & 0xff) << 8) |((input.read() & 0xff) << 16) |((input.read() & 0xff) << 24);} catch (IOException e) {e.printStackTrace();}return 0;}private static int readUIntLE(InputStream input) { // 获取输入流中Little Endian格式的无符号整数return (int) (readIntLE(input) & 0xFFFFFFFFL);}private static float readFloat32LE(InputStream input) { // 获取输入流中Little Endian格式的浮点数byte[] bytes = new byte[4];try {input.read(bytes, 0, 4);} catch (IOException e) {e.printStackTrace();}return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getFloat();}
}/*** 网格文件头*/
class Header {boolean valid = false;int versionNumber = 0;int parts = 0;Box aabb = new Box();int flags = 0;int posOffset = 0;int positionStride = 0;int tangentOffset = 0;int tangentStride = 0;int colorOffset = 0;int colorStride = 0;int uv0Offset = 0;int uv0Stride = 0;int uv1Offset = 0;int uv1Stride = 0;int totalVertices = 0;int verticesSizeInBytes = 0;int indices16Bit = 0;int totalIndices = 0;int indicesSizeInBytes = 0;
}
2.2 业务类
MainActivity.java
package com.zhyan8.loadmodel;import android.os.Bundle;import androidx.appcompat.app.AppCompatActivity;import com.zhyan8.loadmodel.filament.base.FLSurfaceView;public class MainActivity extends AppCompatActivity {private FLSurfaceView mFLSurfaceView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mFLSurfaceView = new MyFLSurfaceView(this);setContentView(mFLSurfaceView);mFLSurfaceView.init();mFLSurfaceView.setRenderMode(FLSurfaceView.RENDERMODE_CONTINUOUSLY);}@Overridepublic void onResume() {super.onResume();mFLSurfaceView.onResume();}@Overridepublic void onPause() {super.onPause();mFLSurfaceView.onPause();}@Overridepublic void onDestroy() {super.onDestroy();mFLSurfaceView.onDestroy();}
}
MyFLSurfaceView.java
package com.zhyan8.loadmodel;import android.content.Context;import com.google.android.filament.Camera;
import com.zhyan8.loadmodel.filament.base.BaseModel;
import com.zhyan8.loadmodel.filament.base.FLSurfaceView;public class MyFLSurfaceView extends FLSurfaceView {private BaseModel mMyModel;public MyFLSurfaceView(Context context) {super(context);}public void init() {mSkyboxColor = new float[] {0.35f, 0.35f, 0.35f, 1};super.init();}@Overridepublic void onDestroy() {mMyModel.destroy();super.onDestroy();}@Overrideprotected void setupScene() { // 设置Scene参数mMyModel = new MyModel(getContext(), mEngine);mScene.addEntity(mMyModel.getRenderable());addRenderCallback(mMyModel.getRenderCallback());}@Overrideprotected void onResized(int width, int height) {double aspect = (double) width / (double) height;mCamera.setProjection(45.0, aspect, 0.1, 1000.0, Camera.Fov.VERTICAL);float[] eye = new float[] {1.5f, 1f, 7.5f}; // cube//float[] eye = new float[] {1.5f, 1f, 500f}; // spider_bot//float[] eye = new float[] {1.5f, 1f, 10f}; // shader_ballfloat[] center = new float[] {0, 0, 0};float[] up = new float[] {0, 1, 0};mCamera.lookAt(eye[0], eye[1], eye[2],center[0], center[1], center[2], up[0], up[1], up[2]);}
}
MyModel.java
package com.zhyan8.loadmodel;import android.content.Context;
import android.opengl.Matrix;import com.google.android.filament.Engine;
import com.google.android.filament.MaterialInstance;
import com.google.android.filament.RenderableManager.PrimitiveType;
import com.zhyan8.loadmodel.filament.base.BaseModel;
import com.zhyan8.loadmodel.filament.utils.MeshUtils;public class MyModel extends BaseModel {private String mMaterialPath = "materials/normal_light.filamat";private String mMeshPath = "models/cube.filamesh";//private String mMeshPath = "models/spider_bot.filamesh";//private String mMeshPath = "models/shader_ball.filamesh";private float[] mModelMatrix; // 模型变换矩阵private float[] mRotateAxis; // 旋转轴private float mRotateAgree = 0; // 旋转角度public MyModel(Context context, Engine engine) {super(context, engine);init();}private void init() {mMaterials = loadMaterials(mMaterialPath);mMaterialInstances = getMaterialInstance(mMaterials);mMaterialMap.put("DefaultMaterial", mMaterialInstances[0]);mMesh = MeshUtils.loadMesh(mContext, mEngine, mMeshPath);mRenderable = getRenderable(PrimitiveType.TRIANGLES);mTransformComponent = mTransformManager.getInstance(mRenderable);mRenderCallback = () -> renderCallback();mModelMatrix = new float[16];mRotateAxis = new float[] { 0.5f, 1f, 1f };}private void renderCallback() {mRotateAgree = (mRotateAgree + 1) % 360;mRotateAxis[0] = mRotateAgree / 180f - 1;mRotateAxis[1] = (float) Math.sin(mRotateAgree / 180f * Math.PI * 0.7f);mRotateAxis[2] = (float) Math.cos(mRotateAgree / 180f * Math.PI * 0.5f);Matrix.setRotateM(mModelMatrix, 0, mRotateAgree, mRotateAxis[0], mRotateAxis[1], mRotateAxis[2]);mTransformManager.setTransform(mTransformComponent, mModelMatrix);}
}
normal_light.mat
material {name : custom_light,shadingModel : unlit, // 禁用所有lighting// 顶点着色器入参MaterialVertexInputs中需要的顶点属性requires : [tangents]
}fragment {void material(inout MaterialInputs material) {prepareMaterial(material); // 在方法返回前必须回调该函数vec3 normal = normalize(getWorldNormalVector()); // 法线向量if (normal.x < 0.0) {normal.x = -normal.x / 2.0;}if (normal.y < 0.0) {normal.y = -normal.y / 2.0;}if (normal.z < 0.0) {normal.z = -normal.z / 2.0;}material.baseColor = vec4(normal, 1.0);}
}
说明: normal_light 材质使用模型的法线信息给模型表面着色。
cube.obj
# 正方体模型# 顶点位置
v 1.0 1.0 -1.0 # V1
v 1.0 -1.0 -1.0 # V2
v 1.0 1.0 1.0 # V3
v 1.0 -1.0 1.0 # V4
v -1.0 1.0 -1.0 # V5
v -1.0 -1.0 -1.0 # V6
v -1.0 1.0 1.0 # V7
v -1.0 -1.0 1.0 # V8# 纹理坐标
vt 0.0 0.0 # VT1
vt 1.0 0.0 # VT2
vt 1.0 1.0 # VT3
vt 0.0 1.0 # VT4# 法线
vn 0.0 1.0 0.0 # VN1 (上面法线)
vn 0.0 0.0 1.0 # VN2 (背面法线)
vn -1.0 0.0 0.0 # VN3 (左面法线)
vn 0.0 -1.0 0.0 # VN4 (下面法线)
vn 1.0 0.0 0.0 # VN5 (右面法线)
vn 0.0 0.0 -1.0 # VN6 (前面法线)# 面(v/vt/vn)
f 1/1/1 5/2/1 7/3/1
f 1/1/1 7/3/1 3/4/1
f 4/1/2 3/2/2 7/3/2
f 4/1/2 7/3/2 8/4/2
f 8/1/3 7/2/3 5/3/3
f 8/1/3 5/3/3 6/4/3
f 6/1/4 2/2/4 4/3/4
f 6/1/4 4/3/4 8/4/4
f 2/1/5 1/2/5 3/3/5
f 2/1/5 3/3/5 4/4/5
f 6/1/6 5/2/6 1/3/6
f 6/1/6 1/3/6 2/4/6
transform.bat
@echo off
setlocal enabledelayedexpansionecho transform materials
set "srcMatDir=../src/main/raw/materials"
set "distMatDir=../src/main/assets/materials"for %%f in ("%srcMatDir%\*.mat") do (set "matfile=%%~nf"matc -p mobile -a opengl -o "!matfile!.filamat" "%%f"move "!matfile!.filamat" "%distMatDir%\!matfile!.filamat"
)echo transform mesh
set "srcMeshDir=../src/main/raw/models"
set "distMeshDir=../src/main/assets/models"for %%f in ("%srcMeshDir%\*.obj" "%srcMeshDir%\*.fbx") do (set "meshfile=%%~nf"filamesh "%%f" "!meshfile!.filamesh"move "!meshfile!.filamesh" "%distMeshDir%\!meshfile!.filamesh"
)echo Processing complete.
pause
说明:需要将 matc.exe 文件、filamesh.exe 文件与 transform.bat 文件放在同一个目录下面,matc.exe 和 filamesh.exe 源自Filament环境搭建中编译生成的 exe 文件。双击 transform.bat 文件,会自动将 /src/main/raw/materials 下面的所有 mat 文件全部转换为 filamat 文件,并移到 /src/main/assets/materials/ 目录下面,同时自动将 /src/main/raw/models下面的所有 obj 或 fbx 文件全部转换为 filamesh 文件,并移到 /src/main/assets/models/ 目录下面。
加载 cube 模型运行效果如下。
加载 spider_bot 模型运行效果如下。
加载 shader_ball 模型运行效果如下。
这篇关于【Filament】加载obj和fbx模型的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!