Android端使用OpenGL ES加载OBJ文件数据

一、obj模型文件概览

在介绍如何用程序加载obj模型文件之前,首先需要了解一下它的格式。
obj文件是最简单的一种3D模型文件,可由3dx MAX或Maya等建模软件导出,广泛应用于3D图形应用(如游戏)程序和3D打印等等,其本质上就是文本文件,里面存储的是模型的顶点坐标,顶点法向量和纹理坐标信息。

下面看一个典型的obj文件

# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# 创建的文件:01.05.2018 15:16:27

#
# object 棒球帽
#

v  22.7219 49.3250 -5.6920
v  22.7255 49.3244 -5.6873
v  24.6979 49.3887 -10.4577
v  24.4561 49.4295 -10.6732
v  22.7288 49.3238 -5.6824
v  24.9195 49.3465 -10.2052
v  26.1314 49.3264 -14.0106
v  25.7093 49.3979 -14.3621
v  26.5186 49.2526 -13.5884
v  22.7318 49.3231 -5.6773
......
vn 0.5713 0.0798 -0.8169
vn 0.5751 0.0783 -0.8144
vn 0.1130 0.2192 -0.9691
vn 0.1180 0.2182 -0.9687
vn 0.5800 0.0763 -0.8110
vn 0.5844 0.0746 -0.8080
vn 0.1342 0.2105 -0.9683
......
vt -0.1830 0.1242 0.0073
vt -0.1789 0.1238 0.0074
vt -0.1828 0.1220 0.0073
vt -0.1779 0.1204 0.0075
vt -0.1863 0.1216 0.0072
vt -0.1873 0.1176 0.0072
vt -0.1843 0.0892 0.0072
vt -0.1872 0.0892 0.0071
vt -0.1875 0.0867 0.0071
......
f 238/240/239 243/245/244 244/247/245 
f 244/247/245 239/241/240 238/240/239 
f 239/241/240 244/247/245 245/248/246 
f 245/248/246 242/244/243 239/241/240 
f 246/249/247 247/250/248 248/251/249 
f 248/251/249 249/252/250 246/249/247 
f 248/251/249 250/253/251 251/254/252 
f 251/254/252 249/252/250 248/251/249 

这里只截取了一部分,从上述obj文件片段中可以看出,其内容是以行为基本单位进行组织的,每种不同前缀开头的行有不同含义:

  • “#”号开头的行为注释,描述模型的一些基本信息,在程序加载的过程中可以忽略。
  • “v”开头的行用于存放顶点坐标,其后面的3个数值分别表示一个顶点的X、Y、Z坐标。
  • “vn”开头的行用于存放顶点法向量,其后面的3个数值分别表示一个顶点的法向量在X轴、Y轴、Z轴上的分量。
  • “vt”开头的行用于存放顶点纹理坐标,其后面的3个数值分别表示纹理坐标的S、T、W分量(S、T为纹理采样坐标,W指的是深度纹理坐标,主要用于3D纹理的采样,OpenGL ES 2.0中对3D纹理还没有普遍支持,故这里不使用)
  • “f”开头的行表示一个面,如果是三角形,(由于OpenGL ES仅支持三角形,故我选择的obj模型都是基于三角形面的)则后面有3组用空格分隔的数据,代表三角形的3个顶点,每组数据包含3个数值,用“/”分隔,依次表示顶点坐标数据索引,顶点纹理坐标数据索引,顶点法向量数据索引。例如有这样一行“ f 238/240/239 243/245/244 244/247/245 ”,则表示这个三角形面中3个顶点的坐标分别来自第238、243、244号“v”开头的行,3个顶点的纹理坐标分别了来自第240、245、247号“vt”开头的行,3个顶点的法向量分别来自第239、244、245号“vn”开头的行。

    有一点需要注意,就是就是我们加载显示obj文件时,顶点和面的数据是必需的,而法向量和纹理数据是可选的。

二、加载并显示

