【Filament】加载obj和fbx模型

news2024/11/27 22:30:29

1 前言

        3D 模型的常用格式主要有 obj、fbx、gltf 等,Filament 中的 filamesh.exe 工具可以将 obj、fbx 格式转换为 filamesh 格式,然后再加载显示。对于 gltf 格式模型,可以通过 ModelViewer 加载显示,这不在本文的讨论范围内。

        1)filamesh 简介

        filamesh 工具的官方介绍如下。

filamesh is a tool to convert meshes into an optimized binary format

Caution! filamesh was designed to operate on trusted inputs. To minimize the risk of
triggering memory corruption vulnerabilities, please make sure that the files passed
to filamesh come from a trusted source, or run filamesh in a sandboxed environment.

Usage:
    filamesh [options] <source mesh> <destination file>

Supported mesh formats:
    FBX, OBJ

Input meshes must have texture coordinates.

Options:
   --help, -h
       print this message

   --license
       Print copyright and license information

   --interleaved, -i
       interleaves mesh attributes

   --compress, -c
       enable compression

   --ignore-uv1, -g
       Ignore the second set of UV coordinates

        注意:原始 obj、fbx 模型一定要包含纹理坐标,否在会转换失败。

        以下是一个简单的 filamesh 工具的使用案例。

filamesh cube.obj cube.filamesh

        2)obj 模型

         obj 模型主要由 v(顶点坐标)、vt(纹理坐标)、vn(法线向量)、f(三角面或四角面的顶点索引序列)组成,如下是一个三角形的 obj 文件。

# 三角形模型

# 顶点位置
v 1.0 1.0 -1.0   # V1
v 1.0 -1.0 -1.0  # V2
v 1.0 1.0 1.0    # V3

# 纹理坐标
vt 0.0 0.0  # VT1
vt 1.0 0.0  # VT2
vt 1.0 1.0  # VT3
vt 0.0 1.0  # VT4

# 法线
vn 0.0 1.0 0.0   # VN1

# 面(v/vt/vn)
f 1/1/1 2/2/1 3/3/1

        对于非设计类人员,也可以使用记事本按照以上格式编辑一些简单的模型,然后再拖拽到 Unity(或 Blender、Maya、3DMax 等)软件中进行预览。

        3)推荐阅读

        读者如果对 Filament 不太熟悉,请回顾以下内容。

  • Filament环境搭建
  • 绘制三角形
  • 绘制矩形
  • 绘制圆形
  • 绘制立方体
  • 纹理贴图
  • 立方体贴图(6张图)

2 立方体贴图

        本文项目结构如下,完整代码资源 → Filament加载obj和fbx模型。

2.1 基础类

        为方便读者将注意力聚焦在 Filament 的输入上,轻松配置复杂的环境依赖逻辑,笔者仿照 OpenGL ES 的写法,抽出了 FLSurfaceView、BaseModel、Mesh、MaterialUtils 和 MeshUtils 类。FLSurfaceView 与 GLSurfaceView 的功能类似,承载了渲染环境配置;BaseModel 用于管理模型的网格和材质;Mesh 用于管理模型的顶点属性;MaterialUtils 和 MeshUtils 中分别提供了一些材质和网格相关的工具。

        build.gradle

...
android {
    ...
    aaptOptions { // 在应用程序打包过程中不压缩的文件
        noCompress 'filamat', 'ktx'
    }
}
 
dependencies {
    implementation fileTree(dir: '../libs', include: ['*.aar'])
    ...
}

        说明:在项目根目录下的 libs 目录中,需要放入以下 aar 文件,它们源自Filament环境搭建中编译生成的 aar。

        FLSurfaceView.java

package com.zhyan8.loadmodel.filament.base;

import android.content.Context;
import android.graphics.Point;
import android.view.Choreographer;
import android.view.Surface;
import android.view.SurfaceView;

import com.google.android.filament.Camera;
import com.google.android.filament.Engine;
import com.google.android.filament.EntityManager;
import com.google.android.filament.Filament;
import com.google.android.filament.Renderer;
import com.google.android.filament.Scene;
import com.google.android.filament.Skybox;
import com.google.android.filament.SwapChain;
import com.google.android.filament.View;
import com.google.android.filament.Viewport;
import com.google.android.filament.android.DisplayHelper;
import com.google.android.filament.android.FilamentHelper;
import com.google.android.filament.android.UiHelper;

import java.util.ArrayList;

