上一期讲了OpenGL ES的光效效果,材质的属性会影响我们看到物体的颜色效果,下面整理了关于OpenGL材质的基础知识点
在 OpenGL ES 中,材质(Material)用于定义物体表面对光照的反应方式,它决定了物体的外观特性(如颜色、光泽度、反射率等)。材质与光照模型结合,能够模拟出金属、塑料、玻璃等不同质感的物体。
我们通常通过定义材质的反射光来定义0penGLES中的材质,正如现实世界中一样。如果一个材质定义为反射红光, 那么在正常的白光下,它将显示红色。在0penGL中(至少在使用光滑着色处理和光效时),材质是没有颜色的。OpenGL具有分别定义材质是怎样反射0penGL光效三要素(环境,散射和高光)的能力。另外,它还具有指定材质自发光(emissive)属性的能力。
下面是不开启默认材质的效果(注释掉开启材质代码 gl.glEnable(GL10.GL_COLOR_MATERIAL),只能看到一个简单的颜色。
指定材质
通常使用 glMaterialfv 和 glMaterialf 指定颜色材质,第一个通常和颜色有关,第二个一般设置光泽度。下面代码是增加了glMaterialfv 颜色是 0.1f, 0.9f, 1.0f ,偏向蓝色。
private fun setMaterial(gl: GL10) {
// 设置环境元素和散射元素的颜色 RGBA 这里设置的比较便蓝色
val ambientLight = floatArrayOf(0f, 0.1f, 0.9f, 1.0f)
// 设置材质环境元素和反射元素
// 第一个参数:GL10.GL_FRONT_AND_BACK 设置面,都会设置成前后
// 第二个参数:GL10.GL_AMBIENT_AND_DIFFUSE 设置环境元素和反射元素 还可以设置 GL10.GL_AMBIENT\GL10.GL_DIFFUSE
// 被感知的颜色一般同时经过环境元素和反射元素所影响。
// 第三个参数:设置颜色
// 第四个参数:偏移量,如果使用重载的buffer传入则就前面三个参数
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT_AND_DIFFUSE, ambientLight, 0)
}
下面是增加了这段代码后,我们看到的正方体的颜色。因为我们制定反射的颜色是蓝色,所以光源照射到以后看起来就是蓝色的。看到蓝色有深有浅就是因为我们环境光和反射光共同作用的效果。
单独设置环境元素和反射元素
private fun setMaterial(gl: GL10) {
// 环境元素 蓝色
val ambientLight = floatArrayOf(0f, 0.1f, 0.9f, 1.0f)
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT, ambientLight, 0)
// 散射元素 红色
val diffuseLight = floatArrayOf(0.9f, 0f, 0.1f, 1.0f)
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, diffuseLight, 0)
}
看一下效果如下,红色是散射元素作用,蓝色是环境元素作用。
高光元素
还可以单独设置场景中高光元素的反射方式,从而控制高光"热点"的亮度。一个叫GL_SHININESS的参数与材质的高光元素一起定义了高光热点的大小。如果你设定了材质的GL_SPECULAR值,你还应该定义其反光度。反光度越高,高光反射越小,所以默认值0.0几乎完全淹没了散射光。高光使三角形边缘突出,高光通常在我们的游戏中经常使用的低面片的物体上表现不佳。在常规0penGL中,有一个称为 着色器(shader)的机制可以用来为低面片物体产生较为理想的结果,但是只有OpenGL ES 2.0,有shader功能。
// 设置高光元素 黄色
val specularLight = floatArrayOf(0.9f, 0.9f, 0f, 1.0f)
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, specularLight, 0)
// 设置反光度 反射度越高 高光反射越小
gl.glMaterialf(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS, 25F )
高光看起来效果并不是很明显。
自发光
通过设定自发光元素,使得材质看上去会发射我们指定的颜色。它并不是真正在发光。例如,其周边物体并不会被发射的光线影响。如果你希望一个物体像灯泡一样发光照亮其他物体,你需要将自发光元素和与物体同一位置处的实际光源结合起来。但是自发光元素可以使物体漂亮地发光。
自发光元素影响整个材质,因此 GL_EMISSION的值将与落入物体指定区域的任何类型的光相叠加。
// 自发光 绿色
val emissionLight = floatArrayOf(0.0f, 0.4f, 0f, 1.0f)
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_EMISSION, emissionLight, 0)
效果如下:颜色叠加后的效果。
总结
材质设置方法 gl.glMaterialfv()和 gl.glMaterialf() ,由环境元素、散射元素、高光元素、自发光共同影响显示效果。
face
指定材质应用于哪些面,可选值:
GL_FRONT:仅应用于正面(默认逆时针方向为正面)。
GL_BACK:仅应用于背面。
GL_FRONT_AND_BACK:同时应用于正面和背面。pname
指定要设置的材质属性类型,常用值:
pname | 含义 | 常用值 |
---|---|---|
GL_AMBIENT | 环境光颜色 | RGB/RGBA 数组 |
GL_DIFFUSE | 漫反射颜色 | RGB/RGBA 数组 |
GL_SPECULAR | 镜面反射颜色 | RGB/RGBA 数组 |
GL_EMISSION | 自发光颜色(物体自身发光) | RGB/RGBA 数组 |
GL_AMBIENT_AND_DIFFUSE | 同时设置环境光和漫反射颜色 | RGB/RGBA 数组 |
GL_SHININESS | 光泽度(镜面高光大小) | 单个浮点数(需用glMaterialf) |
params
包含属性值的浮点数组,数组长度和含义取决于pname:
RGB 格式:长度为 3 的数组[R, G, B],每个值范围[0.0, 1.0]。
RGBA 格式:长度为 4 的数组[R, G, B, A],A 表示透明度。offset
数组的起始偏移量(从第几个元素开始读取),通常为 0。
总体代码
package com.e.opengl
import android.opengl.GLSurfaceView
import android.opengl.GLU
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import java.nio.ShortBuffer
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
class CubeRen2 : GLSurfaceView.Renderer {
private var normals: FloatBuffer
private val vertexBuffer: FloatBuffer
private val indexBuffer: ShortBuffer
private val colorBuffer: FloatBuffer
// 正方体的8个顶点坐标
private val vertices = floatArrayOf(
-0.5f, -0.5f, -0.5f, // 左下后 V0
0.5f, -0.5f, -0.5f, // 右下后 V1
0.5f, 0.5f, -0.5f, // 右上后 V2
-0.5f, 0.5f, -0.5f, // 左上后 V3
-0.5f, -0.5f, 0.5f, // 左下前 V4
0.5f, -0.5f, 0.5f, // 右下前 V5
0.5f, 0.5f, 0.5f, // 右上前 V6
-0.5f, 0.5f, 0.5f // 左上前 V7
)
// 正方体12个三角形的顶点索引(两个三角形组成一个面)
private val indices = shortArrayOf(
0, 1, 2, 0, 2, 3, // 后面
1, 5, 6, 1, 6, 2, // 右面
5, 4, 7, 5, 7, 6, // 前面
4, 0, 3, 4, 3, 7, // 左面
3, 2, 6, 3, 6, 7, // 上面
4, 5, 1, 4, 1, 0 // 下面
)
// 每个顶点的颜色(RGBA)
private val colors = floatArrayOf(
0.0f, 0.0f, 0.0f, 1.0f, // V0黑色
1.0f, 0.0f, 0.0f, 1.0f, // V1红色
1.0f, 1.0f, 0.0f, 1.0f, // V2黄色
0.0f, 1.0f, 0.0f, 1.0f, // V3绿色
0.0f, 0.0f, 1.0f, 1.0f, // V4蓝色
1.0f, 0.0f, 1.0f, 1.0f, // V5紫色
1.0f, 1.0f, 1.0f, 1.0f, // V6白色
0.0f, 1.0f, 1.0f, 1.0f // V7青色
)
private var angleX = 0f
private var angleY = 0f
init {
// 初始化顶点缓冲区
val vbb = ByteBuffer.allocateDirect(vertices.size * 4)
vbb.order(ByteOrder.nativeOrder())
vertexBuffer = vbb.asFloatBuffer()
vertexBuffer.put(vertices)
vertexBuffer.position(0)
// 初始化索引缓冲区
val ibb = ByteBuffer.allocateDirect(indices.size * 2)
ibb.order(ByteOrder.nativeOrder())
indexBuffer = ibb.asShortBuffer()
indexBuffer.put(indices)
indexBuffer.position(0)
// 初始化颜色缓冲区
val cbb = ByteBuffer.allocateDirect(colors.size * 4)
cbb.order(ByteOrder.nativeOrder())
colorBuffer = cbb.asFloatBuffer()
colorBuffer.put(colors)
colorBuffer.position(0)
// 计算法线数组
val normalsFloat = calculateNormals(vertices, indices)
val normalBuffer = ByteBuffer.allocateDirect(normalsFloat.size * 4)
normalBuffer.order(ByteOrder.nativeOrder())
normals = normalBuffer.asFloatBuffer()
normals.put(normalsFloat)
normals.position(0)
}
// 根据顶点和索引计算法线
private fun calculateNormals(vertices: FloatArray, indices: ShortArray): FloatArray {
// 初始化法线数组(每个顶点对应一个法线向量)
val normals = FloatArray(vertices.size)
// 临时存储每个顶点的法线累加值
val tempNormals = Array(vertices.size / 3) { FloatArray(3) { 0.0f } }
// 遍历每个三角形
for (i in indices.indices step 3) {
val i0 = indices[i].toInt()
val i1 = indices[i + 1].toInt()
val i2 = indices[i + 2].toInt()
// 获取三角形的三个顶点坐标
val v0 = floatArrayOf(
vertices[i0 * 3],
vertices[i0 * 3 + 1],
vertices[i0 * 3 + 2]
)
val v1 = floatArrayOf(
vertices[i1 * 3],
vertices[i1 * 3 + 1],
vertices[i1 * 3 + 2]
)
val v2 = floatArrayOf(
vertices[i2 * 3],
vertices[i2 * 3 + 1],
vertices[i2 * 3 + 2]
)
// 计算边向量
val edge1 = floatArrayOf(
v1[0] - v0[0],
v1[1] - v0[1],
v1[2] - v0[2]
)
val edge2 = floatArrayOf(
v2[0] - v0[0],
v2[1] - v0[1],
v2[2] - v0[2]
)
// 计算面法线(叉乘)
val faceNormal = floatArrayOf(
edge1[1] * edge2[2] - edge1[2] * edge2[1],
edge1[2] * edge2[0] - edge1[0] * edge2[2],
edge1[0] * edge2[1] - edge1[1] * edge2[0]
)
// 累加面法线到每个顶点
for (j in 0..2) {
tempNormals[i0][j] += faceNormal[j]
tempNormals[i1][j] += faceNormal[j]
tempNormals[i2][j] += faceNormal[j]
}
}
// 归一化每个顶点的法线
for (i in tempNormals.indices) {
val normal = tempNormals[i]
val length = Math.sqrt(
(normal[0] * normal[0] +
normal[1] * normal[1] +
normal[2] * normal[2]).toDouble()
).toFloat()
if (length > 0) {
normal[0] /= length
normal[1] /= length
normal[2] /= length
}
// 将归一化后的法线存入结果数组
normals[i * 3] = normal[0]
normals[i * 3 + 1] = normal[1]
normals[i * 3 + 2] = normal[2]
}
return normals
}
override fun onSurfaceCreated(gl: GL10, config: EGLConfig) {
// 设置清屏颜色为灰色
gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f)
// 启用深度测试
gl.glEnable(GL10.GL_DEPTH_TEST)
// 启用顶点和颜色数组
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY)
gl.glEnableClientState(GL10.GL_COLOR_ARRAY)
setupLight(gl)
setMaterial(gl)
}
/**
* 材质属性
*/
private fun setMaterial(gl: GL10) {
// // 设置环境元素和散射元素的颜色 RGBA 这里设置的比较便蓝色
// val ambientLight = floatArrayOf(0f, 0.1f, 0.9f, 1.0f)
// // 设置材质环境元素和反射元素
// // 第一个参数:GL10.GL_FRONT_AND_BACK 设置面,都会设置成前后
// // 第二个参数:GL10.GL_AMBIENT_AND_DIFFUSE 设置环境元素和反射元素 还可以设置 GL10.GL_AMBIENT\GL10.GL_DIFFUSE
// // 被感知的颜色一般同时经过环境元素和反射元素所影响。
// // 第三个参数:设置颜色
// // 第四个参数:偏移量,如果使用重载的buffer传入则就前面三个参数
// gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT_AND_DIFFUSE, ambientLight, 0)
// 环境元素 蓝色
val ambientLight = floatArrayOf(0f, 0.1f, 0.9f, 1.0f)
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT, ambientLight, 0)
// 散射元素 红色
val diffuseLight = floatArrayOf(0.9f, 0f, 0.1f, 1.0f)
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, diffuseLight, 0)
// 设置高光元素 黄色
val specularLight = floatArrayOf(0.9f, 0.9f, 0f, 1.0f)
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, specularLight, 0)
// 设置反光度 反射度越高 高光反射越小
gl.glMaterialf(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS, 25F )
// 自发光 绿色
val emissionLight = floatArrayOf(0.0f, 0.4f, 0f, 1.0f)
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_EMISSION, emissionLight, 0)
}
/**
* 设置光效
*/
private fun setupLight(gl: GL10) {
// 启用光照和材质颜色追踪
gl.glEnable(GL10.GL_LIGHTING)
gl.glEnable(GL10.GL_LIGHT0)
// gl.glEnable(GL10.GL_COLOR_MATERIAL)
gl.glEnable(GL10.GL_SPECULAR)
gl.glEnable(GL10.GL_SPOT_CUTOFF)
// 设置环境光
val ambientLight = floatArrayOf(0.2f, 0.2f, 0.2f, 1.0f)
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, ambientLight, 0)
// 设置漫反射光
val diffuseLight = floatArrayOf(0.8f, 0.8f, 0.8f, 1.0f)
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, diffuseLight, 0)
// 设置高光
val specularLight = floatArrayOf(1.0f, 1.0f, 1.0f, 1.0f)
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR, specularLight, 0)
// 设置光源位置
val lightPosition = floatArrayOf(0.0f, 0.0f, -1.0f, 0.0f) // 方向光
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, lightPosition, 0)
// 光源的方向
val lightDirection = floatArrayOf(0f, 0f, 1f)
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPOT_DIRECTION, lightDirection, 0)
gl.glLightf(GL10.GL_LIGHT0, GL10.GL_SPOT_CUTOFF, 10F)
}
override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) {
// 设置视口大小
gl.glViewport(0, 0, width, height)
// 设置投影矩阵
gl.glMatrixMode(GL10.GL_PROJECTION)
gl.glLoadIdentity()
// 设置透视投影
val aspectRatio = width.toFloat() / height
GLU.gluPerspective(gl, 45.0f, aspectRatio, 0.1f, 1000.0f)
// 设置模型视图矩阵
gl.glMatrixMode(GL10.GL_MODELVIEW)
gl.glLoadIdentity()
}
override fun onDrawFrame(gl: GL10) {
// 清除颜色和深度缓冲区
gl.glClear(GL10.GL_COLOR_BUFFER_BIT or GL10.GL_DEPTH_BUFFER_BIT)
gl.glLoadIdentity()
// 设置着色模式
gl.glShadeModel(GL10.GL_SMOOTH)
// 设置观察位置
gl.glTranslatef(0.0f, 0f, -5.0f)
gl.glRotatef(angleX, 1.0f, 0.0f, 0.0f)
gl.glRotatef(angleY, 0.0f, 1.0f, 0.0f)
// 旋转正方体
angleX += 1.0f
angleY += 0.5f
// 启用法线数组
gl.glEnableClientState(GL10.GL_NORMAL_ARRAY)
gl.glNormalPointer(GL10.GL_FLOAT, 0, normals)
// 设置顶点和颜色指针
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer)
gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer)
// 绘制正方体
gl.glDrawElements(
GL10.GL_TRIANGLES, indices.size,
GL10.GL_UNSIGNED_SHORT, indexBuffer
)
// 禁用法线数组
gl.glDisableClientState(GL10.GL_NORMAL_ARRAY)
}
}