根据计算机图形学中的知识,我们加载并显示模型的步骤可分为以下几步:

  1. 将obj文件中的文本信息通过文件IO流读进内存。
  2. 一行行读取,分别用3个数组保存顶点,纹理和法向量数据。
  3. 创建OpenGL场景(这一点Android的GLSurfaceView已经帮我们做好了)
  4. 创建着色器程序,将顶点、纹理等数据传进渲染管线
  5. 启用着色器程序,并设置摄像头位置,启用纹理,添加光照。

步骤一、创建物体类
LoadedObjectVertexNormalTexture.java

public LoadedObjectVertexNormalTexture(MySurfaceView mv,float[] vertices,float[] normals,float texCoors[])
    {
        //初始化顶点坐标与着色数据
        initVertexData(vertices,normals,texCoors);
        //初始化shader
        initShader(mv);
    }

在构造函数中,根据传入的GLSurfaceView以及顶点,纹理,法向量数组,初始化着色器数据和shader。

//初始化顶点坐标与着色数据的方法
    public void initVertexData(float[] vertices,float[] normals,float texCoors[])
    {
        //顶点坐标数据的初始化================begin============================
        vCount=vertices.length/3;

        //创建顶点坐标数据缓冲
        //vertices.length*4是因为一个整数四个字节
        ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);
        vbb.order(ByteOrder.nativeOrder());//设置字节顺序
        mVertexBuffer = vbb.asFloatBuffer();//转换为Float型缓冲
        mVertexBuffer.put(vertices);//向缓冲区中放入顶点坐标数据
        mVertexBuffer.position(0);//设置缓冲区起始位置
        //特别提示:由于不同平台字节顺序不同数据单元不是字节的一定要经过ByteBuffer
        //转换,关键是要通过ByteOrder设置nativeOrder(),否则有可能会出问题
        //顶点坐标数据的初始化================end============================

        //顶点法向量数据的初始化================begin============================
        ByteBuffer cbb = ByteBuffer.allocateDirect(normals.length*4);
        cbb.order(ByteOrder.nativeOrder());//设置字节顺序
        mNormalBuffer = cbb.asFloatBuffer();//转换为Float型缓冲
        mNormalBuffer.put(normals);//向缓冲区中放入顶点法向量数据
        mNormalBuffer.position(0);//设置缓冲区起始位置
        //特别提示:由于不同平台字节顺序不同数据单元不是字节的一定要经过ByteBuffer
        //转换,关键是要通过ByteOrder设置nativeOrder(),否则有可能会出问题
        //顶点着色数据的初始化================end============================

        //顶点纹理坐标数据的初始化================begin============================
        ByteBuffer tbb = ByteBuffer.allocateDirect(texCoors.length*4);
        tbb.order(ByteOrder.nativeOrder());//设置字节顺序
        mTexCoorBuffer = tbb.asFloatBuffer();//转换为Float型缓冲
        mTexCoorBuffer.put(texCoors);//向缓冲区中放入顶点纹理坐标数据
        mTexCoorBuffer.position(0);//设置缓冲区起始位置
        //特别提示:由于不同平台字节顺序不同数据单元不是字节的一定要经过ByteBuffer
        //转换,关键是要通过ByteOrder设置nativeOrder(),否则有可能会出问题
        //顶点纹理坐标数据的初始化================end============================
    }

    //初始化shader
    public void initShader(MySurfaceView mv)
    {
        //加载顶点着色器的脚本内容
        mVertexShader=ShaderUtil.loadFromAssetsFile("vertex.sh", mv.getResources());
        //加载片元着色器的脚本内容
        mFragmentShader=ShaderUtil.loadFromAssetsFile("frag.sh", mv.getResources());
        //基于顶点着色器与片元着色器创建程序
        mProgram = ShaderUtil.createProgram(mVertexShader, mFragmentShader);
        //获取程序中顶点位置属性引用
        maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
        //获取程序中顶点颜色属性引用
        maNormalHandle= GLES20.glGetAttribLocation(mProgram, "aNormal");
        //获取程序中总变换矩阵引用
        muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
        //获取位置、旋转变换矩阵引用
        muMMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMMatrix");
        //获取程序中光源位置引用
        maLightLocationHandle=GLES20.glGetUniformLocation(mProgram, "uLightLocation");
        //获取程序中顶点纹理坐标属性引用
        maTexCoorHandle= GLES20.glGetAttribLocation(mProgram, "aTexCoor");
        //获取程序中摄像机位置引用
        maCameraHandle=GLES20.glGetUniformLocation(mProgram, "uCamera");
    }