/**
 * Filament中待渲染的SurfaceView
 * 功能可以类比OpenGL ES中的GLSurfaceView
 * 用于创建Filament的渲染环境
 */
public class FLSurfaceView extends SurfaceView {
    public static int RENDERMODE_WHEN_DIRTY = 0; // 用户请求渲染才渲染一帧
    public static int RENDERMODE_CONTINUOUSLY = 1; // 持续渲染
    protected int mRenderMode = RENDERMODE_CONTINUOUSLY; // 渲染模式
    protected Choreographer mChoreographer; // 消息控制
    protected DisplayHelper mDisplayHelper; // 管理Display(可以监听分辨率或刷新率的变化)
    protected UiHelper mUiHelper; // 管理SurfaceView、TextureView、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.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.loadmodel.filament.base;

import com.google.android.filament.Box;
import com.google.android.filament.Engine;
import com.google.android.filament.IndexBuffer;
import com.google.android.filament.VertexBuffer;
import com.google.android.filament.VertexBuffer.AttributeType;
import com.google.android.filament.VertexBuffer.VertexAttribute;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;

/**
 * 网格
 * 用于管理模型的顶点属性和顶点索引
 */
public class Mesh {
    private Engine mEngine; // Filament引擎
    private VertexBuffer mVertexBuffer; // 顶点属性缓存
    private IndexBuffer mIndexBuffer; // 顶点索引缓存
    private List<Part> mParts; // 子网格信息
    private Box mBox; // 渲染区域
    private List<String> mMaterialNames; // 材质名

    public Mesh(Engine engine) {
        mEngine = engine;
    }

    public Mesh(Engine engine, float[] vertices, short[] indices, List<Part> parts, Box box, List<String> materialNames) {
        mEngine = engine;
        setVertices(vertices);
        setIndices(indices);
        setParts(parts, indices.length);
        setBox(box);
        mMaterialNames = materialNames;
    }

    public Mesh(Engine engine, VertexBuffer vrtexBuffer, IndexBuffer indexBuffer, List<Part> parts, Box box, List<String> materialNames) {
        mEngine = engine;
        mVertexBuffer = vrtexBuffer;
        mIndexBuffer = indexBuffer;
        mParts = parts;
        setBox(box);
        mMaterialNames = materialNames;
    }

    public Mesh(Engine engine, VertexPosCol[] vertices, short[] indices, List<Part> parts, Box box, List<String> materialNames) {
        mEngine = engine;
        setVertices(vertices);
        setIndices(indices);
        setParts(parts, indices.length);
        setBox(box);
        mMaterialNames = materialNames;
    }

    public Mesh(Engine engine, VertexPosUV[] vertices, short[] indices, List<Part> parts, Box box, List<String> materialNames) {
        mEngine = engine;
        setVertices(vertices);
        setIndices(indices);
        setParts(parts, indices.length);
        setBox(box);
        mMaterialNames = materialNames;
    }

    public void setVertices(float[] vertices) { // 设置顶点属性
        mVertexBuffer = getVertexBuffer(vertices);
    }

    public void setVertices(VertexPosCol[] vertices) { // 设置顶点属性
        mVertexBuffer = getVertexBuffer(vertices);
    }

    public void setVertices(VertexPosUV[] vertices) { // 设置顶点属性
        mVertexBuffer = getVertexBuffer(vertices);
    }

    public void setIndices(short[] indices) { // 设置顶点索引
        mIndexBuffer = getIndexBuffer(indices);
    }

    public void setParts(List<Part> parts, int count) { // 设置顶点索引
        if (parts == null || parts.size() == 0) {
            mParts = new ArrayList<>();
            mParts.add(new Part(0, count, 0, count - 1));
        } else {
            mParts = parts;
        }
    }

    public void setBox(Box box) { // 渲染区域
        if (box == null) {
            mBox = new Box(0, 0, 0, 1, 1, 1);
        } else {
            mBox = box;
        }
    }

    public VertexBuffer getVertexBuffer() { // 获取顶点属性缓存
        return mVertexBuffer;
    }

    public IndexBuffer getIndexBuffer() { // 获取顶点索引缓存
        return mIndexBuffer;
    }

    public List<Part> getParts() { // 获取顶点索引缓存
        return mParts;
    }

    public Box getBox() {
        return mBox;
    }

    public List<String> getMaterialNames() {
        return mMaterialNames;
    }

