Android OpenGL ES2.0从放弃到入门(五)——绘制3D模型(obj+mtl)

之前我们绘制的都是规则的几何图形,今天我们根据3D模型,来绘制现实中的物体,首先看一下我们的实际效果
在这里插入图片描述

了解obj文件

在我们使用之前,先了解下什么是obj文件。

obj文件是3D模型文件格式。由Alias|Wavefront公司为3D建模和动画软件"Advanced Visualizer"开发的一种标准,适合用于3D软件模型之间的互导,也可以通过Maya读写。

它是一种可以保存3D模型信息的文件,我们可以举个例子看看他内部都可以保存哪些信息。

# mtl材质文件
 mtllib testvt.mtl

# o 对象名称(Object name)
o adfaf

# 组名称
g default

# 顶点
v 0 0.5 0
v -0.5 -0.5 0
v 0.5 -0.5 0

# 纹理坐标
vt 0.0 1.0
vt 0.0 0.0
vt 1.0 1.0

# 顶点法线
vn 0 0 1
# 当前图元所用材质
usemtl Default
s off
f 1/1/1 2/2/1 3/3/1

以上为obj文件的大致格式。

  1. # 开头的为注释
  2. v 表示本行指定一个顶点。 前缀后跟着3个单精度浮点数,分别表示该定点的X、Y、Z坐标值
  3. vt 表示本行指定一个纹理坐标。此前缀后跟着两个单精度浮点数。分别表示此纹理坐标的U、V值
  4. vn 表示本行指定一个法线向量。此前缀后跟着3个单精度浮点数,分别表示该法向量的X、Y、Z坐标值
  5. f 表示本行指定一个表面(Face)。一个表面实际上就是一个三角形图元
  6. usemtl 此前缀后只跟着一个参数。该参数指定了从此行之后到下一个以usemtl开头的行之间的所有表面所使用的材质名称。该材质可以在此OBJ文件所附属的MTL文件中找到具体信息。
  7. mtllib 此前缀后只跟着一个参数。该参数指定了此OBJ文件所使用的材质库文件(*.mtl)的文件路径

当然obj的文件格式可不止这些,这里列出来我们常见的一些格式,想了解详细的童鞋,可移步大神的文章3D中的OBJ文件格式详解

有一点值得注意的是,之前我们介绍过,OpenGL ES相比OpenGL舍弃了很多图形绘制,任何的事物都是由三角形绘制而成。obj文件是通过专业的3D模型绘制软件形成的,在电脑上制作的文件,可不一定都是依照三角形去绘制的,而且每个模型数据也存在差异。所以我们的f标签可能存在不同的格式,这里列举下

  • f 1 2 3 这样的行表示以第1、2、3号顶点组成一个三角形。
  • f 1/3 2/5 3/4 这样的行表示以第1、2、3号顶点组成一个三角形,其中第一个顶点的纹理坐标的索引值为3,第二个顶点的纹理坐标的索引值为5,第三个顶点的纹理坐标的索引值为4。
  • f 1/3/4 2/5/6 3/4/2 这样的行表示以第1、2、3号顶点组成一个三角形,其中第一个顶点的纹理坐标的索引值为3,其法线的索引值是4;第二个顶点的纹理坐标的索引值为5,其法线的索引值是6;第三个顶点的纹理坐标的索引值为6,其法线的索引值是2。
  • f 1//4 2//6 3//2这样的行表示以第1、2、3号顶点组成一个三角形,且忽略纹理坐标。其中第一个顶点的法线的索引值是4;第二个顶点的法线的索引值是6;第三个顶点的法线的索引值是2。
  • f 1 2 3 4 同第一种情况,只不过,此处为四边形,而不是三角形。

上述的索引坐标都是从1开始,这里与我们写代码从0开始有些不同

了解mtl文件

我们了解了obj文件后,发现它并没有保存我们物体的颜色。我们物体的颜色保存在mtl文件中。

# 定义一个名为 'Default'的材质
newmtl Default
#exponent指定材质的反射指数,定义了反射高光度
Ns 96.078431
# 材质的环境光
Ka 0 0 0
# 散射光
Kd 0.784314 0.784314 0.784314
# 镜面光
Ks 0 0 0

# 透明度
d 1

# 为漫反射指定颜色纹理文件
map_Kd test_vt.png
map_Ka picture1.png #阴影纹理贴图
map_Ks picture2.png #高光纹理贴图
illum 2 #光照模型

#光照模型属性如下:
 #0. 色彩开,阴影色关
 #1. 色彩开,阴影色开
 #2. 高光开
 #3. 反射开,光线追踪开
 #4. 透明: 玻璃开 反射:光线追踪开
 #5. 反射:菲涅尔衍射开,光线追踪开
 #6. 透明:折射开 反射:菲涅尔衍射关,光线追踪开
 #7. 透明:折射开 反射:菲涅尔衍射开,光线追踪开
 #8. 反射开,光线追踪关
 #9. 透明: 玻璃开 反射:光线追踪关
 #10. 投射阴影于不可见表面

以上是我们mtl文件的大致格式。

模型解析