步骤二、编写从obj文件读取信息的工具类
LoadUtil.java
其中的LoadedObjectVertexNormalTexture方法从Resource资源文件中加载obj格式的数据,存入数组,并生成LoadedObjectVertexNormalTexture对象

//从obj文件中加载携带顶点信息的物体,并自动计算每个顶点的平均法向量
    public static LoadedObjectVertexNormalTexture loadFromFile
    (String fname, Resources r,MySurfaceView mv)
    {
        //加载后物体的引用
        LoadedObjectVertexNormalTexture lo=null;
        //原始顶点坐标列表--直接从obj文件中加载
        ArrayList<Float> alv=new ArrayList<Float>();
        //顶点组装面索引列表--根据面的信息从文件中加载
        ArrayList<Integer> alFaceIndex=new ArrayList<Integer>();
        //结果顶点坐标列表--按面组织好
        ArrayList<Float> alvResult=new ArrayList<Float>();
        //平均前各个索引对应的点的法向量集合Map
        //此HashMap的key为点的索引, value为点所在的各个面的法向量的集合
        HashMap<Integer,HashSet<Normal>> hmn=new HashMap<Integer,HashSet<Normal>>();
        //原始纹理坐标列表
        ArrayList<Float> alt=new ArrayList<Float>();
        //纹理坐标结果列表
        ArrayList<Float> altResult=new ArrayList<Float>();

        try
        {
            InputStream in=r.getAssets().open(fname);
            InputStreamReader isr=new InputStreamReader(in);
            BufferedReader br=new BufferedReader(isr);
            String temps=null;

            //扫面文件,根据行类型的不同执行不同的处理逻辑
            while((temps=br.readLine())!=null)
            {
                //用空格分割行中的各个组成部分
                String[] tempsa=temps.split("[ ]+");
                if(tempsa[0].trim().equals("v"))
                {//此行为顶点坐标
                    //若为顶点坐标行则提取出此顶点的XYZ坐标添加到原始顶点坐标列表中
                    alv.add(Float.parseFloat(tempsa[1]));
                    alv.add(Float.parseFloat(tempsa[2]));
                    alv.add(Float.parseFloat(tempsa[3]));
                }
                else if(tempsa[0].trim().equals("vt"))
                {//此行为纹理坐标行
                    //若为纹理坐标行则提取ST坐标并添加进原始纹理坐标列表中
                    alt.add(Float.parseFloat(tempsa[1])/2.0f);
                    alt.add(Float.parseFloat(tempsa[2])/2.0f);
                }
                else if(tempsa[0].trim().equals("f"))
                {//此行为三角形面
                    /*
                     *若为三角形面行则根据 组成面的顶点的索引从原始顶点坐标列表中
                     *提取相应的顶点坐标值添加到结果顶点坐标列表中,同时根据三个
                     *顶点的坐标计算出此面的法向量并添加到平均前各个索引对应的点
                     *的法向量集合组成的Map中
                    */

                    int[] index=new int[3];//三个顶点索引值的数组

                    //计算第0个顶点的索引,并获取此顶点的XYZ三个坐标
                    index[0]=Integer.parseInt(tempsa[1].split("/")[0])-1;
                    float x0=alv.get(3*index[0]);
                    float y0=alv.get(3*index[0]+1);
                    float z0=alv.get(3*index[0]+2);
                    alvResult.add(x0);
                    alvResult.add(y0);
                    alvResult.add(z0);

                    //计算第1个顶点的索引,并获取此顶点的XYZ三个坐标
                    index[1]=Integer.parseInt(tempsa[2].split("/")[0])-1;
                    float x1=alv.get(3*index[1]);
                    float y1=alv.get(3*index[1]+1);
                    float z1=alv.get(3*index[1]+2);
                    alvResult.add(x1);
                    alvResult.add(y1);
                    alvResult.add(z1);

                    //计算第2个顶点的索引,并获取此顶点的XYZ三个坐标
                    index[2]=Integer.parseInt(tempsa[3].split("/")[0])-1;
                    float x2=alv.get(3*index[2]);
                    float y2=alv.get(3*index[2]+1);
                    float z2=alv.get(3*index[2]+2);
                    alvResult.add(x2);
                    alvResult.add(y2);
                    alvResult.add(z2);

                    //记录此面的顶点索引
                    alFaceIndex.add(index[0]);
                    alFaceIndex.add(index[1]);
                    alFaceIndex.add(index[2]);

                    //通过三角形面两个边向量0-1,0-2求叉积得到此面的法向量
                    //求0号点到1号点的向量
                    float vxa=x1-x0;
                    float vya=y1-y0;
                    float vza=z1-z0;
                    //求0号点到2号点的向量
                    float vxb=x2-x0;
                    float vyb=y2-y0;
                    float vzb=z2-z0;
                    //通过求两个向量的叉积计算法向量
                    float[] vNormal=vectorNormal(getCrossProduct
                            (
                                    vxa,vya,vza,vxb,vyb,vzb
                            ));
                    for(int tempInxex:index)
                    {//记录每个索引点的法向量到平均前各个索引对应的点的法向量集合组成的Map中
                        //获取当前索引对应点的法向量集合
                        HashSet<Normal> hsn=hmn.get(tempInxex);
                        if(hsn==null)
                        {//若集合不存在则创建
                            hsn=new HashSet<Normal>();
                        }
                        //将此点的法向量添加到集合中
                        //由于Normal类重写了equals方法,因此同样的法向量不会重复出现在此点
                        //对应的法向量集合中
                        hsn.add(new Normal(vNormal[0],vNormal[1],vNormal[2]));
                        //将集合放进HsahMap中
                        hmn.put(tempInxex, hsn);
                    }

                    //将纹理坐标组织到结果纹理坐标列表中
                    //第0个顶点的纹理坐标
                    int indexTex=Integer.parseInt(tempsa[1].split("/")[1])-1;
                    altResult.add(alt.get(indexTex*2));
                    altResult.add(alt.get(indexTex*2+1));
                    //第1个顶点的纹理坐标
                    indexTex=Integer.parseInt(tempsa[2].split("/")[1])-1;
                    altResult.add(alt.get(indexTex*2));
                    altResult.add(alt.get(indexTex*2+1));
                    //第2个顶点的纹理坐标
                    indexTex=Integer.parseInt(tempsa[3].split("/")[1])-1;
                    altResult.add(alt.get(indexTex*2));
                    altResult.add(alt.get(indexTex*2+1));
                }
            }

            //生成顶点数组
            int size=alvResult.size();
            float[] vXYZ=new float[size];
            for(int i=0;i<size;i++)
            {
                vXYZ[i]=alvResult.get(i);
            }

            //生成法向量数组
            float[] nXYZ=new float[alFaceIndex.size()*3];
            int c=0;
            for(Integer i:alFaceIndex)
            {
                //根据当前点的索引从Map中取出一个法向量的集合
                HashSet<Normal> hsn=hmn.get(i);
                //求出平均法向量
                float[] tn=Normal.getAverage(hsn);
                //将计算出的平均法向量存放到法向量数组中
                nXYZ[c++]=tn[0];
                nXYZ[c++]=tn[1];
                nXYZ[c++]=tn[2];
            }

            //生成纹理数组
            size=altResult.size();
            float[] tST=new float[size];
            for(int i=0;i<size;i++)
            {
                tST[i]=altResult.get(i);
            }

            //创建3D物体对象
            lo=new LoadedObjectVertexNormalTexture(mv,vXYZ,nXYZ,tST);
        }
        catch(Exception e)
        {
            Log.d("load error", "load error");
            e.printStackTrace();
        }
        return lo;
    }

