【Filament】基于物理的光照(PBR)

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、SurfaceHolder
    protected 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() { // 设置SurfaceView
        mUiHelper = 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 {
        @Override
        public 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 {
        @Override
        public 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());
        }

        @Override
        public void onDetachedFromSurface() { // 解绑Surface时回调
            mDisplayHelper.detach();
            if (mSwapChain != null) {
                mEngine.destroySwapChain(mSwapChain);
                mEngine.flushAndWait();
                mSwapChain = null;
            }
        }

        @Override
        public 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; // 渲染id
    protected int mTransformComponent; // 模型变换组件的id
    protected FLSurfaceView.RenderCallback mRenderCallback; // 每一帧渲染前的回调(一般用于处理模型变换、相机变换等)

    public BaseModel(Context context, Engine engine) {
        mContext = context;
        mEngine = engine;
        mTransformManager = mEngine.getTransformManager();
    }

    public int getRenderable() { // 获取渲染id
        return 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) { // 获取渲染id
        int 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数组转换为ByteBuffer
        ByteBuffer 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数组转换为ByteBuffer
        ByteBuffer 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数组转换为ByteBuffer
        ByteBuffer 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数组转换为ByteBuffer
        ByteBuffer 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转换为ByteBuffer
            buffer.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转换为ByteBuffer
            buffer.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.google.android.filament.VertexBuffer.VertexAttribute;
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(VertexAttribute.COLOR)
                .normalized(VertexAttribute.TANGENTS)
                .attribute(VertexAttribute.POSITION, 0, AttributeType.HALF4, header.posOffset, header.positionStride)
                .attribute(VertexAttribute.TANGENTS, 0, AttributeType.SHORT4, header.tangentOffset, header.tangentStride)
                .attribute(VertexAttribute.COLOR, 0, AttributeType.UBYTE4, header.colorOffset, header.colorStride)
                .attribute(VertexAttribute.UV0, 0, uvType, header.uv0Offset, header.uv0Stride)
                .normalized(VertexAttribute.UV0, uvNormalized(header));
        if (header.uv1Offset != MAX_UINT32 && header.uv1Stride != MAX_UINT32 && header.uv1Offset > -1 && header.uv1Stride > -1) {
            vertexBufferBuilder
                    .attribute(VertexAttribute.UV1, 0, uvType, header.uv1Offset, header.uv1Stride)
                    .normalized(VertexAttribute.UV1, uvNormalized(header));
        }
        VertexBuffer buffer = vertexBufferBuilder.build(engine);
        buffer.setBufferAt(engine, 0, data);
        return buffer;
    }

    private static AttributeType uvType(Header header) { // UV坐标的精度类型
        if (uvNormalized(header)) {
            return AttributeType.SHORT2;
        }
        return AttributeType.HALF2;
    }

    private static boolean uvNormalized(Header header) { // uv坐标是否已正则化
        return (header.flags & HEADER_FLAG_SNORM16_UV) != 0L;
    }

    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) { // 获取Texture
        Bitmap 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) { // 获取Texture
        Bitmap 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) { // 生成Texture
        Texture 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中加载bitmap
        Bitmap 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中加载bitmap
        BitmapFactory.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) { // 加载ibl
        Pair<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;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mFLSurfaceView = new MyFLSurfaceView(this);
        setContentView(mFLSurfaceView);
        mFLSurfaceView.init();
        mFLSurfaceView.setRenderMode(FLSurfaceView.RENDERMODE_CONTINUOUSLY);
    }

    @Override
    public void onResume() {
        super.onResume();
        mFLSurfaceView.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();
        mFLSurfaceView.onPause();
    }

    @Override
    public 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();
    }

    @Override
    public void onDestroy() {
        destroyIbl();
        mMyModel.destroy();
        mEngine.destroyEntity(mLight);
        EntityManager.get().destroy(mLight);
        super.onDestroy();
    }

    @Override
    protected void setupScene() { // 设置Scene参数
        mMyModel = new MyModel(getContext(), mEngine);
        mScene.addEntity(mMyModel.getRenderable());
        setupLight();
        LoadIbl();
        addRenderCallback(() -> cameraTransform());
    }

    @Override
    protected 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_ball
        float 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();
    }

    @Override
    public 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_ball
        Matrix.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 enabledelayedexpansion

