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 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文件的大致格式。
- # 开头的为注释
- v 表示本行指定一个顶点。 前缀后跟着3个单精度浮点数,分别表示该定点的X、Y、Z坐标值
- vt 表示本行指定一个纹理坐标。此前缀后跟着两个单精度浮点数。分别表示此纹理坐标的U、V值
- vn 表示本行指定一个法线向量。此前缀后跟着3个单精度浮点数,分别表示该法向量的X、Y、Z坐标值
- f 表示本行指定一个表面(Face)。一个表面实际上就是一个三角形图元
- usemtl 此前缀后只跟着一个参数。该参数指定了从此行之后到下一个以usemtl开头的行之间的所有表面所使用的材质名称。该材质可以在此OBJ文件所附属的MTL文件中找到具体信息。
- 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
更多相关文章
- OpenGL 实现视频编辑中的转场效果
- 【Android开发学习19】关于GL_NEAREST和GL_LINEAR的选择
- 如何使用Android中的OpenGL ES媒体效果
- Android(安卓)吸入动画效果详解
- android立体图形——三棱锥
- 利用OpenGL ES、手机传感器、相机和调用百度语音包服务,实现AR+语
- android opengl es 纹理贴图资料
- Android(安卓)OpenGL ES2.0 and GLSL 一个简单的Demo
- Android(安卓)右滑关闭当前Activity(类微信)