方法比较长,不过总的思路也很清晰,就先打开文件输入流,循环不断从文件中读取行,根据行的类型不同执行不同的处理逻辑,比如“v”开头的行代表顶点坐标数据,直接用一个数组存储,根据“f”开头的面数据行查找其3个顶点的坐标,纹理索引,最终创建加载的物体对象。这里为了求面的平均法向量,还用到了向量叉乘和求平均的方法。

//求两个向量的叉乘
    public static float[] getCrossProduct(float x1,float y1,float z1,float x2,float y2,float z2)
    {
        //求出两个矢量叉积矢量在XYZ轴的分量ABC
        float A=y1*z2-y2*z1;
        float B=z1*x2-z2*x1;
        float C=x1*y2-x2*y1;

        return new float[]{A,B,C};
    }

表示法向量的类
Normal.java

/**
 * 表示法向量的类
 */
public class Normal {
    //判断两个法向量是否相同的阈值
    public static final float DIFF = 0.0000001f;
    //法向量在X、Y、Z轴的分量
    float nx;
    float ny;
    float nz;
    public Normal(float nx , float ny , float nz ){
        this.nx = nx;
        this.ny = ny;
        this.nz = nz;
    }

    @Override
    public boolean equals(Object obj) {
        //若两个法向量X、Y、Z 3个分量的差都小于指定的阈值则认为这两个法向量相等
        if( obj instanceof Normal ){
            Normal tn = (Normal) obj;
            if( Math.abs(nx - tn.nx) < DIFF && Math.abs(ny - tn.ny) < DIFF && Math.abs(nz - tn.nz) < DIFF ){
                return true;
            }else {
                return false;
            }
        }else {
            return false;
        }
    }

