有关 OpenGL ES 的 Android 开发人员指南描述了如何应用基于触摸输入的相机视图转换(请参阅OpenGL ES:响应触摸事件)。
这基本上是通过根据触摸的 x 和 y 移动设置
angle
属性来实现的……
override fun onTouchEvent(e: MotionEvent): Boolean {
/* … */
renderer.angle += (dx + dy) * TOUCH_SCALE_FACTOR
/* … */
}
...并根据它应用旋转:
override fun onDrawFrame(gl: GL10) {
/* … */
Matrix.setRotateM(rotationMatrix, 0, angle, 0f, 0f, -1.0f)
/* … */
}
但是,在这种方法中,触摸事件的 x 坐标和 y 坐标都组合为一个角度,确定绕 z 轴的旋转。
我尝试分离角度......
override fun onTouchEvent(e: MotionEvent): Boolean {
/* … */
renderer.angleX += dx * TOUCH_SCALE_FACTOR
renderer.angleY += dy * TOUCH_SCALE_FACTOR
/* … */
}
...执行不同的旋转:
override fun onDrawFrame(gl: GL10) {
/* … */
Matrix.setRotateM(rotationMatrix, 0, angleX, 0f, 1f, 0f)
Matrix.setRotateM(rotationMatrix, 0, angleY, 1f, 0f, 0f)
/* … */
}
但是,沿 x 维度拖动时,结果似乎不太正确。
我如何获得这种非常基本的摄像机运动,就像您在不同平台上的几乎所有其他 3D 程序中看到的那样?
(在 iOS SceneKit 中,您可以使用一行来获取此(和其他手势):
sceneView.allowsCameraControl = true
。)
相机可以被认为是在围绕原点的球体表面上移动。因此,球坐标非常适合描述其运动。
为了定义空间中点的位置,球坐标系使用点到原点(球体半径)的距离
r
和两个角度 theta
和 phi
的组合。对于我们的用例,半径将保持固定。 x 方向上的触摸移动将对应于 phi
的变化,y 方向上的移动将对应于 theta
的变化。
互换 OpenGL ES 风格坐标系的轴,球坐标
(r, theta, phi)
按以下方式转换为常规笛卡尔坐标 (x, y, z)
:
x = r * sin(theta) * sin(phi)
y = r * cos(theta)
z = r * sin(theta) * cos(phi)
Matrix.setLookAtM()
方法设置。除了输出矩阵(及其内部的偏移量)之外,此方法还需要三个输入向量。 eye
向量定义了相机在三维空间中的位置,而center
向量指定了相机将要观察的点(在我们的例子中,该点始终是原点)。这为相机留下了一个自由度,即它围绕连接眼睛和中心的线旋转。该旋转由定义相机视场中向上方向的 up
矢量确定。只有该向量的方向有任何相关性,而不是其长度。
在代码中,再次使用
onTouchEvent()
子类的 GLSurfaceView
方法来捕获触摸输入(围绕中点的方向反转可能会也可能不会被删除):
override fun onTouchEvent(e: MotionEvent): Boolean {
/* … */
renderer.phi -= dx * 0.01f
renderer.theta -= dy * 0.01f
/* … */
}
GLSurfaceView.Renderer
子类声明两个角度和固定半径,并执行相机变换。最初,角度设置为 theta = 90°
和 phi = 0°
,对应于 (x, y, z) = (0, 0, r)
:
class CubeRenderer : GLSurfaceView.Renderer {
/* … */
@Volatile
var theta: Double = PI / 2.0
@Volatile
var phi: Double = 0.0
override fun onDrawFrame(unused: GL10) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
val x = RADIUS * sin(theta) * sin(phi)
val y = RADIUS * cos(theta)
val z = RADIUS * sin(theta) * cos(phi)
val ux = -cos(theta) * sin(phi)
val uy = sin(theta)
val uz = -cos(theta) * cos(phi)
Matrix.setLookAtM(
viewMatrix, 0,
x.toFloat(), y.toFloat(), z.toFloat(), // eye
0.0f, 0.0f, 0.0f, // center (= origin)
ux.toFloat(), uy.toFloat(), uz.toFloat() // up
)
Matrix.multiplyMM(vPMatrix, 0, projectionMatrix, 0, viewMatrix, 0)
triangle.draw(vPMatrix)
}
private companion object {
const val RADIUS = 5.0
}
}
up
向量设置为与连接eye
和center
向量的线正交。给定 (r, theta, phi)
向量的球面坐标 eye
,这可以通过将 up
向量设置为 (1, theta - pi/2, phi)
来实现。也就是说,up
向量的θ角比eye
向量的θ角小90°,并且由于其长度并不重要,因此将其设置为1(以简化计算)。
鉴于
sin(x - pi/2) = -cos(x)
和 cos(x - pi/2) = sin(x)
对于任何实数 x
,我们因此得到:
ux = sin(theta - pi/2) * sin(phi) = -cos(theta) * sin(phi)
uy = cos(theta - pi/2) = sin(theta)
uz = sin(theta - pi/2) * cos(phi) = -cos(theta) * cos(phi)
将
eye
向量初始设置为 (r, theta, phi) = (5, pi/2, 0)
,up
向量最初将为 (x, y, z) = (0, 1, 0)
,即指向 y 轴正方向。