从零开始学OpenGLES开发——第三章
第三章 光照,材质
第二章中并没有做过多的说明就直接给大家演示了怎么给一个三角形贴图。
其实贴图只是一个物体的物理外观性质中的一种,除了纹理之外,还有材质性质(属性)
材质属性和纹理不一样,纹理是一张图,最后应用到模型之后,从OpenGL底层来说,是模型上每个像素点都从纹理上找到了自己该拥有的一个像素点。换句话说,一个物体的任何一个点,如果有纹理的物理外观性质,本质上它只是拿到了一组 RGBA(一个像素)的值。对于一个点来说,它的纹理就是一个 颜色值(RGBA),只不过这个颜色的值不是我们手动设置的,而是它从纹理图上通过坐标采样得到的。
材质就要复杂一些了,模型的任何一个点,在材质方面都会拥有多组参数的值,以我们的生活常识来看,至少要包含这几方面:
1、物体的粗糙程度
2、物体的反光程度
其实计算机3D世界里描述的物理外观材质属性,除了上面两个,还有其他的,整体来说最基本的有这五项吧
1、物体的环境颜色(ambient)
2、物体的漫反射颜色(diffuse)
3、物体的镜面反色颜色(specular)
4、物体的自发光颜色(emission)
5、物体的光泽度(shininess)
第一次看到这些术语可能并不能从真实物理世界找到与之对应的现象,所以我们需要在程序中尝试改变这些参数的值来观看它实际产生的效果,进而理解这些材质属性所产生的作用与自然界我们看到的哪些现象是相关的
我列举一下本人肤浅的理解吧
(★申明以下为个人理解★)
以地球为例,地球外面有一层大气层,地球上2/3都是水,1/3是陆地。从月球上看地球的话---->
如果ambient颜色为红色,那么地球的大气层看上就泛红色(注意这里是泛红色,隐约的红色),也就是你看地球的边缘部分(因为视线能穿透大气层,看到外太空,这种红色最明显)
如果diffuse颜色为褐色,那么地球上的陆地就为褐色(假设陆地只有漫反射,没有镜面反射)
如果specular颜色为绿色,那么地球上的2/3的水就呈现绿色(假设海洋只有镜面反射,没有漫反射)
如果emission为紫色,那地球没有被太阳照到的一面,也能自发紫色光。
shininess 不是一个颜色,它是一个程度的参数,范围是0-128。值越大表示越光滑。
想象一下,128的时候,地球看上去像新拆包装台球一样,散发光泽。为0的时候,地球就像满是裂痕,用了十几年的台球,满是伤痕。
(★申明以上为个人理解★)
其实严格来说,纹理也算是材质的一个参数吧,只不过它不是统一值,是根据纹理图来的。
除了材质和纹理外,还不能完全决定物体看上去的样子,还有一样,那就是法线。法线决定了物体表面某一点的垂直方向。这个方向决定了物体反射光,哪个角度最强。真实世界中,物体的外形确定了,法线也就确定了,哪怕是凹凸面,按微积分的思想,任意一个无限小的点,都能和周围的点组成一个平面,这个平面的垂直方向就是这个点的法线。但是计算机里面是不可能这样自动处理的,法线需要您手动设置,也就意味着法线并不一定非得要和平面垂直(一般人没这么干的)
想象一个长长的台阶梯,从很远看去,它是一个斜的平面,但是这个平面的法线,一部分是垂直水平面,一部分平行水平面。
如果忽略平行水平面的法线,那么太阳照在台阶上,从无限远的距离上看,垂直照射的时候,反光反而不强,要垂直照射地球,从正上方看反光才最强。
接下来以实际代码来说明
首先我们需要构建一个表面不平的物体,一个球是最合适的,GLUT里面有构建球的工具函数,但是android上没有,我又不想去找第三方库了,那就手动来吧,下面的代码构建一个球,并且得到一组顶点的Buffer,一组法线的Buffer。
球的球心在(0,0,0),每一个顶点的法线刚好就是点坐标表示的向量值(不过要取单位长度噢)
如果您有兴趣去深究这个球的生成过程,那也可以,都是一堆三角函数元算而已。
int slices = 180 ;
int stacks = 90 ;
float radius = 100f ;
前面三个参数指定求的属性,slices表示把赤道分成多少个块,stacks表示从北极和到南极分多少块,所以stacks是slices的一半。radius表示半径,也决定了球最后的大小。注意这个球是以中心(0,0,0)创建的,也就是最后你不能站在原点看了,要跑到球的外面去看。
在onSurfaceCreated里调用这个函数,然后它执行完之后,会赋值给我vertexBuffer ,normalBuffer ,vertexCount 我们需要的值,vertexCount 表示定点个数,绘制的时候也需要使用这个参数。
重点注意要法线要取单位长度,必须的!虽然说方向参数是没有长度的,但是法线最后的工作的时候需要求长度进行处理,所以长度必须是 1.
继续说,onSurfaceChanged的函数代码如下:
因为球的中心在原点,所以我们就要站在外面看了,所以gluLookAt参数里第一个坐标 z = 300,表示我站在球外了300 > 100的噢!。
最后因为需要使用材质,那么就必须开启光照。在解释三句光照相关的函数之前,我简要回顾一下前两章为什么没有开灯光
OpenGL中,在GLES10.glEnable(GLES10.GL_LIGHTING)没有调用的时候,它是无光照模式。
以我有限的经验来看(是个人经验总结,参考即可)
无光照模式的处理方式是,任意一个点,它向任意方向都是反光的,就像是任意方向都有法线。完全的360度无死角啊。
如果启用了灯光,表示以光照模式渲染,那么就必须要有法线,指定的物体的垂直方向,否则你啥都看不到。
所以那三句代码是,首先开启光照模式
其次是启用编号为0的灯光
再次是设置编号为0的灯光的位置是在(100,200,300)的位置上。
灯光还有一堆参数可以设置,这里就不作深入讲解了,我只说如果只设置上面这三句话
那么编号为0的灯光它是一个点光,这个点光位置是(100,200,300),它向任意角度都发光。
注意到那句代码没
GLES10.glEnable(GLES10.GL_DEPTH_TEST); // 开启深度测试
这句话含义是开启深度测试,深度测试就是系统在绘制的时候,如果发现了同一位置(屏幕上)上已经有一个点了,那么就会比较这两个点哪个离屏幕更近,如果前面先画已经有的更近,则忽略后画的这个点。因为球面有两层,所以需要开启这个测试
GLES10.glClear(GLES10.GL_COLOR_BUFFER_BIT | GLES10.GL_DEPTH_BUFFER_BIT);// 清空相关缓存。
第二个bit参数其实就是指的清理这个缓存。
绘制代码如下:
运行一下吧,看看效果。
想了解各材质的参数的含义,initOpenGLMaterial修改下,那几个参数,然后重新运行一下看效果吧。
第三章 光照,材质
第二章中并没有做过多的说明就直接给大家演示了怎么给一个三角形贴图。
其实贴图只是一个物体的物理外观性质中的一种,除了纹理之外,还有材质性质(属性)
材质属性和纹理不一样,纹理是一张图,最后应用到模型之后,从OpenGL底层来说,是模型上每个像素点都从纹理上找到了自己该拥有的一个像素点。换句话说,一个物体的任何一个点,如果有纹理的物理外观性质,本质上它只是拿到了一组 RGBA(一个像素)的值。对于一个点来说,它的纹理就是一个 颜色值(RGBA),只不过这个颜色的值不是我们手动设置的,而是它从纹理图上通过坐标采样得到的。
材质就要复杂一些了,模型的任何一个点,在材质方面都会拥有多组参数的值,以我们的生活常识来看,至少要包含这几方面:
1、物体的粗糙程度
2、物体的反光程度
其实计算机3D世界里描述的物理外观材质属性,除了上面两个,还有其他的,整体来说最基本的有这五项吧
1、物体的环境颜色(ambient)
2、物体的漫反射颜色(diffuse)
3、物体的镜面反色颜色(specular)
4、物体的自发光颜色(emission)
5、物体的光泽度(shininess)
第一次看到这些术语可能并不能从真实物理世界找到与之对应的现象,所以我们需要在程序中尝试改变这些参数的值来观看它实际产生的效果,进而理解这些材质属性所产生的作用与自然界我们看到的哪些现象是相关的
我列举一下本人肤浅的理解吧
(★申明以下为个人理解★)
以地球为例,地球外面有一层大气层,地球上2/3都是水,1/3是陆地。从月球上看地球的话---->
如果ambient颜色为红色,那么地球的大气层看上就泛红色(注意这里是泛红色,隐约的红色),也就是你看地球的边缘部分(因为视线能穿透大气层,看到外太空,这种红色最明显)
如果diffuse颜色为褐色,那么地球上的陆地就为褐色(假设陆地只有漫反射,没有镜面反射)
如果specular颜色为绿色,那么地球上的2/3的水就呈现绿色(假设海洋只有镜面反射,没有漫反射)
如果emission为紫色,那地球没有被太阳照到的一面,也能自发紫色光。
shininess 不是一个颜色,它是一个程度的参数,范围是0-128。值越大表示越光滑。
想象一下,128的时候,地球看上去像新拆包装台球一样,散发光泽。为0的时候,地球就像满是裂痕,用了十几年的台球,满是伤痕。
(★申明以上为个人理解★)
其实严格来说,纹理也算是材质的一个参数吧,只不过它不是统一值,是根据纹理图来的。
除了材质和纹理外,还不能完全决定物体看上去的样子,还有一样,那就是法线。法线决定了物体表面某一点的垂直方向。这个方向决定了物体反射光,哪个角度最强。真实世界中,物体的外形确定了,法线也就确定了,哪怕是凹凸面,按微积分的思想,任意一个无限小的点,都能和周围的点组成一个平面,这个平面的垂直方向就是这个点的法线。但是计算机里面是不可能这样自动处理的,法线需要您手动设置,也就意味着法线并不一定非得要和平面垂直(一般人没这么干的)
想象一个长长的台阶梯,从很远看去,它是一个斜的平面,但是这个平面的法线,一部分是垂直水平面,一部分平行水平面。
如果忽略平行水平面的法线,那么太阳照在台阶上,从无限远的距离上看,垂直照射的时候,反光反而不强,要垂直照射地球,从正上方看反光才最强。
接下来以实际代码来说明
首先我们需要构建一个表面不平的物体,一个球是最合适的,GLUT里面有构建球的工具函数,但是android上没有,我又不想去找第三方库了,那就手动来吧,下面的代码构建一个球,并且得到一组顶点的Buffer,一组法线的Buffer。
球的球心在(0,0,0),每一个顶点的法线刚好就是点坐标表示的向量值(不过要取单位长度噢)
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
initOpenGLVertexs() ;
}
private FloatBuffer vertexBuffer = null ;
private FloatBuffer normalBuffer = null ;
private int vertexCount = 0 ;
private void glesNormalize(float[] v, int offset) {
float len = (float) Math.sqrt(v[offset+0] * v[offset+0] + v[offset+1] * v[offset+1] + v[offset+2] * v[offset+2]);
v[offset+0] /= len;
v[offset+1] /= len;
v[offset+2] /= len;
}
private void initOpenGLVertexs(){
int slices = 180 ;
int stacks = 90 ;
float radius = 100f ;
double perAngleW = 2 * Math.PI / slices;
double perAngleH = Math.PI / stacks;
float[] vertexArray = new float[slices*stacks*2*3*3];
float[] normalArray = new float[slices*stacks*2*3*3];
int vertexIndex = 0 ;
int normalIndex = 0 ;
for (int a = 0; a < stacks; a++) {
for (int b = 0; b < slices; b++) {
float x1 = (float) (radius * Math.sin(a * perAngleW) * Math.cos(b* perAngleH));
float z1 = (float) (radius * Math.sin(a * perAngleW) * Math.sin(b* perAngleH));
float y1 = (float) (radius * Math.cos(a * perAngleW));
float x2 = (float) (radius * Math.sin((a + 1) * perAngleW) * Math.cos(b * perAngleH));
float z2 = (float) (radius * Math.sin((a + 1) * perAngleW) * Math.sin(b * perAngleH));
float y2 = (float) (radius * Math.cos((a + 1) * perAngleW));
float x3 = (float) (radius * Math.sin((a + 1) * perAngleW) * Math.cos((b + 1) * perAngleH));
float z3 = (float) (radius * Math.sin((a + 1) * perAngleW) * Math.sin((b + 1) * perAngleH));
float y3 = (float) (radius * Math.cos((a + 1) * perAngleW));
float x4 = (float) (radius * Math.sin(a * perAngleW) * Math.cos((b + 1) * perAngleH));
float z4 = (float) (radius * Math.sin(a * perAngleW) * Math.sin((b + 1) * perAngleH));
float y4 = (float) (radius * Math.cos(a * perAngleW));
vertexArray[vertexIndex++] = (x1);
vertexArray[vertexIndex++] = (y1);
vertexArray[vertexIndex++] = (z1);
vertexArray[vertexIndex++] = (x2);
vertexArray[vertexIndex++] = (y2);
vertexArray[vertexIndex++] = (z2);
vertexArray[vertexIndex++] = (x3);
vertexArray[vertexIndex++] = (y3);
vertexArray[vertexIndex++] = (z3);
vertexArray[vertexIndex++] = (x3);
vertexArray[vertexIndex++] = (y3);
vertexArray[vertexIndex++] = (z3);
vertexArray[vertexIndex++] = (x4);
vertexArray[vertexIndex++] = (y4);
vertexArray[vertexIndex++] = (z4);
vertexArray[vertexIndex++] = (x1);
vertexArray[vertexIndex++] = (y1);
vertexArray[vertexIndex++] = (z1);
normalArray[normalIndex++] = (x1);
normalArray[normalIndex++] = (y1);
normalArray[normalIndex++] = (z1);
glesNormalize(normalArray, normalIndex-3);
normalArray[normalIndex++] = (x2);
normalArray[normalIndex++] = (y2);
normalArray[normalIndex++] = (z2);
glesNormalize(normalArray, normalIndex-3);
normalArray[normalIndex++] = (x3);
normalArray[normalIndex++] = (y3);
normalArray[normalIndex++] = (z3);
glesNormalize(normalArray, normalIndex-3);
normalArray[normalIndex++] = (x3);
normalArray[normalIndex++] = (y3);
normalArray[normalIndex++] = (z3);
glesNormalize(normalArray, normalIndex-3);
normalArray[normalIndex++] = (x4);
normalArray[normalIndex++] = (y4);
normalArray[normalIndex++] = (z4);
glesNormalize(normalArray, normalIndex-3);
normalArray[normalIndex++] = (x1);
normalArray[normalIndex++] = (y1);
normalArray[normalIndex++] = (z1);
glesNormalize(normalArray, normalIndex-3);
}
}
vertexCount = vertexArray.length / 3;
vertexBuffer = ByteBuffer.allocateDirect(vertexArray.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
vertexBuffer.put(vertexArray);
vertexBuffer.position(0);
normalBuffer = ByteBuffer.allocateDirect(normalArray.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
normalBuffer.put(normalArray);
normalBuffer.position(0);
}
如果您有兴趣去深究这个球的生成过程,那也可以,都是一堆三角函数元算而已。
int slices = 180 ;
int stacks = 90 ;
float radius = 100f ;
前面三个参数指定求的属性,slices表示把赤道分成多少个块,stacks表示从北极和到南极分多少块,所以stacks是slices的一半。radius表示半径,也决定了球最后的大小。注意这个球是以中心(0,0,0)创建的,也就是最后你不能站在原点看了,要跑到球的外面去看。
在onSurfaceCreated里调用这个函数,然后它执行完之后,会赋值给我vertexBuffer ,normalBuffer ,vertexCount 我们需要的值,vertexCount 表示定点个数,绘制的时候也需要使用这个参数。
重点注意要法线要取单位长度,必须的!虽然说方向参数是没有长度的,但是法线最后的工作的时候需要求长度进行处理,所以长度必须是 1.
继续说,onSurfaceChanged的函数代码如下:
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES10.glViewport(0, 0, width, height); // 设置视口宽度高度。
GLES10.glEnable(GLES10.GL_DEPTH_TEST); // 开启深度测试
// {修改投影矩阵
GLES10.glMatrixMode(GLES10.GL_PROJECTION); // 修改投影矩阵
GLES10.glLoadIdentity(); // 复位,将投影矩阵归零
GLU.gluPerspective(gl, 60.0f, ((float) width) / height, 0.1f, 400f);
// }
//{
GLES10.glMatrixMode(GLES10.GL_MODELVIEW);
GLES10.glLoadIdentity();//归零模型视图
GLU.gluLookAt(gl, 0, 0, 300, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
//}
GLES10.glEnable(GLES10.GL_LIGHTING);
GLES10.glEnable(GLES10.GL_LIGHT0);
GLES10.glLightfv(GLES10.GL_LIGHT0, GLES10.GL_POSITION, new float[]{100, 200, 300, 0}, 0);
}
因为球的中心在原点,所以我们就要站在外面看了,所以gluLookAt参数里第一个坐标 z = 300,表示我站在球外了300 > 100的噢!。
最后因为需要使用材质,那么就必须开启光照。在解释三句光照相关的函数之前,我简要回顾一下前两章为什么没有开灯光
OpenGL中,在GLES10.glEnable(GLES10.GL_LIGHTING)没有调用的时候,它是无光照模式。
以我有限的经验来看(是个人经验总结,参考即可)
无光照模式的处理方式是,任意一个点,它向任意方向都是反光的,就像是任意方向都有法线。完全的360度无死角啊。
如果启用了灯光,表示以光照模式渲染,那么就必须要有法线,指定的物体的垂直方向,否则你啥都看不到。
所以那三句代码是,首先开启光照模式
其次是启用编号为0的灯光
再次是设置编号为0的灯光的位置是在(100,200,300)的位置上。
灯光还有一堆参数可以设置,这里就不作深入讲解了,我只说如果只设置上面这三句话
那么编号为0的灯光它是一个点光,这个点光位置是(100,200,300),它向任意角度都发光。
注意到那句代码没
GLES10.glEnable(GLES10.GL_DEPTH_TEST); // 开启深度测试
这句话含义是开启深度测试,深度测试就是系统在绘制的时候,如果发现了同一位置(屏幕上)上已经有一个点了,那么就会比较这两个点哪个离屏幕更近,如果前面先画已经有的更近,则忽略后画的这个点。因为球面有两层,所以需要开启这个测试
GLES10.glClear(GLES10.GL_COLOR_BUFFER_BIT | GLES10.GL_DEPTH_BUFFER_BIT);// 清空相关缓存。
第二个bit参数其实就是指的清理这个缓存。
接下来我们需要创建一个材质。材质属性是一个全局状态量,当你设置到OpenGL之后,除非手动更改它,那画任意物体都会引用这个材质信息。由于我们只有一个球,没有多个物体,不需要切换,所以可以在前面只指定一次即可。
private float[] ambient = null ;
private float[] diffuse = null ;
private float[] specular = null ;
private float[] emmissive = null ;
private float shininess = 0.0f ;
private void initOpenGLMaterial(){
ambient = new float[]{0.0f,0.2f,0.0f,1.0f};
diffuse = new float[]{0.0f,0.8f,0.0f,1.0f};
specular = new float[]{0.6f,0.6f,0.6f,1.0f};
emmissive = new float[]{0.0f,0.1f,0.0f,1.0f};
shininess = 20f ;
GLES10.glMaterialfv(
GLES10.GL_FRONT_AND_BACK,
GLES10.GL_AMBIENT,
this.ambient, 0);
GLES10.glMaterialfv(
GLES10.GL_FRONT_AND_BACK,
GLES10.GL_DIFFUSE,
this.diffuse, 0);
GLES10.glMaterialfv(
GLES10.GL_FRONT_AND_BACK,
GLES10.GL_SPECULAR,
this.specular, 0);
GLES10.glMaterialfv(
GLES10.GL_FRONT_AND_BACK,
GLES10.GL_EMISSION,
this.emmissive, 0);
GLES10.glMaterialf(
GLES10.GL_FRONT_AND_BACK,
GLES10.GL_SHININESS,
this.shininess);
}
然后在onSurfaceCreated里调用initOpenGLMaterial吧。
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
initOpenGLVertexs() ;
initOpenGLMaterial();
}
绘制代码如下:
public void onDrawFrame(GL10 gl) {
GLES10.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // 清空场景为黑色。
GLES10.glClear(GLES10.GL_COLOR_BUFFER_BIT | GLES10.GL_DEPTH_BUFFER_BIT);// 清空相关缓存。
GLES10.glEnableClientState(GLES10.GL_VERTEX_ARRAY);
GLES10.glVertexPointer(3, GLES10.GL_FLOAT, 0, vertexBuffer);
GLES10.glEnableClientState(GLES10.GL_NORMAL_ARRAY);
GLES10.glNormalPointer(GLES10.GL_FLOAT, 0, normalBuffer);
GLES10.glDrawArrays(GLES10.GL_TRIANGLES, 0, vertexCount);
GLES10.glFlush();
}
运行一下吧,看看效果。
想了解各材质的参数的含义,initOpenGLMaterial修改下,那几个参数,然后重新运行一下看效果吧。