1 前言
自定义Blinn Phong光照模型中实现了基础的自定义光照,与现实的光照还是有些差别,本文将实现更逼真的光照效果,即基于物理的光照(PBR)。
读者如果对 Filament 不太熟悉,请回顾以下内容。
- Filament环境搭建
- 绘制三角形
- 绘制矩形
- 绘制圆形
- 绘制立方体
- 纹理贴图
- 立方体贴图(6张图)
- 加载obj和fbx模型
- 自定义Blinn Phong光照模型
2 基于物理的光照
本文项目结构如下,完整代码资源 → Filament基于物理的光照。
2.1 基础类
为方便读者将注意力聚焦在 Filament 的输入上,轻松配置复杂的环境依赖逻辑,笔者仿照 OpenGL ES 的写法,抽出了 FLSurfaceView、BaseModel、Mesh、MaterialUtils、MeshUtils、TextureUtils、IBLUtils 类。FLSurfaceView 与 GLSurfaceView 的功能类似,承载了渲染环境配置;BaseModel 用于管理模型的网格和材质;Mesh 用于管理模型的顶点属性;MaterialUtils、MeshUtils、TextureUtils 和 IBLUtils 中分别提供了一些材质、网格、纹理和基于图片的光照等相关的工具。
build.gradle
...
android {...aaptOptions { // 在应用程序打包过程中不压缩的文件noCompress 'filamat', 'ktx'}
}dependencies {implementation fileTree(dir: '../libs', include: ['*.aar'])...
}
说明:在项目根目录下的 libs 目录中,需要放入以下 aar 文件,它们源自Filament环境搭建中编译生成的 aar。
FLSurfaceView.java
package com.zhyan8.pbr.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.pbr.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.pbr.filament.base.Mesh.Part;
import com.zhyan8.pbr.filament.utils.MaterialUtils;
import com.zhyan8.pbr.filament.utils.TextureUtils;
import com.zhyan8.pbr.filament.utils.TextureUtils.TextureType;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;}protected Texture[] loadTextures(String texturePath, TextureType type) { // 加载纹理Texture texture = TextureUtils.loadTexture(mContext, mEngine, texturePath, type);if (texture != null) {return new Texture[] {texture};}return null;}protected Texture[] loadTextures(String[] texturePaths, TextureType type) { // 加载纹理Texture[] textures = new Texture[texturePaths.length];for (int i = 0; i < textures.length; i++) {textures[i] = TextureUtils.loadTexture(mContext, mEngine, texturePaths[i], type);}return textures;}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.pbr.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.pbr.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.pbr.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.pbr.filament.base.Mesh;
import com.zhyan8.pbr.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;
}
TextureUtils.java
package com.zhyan8.pbr.filament.utils;import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.util.Log;import com.google.android.filament.Engine;
import com.google.android.filament.Texture;
import com.google.android.filament.android.TextureHelper;import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;/*** 纹理工具类*/
public class TextureUtils {private static String TAG = "TextureUtils";public static final boolean SKIP_BITMAP_COPY = true;public static Texture loadTexture(Context context, Engine engine, String texturePath, TextureType type) { // 获取TextureBitmap bitmap = loadBitmapFromAsset(context, texturePath);if (bitmap != null) {return generateTexture(engine, bitmap, type);}return null;}public static Texture loadTexture(Context context, Engine engine, int resourceId, TextureType type) { // 获取TextureBitmap bitmap = loadBitmapFromDrawable(context, resourceId, type);if (bitmap != null) {return generateTexture(engine, bitmap, type);}return null;}private static Texture generateTexture(Engine engine, Bitmap bitmap, TextureType type) { // 生成TextureTexture texture = new Texture.Builder().width(bitmap.getWidth()).height(bitmap.getHeight()).sampler(Texture.Sampler.SAMPLER_2D).format(internalFormat(type)).levels(0xff).build(engine);if (SKIP_BITMAP_COPY) {TextureHelper.setBitmap(engine, texture, 0, bitmap, new Handler(), () ->Log.i(TAG, "getTexture, Bitmap is released."));} else {ByteBuffer buffer = ByteBuffer.allocateDirect(bitmap.getByteCount());bitmap.copyPixelsToBuffer(buffer);buffer.flip();Texture.PixelBufferDescriptor descriptor = new Texture.PixelBufferDescriptor(buffer,format(bitmap),type(bitmap));texture.setImage(engine, 0, descriptor);}texture.generateMipmaps(engine);return texture;}private static Bitmap loadBitmapFromAsset(Context context, String assetPath) { // 从asset中加载bitmapBitmap bitmap = null;try (InputStream inputStream = context.getAssets().open(assetPath)) {bitmap = BitmapFactory.decodeStream(inputStream);} catch (IOException e) {e.printStackTrace();}return bitmap;}private static Bitmap loadBitmapFromDrawable(Context context, int resourceId, TextureType type) { // 从drawable中加载bitmapBitmapFactory.Options options = new BitmapFactory.Options();options.inPremultiplied = (type == TextureType.COLOR);Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId, options);return bitmap;}private static Texture.InternalFormat internalFormat(TextureType type) { // 获取纹理类型对应的内部格式switch (type) {case COLOR:return Texture.InternalFormat.SRGB8_A8;case NORMAL:case DATA:return Texture.InternalFormat.RGBA8;default:throw new IllegalArgumentException("Unsupported texture type: " + type);}}private static Texture.Format format(Bitmap bitmap) { // 获取bitmap的纹理类型对应的Texture的纹理格式switch (bitmap.getConfig().name()) {case "ALPHA_8":return Texture.Format.ALPHA;case "RGB_565":return Texture.Format.RGB;case "ARGB_8888":case "RGBA_F16":return Texture.Format.RGBA;default:throw new IllegalArgumentException("Unknown bitmap configuration");}}private static Texture.Type type(Bitmap bitmap) { // 获取bitmap的纹理类型对应的Texture的纹理类型switch (bitmap.getConfig().name()) {case "ALPHA_8":return Texture.Type.USHORT;case "RGB_565":return Texture.Type.USHORT_565;case "ARGB_8888":return Texture.Type.UBYTE;case "RGBA_F16":return Texture.Type.HALF;default:throw new IllegalArgumentException("Unsupported bitmap configuration");}}/*** 纹理类型*/public enum TextureType {COLOR,NORMAL,DATA}
}
IBLUtils.java
package com.zhyan8.pbr.filament.utils;import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Pair;import com.google.android.filament.Engine;
import com.google.android.filament.IndirectLight;
import com.google.android.filament.Skybox;
import com.google.android.filament.Texture;import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;/*** 基于图片的光照工具类*/
public class IBLUtils {public static Ibl loadImageBasedLight(Context context, Engine engine, String path) { // 加载iblPair<IndirectLight, Texture> pair1 = loadIndirectLight(context.getAssets(), engine, path);Pair<Skybox, Texture> pair2 = loadSkybox(context.getAssets(), engine, path);return new Ibl(pair1.first, pair1.second, pair2.first, pair2.second);}private static Pair<IndirectLight, Texture> loadIndirectLight(AssetManager assets, Engine engine, String path) { // 加载非直射光Pair<Integer, Integer> size = peekSize(assets, path + "/m0_nx.rgb32f");Texture texture = new Texture.Builder().width(size.first).height(size.second).levels((int) log2(size.first) + 1).format(Texture.InternalFormat.R11F_G11F_B10F).sampler(Texture.Sampler.SAMPLER_CUBEMAP).build(engine);for (int i = 0; i < texture.getLevels(); i++) {if (!loadCubemap(assets, engine, texture, path, "m" + i + "_", i)) {break;}}IndirectLight indirectLight = new IndirectLight.Builder().reflections(texture).intensity(30_000.0f).build(engine);return new Pair<>(indirectLight, texture);}private static Pair<Skybox, Texture> loadSkybox(AssetManager assets, Engine engine, String path) { // 加载天空盒子Pair<Integer, Integer> size = peekSize(assets, path + "/nx.rgb32f");Texture texture = new Texture.Builder().width(size.first).height(size.second).levels(1).format(Texture.InternalFormat.R11F_G11F_B10F).sampler(Texture.Sampler.SAMPLER_CUBEMAP).build(engine);loadCubemap(assets, engine, texture, path, "", 0);Skybox skybox = new Skybox.Builder().environment(texture).build(engine);return new Pair<>(skybox, texture);}private static boolean loadCubemap(AssetManager assets, Engine engine, Texture texture, String path,String prefix, int level) { // 加载立方体纹理// alpha通道不编码不透明度, 而是编码R11G11B10F图像的一些位来表示HDR数据, 我们必须告诉Android不要将RGB通道预先乘以alpha通道BitmapFactory.Options opts = new BitmapFactory.Options();opts.inPremultiplied = false;// R11G11B10F总是4字节每像素int faceSize = texture.getWidth(level) * texture.getHeight(level) * 4;int[] offsets = new int[6];for (int i = 0; i < 6; i++) {offsets[i] = i * faceSize;}ByteBuffer storage = ByteBuffer.allocateDirect(faceSize * 6);String[] suffixes = {"px", "nx", "py", "ny", "pz", "nz"};for (String suffix : suffixes) {try {try (InputStream inputStream = assets.open(path + "/" + prefix + suffix + ".rgb32f")) {Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, opts);if (bitmap != null) {bitmap.copyPixelsToBuffer(storage);}}} catch (IOException e) {e.printStackTrace();return false;}}storage.flip();Texture.PixelBufferDescriptor buffer = new Texture.PixelBufferDescriptor(storage,Texture.Format.RGB, Texture.Type.UINT_10F_11F_11F_REV);texture.setImage(engine, level, buffer, offsets);return true;}private static double log2(float x) { // 计算以2为底x的对数return Math.log(x) / Math.log(2);}private static Pair<Integer, Integer> peekSize(AssetManager assets, String path) { // 获取图片尺寸try (InputStream input = assets.open(path)) {BitmapFactory.Options opts = new BitmapFactory.Options();opts.inJustDecodeBounds = true;BitmapFactory.decodeStream(input, null, opts);return new Pair<>(opts.outWidth, opts.outHeight);} catch (IOException e) {e.printStackTrace();}return new Pair<>(-1, -1);}/*** 基于图片的光照类* Image Based Lighting, IBL*/public static class Ibl {public IndirectLight indirectLight;public Texture indirectLightTexture;public Skybox skybox;public Texture skyboxTexture;public Ibl() {}public Ibl(IndirectLight indirectLight, Texture indirectLightTexture, Skybox skybox, Texture skyboxTexture) {this.indirectLight = indirectLight;this.indirectLightTexture = indirectLightTexture;this.skybox = skybox;this.skyboxTexture = skyboxTexture;}}
}
2.2 业务类
MainActivity.java
package com.zhyan8.pbr;import android.os.Bundle;import androidx.appcompat.app.AppCompatActivity;import com.zhyan8.pbr.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.pbr;import android.content.Context;import com.google.android.filament.Camera;
import com.google.android.filament.Colors;
import com.google.android.filament.EntityManager;
import com.google.android.filament.LightManager;
import com.zhyan8.pbr.filament.base.BaseModel;
import com.zhyan8.pbr.filament.base.FLSurfaceView;
import com.zhyan8.pbr.filament.utils.IBLUtils;public class MyFLSurfaceView extends FLSurfaceView {private BaseModel mMyModel;private int mLight;private IBLUtils.Ibl mIbl; // 基于图片的光照private float mRotateAgree = 0;public MyFLSurfaceView(Context context) {super(context);}public void init() {mSkyboxColor = new float[] {0.35f, 0.35f, 0.35f, 1};super.init();}@Overridepublic void onDestroy() {destroyIbl();mMyModel.destroy();mEngine.destroyEntity(mLight);EntityManager.get().destroy(mLight);super.onDestroy();}@Overrideprotected void setupScene() { // 设置Scene参数mMyModel = new MyModel(getContext(), mEngine);mScene.addEntity(mMyModel.getRenderable());setupLight();LoadIbl();addRenderCallback(() -> cameraTransform());}@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);mCamera.lookAt(0, 1.5, 0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);}private void setupLight() {mLight = EntityManager.get().create();float[] color = Colors.cct(6_500.0f);new LightManager.Builder(LightManager.Type.DIRECTIONAL).color(color[0], color[1], color[2]).intensity(110_000.0f).direction(-0.753f, -1.0f, 0.890f).castShadows(true).build(mEngine, mLight);mScene.addEntity(mLight);}private void LoadIbl() {mIbl = IBLUtils.loadImageBasedLight(getContext(), mEngine, "envs/flower_road_no_sun_2k");mIbl.indirectLight.setIntensity(40_000.0f);mScene.setSkybox(mIbl.skybox);mScene.setIndirectLight(mIbl.indirectLight);}private void destroyIbl() {mEngine.destroySkybox(mIbl.skybox);mEngine.destroyTexture(mIbl.skyboxTexture);mEngine.destroyIndirectLight(mIbl.indirectLight);mEngine.destroyTexture(mIbl.indirectLightTexture);}private void cameraTransform() {// cube//float radius = 8;//float eyeY = 1.5f;// spider_bot//float radius = 400;//float eyeY = 150;// shader_ballfloat radius = 5.5f;float eyeY = 1.5f;mRotateAgree += 0.005f;double x = Math.cos(mRotateAgree) * radius;double z = Math.sin(mRotateAgree) * radius;mCamera.lookAt(x, eyeY, z, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);}
}
MyModel.java
package com.zhyan8.pbr;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.google.android.filament.Texture;
import com.google.android.filament.TextureSampler;
import com.zhyan8.pbr.filament.base.BaseModel;
import com.zhyan8.pbr.filament.utils.MeshUtils;
import com.zhyan8.pbr.filament.utils.TextureUtils;
import com.zhyan8.pbr.filament.utils.TextureUtils.TextureType;public class MyModel extends BaseModel {private String materialPath = "materials/textured_pbr.filamat";//private String meshPath = "models/cube.filamesh";//private String meshPath = "models/spider_bot.filamesh";private String meshPath = "models/shader_ball.filamesh";private Texture mBaseColor;private Texture mNormal;private Texture mAoRoughnessMetallic;public MyModel(Context context, Engine engine) {super(context, engine);init();}@Overridepublic void destroy() {mEngine.destroyTexture(mBaseColor);mEngine.destroyTexture(mNormal);mEngine.destroyTexture(mAoRoughnessMetallic);super.destroy();}private void init() {mMaterials = loadMaterials(materialPath);mMaterialInstances = getMaterialInstance(mMaterials);setupMaterial(mMaterialInstances[0]);mMaterialMap.put("DefaultMaterial", mMaterialInstances[0]);mMesh = MeshUtils.loadMesh(mContext, mEngine, meshPath);mRenderable = getRenderable(PrimitiveType.TRIANGLES);mTransformComponent = mTransformManager.getInstance(mRenderable);transformModel();}private void setupMaterial(MaterialInstance materialInstance) {mBaseColor = TextureUtils.loadTexture(mContext, mEngine, "textures/floor_basecolor.png", TextureType.COLOR);mNormal = TextureUtils.loadTexture(mContext, mEngine, "textures/floor_normal.png", TextureType.NORMAL);mAoRoughnessMetallic = TextureUtils.loadTexture(mContext, mEngine,"textures/floor_ao_roughness_metallic.png", TextureType.DATA);TextureSampler sampler = new TextureSampler();sampler.setAnisotropy(8.0f);materialInstance.setParameter("baseColor", mBaseColor, sampler);materialInstance.setParameter("normal", mNormal, sampler);materialInstance.setParameter("aoRoughnessMetallic", mAoRoughnessMetallic, sampler);}private void transformModel() {float[] modelMatrix = new float[16];Matrix.setIdentityM(modelMatrix, 0);// cube//Matrix.translateM(modelMatrix, 0, 0, -0.5f, 0);// spider_bot//Matrix.setRotateM(modelMatrix, 0, 90, -1, 0, 0);//Matrix.translateM(modelMatrix, 0, 0, 0, -70);// shader_ballMatrix.translateM(modelMatrix, 0, 0, -1.2f, 0);mTransformManager.setTransform(mTransformComponent, modelMatrix);}
}
textured_pbr.mat
material {name : textured_pbr,shadingModel : lit,// 自定义变量参数parameters : [{type : sampler2d,name : baseColor // 基础颜色},{type : sampler2d,name : aoRoughnessMetallic // 环境光遮蔽、粗糙度、金属度},{type : sampler2d,name : normal // 法线}],// 顶点着色器入参MaterialVertexInputs中需要的顶点属性requires: [uv0]
}fragment {void material(inout MaterialInputs material) {material.normal = texture(materialParams_normal, getUV0()).xyz * 2.0 - 1.0; // 法线纹理必须在prepareMaterial之前解析prepareMaterial(material);material.baseColor = texture(materialParams_baseColor, getUV0());vec3 aoRoughnessMetallic = texture(materialParams_aoRoughnessMetallic, getUV0()).rgb;material.ambientOcclusion = aoRoughnessMetallic.r; // 环境光遮蔽material.roughness = aoRoughnessMetallic.g; // 粗糙度material.metallic = aoRoughnessMetallic.b; // 金属度}
}
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/ 目录下面。
加载 spider_bot 模型运行效果如下。
加载 shader_ball 模型运行效果如下。