之前我们绘制的都是规则的几何图形,今天我们根据3D模型,来绘制现实中的物体,首先看一下我们的实际效果
在我们使用之前,先了解下什么是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文件的大致格式。
当然obj的文件格式可不止这些,这里列出来我们常见的一些格式,想了解详细的童鞋,可移步大神的文章3D中的OBJ文件格式详解
有一点值得注意的是,之前我们介绍过,OpenGL ES相比OpenGL舍弃了很多图形绘制,任何的事物都是由三角形绘制而成。obj文件是通过专业的3D模型绘制软件形成的,在电脑上制作的文件,可不一定都是依照三角形去绘制的,而且每个模型数据也存在差异。所以我们的f标签可能存在不同的格式,这里列举下
上述的索引坐标都是从1开始,这里与我们写代码从0开始有些不同
我们了解了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