You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OpenGL ES 2.0中如何检测绘制对象的点击事件?

OpenGL ES 点击检测实现方案

我是OpenGL新手,现在需要给程序添加绘制对象的点击检测功能。已经有坐标数组并完成了对象绘制,想实现点击渲染的点,但不知道怎么做。之前试过在onTouchEvent里检测点击,但触摸坐标和OpenGL坐标对不上;了解过射线拾取算法,想知道有没有更简便的方法在OpenGL ES里实现对象点击检测。

相关代码

点数组与缓冲区数组

var ppl: Array<FloatArray> = arrayOf(
        floatArrayOf(30f,1f,0f,0f,0f,1f),  ///x,y,z,isActive,isWork,signal
        floatArrayOf(-10f,10f,0f,1f,1f,1f),
        floatArrayOf(0f,0f,0f,1f,0f,0f),
        floatArrayOf(70f,50f,50f,1f,0f,1f)
    )

var mBufferLen = intArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0) //0/Floor/Floor2/Point/Ball/Drop/Grid/Size/Search/Work

onSurfaceCreated 方法

override fun onSurfaceCreated(gl1: GL10, pConfig: javax.microedition.khronos.egl.EGLConfig?) {
    val gl = gl1 as GL11 //需要GL1.1的功能
    //设置背景颜色
    //gl.glEnable(GL11.GL_LIGHTING)
    gl.glClearColor(0.01f, 0.5f, 0.3f, 1.0f) //深绿色
    BuildDrop(gl)
    BuildFloor2(gl)
    BuildFloor(gl)
    BuildGrid(gl)
    BuildSizeGrid(gl, spinnerIndicator)
    BuildDropT(gl)
    BuildWorkMark(gl)
}

BuildDrop 方法

fun BuildDrop(gl: GL11) {
    //每个drop使用相同坐标,绘制时通过glRotate和glTranslate调整
    val vtx = floatArrayOf(
        // X, Y, Z
        0f, 0f, 0f,
        -1f, -1f, 0f,
        1f, -1f, 0f,
        -1f, -1f, 0f,
        1f, -1f, 0f,
        0f, -2f, 0f,
    )
    StoreVertexData(gl, vtx, mDROP) //存储到GPU缓冲区
}

StoreVertexData 方法

fun StoreVertexData(gl: GL11, pVertices: FloatArray, pObjectNum: Int) {
    val buffer: FloatBuffer = ByteBuffer.allocateDirect(pVertices.size * 4) //float占4字节
        .order(ByteOrder.nativeOrder()) //使用设备原生字节序
        .asFloatBuffer() //从ByteBuffer创建FloatBuffer
        .put(pVertices) //将坐标存入FloatBuffer
    gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, pObjectNum) //绑定为当前对象
    buffer.position(0)
    //分配内存并写入缓冲区数据
    gl.glBufferData(GL11.GL_ARRAY_BUFFER, buffer.capacity() * 4, buffer, GL11.GL_STATIC_DRAW)
    gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0) //解绑缓冲区
    mBufferLen[pObjectNum] = buffer.capacity() / 3 //存储用于绘制的数量
}

绘制点的方法 DrawDropMarks

private fun DrawDropMarks(gl: GL11, pDropCoords: Array<FloatArray>) {
    var pDropCoords = pDropCoords
    var pAngX = 0f
    var pAngY = 0f
    val TriCnt = pDropCoords.size //三角形数量
    var isActive = 10f
    var isWork = 10f
    var signal = 10f
    val centerX = (display.width/2)
    val centerY = (display.height/2)
    for (ctr in 0 until TriCnt) {
        gl.glColor4f(1f, 0f, 0f, 1.0f) //标记默认颜色
        gl.glPushMatrix() //translate/rotate仅影响当前三角形
        gl.glTranslatef(centerX.toFloat(), centerY.toFloat(), 0f)
        //gl.glTranslatef(pDropCoords[ctr][0], pDropCoords[ctr][1], pDropCoords[ctr][2])
        if (MultiBillboard) //单独计算每个三角形的广告牌角度
        {
            var hypLen: Float
            val distX = mCamXpos - pDropCoords[ctr][0]
            val distY = mCamYpos - pDropCoords[ctr][1]
            val distZ = mCamZpos - pDropCoords[ctr][2]
            isActive = pDropCoords[ctr][3]
            isWork = pDropCoords[ctr][4]
            signal = pDropCoords[ctr][5]
            //2D斜边长度
            hypLen = sqrt((distX * distX + distZ * distZ).toDouble()).toFloat() //地面上的距离
            pAngY = 270 - atan2(distZ.toDouble(), distX.toDouble()).toFloat() * mRad2Deg.toFloat()
            //第三维度角度
            pAngX = atan2(distY.toDouble(), hypLen.toDouble()).toFloat() * mRad2Deg.toFloat()
/*            Log.d("x",pAngX.toString())
            Log.d("y",pAngY.toString())
            Log.d("hyp",hypLen.toString())*/
        }
        gl.glRotatef(pAngY, 0f, 1f, 0f)
        gl.glRotatef(pAngX, 1f, 0f, 0f)
        mark = (sin((System.currentTimeMillis() * 2 * Math.PI / FLASH_DURATION) * 0.5) + 0.5)
        //调整三角形大小
        gl.glScalef(lengthRatio, lengthRatio, lengthRatio)
        if (signal == 0f){
            ppl = remove(ppl,ctr)
            pl += pDropCoords[ctr]
        }
        if ((isActive == 1f)){
            //初始化三角形顶点缓冲区
            gl.glEnableClientState(GL10.GL_VERTEX_ARRAY)
            gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mSEARCH)
            gl.glVertexPointer(3, GL11.GL_FLOAT, 0, 0)
            if (isWork == 0f) {
                gl.glColor4f(0.3f, 1.0f, 0f, 1.0f)
            }
            else gl.glColor4f(1f, 1.0f, 0f, 1.0f)// 激活标记颜色
            //gl.glColor4f(0.3f, 1.0f, 0f, 1.0f) // 激活标记颜色
            gl.glDrawArrays(GL11.GL_LINES, 0, mBufferLen[mSEARCH])
        } //单个drop
        if ((isWork == 1f) && (isActive == 1f)){
            //初始化三角形顶点缓冲区
            gl.glEnableClientState(GL10.GL_VERTEX_ARRAY)
            gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mWORK)
            gl.glVertexPointer(3, GL11.GL_FLOAT, 0, 0)
            gl.glColor4f(1f, 1.0f, 0f, 1.0f) // 激活标记颜色
            //gl.glColor4f(0.3f, 1.0f, 0f, 1.0f) // 激活标记颜色
            gl.glDrawArrays(GL11.GL_LINES, 0, mBufferLen[mWORK])
        } //单个drop
        gl.glPopMatrix() //当前三角形绘制完成
    }
    gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0) //解绑缓冲区
}

