1 前言
光照元素主要有環境光(ambient)、漫反射光(diffuse)、鏡面反射光(specular),基礎的光照模型主要有蘭伯特(Lambert)光照模型、馮氏(Phong)光照模型和改進的馮氏(Blinn Phong)光照模型。
Lambert 光照模型只包含漫反射光的計算,Phong 光照模型和 Blinn Phong 光照模型都包含環境光、漫反射光、鏡面反射光的計算,兩者的區別在與鏡面反射光的計算,Phong 光照模型根據反向量和觀察向量計算鏡面反射光,Blinn Phong 光照模型根據半向量和法向量計算鏡面反射光。
Blinn Phong 光照模型中環境光、漫反射光、鏡面反射光的計算如下。
vec3 ambientColor() { // 環境光
vec3 ambient = materialParams.ambientColor * materialParams.albedo;
return ambient;
}
vec3 diffuseColor(vec3 normal, vec3 lightDir) { // 漫反射光
float factor = max(dot(normal, lightDir), 0.0);
vec3 diffuse = factor * materialParams.lightColor * materialParams.albedo;
return diffuse;
}
vec3 specularColor(vec3 normal, vec3 lightDir, vec3 viewDire) { // 鏡面反射光
vec3 halfVec = normalize(lightDir + viewDire); // 半向量
float factor = pow(max(dot(normal, halfVec), 0.0), materialParams.gloss);
vec3 specular = factor * materialParams.lightColor * materialParams.specularStrength;
return specular;
}
最終的渲染顏色由以上三種光的顏色疊加而成,如下。
material.baseColor = vec4(ambient + diffuse + specular, 1);
2 自定義光照
本文專案結構如下,完整程式碼資源 → Filament自定義Blinn Phong光照模型。
2.1 基礎類
為方便讀者將注意力聚焦在 Filament 的輸入上,輕鬆配置複雜的環境依賴邏輯,筆者仿照 OpenGL ES 的寫法,抽出了 FLSurfaceView、BaseModel、Mesh、MaterialUtils 和 MeshUtils 類。FLSurfaceView 與 GLSurfaceView 的功能類似,承載了渲染環境配置;BaseModel 用於管理模型的網格和材質;Mesh 用於管理模型的頂點屬性;MaterialUtils 和 MeshUtils 中分別提供了一些材質和網格相關的工具。
build.gradle
...
android {
...
aaptOptions { // 在應用程式打包過程中不壓縮的檔案
noCompress 'filamat', 'ktx'
}
}
dependencies {
implementation fileTree(dir: '../libs', include: ['*.aar'])
...
}
說明:在專案根目錄下的 libs 目錄中,需要放入以下 aar 檔案,它們源自Filament環境搭建中編譯生成的 aar。
FLSurfaceView.java
package com.zhyan8.customlight.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);
}
/**
* 幀回撥
*/
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.loadmodel.filament.base;
import android.content.Context;
import com.google.android.filament.Engine;
import com.google.android.filament.EntityManager;
import com.google.android.filament.Material;
import com.google.android.filament.MaterialInstance;
import com.google.android.filament.RenderableManager;
import com.google.android.filament.RenderableManager.PrimitiveType;
import com.google.android.filament.Texture;
import com.google.android.filament.TransformManager;
import com.zhyan8.loadmodel.filament.utils.MaterialUtils;
import com.zhyan8.loadmodel.filament.base.Mesh.Part;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 模型基類
* 管理模型的網格、材質、渲染id
*/
public class BaseModel {
private static String TAG = "BaseModel";
protected Context mContext; // 上下文
protected Engine mEngine; // Filament引擎
protected TransformManager mTransformManager; // 模型變換管理器
protected Mesh mMesh; // 模型網格
protected Material[] mMaterials; // 模型材質
protected MaterialInstance[] mMaterialInstances; // 模型材質例項
protected Map<String, MaterialInstance> mMaterialMap = new HashMap<>(); // 材質名->材質
protected Texture[] mTextures; // 紋理
protected int mRenderable; // 渲染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;
}
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.customlight.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.customlight.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.customlight.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.customlight.filament.base.Mesh;
import com.zhyan8.customlight.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;
}
2.2 業務類
MainActivity.java
package com.zhyan8.customlight;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import com.zhyan8.customlight.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.customlight;
import android.content.Context;
import com.google.android.filament.Camera;
import com.zhyan8.customlight.filament.base.BaseModel;
import com.zhyan8.customlight.filament.base.FLSurfaceView;
public class MyFLSurfaceView extends FLSurfaceView {
private BaseModel mMyModel;
public MyFLSurfaceView(Context context) {
super(context);
}
public void init() {
mSkyboxColor = new float[] {0.35f, 0.35f, 0.35f, 1};
super.init();
}
@Override
public void onDestroy() {
mMyModel.destroy();
super.onDestroy();
}
@Override
protected void setupScene() { // 設定Scene引數
mMyModel = new MyModel(getContext(), mEngine);
mScene.addEntity(mMyModel.getRenderable());
addRenderCallback(mMyModel.getRenderCallback());
}
@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);
float[] eye = new float[] {1.5f, 1f, 7.5f}; // cube
//float[] eye = new float[] {1.5f, 1f, 500f}; // spider_bot
//float[] eye = new float[] {1.5f, 1f, 10f}; // shader_ball
float[] center = new float[] {0, 0, 0};
float[] up = new float[] {0, 1, 0};
mCamera.lookAt(eye[0], eye[1], eye[2],center[0], center[1], center[2], up[0], up[1], up[2]);
}
}
MyModel.java
package com.zhyan8.customlight;
import android.content.Context;
import android.opengl.Matrix;
import com.google.android.filament.Engine;
import com.google.android.filament.MaterialInstance;
import com.google.android.filament.RenderableManager.PrimitiveType;
import com.zhyan8.customlight.filament.base.BaseModel;
import com.zhyan8.customlight.filament.utils.MeshUtils;
public class MyModel extends BaseModel {
private String mMaterialPath = "materials/custom_light.filamat";
private String mMeshPath = "models/cube.filamesh";
//private String mMeshPath = "models/spider_bot.filamesh";
//private String mMeshPath = "models/shader_ball.filamesh";
private float[] mModelMatrix; // 模型變換矩陣
private float[] mRotateAxis; // 旋轉軸
private float mRotateAgree = 0; // 旋轉角度
public MyModel(Context context, Engine engine) {
super(context, engine);
init();
}
private void init() {
mMaterials = loadMaterials(mMaterialPath);
mMaterialInstances = getMaterialInstance(mMaterials);
setupMaterial(mMaterialInstances[0]);
mMaterialMap.put("DefaultMaterial", mMaterialInstances[0]);
mMesh = MeshUtils.loadMesh(mContext, mEngine, mMeshPath);
mRenderable = getRenderable(PrimitiveType.TRIANGLES);
mTransformComponent = mTransformManager.getInstance(mRenderable);
mRenderCallback = () -> renderCallback();
mModelMatrix = new float[16];
mRotateAxis = new float[] { 0.5f, 1f, 1f };
}
private void setupMaterial(MaterialInstance materialInstance) {
materialInstance.setParameter("albedo", 0.7f, 0.1f, 0.2f); // 模型顏色
materialInstance.setParameter("ambientColor", 0.2f, 0.2f, 0.2f); // 環境光顏色
materialInstance.setParameter("lightPos", 100f, 500f, 50f); // 燈光位置
materialInstance.setParameter("lightColor", 0.9f, 0.9f, 0.9f); // 燈光顏色
materialInstance.setParameter("specularStrength", 0.1f); // 鏡面反射光強度
materialInstance.setParameter("gloss", 4f); // 鏡面反射光澤度
}
private void renderCallback() {
mRotateAgree = (mRotateAgree + 1) % 360;
mRotateAxis[0] = mRotateAgree / 180f - 1;
mRotateAxis[1] = (float) Math.sin(mRotateAgree / 180f * Math.PI * 0.7f);
mRotateAxis[2] = (float) Math.cos(mRotateAgree / 180f * Math.PI * 0.5f);
Matrix.setRotateM(mModelMatrix, 0, mRotateAgree, mRotateAxis[0], mRotateAxis[1], mRotateAxis[2]);
mTransformManager.setTransform(mTransformComponent, mModelMatrix);
}
}
custom_light.mat
material {
name : custom_light,
shadingModel : unlit, // 禁用所有lighting
// 自定義變數引數
parameters : [
{
type : float3,
name : albedo // 模型顏色
},
{
type : float3,
name : ambientColor // 環境光顏色
},
{
type : float3,
name : lightPos // 燈光位置
},
{
type : float3,
name : lightColor // 燈光顏色
},
{
type : float,
name : specularStrength // 鏡面反射光強度
},
{
type : float,
name : gloss // 鏡面反射光澤度
}
],
// 頂點著色器入參MaterialVertexInputs中需要的頂點屬性
requires : [
tangents
]
}
fragment {
vec3 ambientColor() { // 環境光
vec3 ambient = materialParams.ambientColor * materialParams.albedo;
return ambient;
}
vec3 diffuseColor(vec3 normal, vec3 lightDir) { // 漫反射光
float factor = max(dot(normal, lightDir), 0.0);
vec3 diffuse = factor * materialParams.lightColor * materialParams.albedo;
return diffuse;
}
vec3 specularColor(vec3 normal, vec3 lightDir, vec3 viewDire) { // 鏡面反射光(Blinn Phong 光照模型)
vec3 halfVec = normalize(lightDir + viewDire); // 半向量
float factor = pow(max(dot(normal, halfVec), 0.0), materialParams.gloss);
vec3 specular = factor * materialParams.lightColor * materialParams.specularStrength;
return specular;
}
void material(inout MaterialInputs material) {
prepareMaterial(material); // 在方法返回前必須回撥該函式
vec3 pos = getWorldPosition(); // 頂點的世界座標
vec3 normal = normalize(getWorldNormalVector()); // 法線向量
vec3 lightDir = normalize(materialParams.lightPos - pos); // 頂點指向光源的向量
vec3 viewDire = normalize(getWorldCameraPosition() - pos); // 頂點指向相機的向量
vec3 ambient = ambientColor();
vec3 diffuse = diffuseColor(normal, lightDir);
vec3 specular = specularColor(normal, lightDir, viewDire);
material.baseColor = vec4(ambient + diffuse + specular, 1);
}
}
說明:custom_light 材質實現了 Blinn Phong 光照模型。
cube.obj
# 正方體模型
# 頂點位置
v 1.0 1.0 -1.0 # V1
v 1.0 -1.0 -1.0 # V2
v 1.0 1.0 1.0 # V3
v 1.0 -1.0 1.0 # V4
v -1.0 1.0 -1.0 # V5
v -1.0 -1.0 -1.0 # V6
v -1.0 1.0 1.0 # V7
v -1.0 -1.0 1.0 # V8
# 紋理座標
vt 0.0 0.0 # VT1
vt 1.0 0.0 # VT2
vt 1.0 1.0 # VT3
vt 0.0 1.0 # VT4
# 法線
vn 0.0 1.0 0.0 # VN1 (上面法線)
vn 0.0 0.0 1.0 # VN2 (背面法線)
vn -1.0 0.0 0.0 # VN3 (左面法線)
vn 0.0 -1.0 0.0 # VN4 (下面法線)
vn 1.0 0.0 0.0 # VN5 (右面法線)
vn 0.0 0.0 -1.0 # VN6 (前面法線)
# 面(v/vt/vn)
f 1/1/1 5/2/1 7/3/1
f 1/1/1 7/3/1 3/4/1
f 4/1/2 3/2/2 7/3/2
f 4/1/2 7/3/2 8/4/2
f 8/1/3 7/2/3 5/3/3
f 8/1/3 5/3/3 6/4/3
f 6/1/4 2/2/4 4/3/4
f 6/1/4 4/3/4 8/4/4
f 2/1/5 1/2/5 3/3/5
f 2/1/5 3/3/5 4/4/5
f 6/1/6 5/2/6 1/3/6
f 6/1/6 1/3/6 2/4/6
transform.bat
@echo off
setlocal 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/ 目錄下面。
載入 cube 模型執行效果如下。
載入 spider_bot 模型執行效果如下。
載入 shader_ball 模型執行效果如下。
宣告:本文轉自【Filament】自定義Blinn Phong光照模型。