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值)
- 点击时读取触摸位置的像素颜色
- 对比颜色反查对应标记
实现要点:
- 绘制时先渲染离屏缓冲区,仅用唯一颜色绘制标记(关闭纹理、光照)
- 点击时调用
glReadPixels获取触摸点颜色 - 通过颜色映射找到对应的标记
内容的提问来源于stack exchange,提问作者g3ryyy
相关产品推荐
相关产品推荐

