之前我们绘制的都是规则的几何图形,今天我们根据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 0v -0.5 -0.5 0v 0.5 -0.5 0# 纹理坐标vt 0.0 1.0vt 0.0 0.0vt 1.0 1.0# 顶点法线vn 0 0 1# 当前图元所用材质usemtl Defaults offf 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.pngmap_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

更多相关文章

  1. OpenGL 实现视频编辑中的转场效果
  2. 【Android开发学习19】关于GL_NEAREST和GL_LINEAR的选择
  3. 如何使用Android中的OpenGL ES媒体效果
  4. Android(安卓)吸入动画效果详解
  5. android立体图形——三棱锥
  6. 利用OpenGL ES、手机传感器、相机和调用百度语音包服务,实现AR+语
  7. android opengl es 纹理贴图资料
  8. Android(安卓)OpenGL ES2.0 and GLSL 一个简单的Demo
  9. Android(安卓)右滑关闭当前Activity(类微信)

随机推荐

  1. 开发中常遇到的问题--日期格式化转换.
  2. 什么是asp.net core?介绍Asp.Net Core的优
  3. asp.net core实例详解三(新建项目)
  4. vs 中引用的问题--出现小叹号
  5. 推荐10款常用的获取本机ip用法,欢迎下载!
  6. asp.net core实例详解二(环境设置)
  7. 有关浮点数类型的文章推荐3篇
  8. C# 一些面试试题的实例教程
  9. ASP.NET Core实例详解一
  10. 有关在线文件的文章推荐10篇