实现点击检测的可行方案

1. 先解决坐标不匹配问题

屏幕触摸坐标是像素坐标系(原点左上角,Y轴向下),OpenGL用的是标准化设备坐标系(NDC)(范围[-1,1],原点中心,Y轴向上),需要两步转换:

步骤1:屏幕坐标转NDC坐标

fun screenToNdc(touchX: Float, touchY: Float, viewWidth: Int, viewHeight: Int): Pair<Float, Float> {
    val ndcX = (touchX / viewWidth) * 2f - 1f
    // 翻转Y轴,适配OpenGL坐标系
    val ndcY = 1f - (touchY / viewHeight) * 2f
    return Pair(ndcX, ndcY)
}

步骤2:NDC转世界空间射线

用投影矩阵和视图矩阵的逆矩阵,将NDC点转换为世界空间的射线(起点是相机位置,方向由近远平面点计算):

// 假设你已持有投影矩阵projectionMatrix和视图矩阵viewMatrix
val viewProjMatrix = FloatArray(16)
Matrix.multiplyMM(viewProjMatrix, 0, projectionMatrix, 0, viewMatrix, 0)
val invViewProj = FloatArray(16)
Matrix.invertM(invViewProj, 0, viewProjMatrix, 0)

// NDC近平面(z=-1)和远平面(z=1)点
val nearPoint = floatArrayOf(ndcX, ndcY, -1f, 1f)
val farPoint = floatArrayOf(ndcX, ndcY, 1f, 1f)

// 转换为世界空间点
val nearWorld = FloatArray(4)
val farWorld = FloatArray(4)
Matrix.multiplyMV(nearWorld, 0, invViewProj, 0, nearPoint, 0)
Matrix.multiplyMV(farWorld, 0, invViewProj, 0, farPoint, 0)

// 计算并归一化射线方向
val rayDir = floatArrayOf(
    farWorld[0] - nearWorld[0],
    farWorld[1] - nearWorld[1],
    farWorld[2] - nearWorld[2]
)
Matrix.normalize(rayDir, rayDir)

// 射线起点就是相机位置:mCamXpos, mCamYpos, mCamZpos

2. 简化碰撞检测(针对你的标记)

你的标记是带大小的图形,可当成球体检测射线相交,比完整射线拾取更简单:

fun raySphereIntersect(
    rayOrigin: FloatArray, // 相机位置 [x,y,z]
    rayDir: FloatArray,    // 射线方向 [x,y,z]
    sphereCenter: FloatArray, // 标记中心 [x,y,z]
    sphereRadius: Float
): Boolean {
    val oc = floatArrayOf(
        rayOrigin[0] - sphereCenter[0],
        rayOrigin[1] - sphereCenter[1],
        rayOrigin[2] - sphereCenter[2]
    )
    val a = Matrix.dot(rayDir, rayDir)
    val b = 2f * Matrix.dot(oc, rayDir)
    val c = Matrix.dot(oc, oc) - sphereRadius * sphereRadius
    val discriminant = b * b - 4f * a * c
    return discriminant >= 0f
}

在onTouchEvent中遍历所有标记,用上述方法检测,第一个相交的就是被点击的标记,半径可根据lengthRatio调整(比如lengthRatio * 2f)。

3. 更直观的颜色拾取方案

如果场景不复杂,颜色拾取是更简单的选择:

  • 给每个可点击标记分配唯一颜色(比如用索引转RGB值)
  • 点击时读取触摸位置的像素颜色
  • 对比颜色反查对应标记

实现要点:

  1. 绘制时先渲染离屏缓冲区,仅用唯一颜色绘制标记(关闭纹理、光照)
  2. 点击时调用glReadPixels获取触摸点颜色
  3. 通过颜色映射找到对应的标记

内容的提问来源于stack exchange,提问作者g3ryyy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 19:42:00