    public void destroy() {
        mEngine.destroyVertexBuffer(mVertexBuffer);
        mEngine.destroyIndexBuffer(mIndexBuffer);
        if (mParts != null) {
            mParts.clear();
        }
        if (mMaterialNames != null) {
            mMaterialNames.clear();
        }
    }

    private VertexBuffer getVertexBuffer(float[] values) { // 获取顶点属性缓存
        ByteBuffer vertexData = getByteBuffer(values);
        int vertexCount = values.length / 3;
        int vertexSize = Float.BYTES * 3;
        VertexBuffer vertexBuffer = new VertexBuffer.Builder()
                .bufferCount(1)
                .vertexCount(vertexCount)
                .attribute(VertexAttribute.POSITION, 0, AttributeType.FLOAT3, 0, vertexSize)
                .build(mEngine);
        vertexBuffer.setBufferAt(mEngine, 0, vertexData);
        return vertexBuffer;
    }

    private VertexBuffer getVertexBuffer(VertexPosCol[] values) { // 获取顶点属性缓存
        ByteBuffer vertexData = getByteBuffer(values);
        int vertexCount = values.length;
        int vertexSize = VertexPosCol.BYTES;
        VertexBuffer vertexBuffer = new VertexBuffer.Builder()
                .bufferCount(1)
                .vertexCount(vertexCount)
                .attribute(VertexAttribute.POSITION, 0, AttributeType.FLOAT3, 0, vertexSize)
                .attribute(VertexAttribute.COLOR,    0, AttributeType.UBYTE4, 3 * Float.BYTES, vertexSize)
                .normalized(VertexAttribute.COLOR)
                .build(mEngine);
        vertexBuffer.setBufferAt(mEngine, 0, vertexData);
        return vertexBuffer;
    }

    private VertexBuffer getVertexBuffer(VertexPosUV[] values) { // 获取顶点属性缓存
        ByteBuffer vertexData = getByteBuffer(values);
        int vertexCount = values.length;
        int vertexSize = VertexPosUV.BYTES;
        VertexBuffer vertexBuffer = new VertexBuffer.Builder()
                .bufferCount(1)
                .vertexCount(vertexCount)
                .attribute(VertexAttribute.POSITION, 0, AttributeType.FLOAT3, 0, vertexSize)
                .attribute(VertexAttribute.UV0,    0, AttributeType.FLOAT2, 3 * Float.BYTES, vertexSize)
                .build(mEngine);
        vertexBuffer.setBufferAt(mEngine, 0, vertexData);
        return vertexBuffer;
    }

    private IndexBuffer getIndexBuffer(short[] values) { // 获取顶点索引缓存
        ByteBuffer indexData = getByteBuffer(values);
        int indexCount = values.length;
        IndexBuffer indexBuffer = new IndexBuffer.Builder()
                .indexCount(indexCount)
                .bufferType(IndexBuffer.Builder.IndexType.USHORT)
                .build(mEngine);
        indexBuffer.setBuffer(mEngine, indexData);
        return indexBuffer;
    }