echo 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 模型运行效果如下。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/299288.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

超维空间M1无人机使用说明书——01、ROS机载电脑使用说明——远程连接

引言&#xff1a;远程连接通常采用两种方式&#xff0c;一种是通过可视化软件&#xff0c;如VNC、Nomachine等&#xff0c;另外一种是使用SSH。各有优缺点&#xff0c;两种远程登录方式的优缺点做一个简单的对比&#xff1a; 1、SSH优缺点 优点:1、消耗网络资源 2、运行稳定 …

Stable Diffusion好用的显卡推荐

Stable Diffusion 是一款顶级的人工智能艺术生成工具&#xff0c;以其快速的性能、用户友好的界面和显着的效果而闻名。然而&#xff0c;在沉浸体验之前&#xff0c;有必要验证您的计算机&#xff08;显卡&#xff09;是否符合最佳功能所需的严格规范。今天我们将介绍三款高性价…

专业级的渗透测试服务,助力航空业数字化安全启航

​某知名航空公司是中国首批民营航空公司之一&#xff0c;运营国内外航线200多条&#xff0c;也是国内民航最高客座率的航空公司之一。在数字化发展中&#xff0c;该航空公司以数据驱动决策&#xff0c;通过精细化管理、数字创新和模式优化等方式&#xff0c;实现了精准营销和个…

olap/spark-tungsten:codegen

15721这一章没什么好说的&#xff0c;不再贴课程内容了。codegen和simd在工业界一般只会选一种实现。比如phothon之前用codegen&#xff0c;然后改成了向量化引擎。一般gen的都是weld IR/LLVM IR/当前语言&#xff0c;gen成C的也要检查是不是有本地预编译版本&#xff0c;要不没…

中通快递查询,中通快递单号查询,批量删除不需要的快递单号

快递单号的管理现在是许多企业和个人日常工作中不可或缺的一部分&#xff0c;面对堆积如山的快递单号&#xff0c;如何快速、准确地处理成了许多人的难题。今天&#xff0c;我们将为大家带来一款强大的快递单号处理软件——快递批量查询高手&#xff0c;让你从此告别繁琐的手动…

1.3学习记录

PCB学习 CH340C有内部晶振&#xff0c;而CH340G无内部晶振&#xff0c;故需要更多的元器件 1.5学习 光储充 光储充&#xff0c;顾名思义&#xff0c;这种一体化充电站的核心由三部分组成——光伏发电、储能电池和充电桩。这三部分组成一个微网&#xff0c;利用光伏发电&…

税法相关的基础知识

文章目录 税法原则1.税法基本原则2.税法适用原则 来和大家聊聊税法相关的基础知识 税法原则 1.税法基本原则 2.税法适用原则

DynaForm 各版本安装指南

DynaForm下载链接 https://pan.baidu.com/s/1AgsSyjgRi-y0ujRwSwXtHQ?pwd0531 1.鼠标右击【DynaForm5.9.4(64bit)】压缩包&#xff08;win1及以上系统需先点击“显示更多选项”&#xff09;选择【解压到 DynaForm5.9.4(64bit)】。 2.打开解压后的文件夹&#xff0c;鼠标右击…

python抓取异步数据(加载更多类型的),并下载图片,保存到文件夹,实例

源码&#xff1a; {"subjects":[{"episodes_info":"","rate":"8.8","cover_x":3000,"title":"奥本海默","url":"https:\/\/movie.douban.com\/subject\/35593344\/",&qu…

基于 IP 多播的网络会议程序(2024)

1.题目描述 局域网 IP 多播程序&#xff0c;设计一个图形界面的网络会议程序&#xff08;实现文本多播方式即可&#xff09;。 2.演示Demo 3.参考代码 广播发送代码 //服务端 #include <winsock2.h> #include <iostream> #include <list>#pragma comment(l…

