【Filament】加载obj和fbx模型

2024-01-05 08:20
文章标签 加载 模型 fbx obj filament

本文主要是介绍【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模型的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/572250

相关文章

C#集成DeepSeek模型实现AI私有化的流程步骤(本地部署与API调用教程)

《C#集成DeepSeek模型实现AI私有化的流程步骤(本地部署与API调用教程)》本文主要介绍了C#集成DeepSeek模型实现AI私有化的方法,包括搭建基础环境,如安装Ollama和下载DeepS... 目录前言搭建基础环境1、安装 Ollama2、下载 DeepSeek R1 模型客户端 ChatBo

SpringBoot快速接入OpenAI大模型的方法(JDK8)

《SpringBoot快速接入OpenAI大模型的方法(JDK8)》本文介绍了如何使用AI4J快速接入OpenAI大模型,并展示了如何实现流式与非流式的输出,以及对函数调用的使用,AI4J支持JDK8... 目录使用AI4J快速接入OpenAI大模型介绍AI4J-github快速使用创建SpringBoot

spring-boot-starter-thymeleaf加载外部html文件方式

《spring-boot-starter-thymeleaf加载外部html文件方式》本文介绍了在SpringMVC中使用Thymeleaf模板引擎加载外部HTML文件的方法,以及在SpringBoo... 目录1.Thymeleaf介绍2.springboot使用thymeleaf2.1.引入spring

0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型的操作流程

《0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeekR1模型的操作流程》DeepSeekR1模型凭借其强大的自然语言处理能力,在未来具有广阔的应用前景,有望在多个领域发... 目录0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型,3步搞定一个应

Deepseek R1模型本地化部署+API接口调用详细教程(释放AI生产力)

《DeepseekR1模型本地化部署+API接口调用详细教程(释放AI生产力)》本文介绍了本地部署DeepSeekR1模型和通过API调用将其集成到VSCode中的过程,作者详细步骤展示了如何下载和... 目录前言一、deepseek R1模型与chatGPT o1系列模型对比二、本地部署步骤1.安装oll

关于Spring @Bean 相同加载顺序不同结果不同的问题记录

《关于Spring@Bean相同加载顺序不同结果不同的问题记录》本文主要探讨了在Spring5.1.3.RELEASE版本下,当有两个全注解类定义相同类型的Bean时,由于加载顺序不同,最终生成的... 目录问题说明测试输出1测试输出2@Bean注解的BeanDefiChina编程nition加入时机总结问题说明

Spring AI Alibaba接入大模型时的依赖问题小结

《SpringAIAlibaba接入大模型时的依赖问题小结》文章介绍了如何在pom.xml文件中配置SpringAIAlibaba依赖,并提供了一个示例pom.xml文件,同时,建议将Maven仓... 目录(一)pom.XML文件:(二)application.yml配置文件(一)pom.xml文件:首

如何在本地部署 DeepSeek Janus Pro 文生图大模型

《如何在本地部署DeepSeekJanusPro文生图大模型》DeepSeekJanusPro模型在本地成功部署,支持图片理解和文生图功能,通过Gradio界面进行交互,展示了其强大的多模态处... 目录什么是 Janus Pro1. 安装 conda2. 创建 python 虚拟环境3. 克隆 janus

本地私有化部署DeepSeek模型的详细教程

《本地私有化部署DeepSeek模型的详细教程》DeepSeek模型是一种强大的语言模型,本地私有化部署可以让用户在自己的环境中安全、高效地使用该模型,避免数据传输到外部带来的安全风险,同时也能根据自... 目录一、引言二、环境准备(一)硬件要求(二)软件要求(三)创建虚拟环境三、安装依赖库四、获取 Dee

DeepSeek模型本地部署的详细教程

《DeepSeek模型本地部署的详细教程》DeepSeek作为一款开源且性能强大的大语言模型,提供了灵活的本地部署方案,让用户能够在本地环境中高效运行模型,同时保护数据隐私,在本地成功部署DeepSe... 目录一、环境准备(一)硬件需求(二)软件依赖二、安装Ollama三、下载并部署DeepSeek模型选