知道了文件的格式,我们就要开始解析我们的3D文件了。首先我们建两个类,分别保存我们解析到了obj和mtl的文件信息。

	public class ObjInfo {
    /**
     * 对象名称
     */
    public String name;
    /**
     * 材质
     */
    public MtlInfo mtlData;
    /**
     * 顶点、纹理、法向量一一对应后的数据
     */
    public float[] aVertices;
    // 顶点纹理可能会没有
    public float[] aTexCoords;
    public float[] aNormals;

    /**
     * index数组(顶点、纹理、法向量一一对应后,以下三个列表会清空)
     */
    // 顶点index数组
    public ArrayList<Integer> vertexIndices = new ArrayList<Integer>();
    // 纹理index数组
    public ArrayList<Integer> texCoordIndices = new ArrayList<Integer>();
    // 法向量index数组
    public ArrayList<Integer> normalIndices = new ArrayList<Integer>();
    }
	public class MtlInfo {
    // 材质对象名称
    public String name;
    // 环境光
    public int Ka_Color;
    // 散射光
    public int Kd_Color;
    // 镜面光
    public int Ks_Color;
    // 高光调整参数
    public float ns;
    // 溶解度,为0时完全透明,1完全不透明
    public float alpha = 1f;
    // map_Ka,map_Kd,map_Ks:材质的环境(ambient),散射(diffuse)和镜面(specular)贴图
    public String Ka_Texture;
    public String Kd_Texture;
    public String Ks_ColorTexture;
    public String Ns_Texture;
    public String alphaTexture;
    public String bumpTexture;
    }

有了保存的实体类,接下来我们就来用代码解析obj和mtl文件

public class ObjLoaderUtil {

    private static final String TAG = "ObjLoaderUtil";