    @Override
    public int hashCode() {
        return 1;
    }

    //求法向量平均值的工具方法
    public static float[] getAverage(Set<Normal> sn){
        //存放法向量X、Y、Z分量和的数组
        float[] result = new float[3];
        for( Normal n : sn ){
            result[0] += n.nx;
            result[1] += n.ny;
            result[2] += n.nz;
        }
        return LoadUtil.vectorNormal(result);

    }
}

步骤三、接收纹理数据并启用光源和纹理
LoadedObjectVertexNormalTexture.java

 //绘制物体的方法
    public void drawSelf(int texId)
    {
        //制定使用某套着色器程序
        GLES20.glUseProgram(mProgram);
        //将最终变换矩阵传入着色器程序
        GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0);
        //将位置、旋转变换矩阵传入着色器程序
        GLES20.glUniformMatrix4fv(muMMatrixHandle, 1, false, MatrixState.getMMatrix(), 0);
        //将光源位置传入着色器程序
        GLES20.glUniform3fv(maLightLocationHandle, 1, MatrixState.lightPositionFB);
        //将摄像机位置传入着色器程序
        GLES20.glUniform3fv(maCameraHandle, 1, MatrixState.cameraFB);
        // 将顶点位置数据传入渲染管线
        GLES20.glVertexAttribPointer
                (
                        maPositionHandle,
                        3,
                        GLES20.GL_FLOAT,
                        false,
                        3*4,
                        mVertexBuffer
                );
        //将顶点法向量数据传入渲染管线
        GLES20.glVertexAttribPointer
                (
                        maNormalHandle,
                        3,
                        GLES20.GL_FLOAT,
                        false,
                        3*4,
                        mNormalBuffer
                );
        //为画笔指定顶点纹理坐标数据
        GLES20.glVertexAttribPointer
                (
                        maTexCoorHandle,
                        2,
                        GLES20.GL_FLOAT,
                        false,
                        2*4,
                        mTexCoorBuffer
                );
        //启用顶点位置、法向量、纹理坐标数据
        GLES20.glEnableVertexAttribArray(maPositionHandle);
        GLES20.glEnableVertexAttribArray(maNormalHandle);
        GLES20.glEnableVertexAttribArray(maTexCoorHandle);
        //绑定纹理
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
        //绘制加载的物体
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vCount);
    }

