OpenGL ES 设置光效效果

发布于:2025-06-21 ⋅ 阅读:(18) ⋅ 点赞:(0)

上一篇文章记录了如何绘制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. 顶点法线的作用
  • 定义表面方向:法线是垂直于顶点所在表面的单位向量(长度为 1)。
  • 计算光照:OpenGL 使用法线来确定光线与表面的夹角,从而计算反射光强度。
  • 平滑着色:通过顶点法线插值,可实现平滑的光照过渡(如 Phong 着色)
  1. 法线的数学特性
  • 单位向量:法线必须归一化(长度为 1),否则光照计算会失真。
  • 方向敏感性:法线方向决定表面朝向(正面 / 背面),影响光照和背面剔除。
  • 多边形面法线:对三角形等平面,所有顶点共享同一个法线(如立方体的面)。
  • 平滑表面法线:对曲面(如球体),每个顶点有不同的法线以实现平滑过渡。
  1. 常见问题
    表面全黑:可能是法线未归一化,或方向错误。
    光照不均匀:检查法线插值是否正确,或是否需要平滑着色。
    背面显示异常:确认背面剔除设置和法线方向是否一致。

完整代码

下面是包括上面各种光源设置的代码,包括顶点法线:

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)
    }

    /**
     * 设置光效
     */
    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)
    }
}

效果和前一篇文章差不多,区别就是根据各种光源设置,显示的光亮等程度有变化。


网站公告

今日签到

点亮在社区的每一天
去签到