    private ByteBuffer getByteBuffer(float[] values) { // float数组转换为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.loadmodel.filament.utils;

import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import com.google.android.filament.Engine;
import com.google.android.filament.Material;

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;

/**
 * 材质工具类
 */
public class MaterialUtils {
    private static String TAG = "MaterialUtils";

    public static Material loadMaterial(Context context, Engine engine, String materialPath) { // 加载材质
        Buffer buffer = readUncompressedAsset(context, materialPath);
        if (buffer != null) {
            Material material = (new Material.Builder()).payload(buffer, buffer.remaining()).build(engine);
            material.compile(
                    Material.CompilerPriorityQueue.HIGH,
                    Material.UserVariantFilterBit.ALL,
                    new Handler(Looper.getMainLooper()),
                    () -> Log.i(TAG, "Material " + material.getName() + " compiled."));
            engine.flush();
            return material;
        }
        return null;
    }

    private static Buffer readUncompressedAsset(Context context, String assetPath) { // 加载资源
        ByteBuffer dist = null;
        try {
            AssetFileDescriptor fd = context.getAssets().openFd(assetPath);
            try(FileInputStream fis = fd.createInputStream()) {
                dist = ByteBuffer.allocate((int) fd.getLength());
                try (ReadableByteChannel src = Channels.newChannel(fis)) {
                    src.read(dist);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (dist != null) {
            return dist.rewind();
        }
        return null;
    }
}

        MeshUtils.java

package com.zhyan8.loadmodel.filament.utils;

import android.content.Context;
import android.util.Log;

import com.google.android.filament.Box;
import com.google.android.filament.Engine;
import com.google.android.filament.IndexBuffer;
import com.google.android.filament.VertexBuffer;
import com.google.android.filament.VertexBuffer.AttributeType;
import com.zhyan8.loadmodel.filament.base.Mesh;
import com.zhyan8.loadmodel.filament.base.Mesh.Part;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

/**
 * 材质工具类
 */
public class MeshUtils {
    private static final String FILAMESH_FILE_IDENTIFIER = "FILAMESH";
    private static final long HEADER_FLAG_SNORM16_UV = 0x2L;
    private static final long MAX_UINT32 = 4294967295L;

    public static Mesh loadMesh(Context context, Engine engine, String meshPath) {
        try (InputStream inputStream = context.getAssets().open(meshPath)) {
            Header header = readHeader(inputStream);
            ReadableByteChannel channel = Channels.newChannel(inputStream);
            ByteBuffer vertexBufferData = readSizedData(channel, header.verticesSizeInBytes);
            ByteBuffer indexBufferData = readSizedData(channel, header.indicesSizeInBytes);
            List<Part> parts = readParts(header, inputStream);
            List<String> materialNames = readMaterials(inputStream);

            VertexBuffer vertexBuffer = createVertexBuffer(engine, header, vertexBufferData);
            IndexBuffer indexBuffer = createIndexBuffer(engine, header, indexBufferData);
            return new Mesh(engine, vertexBuffer, indexBuffer, parts, header.aabb, materialNames);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static Header readHeader(InputStream input) { // 读取文件头信息
        Header header = new Header();
        if (!readMagicNumber(input)) {
            Log.e("Filament", "Invalid filamesh file.");
            return header;
        }
        header.versionNumber = readUIntLE(input);
        header.parts = readUIntLE(input);
        header.aabb = new Box(
                readFloat32LE(input), readFloat32LE(input), readFloat32LE(input),
                readFloat32LE(input), readFloat32LE(input), readFloat32LE(input));
        header.flags = readUIntLE(input);
        header.posOffset = readUIntLE(input);
        header.positionStride = readUIntLE(input);
        header.tangentOffset = readUIntLE(input);
        header.tangentStride = readUIntLE(input);
        header.colorOffset = readUIntLE(input);
        header.colorStride = readUIntLE(input);
        header.uv0Offset = readUIntLE(input);
        header.uv0Stride = readUIntLE(input);
        header.uv1Offset = readUIntLE(input);
        header.uv1Stride = readUIntLE(input);
        header.totalVertices = readUIntLE(input);
        header.verticesSizeInBytes = readUIntLE(input);
        header.indices16Bit = readUIntLE(input);
        header.totalIndices = readUIntLE(input);
        header.indicesSizeInBytes = readUIntLE(input);
        header.valid = true;
        return header;
    }

    private static ByteBuffer readSizedData(ReadableByteChannel channel, int sizeInBytes) { // 读取模型顶点数据
        ByteBuffer buffer = ByteBuffer.allocateDirect(sizeInBytes);
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        try {
            channel.read(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
        buffer.flip();
        return buffer;
    }

    private static List<Part> readParts(Header header, InputStream input) { // 读取子网格属性
        List<Part> parts = new ArrayList<>(header.parts);
        for (int i = 0; i < header.parts; i++) {
            Part p = new Part();
            p.offset = readUIntLE(input);
            p.indexCount = readUIntLE(input);
            p.minIndex = readUIntLE(input);
            p.maxIndex = readUIntLE(input);
            p.materialID = readUIntLE(input);
            float minX = readFloat32LE(input);
            float minY = readFloat32LE(input);
            float minZ = readFloat32LE(input);
            float maxX = readFloat32LE(input);
            float maxY = readFloat32LE(input);
            float maxZ = readFloat32LE(input);
            p.aabb = new Box(minX, minY, minZ, maxX, maxY, maxZ);
            parts.add(p);
        }
        return parts;
    }

    private static boolean readMagicNumber(InputStream input) { // 读取魔法数字, 用于判断是否是有效的filamesh文件
        byte[] temp = new byte[FILAMESH_FILE_IDENTIFIER.length()];
        int bytesRead = 0;
        try {
            bytesRead = input.read(temp);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        if (bytesRead != FILAMESH_FILE_IDENTIFIER.length()) {
            return false;
        }
        String tempS = new String(temp, Charset.forName("UTF-8"));
        return tempS.equals(FILAMESH_FILE_IDENTIFIER);
    }

    private static List<String> readMaterials(InputStream input) { // 读取材质
        int numMaterials = readUIntLE(input);
        List<String> materials = new ArrayList<>(numMaterials);
        for (int i = 0; i < numMaterials; i++) {
            int dataLength = readUIntLE(input);
            byte[] data = new byte[dataLength];
            try {
                input.read(data);
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                input.skip(1);
            } catch (IOException e) {
                e.printStackTrace();
            }
            materials.add(new String(data, Charset.forName("UTF-8")));
        }
        return materials;
    }

    private static IndexBuffer createIndexBuffer(Engine engine, Header header, ByteBuffer data) { // 创建顶点索引缓冲
        IndexBuffer.Builder.IndexType indexType = (header.indices16Bit != 0) ?
                IndexBuffer.Builder.IndexType.USHORT : IndexBuffer.Builder.IndexType.UINT;
        IndexBuffer buffer = new IndexBuffer.Builder()
                .bufferType(indexType)
                .indexCount(header.totalIndices)
                .build(engine);
        buffer.setBuffer(engine, data);
        return buffer;
    }

    private static VertexBuffer createVertexBuffer(Engine engine, Header header, ByteBuffer data) { // 创建顶点属性缓冲
        AttributeType uvType = uvType(header);
        VertexBuffer.Builder vertexBufferBuilder = new VertexBuffer.Builder()
                .bufferCount(1)
                .vertexCount(header.totalVertices)
                .normalized(VertexBuffer.VertexAttribute.COLOR)
                .normalized(VertexBuffer.VertexAttribute.TANGENTS)
                .attribute(VertexBuffer.VertexAttribute.POSITION, 0, VertexBuffer.AttributeType.HALF4, header.posOffset, header.positionStride)
                .attribute(VertexBuffer.VertexAttribute.TANGENTS, 0, AttributeType.SHORT4, header.tangentOffset, header.tangentStride)
                .attribute(VertexBuffer.VertexAttribute.COLOR, 0, AttributeType.UBYTE4, header.colorOffset, header.colorStride)
                .attribute(VertexBuffer.VertexAttribute.UV0, 0, uvType, header.uv0Offset, header.uv0Stride);
        if (header.uv1Offset != MAX_UINT32 && header.uv1Stride != MAX_UINT32) {
            vertexBufferBuilder
                    .attribute(VertexBuffer.VertexAttribute.UV1, 0, uvType, header.uv1Offset, header.uv1Stride)
                    .normalized(VertexBuffer.VertexAttribute.UV1, true);
        }
        VertexBuffer buffer = vertexBufferBuilder.build(engine);
        buffer.setBufferAt(engine, 0, data);
        return buffer;
    }

    private static AttributeType uvType(Header header) { // UV坐标的精度类型
        if ((header.flags & HEADER_FLAG_SNORM16_UV) != 0L) {
            return AttributeType.SHORT2;
        }
        return AttributeType.HALF2;
    }

    private static int readIntLE(InputStream input) { // 获取输入流中Little Endian格式的整数
        try {
            return (input.read() & 0xff) |
                    ((input.read() & 0xff) << 8) |
                    ((input.read() & 0xff) << 16) |
                    ((input.read() & 0xff) << 24);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return 0;
    }

    private static int readUIntLE(InputStream input) { // 获取输入流中Little Endian格式的无符号整数
        return (int) (readIntLE(input) & 0xFFFFFFFFL);
    }

    private static float readFloat32LE(InputStream input) { // 获取输入流中Little Endian格式的浮点数
        byte[] bytes = new byte[4];
        try {
            input.read(bytes, 0, 4);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getFloat();
    }
}

/**
 * 网格文件头
 */
class Header {
    boolean valid = false;
    int versionNumber = 0;
    int parts = 0;
    Box aabb = new Box();
    int flags = 0;
    int posOffset = 0;
    int positionStride = 0;
    int tangentOffset = 0;
    int tangentStride = 0;
    int colorOffset = 0;
    int colorStride = 0;
    int uv0Offset = 0;
    int uv0Stride = 0;
    int uv1Offset = 0;
    int uv1Stride = 0;
    int totalVertices = 0;
    int verticesSizeInBytes = 0;
    int indices16Bit = 0;
    int totalIndices = 0;
    int indicesSizeInBytes = 0;
}

2.2 业务类

        MainActivity.java

package com.zhyan8.loadmodel;

import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

import com.zhyan8.loadmodel.filament.base.FLSurfaceView;

public class MainActivity extends AppCompatActivity {
    private FLSurfaceView mFLSurfaceView;

    @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.loadmodel;

import android.content.Context;

import com.google.android.filament.Camera;
import com.zhyan8.loadmodel.filament.base.BaseModel;
import com.zhyan8.loadmodel.filament.base.FLSurfaceView;

public class MyFLSurfaceView extends FLSurfaceView {
    private BaseModel mMyModel;

    public MyFLSurfaceView(Context context) {
        super(context);
    }

    public void init() {
        mSkyboxColor = new float[] {0.35f, 0.35f, 0.35f, 1};
        super.init();
    }

    @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.loadmodel;

import android.content.Context;
import android.opengl.Matrix;

import com.google.android.filament.Engine;
import com.google.android.filament.MaterialInstance;
import com.google.android.filament.RenderableManager.PrimitiveType;
import com.zhyan8.loadmodel.filament.base.BaseModel;
import com.zhyan8.loadmodel.filament.utils.MeshUtils;

public class MyModel extends BaseModel {
    private String mMaterialPath = "materials/normal_light.filamat";
    private String mMeshPath = "models/cube.filamesh";
    //private String mMeshPath = "models/spider_bot.filamesh";
    //private String mMeshPath = "models/shader_ball.filamesh";

    private float[] mModelMatrix; // 模型变换矩阵
    private float[] mRotateAxis; // 旋转轴
    private float mRotateAgree = 0; // 旋转角度

    public MyModel(Context context, Engine engine) {
        super(context, engine);
        init();
    }

    private void init() {
        mMaterials = loadMaterials(mMaterialPath);
        mMaterialInstances = getMaterialInstance(mMaterials);
        mMaterialMap.put("DefaultMaterial", mMaterialInstances[0]);
        mMesh = MeshUtils.loadMesh(mContext, mEngine, mMeshPath);
        mRenderable = getRenderable(PrimitiveType.TRIANGLES);
        mTransformComponent = mTransformManager.getInstance(mRenderable);
        mRenderCallback = () -> renderCallback();
        mModelMatrix = new float[16];
        mRotateAxis = new float[] { 0.5f, 1f, 1f };
    }

    private void renderCallback() {
        mRotateAgree = (mRotateAgree + 1) % 360;
        mRotateAxis[0] = mRotateAgree / 180f - 1;
        mRotateAxis[1] = (float) Math.sin(mRotateAgree / 180f * Math.PI * 0.7f);
        mRotateAxis[2] = (float) Math.cos(mRotateAgree / 180f * Math.PI * 0.5f);
        Matrix.setRotateM(mModelMatrix, 0, mRotateAgree, mRotateAxis[0], mRotateAxis[1], mRotateAxis[2]);
        mTransformManager.setTransform(mTransformComponent, mModelMatrix);
    }
}

        normal_light.mat

material {
    name : custom_light,

    shadingModel : unlit, // 禁用所有lighting

    // 顶点着色器入参MaterialVertexInputs中需要的顶点属性
    requires : [
        tangents
    ]
}

fragment {
    void material(inout MaterialInputs material) {
        prepareMaterial(material); // 在方法返回前必须回调该函数
        vec3 normal = normalize(getWorldNormalVector()); // 法线向量
        if (normal.x < 0.0) {
            normal.x = -normal.x / 2.0;
        }
        if (normal.y < 0.0) {
            normal.y = -normal.y / 2.0;
        }
        if (normal.z < 0.0) {
            normal.z = -normal.z / 2.0;
        }
        material.baseColor = vec4(normal, 1.0);
    }
}

        说明: normal_light 材质使用模型的法线信息给模型表面着色。

        cube.obj

# 正方体模型

# 顶点位置
v 1.0 1.0 -1.0   # V1
v 1.0 -1.0 -1.0  # V2
v 1.0 1.0 1.0    # V3
v 1.0  -1.0 1.0  # V4
v -1.0 1.0 -1.0  # V5
v -1.0 -1.0 -1.0 # V6
v -1.0 1.0 1.0   # V7
v -1.0 -1.0 1.0  # V8

# 纹理坐标
vt 0.0 0.0  # VT1
vt 1.0 0.0  # VT2
vt 1.0 1.0  # VT3
vt 0.0 1.0  # VT4

# 法线
vn 0.0 1.0 0.0   # VN1 (上面法线)
vn 0.0 0.0 1.0   # VN2 (背面法线)
vn -1.0 0.0 0.0  # VN3 (左面法线)
vn 0.0 -1.0 0.0  # VN4 (下面法线)
vn 1.0 0.0 0.0   # VN5 (右面法线)
vn 0.0 0.0 -1.0  # VN6 (前面法线)

# 面(v/vt/vn)
f 1/1/1 5/2/1 7/3/1
f 1/1/1 7/3/1 3/4/1
f 4/1/2 3/2/2 7/3/2
f 4/1/2 7/3/2 8/4/2
f 8/1/3 7/2/3 5/3/3
f 8/1/3 5/3/3 6/4/3
f 6/1/4 2/2/4 4/3/4
f 6/1/4 4/3/4 8/4/4
f 2/1/5 1/2/5 3/3/5
f 2/1/5 3/3/5 4/4/5
f 6/1/6 5/2/6 1/3/6
f 6/1/6 1/3/6 2/4/6

        transform.bat

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java基础篇】常见的字符编码、以及它们的区别

常见的字符编码、以及它们的区别 ✔️ 解析✔️扩展知识仓✔️Unicode和UTF-8有啥关系?✔️有了UTF-8&#xff0c;为什么要出现GBK✔️为什么会出现乱码 ✔️ 解析 就像电报只能发出 ”滴” 和 ”答” 声一样&#xff0c;计算机只认识 0 和 1 两种字符&#xff0c;但是&#x…

Python从入门到网络爬虫(函数详解)

前言 函数是变成语言中最常见的语法&#xff0c;函数的本质就是功能的封装。使用函数可以大大提高编程效率与程序的可读性。函数是能够实现特定功能的计算机代码而已&#xff0c;他是一种特定的代码组结构。 函数的作用 1.提升代码的重复利用率&#xff0c;避免重复开发相同代…

ssm基于vue框架和elementui组件的手机官网论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本手机官网就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息&#x…

【UEFI基础】EDK网络框架(UNDI)

UNDI UNDI代码综述 UNDI全称Universal Network Driver Interface&#xff0c;它虽然属于UEFI网络框架的一部分&#xff0c;但是并没有在EDK开源代码中实现。不过目前主流网卡厂商都会提供UEFI下的网络驱动&#xff0c;并且大部分都实现了UNDI&#xff0c;这样BIOS下就可以通过…

鸿蒙APP上线注意事项

在将鸿蒙APP上线之前&#xff0c;开发者需要注意一些关键的事项&#xff0c;以确保应用的顺利发布和良好运营。以下是一些建议的注意事项&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.遵循应用市…

【常用排序算法】冒泡排序

冒泡排序 冒泡排序基本思想&#xff1a;N 个数的数组&#xff0c;经过N-1轮排序。 升序 大的值下沉&#xff0c;小的值上浮。降序 小的值下沉&#xff0c;小的字上浮 import java.util.Arrays; public class BubbleSort {public static void main(String[] args) {int[] values…

电脑视频需要分屏怎么做

在当今数字时代&#xff0c;人们对于视频的需求越来越高。有时候&#xff0c;我们可能想在同一屏幕上同时播放多个视频&#xff0c;进行对比、观看、剪辑或者其他目的。那么&#xff0c;视频分屏应该怎么做呢&#xff1f; 在本篇文章中&#xff0c;我们将会详细的为你介绍视频分…

Geotrust DV通配符证书保护域名数量

Geotrust是一家知名的SSL证书提供商&#xff0c;旗下有多种类型的SSL数字证书&#xff0c;保护网站数据在传输过程中的安全性和完整性&#xff0c;帮助用户确认其网站的安全。通配符SSL证书是Geotrust颁发的一种可以同时保护多个域名站点的SSL证书。今天就随SSL盾小编了解Geotr…

conda虚拟环境搭建和打包,删除,移动等全流程及相关问题汇总

私人笔记无偿分享&#xff0c;更多内容请访问&#xff1a;链接&#xff1a;https://pan.baidu.com/s/19mS5N9XJ_AotF20kUwSA3w?pwdp5kx 提取码&#xff1a;p5kx 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 4.4. anaconda虚拟环境搭建&#xff1a; 网址&am…

解决在test以外的目录下导入junit无效

以上引用来自src目录下的文件&#xff0c;可以看到&#xff0c;和junit有关的导入都飘红&#xff0c;但明明junit已经被正确导入进了项目中。 再看右侧的Maven的依赖下方&#xff0c;junit的右边有一个很不起眼的(test) 这是因为junit作为测试框架&#xff0c;可能包含仅适用于…

Halcon区域的面积和中心点area_center

Halcon区域的面积和中心点 提到区域的特征&#xff0c;最常用的莫过于区域的面积和中心点坐标信息。实际工作中&#xff0c;经常会使用面积或中心点进行特征的选择和定位。Halcon中的area_center算子就是用于实现这一功能的&#xff0c;该算子一次返回以下两个结果。 &#xf…

【KingbaseES】实现MySql函数WEEKS_BETWEEN

WEEKS_BETWEEN CREATE OR REPLACE FUNCTION weeks_between(start_date date, end_date date) RETURNS integer AS $$ BEGIN RETURN EXTRACT(WEEK FROM end_date) - EXTRACT(WEEK FROM start_date); END; $$ LANGUAGE plpgsql IMMUTABLE;结果展示

嵌入式Linux之MX6ULL裸机开发学习笔记(IMX启动方式-启动设备的选择)

一,硬件启动方式选择 1.启动方式的选择 6ull支持多种启动方式。 比如可以从 SD/EMMC、 NAND Flash、 QSPI Flash等启动。 6ull是怎么支持多种外置flash启动程序的。 1.启动方式选择&#xff1a; BOOT_MODE0 and BOOT_MODE1&#xff0c;这两个是两个IO来控制的&#xff0c;…

Unity 使用Sprite绘制一条自定义图片的线

Unity 使用Sprite绘制一条自定义图片的线 前言项目场景布置代码编写总结 运行效果感谢 前言 遇到一个需要绘制自定义形状的需求。那只能利用Sprite来绘制一条具有自定义图片的线&#xff0c;通过代码动态设置起点、终点以及线宽&#xff0c;实现灵活的线条效果。 项目 场景…

嵌入式Linux之MX6ULL裸机开发学习笔记(汇编LED灯点亮)

汇编LED驱动实验 1.驱动编写 首先创建在vscode上创建工作区&#xff0c;创建led.s汇编文件&#xff0c;然后编写以下程序 .global _start 全局标号 _start: /* 使能所有外设时钟 */ ldr r0,0x020c4068 CCGR0 ldr r1,0xffffffff 要向CCGR0写入的数据 str r1,[r0] 将0xff…

UE5 C++(十一)— 碰撞检测

文章目录 代理绑定BeginOverlap和EndOverlapHit事件的代理绑定碰撞设置 代理绑定BeginOverlap和EndOverlap 首先&#xff0c;创建自定义ActorC类 MyCustomActor 添加碰撞组件 #include "Components/BoxComponent.h"public:UPROPERTY(VisibleAnywhere, BlueprintRea…

IO进程线程 day4

进程状态间的转化 创建出三个进程完成两个文件之间拷贝工作&#xff0c;子进程1拷贝前一半内容&#xff0c;子进程2拷贝后一半内容&#xff0c;父进程回收子进程的资源 #include <head.h> int main(int argc, const char *argv[]) {FILE *fp1NULL,*fp2NULL;//定义两个文…

Java Arrays.copyOfRange的用法

Arrays.copyOfRange的使用方法&#xff1a; 将一个数组拷贝至另一个数组中 参数&#xff1a; original&#xff1a;第一个参数为要拷贝的数组对象 from&#xff1a;第二个参数为拷贝的开始位置&#xff08;包含&#xff09; to&#xff1a;第三个参数为拷贝的结束位置&#x…

小程序组件内的数据监听器

数据监听器可以用于监听和响应任何属性和数据字段的变化。从小程序基础库版本 2.6.1 开始支持。 有时&#xff0c;在一些数据字段被 setData 设置时&#xff0c;需要执行一些操作。例如&#xff0c; 一个值取决于另外两个值的变化&#xff0c;this.data.sum 永远是 this.data.…

大创项目推荐 深度学习卷积神经网络垃圾分类系统 - 深度学习 神经网络 图像识别 垃圾分类 算法 小程序

文章目录 0 简介1 背景意义2 数据集3 数据探索4 数据增广(数据集补充)5 垃圾图像分类5.1 迁移学习5.1.1 什么是迁移学习&#xff1f;5.1.2 为什么要迁移学习&#xff1f; 5.2 模型选择5.3 训练环境5.3.1 硬件配置5.3.2 软件配置 5.4 训练过程5.5 模型分类效果(PC端) 6 构建垃圾…