下面是我们用来显示3D内容的GLSurfaceView

class MySurfaceView extends GLSurfaceView
{
    private final float TOUCH_SCALE_FACTOR = 180.0f/320;//角度缩放比例
    private SceneRenderer mRenderer;//场景渲染器

    private float mPreviousY;//上次的触控位置Y坐标
    private float mPreviousX;//上次的触控位置X坐标

    int textureId;//系统分配的纹理id

    public MySurfaceView(Context context) {
        super(context);
        this.setEGLContextClientVersion(2); //设置使用OPENGL ES2.0
        mRenderer = new SceneRenderer();    //创建场景渲染器
        setRenderer(mRenderer);             //设置渲染器
        setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);//设置渲染模式为主动渲染
    }

    //触摸事件回调方法
    @Override
    public boolean onTouchEvent(MotionEvent e)
    {
        float y = e.getY();
        float x = e.getX();
        switch (e.getAction()) {
            case MotionEvent.ACTION_MOVE:
                float dy = y - mPreviousY;//计算触控笔Y位移
                float dx = x - mPreviousX;//计算触控笔X位移
                mRenderer.yAngle += dx * TOUCH_SCALE_FACTOR;//设置沿x轴旋转角度
                mRenderer.xAngle+= dy * TOUCH_SCALE_FACTOR;//设置沿z轴旋转角度
                requestRender();//重绘画面
        }
        mPreviousY = y;//记录触控笔位置
        mPreviousX = x;//记录触控笔位置
        return true;
    }

    private class SceneRenderer implements GLSurfaceView.Renderer
    {
        float yAngle;//绕Y轴旋转的角度
        float xAngle; //绕Z轴旋转的角度
        //从指定的obj文件中加载对象
        LoadedObjectVertexNormalTexture lovo;

        public void onDrawFrame(GL10 gl)
        {
            //清除深度缓冲与颜色缓冲
            GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);

            //坐标系推远
            MatrixState.pushMatrix();
            MatrixState.translate(0, -2f, -25f);   //ch.obj
            //绕Y轴、Z轴旋转
            MatrixState.rotate(yAngle, 0, 1, 0);
            MatrixState.rotate(xAngle, 1, 0, 0);

            //若加载的物体部位空则绘制物体
            if(lovo!=null)
            {
                lovo.drawSelf(textureId);
            }
            MatrixState.popMatrix();
        }

        public void onSurfaceChanged(GL10 gl, int width, int height) {
            //设置视窗大小及位置
            GLES20.glViewport(0, 0, width, height);
            //计算GLSurfaceView的宽高比
            float ratio = (float) width / height;
            //调用此方法计算产生透视投影矩阵
            MatrixState.setProjectFrustum(-ratio, ratio, -1, 1, 2, 500);
            //调用此方法产生摄像机9参数位置矩阵
            MatrixState.setCamera(0,0,50,0f,0f,-20f,0f,1.0f,0.0f);
        }

        public void onSurfaceCreated(GL10 gl, EGLConfig config)
        {
            //设置屏幕背景色RGBA
            GLES20.glClearColor(0.0f,0.0f,0.0f,1.0f);
            //打开深度检测
            GLES20.glEnable(GLES20.GL_DEPTH_TEST);
            //打开背面剪裁
            //GLES20.glEnable(GLES20.GL_CULL_FACE);
            //初始化变换矩阵
            MatrixState.setInitStack();
            //初始化光源位置
            MatrixState.setLightLocation(40, 40, 40);
            //加载要绘制的物体
            lovo=LoadUtil.loadFromFile("hat.obj", MySurfaceView.this.getResources(),MySurfaceView.this);
            //加载纹理
            textureId=initTexture(R.drawable.hat_t);
        }
    }
    public int initTexture(int drawableId)//textureId
    {
        //生成纹理ID
        int[] textures = new int[1];
        GLES20.glGenTextures
                (
                        1,          //产生的纹理id的数量
                        textures,   //纹理id的数组
                        0           //偏移量
                );
        int textureId=textures[0];
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_REPEAT);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_REPEAT);

        //通过输入流加载图片===============begin===================
        InputStream is = this.getResources().openRawResource(drawableId);
        Bitmap bitmapTmp;
        try
        {
            bitmapTmp = BitmapFactory.decodeStream(is);
        }
        finally
        {
            try
            {
                is.close();
            }
            catch(IOException e)
            {
                e.printStackTrace();
            }
        }
        //通过输入流加载图片===============end=====================
        GLUtils.texImage2D
                (
                        GLES20.GL_TEXTURE_2D, //纹理类型
                        0,
                        GLUtils.getInternalFormat(bitmapTmp),
                        bitmapTmp, //纹理图像
                        GLUtils.getType(bitmapTmp),
                        0 //纹理边框尺寸
                );
        bitmapTmp.recycle();          //纹理加载成功后释放图片
        return textureId;
    }