    /**
     * 解析
     *
     * @param fname assets的obj文件路径
     * @param res   Resources
     * @return
     */
    public static ArrayList<ObjInfo> load(String fname, Resources res) throws Exception {
        // 返回的数据列表
        ArrayList<ObjInfo> objectList = new ArrayList<ObjInfo>();
        if (res == null || TextUtils.isEmpty(fname)) {
            return objectList;
        }
        /**
         * 所有顶点信息
         */
        // 顶点数据
        ArrayList<Float> vertices = new ArrayList<Float>();
        // 纹理数据
        ArrayList<Float> texCoords = new ArrayList<Float>();
        // 法向量数据
        ArrayList<Float> normals = new ArrayList<Float>();
        // 全部材质列表
        HashMap<String, MtlInfo> mtlMap = null;
        // Ojb索引数据
        ObjInfo currObjInfo = new ObjInfo();
        // 当前材质名称
        String currMaterialName = null;
        // 是否有面数据的标识
        boolean currObjHasFaces = false;

        try {
            // 每一行的信息
            String line = null;
            // 读取assets下文件
            InputStream in = res.getAssets().open(fname);
            InputStreamReader isr = new InputStreamReader(in);
            BufferedReader buffer = new BufferedReader(isr);
            // 循环读取每一行的数据
            while ((line = buffer.readLine()) != null) {
                // 忽略 空行和注释
                if (line.length() == 0 || line.charAt(0) == '#') {
                    continue;
                }
                // 以空格分割String
                StringTokenizer parts = new StringTokenizer(line, " ");
                int numTokens = parts.countTokens();
                if (numTokens == 0) {
                    continue;
                }
                // 打头的字符
                String type = parts.nextToken();
                switch (type) {
                    case ObjLoaderUtil.MTLLIB:
                        // 材质
                        if (!parts.hasMoreTokens()) {
                            continue;
                        }
                        // 需要重命名材质文件,暂定同一路径下(goku/goku.mtl)
                        String materialLibPath = "" + parts.nextToken();
                        if (TextUtils.isEmpty(materialLibPath) == false) {
                            mtlMap = MtlLoaderUtil.load(materialLibPath, res);
                        }
                        break;
                    case ObjLoaderUtil.O:
                        // 对象名称
                        String objName = parts.hasMoreTokens() ? parts.nextToken() : "def";
                        // 面数据
                        if (currObjHasFaces) {
                            // 添加到数组中
                            objectList.add(currObjInfo);
                            // 创建新的索引对象
                            currObjInfo = new ObjInfo();
                            currObjHasFaces = false;
                        }
                        currObjInfo.name = objName;
                        // 对应材质
                        if (TextUtils.isEmpty(currMaterialName) == false && mtlMap != null) {
                            currObjInfo.mtlData = mtlMap.get(currMaterialName);
                        }
                        break;
                    case ObjLoaderUtil.V:
                        //顶点
                        vertices.add(Float.parseFloat(parts.nextToken()));
                        vertices.add(Float.parseFloat(parts.nextToken()));
                        vertices.add(Float.parseFloat(parts.nextToken()));
                        break;
                    case ObjLoaderUtil.VT:
                        // 纹理
                        // 这里纹理的Y值,需要(Y = 1-Y0),原因是openGl的纹理坐标系与android的坐标系存在Y值镜像的状态
                        texCoords.add(Float.parseFloat(parts.nextToken()));
                        texCoords.add(1f - Float.parseFloat(parts.nextToken()));
                        break;
                    case ObjLoaderUtil.VN:
                        // 法向量
                        normals.add(Float.parseFloat(parts.nextToken()));
                        normals.add(Float.parseFloat(parts.nextToken()));
                        normals.add(Float.parseFloat(parts.nextToken()));
                        break;
                    case ObjLoaderUtil.USEMTL:
                        // 使用材质
                        // 材质名称
                        currMaterialName = parts.nextToken();
                        if (currObjHasFaces) {
                            // 添加到数组中
                            objectList.add(currObjInfo);
                            // 创建一个index对象
                            currObjInfo = new ObjInfo();
                            currObjHasFaces = false;
                        }
                        // 材质名称
                        if (TextUtils.isEmpty(currMaterialName) == false && mtlMap != null) {
                            currObjInfo.mtlData = mtlMap.get(currMaterialName);
                        }
                        break;
                    case ObjLoaderUtil.F:
                        // "f"面属性  索引数组
                        // 当前obj对象有面数据
                        currObjHasFaces = true;
                        // 是否为矩形(android 均为三角形,这里暂时先忽略多边形的情况)
                        boolean isQuad = numTokens == 5;
                        int[] quadvids = new int[4];
                        int[] quadtids = new int[4];
                        int[] quadnids = new int[4];

                        // 如果含有"//" 替换
                        boolean emptyVt = line.indexOf("//") > -1;
                        if (emptyVt) {
                            line = line.replace("//", "/");
                        }
                        // "f 103/1/1 104/2/1 113/3/1"以" "分割
                        parts = new StringTokenizer(line);
                        // “f”
                        parts.nextToken();
                        // "103/1/1 104/2/1 113/3/1"再以"/"分割
                        StringTokenizer subParts = new StringTokenizer(parts.nextToken(), "/");
                        int partLength = subParts.countTokens();

                        // 纹理数据
                        boolean hasuv = partLength >= 2 && !emptyVt;
                        // 法向量数据
                        boolean hasn = partLength == 3 || (partLength == 2 && emptyVt);
                        // 索引index
                        int idx;
                        for (int i = 1; i < numTokens; i++) {
                            if (i > 1) {
                                subParts = new StringTokenizer(parts.nextToken(), "/");
                            }
                            // 顶点索引
                            idx = Integer.parseInt(subParts.nextToken());
                            if (idx < 0) {
                                idx = (vertices.size() / 3) + idx;
                            } else {
                                idx -= 1;
                            }
                            if (!isQuad) {
                                currObjInfo.vertexIndices.add(idx);
                            } else {
                                quadvids[i - 1] = idx;
                            }
                            // 纹理索引
                            if (hasuv) {
                                idx = Integer.parseInt(subParts.nextToken());
                                if (idx < 0) {
                                    idx = (texCoords.size() / 2) + idx;
                                } else {
                                    idx -= 1;
                                }
                                if (!isQuad) {
                                    currObjInfo.texCoordIndices.add(idx);
                                } else {
                                    quadtids[i - 1] = idx;
                                }
                            }
                            // 法向量数据
                            if (hasn) {
                                idx = Integer.parseInt(subParts.nextToken());
                                if (idx < 0) {
                                    idx = (normals.size() / 3) + idx;
                                } else {
                                    idx -= 1;
                                }
                                if (!isQuad) {
                                    currObjInfo.normalIndices.add(idx);
                                } else {
                                    quadnids[i - 1] = idx;
                                }
                            }
                        }
                        // 如果是多边形
                        if (isQuad) {
                            int[] indices = new int[]{0, 1, 2, 0, 2, 3};
                            for (int i = 0; i < 6; ++i) {
                                int index = indices[i];
                                currObjInfo.vertexIndices.add(quadvids[index]);
                                currObjInfo.texCoordIndices.add(quadtids[index]);
                                currObjInfo.normalIndices.add(quadnids[index]);
                            }
                        }
                        break;
                    default:
                        break;
                }

            }
            //
            buffer.close();
            // 存在索引面数据,添加到index列表中
            if (currObjHasFaces) {
                // 添加到数组中
                objectList.add(currObjInfo);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception(e.getMessage(), e.getCause());
        }
        //###############################顶点、法向量、纹理一一对应#################################
        // 循环索引对象列表
        int numObjects = objectList.size();
        for (int j = 0; j < numObjects; ++j) {
            ObjInfo ObjInfo = objectList.get(j);

            int i;
            // 顶点数据 初始化
            float[] aVertices = new float[ObjInfo.vertexIndices.size() * 3];
            // 顶点纹理数据 初始化
            float[] aTexCoords = new float[ObjInfo.texCoordIndices.size() * 2];
            // 顶点法向量数据 初始化
            float[] aNormals = new float[ObjInfo.normalIndices.size() * 3];
            // 按照索引,重新组织顶点数据
            for (i = 0; i < ObjInfo.vertexIndices.size(); ++i) {
                // 顶点索引,三个一组做为一个三角形
                int faceIndex = ObjInfo.vertexIndices.get(i) * 3;
                int vertexIndex = i * 3;
                try {
                    // 按照索引,重新组织顶点数据
                    aVertices[vertexIndex] = vertices.get(faceIndex);
                    aVertices[vertexIndex + 1] = vertices.get(faceIndex + 1);
                    aVertices[vertexIndex + 2] = vertices.get(faceIndex + 2);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            // 按照索引组织 纹理数据
            if (texCoords != null && texCoords.size() > 0) {
                for (i = 0; i < ObjInfo.texCoordIndices.size(); ++i) {
                    int texCoordIndex = ObjInfo.texCoordIndices.get(i) * 2;
                    int ti = i * 2;
                    aTexCoords[ti] = texCoords.get(texCoordIndex);
                    aTexCoords[ti + 1] = texCoords.get(texCoordIndex + 1);
                }
            }
            // 按照索引组织 法向量数据
            for (i = 0; i < ObjInfo.normalIndices.size(); ++i) {
                int normalIndex = ObjInfo.normalIndices.get(i) * 3;
                int ni = i * 3;
                if (normals.size() == 0) {
                    throw new Exception("There are no normals specified for this model. Please re-export with normals.");
                }
                aNormals[ni] = normals.get(normalIndex);
                aNormals[ni + 1] = normals.get(normalIndex + 1);
                aNormals[ni + 2] = normals.get(normalIndex + 2);
            }
            // 数据设置到oid.targetObj中
            ObjInfo.aVertices = aVertices;
            ObjInfo.aTexCoords = aTexCoords;
            ObjInfo.aNormals = aNormals;
            //
            if (ObjInfo.vertexIndices != null) {
                ObjInfo.vertexIndices.clear();
            }
            if (ObjInfo.texCoordIndices != null) {
                ObjInfo.texCoordIndices.clear();
            }
            if (ObjInfo.normalIndices != null) {
                ObjInfo.normalIndices.clear();
            }
        }
        return objectList;
    }

    /**
     * obj需解析字段
     */
    // obj对应的材质文件
    private static final String MTLLIB = "mtllib";
    // 组名称
    private static final String G = "g";
    // o 对象名称(Object name)
    private static final String O = "o";
    // 顶点
    private static final String V = "v";
    // 纹理坐标
    private static final String VT = "vt";
    // 顶点法线
    private static final String VN = "vn";
    // 使用的材质
    private static final String USEMTL = "usemtl";
    // v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3(索引起始于1)
    private static final String F = "f";
}

有个地方值得注意,在我们解vt字段的时候,我们最终得到的是Y = 1-Y0,个人猜测:在纹理坐标系中,坐标原点是左下角(0,0),而在android等硬件设备中,屏幕的坐标系原点是在左上角(0,0),两者成一种镜像的感觉。那么当OpenGL的纹理贴图映射到android屏幕上的时候,y的值就要有所变化。

public class MtlLoaderUtil {

    private static final String TAG = "MtlLoaderUtil";
    /**
     * 加载材质的方法
     *
     * @param fname assets的mtl文件路径
     * @param res
     * @return
     */
    public static HashMap<String, MtlInfo> load(String fname, Resources res) throws Exception {
        // 材质数组
        HashMap<String, MtlInfo> mMTLMap = new HashMap<String, MtlInfo>();
        //
        if (res == null || TextUtils.isEmpty(fname)) {
            return mMTLMap;
        }
        //
        MtlInfo currMtlInfo = null;
        try {
            // 读取assets下文件
            InputStream in = res.getAssets().open(fname);
            InputStreamReader isr = new InputStreamReader(in);
            BufferedReader buffer = new BufferedReader(isr);
            // 行数据
            String line;
            //
            while ((line = buffer.readLine()) != null) {
                // Skip comments and empty lines.
                if (line.length() == 0 || line.charAt(0) == '#') {
                    continue;
                }
                //
                StringTokenizer parts = new StringTokenizer(line, " ");
                int numTokens = parts.countTokens();
                if (numTokens == 0) {
                    continue;
                }
                //
                String type = parts.nextToken();
                type = type.replaceAll("\\t", "");
                type = type.replaceAll(" ", "");
                switch (type) {
                    case MtlLoaderUtil.NEWMTL:
                        // 定义一个名为 'xxx'的材质
                        String name = parts.hasMoreTokens() ? parts.nextToken() : "def";
                        // 将上一个对象加入到列表中
                        if (currMtlInfo != null) {
                            mMTLMap.put(currMtlInfo.name, currMtlInfo);
                        }
                        // 创建材质对象
                        currMtlInfo = new MtlInfo();
                        // 材质对象名称
                        currMtlInfo.name = name;
                        break;
                    case MtlLoaderUtil.KA:
                        // 环境光
                        currMtlInfo.Ka_Color = getColorFromParts(parts);
                        break;
                    case MtlLoaderUtil.KD:
                        // 散射光
                        currMtlInfo.Kd_Color = getColorFromParts(parts);
                        break;
                    case MtlLoaderUtil.KS:
                        // 镜面光
                        currMtlInfo.Ks_Color = getColorFromParts(parts);
                        break;
                    case MtlLoaderUtil.NS:
                        // 高光调整参数
                        String ns = parts.nextToken();
                        currMtlInfo.ns = Float.parseFloat(ns);
                        break;
                    case MtlLoaderUtil.D:
                        // 溶解度,为0时完全透明,1完全不透明
                        currMtlInfo.alpha = Float.parseFloat(parts.nextToken());
                        break;
                    case MtlLoaderUtil.MAP_KA:
                        currMtlInfo.Ka_Texture = parts.nextToken();
                        break;
                    case MtlLoaderUtil.MAP_KD:
                        currMtlInfo.Kd_Texture = parts.nextToken();
                        break;
                    case MtlLoaderUtil.MAP_KS:
                        currMtlInfo.Ks_ColorTexture = parts.nextToken();
                        break;
                    case MtlLoaderUtil.MAP_NS:
                        currMtlInfo.Ns_Texture = parts.nextToken();
                        break;
                    case MtlLoaderUtil.MAP_D:
                    case MtlLoaderUtil.MAP_TR:
                        currMtlInfo.alphaTexture = parts.nextToken();
                        break;
                    case MtlLoaderUtil.MAP_BUMP:
                        currMtlInfo.bumpTexture = parts.nextToken();
                        break;
                    default:
                        break;
                }
            }
            if (currMtlInfo != null) {
                mMTLMap.put(currMtlInfo.name, currMtlInfo);
            }
            buffer.close();
        } catch (Exception e) {
            Log.e(TAG, e.getMessage());
            throw new Exception(e.getMessage(), e.getCause());
        }
        return mMTLMap;
  
    /**
     * 材质需解析字段
     */
    // 定义一个名为 'xxx'的材质
    private static final String NEWMTL = "newmtl";
    // 材质的环境光(ambient color)
    private static final String KA = "Ka";
    // 散射光(diffuse color)用Kd
    private static final String KD = "Kd";
    // 镜面光(specular color)用Ks
    private static final String KS = "Ks";
    // 反射指数 定义了反射高光度。该值越高则高光越密集,一般取值范围在0~1000。
    private static final String NS = "Ns";
    // 渐隐指数描述 参数factor表示物体融入背景的数量,取值范围为0.0~1.0,取值为1.0表示完全不透明,取值为0.0时表示完全透明。
    private static final String D = "d";
    // 滤光透射率
    private static final String TR = "Tr";
    // map_Ka,map_Kd,map_Ks:材质的环境(ambient),散射(diffuse)和镜面(specular)贴图
    private static final String MAP_KA = "map_Ka";
    private static final String MAP_KD = "map_Kd";
    private static final String MAP_KS = "map_Ks";
    private static final String MAP_NS = "map_Ns";
    private static final String MAP_D = "map_d";
    private static final String MAP_TR = "map_Tr";
    private static final String MAP_BUMP = "map_Bump";

    /**
     * 返回一个oxffffffff格式的颜色值
     *
     * @param parts
     * @return
     */
    private static int getColorFromParts(StringTokenizer parts) {
        int r = (int) (Float.parseFloat(parts.nextToken()) * 255f);
        int g = (int) (Float.parseFloat(parts.nextToken()) * 255f);
        int b = (int) (Float.parseFloat(parts.nextToken()) * 255f);
        return Color.rgb(r, g, b);
    }
}

以上两个工具类看着有点长,实际上就是对文件中的各个字段进行解析,实际应用的话可以直接粘贴复制过去。

构建模型类

咋一看于和上面的似乎有冲突,解析的出来的类不就是模型吗?解析出来的只是我们对接3D文件的,对我们业务上我们还需要再创建些类去保存他们。举个例子,假设我们想去绘制一个人,我就可以去创建一个保存人信息的类。但是,这个人不一定是一笔画完的,他可能会由很多部分组成,有脑袋有身子胳膊有腿,这些部件每个的纹理也都是不一样的,那么这些部件我们是不是也应该弄个类去保存他,这些所有的部件拼装在一起,才是个人。
接下来我们创建个整体物体类:

public class GLGroup {
    private static final String TAG = "GLGroup";

    /**
     * 上下文对象
     */
    private PlaneGlSurfaceView mBaseScene = null;

    /**
     * 构造方法
     *
     * @param scene
     */
    public GLGroup(PlaneGlSurfaceView scene) {
        this.mBaseScene = scene;

    }

    /**
     * 获取上下文对象
     *
     * @return
     */
    public PlaneGlSurfaceView getBaseScene() {
        return mBaseScene;
    }

    /**
     * 物体的属性值
     */
    // 缩放大小
    protected float mSpriteScale = 1f;
    // alpha数值
    protected float mSpriteAlpha = 1;
    // 旋转
    protected float mSpriteAngleX = 0;
    protected float mSpriteAngleY = 0;
    protected float mSpriteAngleZ = 0;

    public float getSpriteScale() {
        return mSpriteScale;
    }

    public void setSpriteScale(float mSpriteScale) {
        this.mSpriteScale = mSpriteScale;
    }

    public float getSpriteAlpha() {
        return mSpriteAlpha;
    }

    public void setSpriteAlpha(float mSpriteAlpha) {
        this.mSpriteAlpha = mSpriteAlpha;
    }

    public float getSpriteAngleX() {
        return mSpriteAngleX;
    }

    public void setSpriteAngleX(float mSpriteAngleX) {
        this.mSpriteAngleX = mSpriteAngleX;
    }

    public float getSpriteAngleY() {
        return mSpriteAngleY;
    }

    public void setSpriteAngleY(float mSpriteAngleY) {
        this.mSpriteAngleY = mSpriteAngleY;
    }

    public float getSpriteAngleZ() {
        return mSpriteAngleZ;
    }

    public void setSpriteAngleZ(float mSpriteAngleZ) {
        this.mSpriteAngleZ = mSpriteAngleZ;
    }

    /**
     * 绘制方法
     */
    public void onDraw(MatrixState matrixState) 

    }
}

接下来我们创建个保存物体各个“部件"的类

public class GLEntity {
    public void onDraw(MatrixState matrixState) {

    }
}

以上均为基础类,我们绘制具体物体继承即可。

顶点着色器和片元着色器

之前我们绘制图形的时候也写过,不过都是些简单的,但是想绘制现实中的物体可就不那么简单了,需要很复杂的光照计算

顶点着色器:

uniform mat4 uMVPMatrix; //总变换矩阵
uniform mat4 uMMatrix; //变换矩阵
uniform vec3 uLightLocation;	//光源位置
uniform vec3 uCamera;	//摄像机位置
attribute vec3 aPosition;  //顶点位置
attribute vec3 aNormal;    //顶点法向量
attribute vec2 aTexCoor;    //顶点纹理坐标
//用于传递给片元着色器的变量
varying vec4 ambient;
varying vec4 diffuse;
varying vec4 specular;
varying vec2 vTextureCoord;
//定位光光照计算的方法
void pointLight(					//定位光光照计算的方法
  in vec3 normal,				//法向量
  inout vec4 ambient,			//环境光最终强度
  inout vec4 diffuse,				//散射光最终强度
  inout vec4 specular,			//镜面光最终强度
  in vec3 lightLocation,			//光源位置
  in vec4 lightAmbient,			//环境光强度
  in vec4 lightDiffuse,			//散射光强度
  in vec4 lightSpecular			//镜面光强度
){
  ambient=lightAmbient;			//直接得出环境光的最终强度
  vec3 normalTarget=aPosition+normal;	//计算变换后的法向量
  vec3 newNormal=(uMMatrix*vec4(normalTarget,1)).xyz-(uMMatrix*vec4(aPosition,1)).xyz;
  newNormal=normalize(newNormal); 	//对法向量规格化
  //计算从表面点到摄像机的向量
  vec3 eye= normalize(uCamera-(uMMatrix*vec4(aPosition,1)).xyz);
  //计算从表面点到光源位置的向量vp
  vec3 vp= normalize(lightLocation-(uMMatrix*vec4(aPosition,1)).xyz);
  vp=normalize(vp);//格式化vp
  vec3 halfVector=normalize(vp+eye);	//求视线与光线的半向量
  float shininess=50.0;				//粗糙度,越小越光滑
  float nDotViewPosition=max(0.0,dot(newNormal,vp)); 	//求法向量与vp的点积与0的最大值
  diffuse=lightDiffuse*nDotViewPosition;				//计算散射光的最终强度
  float nDotViewHalfVector=dot(newNormal,halfVector);	//法线与半向量的点积
  float powerFactor=max(0.0,pow(nDotViewHalfVector,shininess)); 	//镜面反射光强度因子
  specular=lightSpecular*powerFactor;    			//计算镜面光的最终强度
}

void main()
{
   gl_Position = uMVPMatrix * vec4(aPosition,1); //根据总变换矩阵计算此次绘制此顶点位置
   vec4 ambientTemp, diffuseTemp, specularTemp;   //存放环境光、散射光、镜面反射光的临时变量
   pointLight(normalize(aNormal),ambientTemp,diffuseTemp,specularTemp,uLightLocation,vec4(0.15,0.15,0.15,1.0),vec4(0.9,0.9,0.9,1.0),vec4(0.4,0.4,0.4,1.0));
   ambient=ambientTemp;
   diffuse=diffuseTemp;
   specular=specularTemp;
   vTextureCoord = aTexCoor;//将接收的纹理坐标传递给片元着色器
}                      

片元着色器:

precision mediump float;
uniform sampler2D sTexture;//纹理内容数据
//接收从顶点着色器过来的参数
varying vec4 ambient;
varying vec4 diffuse;
varying vec4 specular;
varying vec2 vTextureCoord;

// alpha值
uniform float uOpacity;

void main()
{
   //将计算出的颜色给此片元
      vec4 finalColor=texture2D(sTexture, vTextureCoord);
      finalColor.a *= uOpacity;
      //给此片元颜色值
      gl_FragColor = finalColor*ambient+finalColor*specular+finalColor*diffuse;
}   

关于光照的一些知识,可以参考大神的文章计算机图形学基础知识-光照材质OpenGL ES 入门之旅 – GLSL光照计算。如果和我一样是学渣级别,那就粘贴复制吧。

开始渲染模型

以上的工作都做完了,我们就要开始去绘制了。

public class GokuGroup extends GLGroup {
    private static final String TAG = GokuGroup.class.getSimpleName();
    private ArrayList<ObjInfo> objDatas;
    private ArrayList<GLEntity> mObjSprites = new ArrayList<GLEntity>();

    public GokuGroup(PlaneGlSurfaceView scene) {
        super(scene);
        try {
            objDatas = ObjLoaderUtil.load("redcar.obj", scene.getResources());
            init();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void initObjs() {
        mObjSprites.clear();
        if (objDatas != null) {
            for (int i = 0; i < objDatas.size(); i++) {
                ObjInfo data = objDatas.get(i);
                //
                int diffuseColor = data.mtlData != null ? data.mtlData.Kd_Color : 0xffffffff;
                float alpha = data.mtlData != null ? data.mtlData.alpha : 1.0f;
                String texturePath = data.mtlData != null ? data.mtlData.Kd_Texture : "";

                // 构造对象
                if (data.aTexCoords != null && data.aTexCoords.length != 0 && TextUtils.isEmpty(texturePath) == false) {
                    Bitmap bmp = BitmapUtil.getBitmapFromAsset(getBaseScene().getContext(), texturePath);
                    GLEntity spirit = new GokuEntity(getBaseScene(), data.aVertices, data.aNormals, data.aTexCoords, alpha, bmp);
                    mObjSprites.add(spirit);
                } else {
                    GLEntity spirit = new GLObjColorEntity(getBaseScene(), data.aVertices, data.aNormals, diffuseColor, alpha);
                    mObjSprites.add(spirit);
                }
            }
        }
    }

    private void init() {
        mSpriteScale = 5f;
        // alpha数值
        mSpriteAlpha = 1;
        // 旋转
        mSpriteAngleX = -90f;
        mSpriteAngleY = 0;
        mSpriteAngleZ = 0;
    }

    @Override
    public void onDraw(MatrixState matrixState) {
        super.onDraw(matrixState);
        matrixState.scale(getSpriteScale(), getSpriteScale(), getSpriteScale());
//         旋转
        matrixState.rotate(this.getSpriteAngleY(), 0, 1, 0);
        matrixState.rotate(this.getSpriteAngleX(), 1, 0, 0);
        // 绘制
        for (int i = 0; i < mObjSprites.size(); i++) {
            GLEntity sprite = mObjSprites.get(i);
            sprite.onDraw(matrixState);
        }
    }
}

之所以起名叫goku,是因为最开始我下载了孙悟空的模型,不过后来发现模型中没有顶点法线,导致整个模型光照出问题。

这里有一点需要注意,在调用OpenGL相关的api时,需要在的OpenGL自己的线程中去调用,也就是GLSurfaceView.Renderer的回调方法中,否则程序会抛出com.mxnavi.opengl4android E/libEGL: call to OpenGL ES API with no current context (logged once per thread)的错误,不一定崩溃,但是可能会导致某些功能不可用。

接下来写下我们每个“部件”去绘制的实体类:

public class GokuEntity extends GLEntity {
    //自定义渲染管线着色器程序id
    int mProgram;
    //总变换矩阵引用
    int muMVPMatrixHandle;
    //位置、旋转变换矩阵
    int muMMatrixHandle;
    //顶点位置属性引用
    int maPositionHandle;
    //顶点法向量属性引用
    int maNormalHandle;
    //光源位置属性引用
    int maLightLocationHandle;
    //摄像机位置属性引用
    int maCameraHandle;
    //顶点纹理坐标属性引用
    int maTexCoorHandle;
    // 顶点颜色
    int muColorHandle;
    // 材质中透明度
    int muOpacityHandle;
    //顶点着色器代码脚本
    String mVertexShader;
    //片元着色器代码脚本
    String mFragmentShader;

    //顶点坐标数据缓冲
    FloatBuffer mVertexBuffer;
    //顶点法向量数据缓冲
    FloatBuffer mNormalBuffer;
    //顶点纹理坐标数据缓冲
    FloatBuffer mTexCoorBuffer;

    // 材质中alpha
    protected float mAlpha;
    // 需转化为纹理的图片
    protected Bitmap mBmp;
    //
    int vCount = 0;
    // 纹理是否已加载
    protected boolean isInintFinsh = false;
    // 纹理id
    protected int textureId;

    public GokuEntity(PlaneGlSurfaceView scene, float[] vertices, float[] normals, float texCoors[], float alpha, Bitmap bmp) {
        //初始化顶点坐标与着色数据
        initVertexData(vertices, normals, texCoors, alpha, bmp);
        //初始化shader
        initShader(scene.getResources());
    }

    //初始化顶点坐标与着色数据的方法
    public void initVertexData(float[] vertices, float[] normals, float texCoors[], float alpha, Bitmap bmp) {
        this.mAlpha = alpha;
        this.mBmp = bmp;
        //顶点坐标数据的初始化================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(Resources res) {
        //加载顶点着色器的脚本内容
        mVertexShader = ShaderUtil.loadFromAssetsFile("shader/texture_vertex.sh", res);
        //加载片元着色器的脚本内容
        mFragmentShader = ShaderUtil.loadFromAssetsFile("shader/texture_frag.sh", res);
        //基于顶点着色器与片元着色器创建程序
        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");
        // 顶点颜色
        muColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
        // alpha
        muOpacityHandle = GLES20.glGetUniformLocation(mProgram, "uOpacity");
    }

    /**
     * 初始化纹理
     */
    private void initTexture() {
        // 两球之间连线的纹理图片
        if (mBmp != null) {
            textureId = TextureUtil.getTextureIdByBitmap(mBmp);
        }
    }

    @Override
    public void onDraw(MatrixState matrixState) {
        // 加载纹理
        if (isInintFinsh == false) {
            initTexture();
            isInintFinsh = true;
        }

        //制定使用某套着色器程序
        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
                );
        // 材质alpha
        GLES20.glUniform1f(muOpacityHandle, mAlpha);
        // 启用顶点纹理数组
        GLES20.glEnableVertexAttribArray(maTexCoorHandle);
        //启用顶点位置、法向量、纹理坐标数据
        GLES20.glEnableVertexAttribArray(maPositionHandle);
        GLES20.glEnableVertexAttribArray(maNormalHandle);
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);

        //绘制加载的物体
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vCount);
    }

}

有了实体类,我们就需要在GLSurfaceView.Renderer中去开始我们的调用:

public class GokuRenderer implements GLSurfaceView.Renderer {

    private static final String TAG = "GokuRenderer";
    /**
     * 物体类
     */
    GokuGroup mSpriteGroup = null;
    private final float TOUCH_SCALE_FACTOR = 180.0f / 320;//角度缩放比例
    MatrixState matrixState;

    public PlaneGlSurfaceView mGLSurfaceView;

    public GokuRenderer(PlaneGlSurfaceView glSurfaceView) {
        this.mGLSurfaceView = glSurfaceView;
        matrixState = new MatrixState();
        // 初始化obj+mtl文件
        mSpriteGroup = new GokuGroup(mGLSurfaceView);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        // TODO GlThread
        // 清除深度缓冲与颜色缓冲
        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
        // 设置屏幕背景色RGBA
        /**
         * 绘制物体
         */
        matrixState.pushMatrix();
        mSpriteGroup.onDraw(matrixState);
        matrixState.popMatrix();
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        // TODO GlThread
        GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        //开启混合
        gl.glEnable(GL10.GL_BLEND);
        gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
        // 设置屏幕背景色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(1000, 1000, 1000);
        initUI();
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        // TODO GlThread
        // viewPort
        GLES20.glViewport(0, 0, width, height);
        float ratio = (float) width / height;
        matrixState.setProjectFrustum(-ratio, ratio, -1, 1,
                LeGLConfig.PROJECTION_NEAR, LeGLConfig.PROJECTION_FAR);
        // camera
        matrixState.setCamera(LeGLConfig.EYE_X, LeGLConfig.EYE_Y, LeGLConfig.EYE_Z,
                LeGLConfig.VIEW_CENTER_X, LeGLConfig.VIEW_CENTER_Y, LeGLConfig.VIEW_CENTER_Z,
                0f, 1f, 0f);
    }

    /**
     * 初始化场景中的精灵实体类
     */
    private void initUI() {
        mSpriteGroup.initObjs();
    }
}

于是我们在activity去将Renderer注册进去

	mGLView = (PlaneGlSurfaceView) findViewById(R.id.glsv_plane);
    GokuRenderer gokuRenderer = new GokuRenderer(mGLView);
    mGLView.setRenderer(gokuRenderer);
    // 渲染模式(被动渲染)
    mGLView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

手势的加入

其实,以上的代码运行下,我们就可以看到我们的成果了.
在这里插入图片描述
为了让我们的产品看着更加立体,我们加入一些手势操作,物体可以随着我们的手势旋转,那就很完美了。

修改PlaneGlSurfaceView加入手势监听:

public class PlaneGlSurfaceView extends GLSurfaceView {

    private OnTouchEventListener touchListener;
    
	...

    public void setOnTouchListener(OnTouchEventListener listener) {
        touchListener = listener;
    }

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

    //触摸事件回调方法
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        float y = e.getY();
        float x = e.getX();
        switch (e.getAction()) {
            case MotionEvent.ACTION_MOVE:
                //计算触控笔Y位移
                float dy = y - mPreviousY;
                //计算触控笔X位移
                float dx = x - mPreviousX;
                //
                if (touchListener != null) {
                    touchListener.onTouchEvent(dx, dy);
                }
                break;
            default:
                break;

        }
        mPreviousY = y;//记录触控笔位置
        mPreviousX = x;//记录触控笔位置
        return true;
    }

    /**
     * 触摸监听接口
     */
    public interface OnTouchEventListener {
        void onTouchEvent(float dx, float dy);
    }
}

修改GokuRenderer去实现监听

public class GokuRenderer implements GLSurfaceView.Renderer {

	...

    public PlaneGlSurfaceView.OnTouchEventListener getTouchEventListener() {
        return touchEventListener;
    }

    /**
     * 触摸回调
     */
    PlaneGlSurfaceView.OnTouchEventListener touchEventListener = new PlaneGlSurfaceView.OnTouchEventListener() {
        @Override
        public void onTouchEvent(float dx, float dy) {
            float yAngle = mSpriteGroup.getSpriteAngleY();
            yAngle += dx * TOUCH_SCALE_FACTOR;
            mSpriteGroup.setSpriteAngleY(yAngle);

//            float xAngle = mSpriteGroup.getSpriteAngleX();
//            xAngle += dy * TOUCH_SCALE_FACTOR;
//            mSpriteGroup.setSpriteAngleX(xAngle);

            mGLSurfaceView.requestRender();//重绘画面
        }
    };
}

大功告成,这次就可以随着手势去旋转了。

源码

所有文章的代码,托管在Github上——OpenGL4Android

  • 10
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值