OpenGL ES 设置光效效果
文章目录
- 阴影模型
- 光效三要素
- 开启光效
- 设置环境光
- 设置漫反射光
- 设置高光
- 设置光源位置
- 光源的方向
- 聚光截止角 (Spot Cutoff Angle):
- 顶点法线
- 完整代码
上一篇文章记录了如何绘制3D图形以及对应视口,这篇文章在此基础上,为绘制3D效果图增加光效效果
阴影模型
GL_FLAT(恒定)、GL_SMOOTH(光滑)
在 OpenGL ES 中,GL_FLAT 和 GL_SMOOTH 是两种基本的着色模式,它们定义了如何在多边形表面上插值颜色。这两种模式在不同的渲染管线阶段工作,会产生截然不同的视觉效果。
- GL_FLAT(平面着色):
每个多边形只使用一个颜色值
颜色值取自多边形的最后一个顶点
不进行颜色插值
产生块状、不连续的视觉效果
计算开销较低
- GL_SMOOTH(平滑着色):
在多边形表面进行颜色插值
基于每个顶点的颜色计算
产生连续、平滑的视觉效果
计算开销较高
- 视觉效果对比
使用 GL_FLAT 时,每个多边形显示为单一颜色,边界清晰可见,常用于风格化渲染或需要明显区分不同多边形的场景。而 GL_SMOOTH 会在多边形表面平滑过渡颜色,适合表现圆润的物体和自然光照效果。
OpenGL ES默认采用了平滑着色的方式 ,对比上一篇绘制正方形的样式
gl.glShadeModel(GL10.GL_SMOOTH)
下面采用了平面着色绘制后的效果。
gl.glShadeModel(GL10.GL_FLAT)
因为每个面采用两个正方形绘制的,所以采用平滑绘制后,每个三角形颜色互不相同。
光效三要素
环境元素(ambient component)
散射元素(diffuse component)
高光元素(specular component)
开启光效
gl.glEnable(GL10.GL_LIGHTING)
如果仅仅是开启光效,效果如下,颜色全部没有了,变成了一个黑色的正方体。当未开启光效时OpenGL有默认的光效效果。开启以后需要自己设置光源等设置。
设置环境光
val ambientLight = floatArrayOf(0.2f, 0.2f, 0.2f, 1.0f)gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, ambientLight)
- 设置光源氏通过上述方法设置的,其第一个参数如下:
GL10.GL_LIGHT0
这是第一个参数,表示要配置的光源编号。OpenGL ES 最多支持 8 个光源(GL_LIGHT0 到 GL_LIGHT7)。这里选择的是 0 号光源。
- 第二个参数如下:
指定要设置的光源属性类型。GL_AMBIENT 表示环境光属性。环境光是一种全局光照,均匀照亮场景中的所有物体,没有特定方向。除了GL_AMBIENT,还可以设置下面的参数。
参数 | 含义 |
---|---|
GL_AMBIENT | 表示环境光属性 |
GL_DIFFUSE | 漫反射光属性,产生定向照明效果 |
GL_SPECULAR | 镜面反射光属性,产生高光效果 |
GL_POSITION | 光源位置,用于定位点光源、聚光灯等 |
GL_SPOT_DIRECTION | 聚光灯方向 |
GL_SPOT_EXPONENT | 聚光指数,控制聚光的集中程度 |
GL_SPOT_CUTOFF | 聚光截止角度 |
GL_CONSTANT_ATTENUATION | 常量衰减因子 |
GL_LINEAR_ATTENUATION | 线性衰减因子 |
GL_QUADRATIC_ATTENUATION | 二次方衰减因子 |
环境光模拟的是光线在环境中多次反射后形成的均匀光照,例如室内墙壁反射的光线、室外大气散射的自然光。这种光没有明确的光源方向,会均匀地从所有方向照射到物体上。通常环境光设置的比较小
设置漫反射光
// 设置漫反射光val diffuseLight = floatArrayOf(0.8f, 0.8f, 0.8f, 1.0f)gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, diffuseLight, 0)
当光线照射到粗糙表面(如纸张、岩石、布料)时,会因表面微观结构的不规则性向各个方向散射,这种反射称为漫反射。
特点:无论观察者从哪个角度看,漫反射光的强度基本一致,表面呈现均匀的明暗效果(如磨砂玻璃、粉笔字的反光)。
-
diffuseLight:漫反射光的颜色和强度,格式为[R, G, B, A]
若将diffuseLight设为(0, 0, 0, 1),物体将失去主要光照,仅依赖环境光和镜面反射光,可能显得过暗;
若设为(1, 1, 1, 1),漫反射光过强,可能导致物体过亮,失去明暗层次。漫反射光通常是光照模型中的 “主力”,强度应高于环境光(如环境光设为 0.2,漫反射设为 0.8),以突出光源方向的影响。强光场景(如室外正午):漫反射光可设为 0.9-1.0,模拟太阳直射;弱光场景(如室内灯光):设为 0.5-0.7,避免物体过亮。金属材质:漫反射系数较低(如 0.3-0.5),镜面反射更强;布料材质:漫反射系数较高(0.6-0.8),镜面反射较弱。
漫反射光是计算机图形学中构建真实感的关键要素,它通过模拟光线在粗糙表面的散射现象,为物体赋予立体形态和固有色表现。在代码中合理设置漫反射光的强度和颜色,能显著提升场景的视觉真实度,而其与环境光、镜面反射光的配合,则构成了经典光照模型的基础框架。
设置高光
// 启用光照和高光gl.glEnable(GL10.GL_LIGHTING);gl.glEnable(GL10.GL_LIGHT0);gl.glEnable(GL10.GL_SPECULAR); // 启用高光反射// 设置高光val specularLight = floatArrayOf(1.0f, 1.0f, 1.0f, 1.0f)gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR, specularLight, 0)
在 OpenGL ES 中,高光(Specular Highlights) 是模拟光滑物体表面反射强光的视觉效果,用于表现物体的光泽度和材质特性。以下从物理原理、计算模型、代码实现及应用技巧四个方面详细解析:
- 物理原理
当光线照射到光滑表面(如金属、玻璃、水面)时,会遵循反射定律(入射角等于反射角),在特定方向形成明亮的反射光斑,这就是高光。
对比漫反射:漫反射向各个方向均匀散射光线,而高光仅在反射方向附近可见,且强度随观察角度变化。 - 高光与材质的关系
金属材质:高光强烈且集中(如不锈钢的高光几乎为白色);
非金属材质(如塑料、陶瓷):高光较柔和,颜色接近光源色;
粗糙表面(如木材、布料):几乎无明显高光。
高光是实现真实感渲染的关键要素,它通过模拟光线在光滑表面的镜面反射,为物体赋予材质特性(如金属光泽、玻璃反光)和动态视觉效果。合理调整高光参数(颜色、强度、指数)能显著提升模型的真实度,使其在不同光照条件下呈现出符合物理规律的视觉表现。
设置光源位置
// 设置光源位置val lightPosition = floatArrayOf(0.0f, 0.0f, -1.0f, 0.0f) // 方向光gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, lightPosition, 0)
/*** 设置光源的位置或方向* @param light 光源编号(如 GL10.GL_LIGHT0 表示第一个光源)* @param pname 参数名称(GL10.GL_POSITION 表示设置位置)* @param params 包含位置或方向的浮点数数组(长度为4)* @param offset 数组起始偏移量*/
public abstract void glLightfv(int light, int pname, float[] params, int offset);
第三个参数:
格式为 [x, y, z, w],其中:
x, y, z:表示三维空间中的坐标或方向向量;
w 的值决定光源类型:
w = 0:表示方向光(平行光),此时 [x,y,z] 表示光线方向(如太阳);
w = 1:表示位置光(点光源 / 聚光灯),此时 [x,y,z] 表示光源在世界坐标系中的位置。
-
平行光
所有光线平行,无具体位置,仅需指定方向;
光照强度不随距离衰减,适用于模拟太阳等远距离光源。 -
点光源(Point Light)
光线从 [x,y,z] 位置向四面八方发散;
光照强度随距离衰减(默认按 1/d² 衰减,可通过 GL_ATTENUATION 调整)。 -
聚光灯(Spotlight)(需要额外设置光源的角度和方向)
光线从位置 [x,y,z] 出发,沿 SPOT_DIRECTION 指定方向呈锥形发射;
仅当物体位于锥角内时才会被照亮。
效果:光源随相机移动,类似手电筒效果。
光源的方向
// 光源的方向val lightDirection = floatArrayOf(0f, 0f, 1f)gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPOT_DIRECTION, lightDirection, 0)
这个函数用于设置 OpenGL 中指定光源(这里是GL_LIGHT0)的聚光灯方向向量。聚光灯是一种有方向的锥形光源,只有位于锥形区域内的物体才会被照亮,方向向量决定了这个锥形的朝向。
- GL10.GL_LIGHT0:指定要配置的光源编号。OpenGL ES 1.0 最多支持 8 个光源(GL_LIGHT0到GL_LIGHT7)。
- GL10.GL_SPOT_DIRECTION:指定要设置的光源属性为聚光灯方向。
- lightDirection:一个包含三个浮点数的数组[x, y, z],表示聚光灯的方向向量。这个向量从光源位置出发,指向聚光灯照射的方向。
- 0:数组lightDirection的起始偏移量,表示从数组的第 0 个元素开始读取方向向量数据。
使用场景
聚光灯常用于模拟手电筒、舞台灯光等效果。例如,在游戏中模拟角色手持电筒的照明效果,或者在 3D 模型展示中突出显示特定区域。
聚光截止角 (Spot Cutoff Angle):
gl.glLightf(GL10.GL_LIGHT0, GL10.GL_SPOT_CUTOFF, 90F)
设置聚光灯特效的光线角度。第三个参数代表角度,中心线到边缘的角度,所以最大可以设置180度(代表360度可以照射)。角度外的物体无法照射光源。一般配合光源位置角度一起使用。
顶点法线
在 Android OpenGL ES 中,顶点法线(Vertex Normal)是实现光照效果的关键概念,它定义了顶点表面的方向。下面从基础概念到代码实现进行详细说明:
- 顶点法线的作用
- 定义表面方向:法线是垂直于顶点所在表面的单位向量(长度为 1)。
- 计算光照:OpenGL 使用法线来确定光线与表面的夹角,从而计算反射光强度。
- 平滑着色:通过顶点法线插值,可实现平滑的光照过渡(如 Phong 着色)
- 法线的数学特性
- 单位向量:法线必须归一化(长度为 1),否则光照计算会失真。
- 方向敏感性:法线方向决定表面朝向(正面 / 背面),影响光照和背面剔除。
- 多边形面法线:对三角形等平面,所有顶点共享同一个法线(如立方体的面)。
- 平滑表面法线:对曲面(如球体),每个顶点有不同的法线以实现平滑过渡。
- 常见问题
表面全黑:可能是法线未归一化,或方向错误。
光照不均匀:检查法线插值是否正确,或是否需要平滑着色。
背面显示异常:确认背面剔除设置和法线方向是否一致。
完整代码
下面是包括上面各种光源设置的代码,包括顶点法线:
class CubeRen2 : GLSurfaceView.Renderer {private var normals: FloatBufferprivate val vertexBuffer: FloatBufferprivate val indexBuffer: ShortBufferprivate val colorBuffer: FloatBuffer// 正方体的8个顶点坐标private val vertices = floatArrayOf(-0.5f, -0.5f, -0.5f, // 左下后 V00.5f, -0.5f, -0.5f, // 右下后 V10.5f, 0.5f, -0.5f, // 右上后 V2-0.5f, 0.5f, -0.5f, // 左上后 V3-0.5f, -0.5f, 0.5f, // 左下前 V40.5f, -0.5f, 0.5f, // 右下前 V50.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 = 0fprivate var angleY = 0finit {// 初始化顶点缓冲区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] /= lengthnormal[1] /= lengthnormal[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)}/*** 设置光效*/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() / heightGLU.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.0fangleY += 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)}
}
效果和前一篇文章差不多,区别就是根据各种光源设置,显示的光亮等程度有变化。