这里面用到了一个矩阵变换和保存矩阵状态的类MatrixState
MatrixState.java

//存储系统矩阵状态的类
public class MatrixState
{
    private static float[] mProjMatrix = new float[16];//4x4矩阵 投影用
    private static float[] mVMatrix = new float[16];//摄像机位置朝向9参数矩阵
    private static float[] currMatrix;//当前变换矩阵
    public static float[] lightLocation=new float[]{0,0,0};//定位光光源位置
    public static FloatBuffer cameraFB;
    public static FloatBuffer lightPositionFB;

    public static Stack<float[]> mStack=new Stack<float[]>();//保护变换矩阵的栈

    public static void setInitStack()//获取不变换初始矩阵
    {
        currMatrix=new float[16];
        Matrix.setRotateM(currMatrix, 0, 0, 1, 0, 0);
    }

    public static void pushMatrix()//保护变换矩阵
    {
        mStack.push(currMatrix.clone());
    }

    public static void popMatrix()//恢复变换矩阵
    {
        currMatrix=mStack.pop();
    }

    public static void translate(float x,float y,float z)//设置沿xyz轴移动
    {
        Matrix.translateM(currMatrix, 0, x, y, z);
    }

    public static void rotate(float angle,float x,float y,float z)//设置绕xyz轴移动
    {
        Matrix.rotateM(currMatrix,0,angle,x,y,z);
    }


    //设置摄像机
    public static void setCamera
    (
            float cx,   //摄像机位置x
            float cy,   //摄像机位置y
            float cz,   //摄像机位置z
            float tx,   //摄像机目标点x
            float ty,   //摄像机目标点y
            float tz,   //摄像机目标点z
            float upx,  //摄像机UP向量X分量
            float upy,  //摄像机UP向量Y分量
            float upz   //摄像机UP向量Z分量
    )
    {
        Matrix.setLookAtM
                (
                        mVMatrix,
                        0,
                        cx,
                        cy,
                        cz,
                        tx,
                        ty,
                        tz,
                        upx,
                        upy,
                        upz
                );

        float[] cameraLocation=new float[3];//摄像机位置
        cameraLocation[0]=cx;
        cameraLocation[1]=cy;
        cameraLocation[2]=cz;

        ByteBuffer llbb = ByteBuffer.allocateDirect(3*4);
        llbb.order(ByteOrder.nativeOrder());//设置字节顺序
        cameraFB=llbb.asFloatBuffer();
        cameraFB.put(cameraLocation);
        cameraFB.position(0);
    }

    //设置透视投影参数
    public static void setProjectFrustum
    (
            float left,     //near面的left
            float right,    //near面的right
            float bottom,   //near面的bottom
            float top,      //near面的top
            float near,     //near面距离
            float far       //far面距离
    )
    {
        Matrix.frustumM(mProjMatrix, 0, left, right, bottom, top, near, far);
    }