IMS基本架构

IP Multimedia Core Network Subsystem (IMS)商用已久&#xff0c;相对于CS domain的语音方案&#xff0c;IMS则是基于IETF定义的会话控制功能与多媒体传输功能通过IP-CAN实现的 全IP完整语音解决方案。 IMS能为无线和有线用户实现语音、视频、消息、数据等服务。便于运营商通过…

基于OpenCV的图像翻转和镜像

我们将解释如何在Python中实现图像的镜像或翻转。大家只需要了解各种矩阵运算和矩阵操作背后的基本数学即可。 01. 依赖包要求 NumPy —用于矩阵运算并对其进行处理。 OpenCV —用于读取图像并将其转换为2D数组&#xff08;矩阵&#xff09;。 Matplotlib —用于将矩阵绘制为…

【CSS】文字描边的三种实现方式

目录 1. 可行的几种方式1.1. text-shadow 描边代码优缺点 1.2. text-stroke 描边实现优缺点 1.3. svg 描边实现优缺点 总结 1. 可行的几种方式 text-shadow–webkit-text-strokesvg 1.1. text-shadow 描边 MDN text-shadow 代码 <div class"text stroke">…

Mybatis简易搭建并查询数据库表内所有数据

搭建步骤 1.在mysql中创建user表&#xff0c;添加数据2.创建maven模块&#xff0c;导入坐标3.在资源文件夹中导入需要用到的logback.xml4.编写MyBatis 核心配置文件->替换连接信息 解决硬编码问题5.编写 SQL映射文件->统一管理sgl语句&#xff0c;解决硬编码问题6.编码定…

软件测试之单元测试、系统测试、集成测试知识总结

一、单元测试的概念 单元测试是对软件基本组成单元进行的测试&#xff0c;如函数或一个类的方法。当然这里的基本单元不仅仅指的是一个函数或者方法&#xff0c;有可能对应多个程序文件中的一组函数。 单元也具有一些基本的属性。比如&#xff1a;明确的功能、规格定义&#…

利用VIN接口实现自动识别车型功能

如何利用vin接口实现自动识别车型功能&#xff0c;首先实现此功能我们来大概分析一下流程图&#xff1a; ## 1-收集VIN信息&#xff1a;用户输入或扫描车辆的VIN码。 ## 2-OCR识别&#xff1a;把用户所扫描的内容进行识别。 ## 3-发起API请求&#xff1a;通过编程语言或工具…

Windows11 - Ubuntu 双系统及 ROS、ROS2 安装

系列文章目录 前言 一、Windows11 - Ubuntu 双系统安装 硬件信息&#xff1a; 设备名称 DESKTOP-B62D6KE 处理器 13th Gen Intel(R) Core(TM) i5-13500H 2.60 GHz 机带 RAM 40.0 GB (39.8 GB 可用) 设备 ID 7673EF86-8370-41D0-8831-84926668C05A 产品 ID 00331-10000-0000…

mysql生成数据库字典文档

项目交付离不开项目数据库字典文档。下面用python轻松生成交付文档字典。 一 生成doc文档数据库字典效果&#xff1a; 1 生成doc文件&#xff0c;如下图&#xff1a; 2 打开文件字典格式内容 &#xff0c;如下图&#xff1a; 二 python生成doc字典文档代码 生成doc数据库字典…

uniapp 文字超出多少字,显示收起全文按钮效果demo(整理)

收起展开 <template><view class"font30 color000 mL30 mR30"><text :class"showFullText ? : clamp-text">{{ text }}</text><view v-if"showToggleBtn && text.length > 42" click"toggleShowFu…

STM32实战之IAP代码升级

目录 1 IAP介绍 2 内存分区 3 整体设计流程图 4 Boot Loader的代码编写 5 APP1代码编写 6 APP2代码编写 stm32内部flash操作相关函数 1 IAP介绍 IAP&#xff08;In Application Programming&#xff09;即在应用编程&#xff0c; IAP 是用户自己的程序在运行过程中…