OpenGL ES绘制3D图形以及设置视口

发布于:2025-06-13 ⋅ 阅读:(14) ⋅ 点赞:(0)

关于 glDrawElements

基本概念

glDrawElements 是 OpenGL 中用于渲染图元的核心函数之一,它允许你使用索引缓冲区(Index Buffer)来指定顶点的绘制顺序,从而实现高效的渲染。这个函数在处理需要重复使用顶点数据的场景时特别有用,比如 3D 模型渲染。

使用场景

  • 渲染包含大量重复顶点的复杂模型(如立方体、地形网格)。
  • 共享顶点属性(如位置、法线、纹理坐标)。
void glDrawElements(
    int mode,           // 绘制模式(如 GL_TRIANGLES、GL_LINES)
    int count,          // 索引数量
    int type,           // 索引数据类型(如 GL_UNSIGNED_SHORT)
    java.nio.Buffer indices  // 索引缓冲区
);

mode 绘制模式

模式 描述
GL_POINTS 将每个顶点作为一个点绘制。
GL_LINES 将顶点两两连接为线段(v0-v1, v2-v3, …)。
GL_LINE_STRIP 连接所有顶点为连续线段(v0-v1-v2-…)。
GL_LINE_LOOP 连接所有顶点为闭合线段(最后一个点连回第一个点)。
GL_TRIANGLES 将顶点每三个一组构成三角形(v0-v1-v2, v3-v4-v5, …)。
GL_TRIANGLE_STRIP 相邻三个顶点构成三角形(v0-v1-v2, v1-v2-v3, …)。
GL_TRIANGLE_FAN 以第一个顶点为中心,与后续相邻顶点构成扇形三角形(v0-v1-v2, v0-v2-v3, …)。

count 索引数量

需要绘制的索引总数。例如,渲染一个立方体需要 36 个索引(12 个三角形 × 3 个顶点)。

type 索引数据类型

类型 描述
GL_UNSIGNED_BYTE 8 位无符号整数(范围:0~255)。
GL_UNSIGNED_SHORT 16 位无符号整数(范围:0~65535)。
GL_UNSIGNED_INT 32 位无符号整数(范围:0~4294967295)。
顶点数 ≤ 255:使用 GL_UNSIGNED_BYTE。
顶点数 ≤ 65535:使用 GL_UNSIGNED_SHORT(最常用)。
顶点数 > 65535:使用 GL_UNSIGNED_INT(需 OpenGL ES 3.0+)

indices 索引缓冲区

存储顶点索引的缓冲区对象(如 java.nio.Buffer)。索引值对应顶点数组中的位置。

工作原理

  • 顶点数组:定义所有顶点的属性(如位置、颜色)。
  • 索引数组:指定顶点的绘制顺序。
  • glDrawElements:根据索引从顶点数组中提取顶点,并按指定模式绘制。

绘制正方体实例

先看效果
在这里插入图片描述
先上全部代码

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 CubeRenderer : GLSurfaceView.Renderer {
    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)
    }

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

    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.glTranslatef(0.0f, 0.0f, -5.0f) // 将正方体移到屏幕中央前方

        // 旋转正方体
        angleX += 1.0f
        angleY += 0.5f
        gl.glRotatef(angleX, 1.0f, 0.0f, 0.0f) // 绕X轴旋转
        gl.glRotatef(angleY, 0.0f, 1.0f, 0.0f) // 绕Y轴旋转

        // 设置顶点和颜色指针
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer)
        gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer)

        // 使用 glDrawElements 绘制正方体
        gl.glDrawElements(
            GL10.GL_TRIANGLES, indices.size,
            GL10.GL_UNSIGNED_SHORT, indexBuffer
        )
    }
}

代码中都有标注,核心代码在 onDrawFrame 中,最后一行通过
gl.glDrawElements( GL10.GL_TRIANGLES, indices.size,GL10.GL_UNSIGNED_SHORT, indexBuffer)

读取顶点数据等信息,绘制正方体。

注意:    
创建Buffer时,vbb.order(ByteOrder.nativeOrder()) 一般得加上。
它是 Java NIO 缓冲区操作中的关键步骤,用于设置字节序(Byte Order),确保数据在内存中的存储方式与设备硬件一致。

不添加这行有时候你会发现绘制的没啥错,就是不显示图像!!!

视口

透视投影(Perspective Projection)

基本概念
模拟人眼视觉:远处物体看起来更小,产生 "近大远小" 的效果。
视锥体(Frustum):由近平面、远平面和四个侧面组成的截头四棱锥,只有视锥体内的物体可见。
// 方法 1:使用 glFrustumf
glFrustumf(left, right, bottom, top, near, far);

// 方法 2:使用 GLU.gluPerspective 内部也是使用 glFrustumf 来实现
GLU.gluPerspective(fovy, aspect, zNear, zFar);
fovy(视野角度):角度越大,视野越宽广(类似广角镜头);角度越小,视野越狭窄(类似长焦镜头)。
aspect(宽高比):需与视口宽高比匹配,否则会导致图像拉伸。
near 和 far:影响深度精度和可见距离,比值过大会导致深度冲突(Z-Fighting)

修改上面的代码,使用循环多绘制一些正方体

   override fun onDrawFrame(gl: GL10) {
        // 清除颜色和深度缓冲区
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT or GL10.GL_DEPTH_BUFFER_BIT)

        // 旋转正方体
        angleX += 1.0f
        angleY += 0.5f


        // 设置顶点和颜色指针
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer)
        gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer)

        for (i in 0..10) {
            // 使用 glDrawElements 绘制正方体
            // 设置模型视图矩阵
            gl.glLoadIdentity()
            gl.glTranslatef(0.0f, -1f, -(5.0f * i.toFloat())) // 修改平移距离使绘制看起来远近的效果
            gl.glRotatef(angleX, 1.0f, 0.0f, 0.0f) // 绕X轴旋转
            gl.glRotatef(angleY, 0.0f, 1.0f, 0.0f) // 绕Y轴旋转
            gl.glDrawElements(
                GL10.GL_TRIANGLES, indices.size,
                GL10.GL_UNSIGNED_SHORT, indexBuffer
            )
        }
    }

效果如下图,有一种越远越小的感觉。

在这里插入图片描述

正交投影(Orthographic Projection)

基本概念
平行投影:光线从无限远处平行照射物体,物体大小与距离无关。
保持比例:物体的真实尺寸和角度在投影后保持不变,平行线投影后仍平行。
// 方法 1:3D 正交投影(OpenGL ES 1.x/2.0)
void glOrthof(float left, float right, float bottom, float top, float near, float far);

// 方法 2:2D 正交投影(OpenGL ES 1.x,简化版)
void GLU.gluOrtho2D(float left, float right, float bottom, float top);

将上面透视投影的方法换成正交投影,同样绘制多个正方体。

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)
        // 正交投影
        GLU.gluOrtho2D(gl, -5F, 5F, -5F, 5F)

        // 设置模型视图矩阵
        gl.glMatrixMode(GL10.GL_MODELVIEW)
        gl.glLoadIdentity()
    }

效果图如下,只能看到最前面的一个正方体

在这里插入图片描述

正交投影和透视投影对比

特性 正交投影 透视投影
特性 正交投影 透视投影
视觉效果 无近大远小,深度感弱 近大远小,深度感强
投影矩阵 线性变换(仅缩放和平移) 非线性变换(包含除法)
深度精度 均匀分布 近平面精度高,远平面精度低
物体大小 与距离无关 随距离增加而减小
适用场景 2D 游戏、UI、地图、CAD 3D 游戏、VR、真实感渲染

网站公告

今日签到

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