    //设置正交投影参数
    public static void setProjectOrtho
    (
            float left,     //near面的left
            float right,    //near面的right
            float bottom,   //near面的bottom
            float top,      //near面的top
            float near,     //near面距离
            float far       //far面距离
    )
    {
        Matrix.orthoM(mProjMatrix, 0, left, right, bottom, top, near, far);
    }

    //获取具体物体的总变换矩阵
    public static float[] getFinalMatrix()
    {
        float[] mMVPMatrix=new float[16];
        Matrix.multiplyMM(mMVPMatrix, 0, mVMatrix, 0, currMatrix, 0);
        Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mMVPMatrix, 0);
        return mMVPMatrix;
    }

    //获取具体物体的变换矩阵
    public static float[] getMMatrix()
    {
        return currMatrix;
    }

    //设置灯光位置的方法
    public static void setLightLocation(float x,float y,float z)
    {
        lightLocation[0]=x;
        lightLocation[1]=y;
        lightLocation[2]=z;
        ByteBuffer llbb = ByteBuffer.allocateDirect(3*4);
        llbb.order(ByteOrder.nativeOrder());//设置字节顺序
        lightPositionFB=llbb.asFloatBuffer();
        lightPositionFB.put(lightLocation);
        lightPositionFB.position(0);
    }
}

最后不要忘了编写我们的顶点着色器和片元着色器,和我文章里之前写过的那些着色器程序基本类似,都是正常的格式,主要注意添加光源和光照模型,比较简单,这里就不再赘述了。

最终的效果如下所示:
这里写图片描述
是不是挺酷炫的?

  • 5
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
实现三个骰子的摇动动画可以使用Android的动画系统,具体步骤如下: 1. 首先定义三个ImageView控件,分别表示三个骰子。 2. 在res目录下创建一个anim文件夹,用于存放动画文件。 3. 在anim文件夹下创建三个xml文件,分别表示三个骰子的摇动动画。 4. 编辑每个xml文件,实现骰子的摇动动画,例如: ```xml <!-- 骰子1的摇动动画 --> <set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_interpolator"> <rotate android:duration="100" android:fromDegrees="0" android:pivotX="50%" android:pivotY="50%" android:repeatCount="5" android:repeatMode="reverse" android:toDegrees="30" /> <rotate android:duration="100" android:fromDegrees="30" android:pivotX="50%" android:pivotY="50%" android:repeatCount="5" android:repeatMode="reverse" android:toDegrees="-30" /> <rotate android:duration="100" android:fromDegrees="-30" android:pivotX="50%" android:pivotY="50%" android:repeatCount="5" android:repeatMode="reverse" android:toDegrees="0" /> </set> ``` 5. 在代码中使用AnimationUtils动画,并将动画应用到每个ImageView控件上,例如: ```java // 动画文件 Animation anim1 = AnimationUtils.loadAnimation(this, R.anim.anim_dice1); Animation anim2 = AnimationUtils.loadAnimation(this, R.anim.anim_dice2); Animation anim3 = AnimationUtils.loadAnimation(this, R.anim.anim_dice3); // 应用动画到ImageView控件 ImageView dice1 = findViewById(R.id.dice1); ImageView dice2 = findViewById(R.id.dice2); ImageView dice3 = findViewById(R.id.dice3); dice1.startAnimation(anim1); dice2.startAnimation(anim2); dice3.startAnimation(anim3); ``` 至此,就可以实现三个骰子的摇动动画。 如果需要使用OpenGL ES实现3D抛骰子,可以参考以下步骤: 1. 在res目录下创建一个raw文件夹,用于存放骰子模型的obj文件和材质文件。 2. 使用OpenGL ES骰子模型,可以使用第三方库如OBJParser或者自行编写解析代码。 3. 实现骰子的抛掷动画,可以使用随机数生成每个骰子的旋转角度,然后使用OpenGL ES的旋转功能实现。 4. 实现骰子的投掷效果,可以使用物理引擎如Bullet Physics或者自行编写物理模拟代码。 5. 在SurfaceView中绘制骰子模型,并在onDrawFrame方法中更新骰子的位置和旋转角度。
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值