在OpenGL ES 2.0中通过触摸来旋转3D对象

本文由泰然教程组出品,翻译:Sile,Sharyu,蓝羽;校对:yuezang;转载请通知泰然!
在这个教程中,你将学到用OpenGL ES 2.0GLKit通过触摸来旋转一个3D物体。

我们先从简单开始,首先向你介绍随着用户的拖拽,怎样通过沿着x轴或y轴旋转一定数量的度数来旋转一个3D物体。然后使用四元数介绍一个更高级的技术。

这个教程会从Beginning OpenGL ES 2.0 with GLKit Tutorial提取示例工程,如果你没有准备好这个工程请下载

请牢记我是自学的,我的数学相当生疏,所以如果犯了错或者没有很正确的解释一些事情请抱歉。如果有人有更好的或更正确的方法来说明,请指出!

言归正传,让我们开始旋转。

简单旋转:尝试1

开始,运行sample project,你会看到立方体已经在固定的旋转了。

如果你打开HelloGLKitViewController.m,找到update方法,你会发现下面的代码进行了固定的旋转

_rotation += 90 * self.timeSinceLastUpdate;
modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, GLKMathDegreesToRadians(25), 1, 0, 0);
modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, GLKMathDegreesToRadians(_rotation), 0, 1, 0);

我们的目标是替换上面代码,使用户能通过拖动鼠标来旋转立方体,而不是一成不变的旋转。

我们首先尝试一下通过移动旋转模型到一个实例变量,将它改为用户拖拽的方式。对HelloGLKitViewController.m作以下修改:

// Add to HelloGLKitViewController private variables
GLKMatrix4 _rotMatrix;
 
// Add to bottom of setupGL
_rotMatrix = GLKMatrix4Identity;
 
// In update, replace the 3 rotation lines shown in the previous snippet with this
modelViewMatrix = GLKMatrix4Multiply(modelViewMatrix, _rotMatrix);
 
// Remove everything inside touchesBegan
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
 
}
 
// Add new touchesMoved method
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
 
    UITouch * touch = [touches anyObject];
    CGPoint location = [touch locationInView:self.view];    
    CGPoint lastLoc = [touch previousLocationInView:self.view];
    CGPoint diff = CGPointMake(lastLoc.x - location.x, lastLoc.y - location.y);
 
    float rotX = -1 * GLKMathDegreesToRadians(diff.y / 2.0);
    float rotY = -1 * GLKMathDegreesToRadians(diff.x / 2.0);
 
    GLKVector3 xAxis = GLKVector3Make(1, 0, 0);
    _rotMatrix = GLKMatrix4Rotate(_rotMatrix, rotX, xAxis.x, xAxis.y, xAxis.z);
    GLKVector3 yAxis = GLKVector3Make(0, 1, 0);
    _rotMatrix = GLKMatrix4Rotate(_rotMatrix, rotY, yAxis.x, yAxis.y, yAxis.z);
 
}

好了,这里我们初始化旋转模型到本体(没有变化),随着用户的拖拽,我们利用GLKMatrix4Rotate来对立方体进行一定度数的旋转。用户每移动一个像素,立方体旋转12度。

记住x轴是水平穿过屏幕的,y轴是垂直的。所以当用户从左拖到右,我们实际上是想要围着y轴旋转(rotY),反过来也是这样。

编译运行,你会注意到立方体刚开始旋转正常,但是不久后它就脱离了你的预期,奇怪的往斜方向旋转。

简单旋转:尝试2

当你对一个物体运用变换,你可以把它当作物体到一个世界新空间的活动坐标系。

要理解我所说的,运行app,从立方体的右上角扫过立方体中间。你已经按如下方法有效的修改了物体的坐标系统,使其位于不同的世界空间:

_rotMatrix是一种将物体移动到新空间的变换。当用户移动时代码会修改_rotMatrix,所以当我们告诉它围着x轴或y轴旋转,它就会在物体空间围着xy轴旋转。

尝试着自己实现它:做另外一个移动,从右到左(注意不是上移或下移),你会看到它是怎么围绕新的y轴旋转的。

但是如果我们是想沿着世界空间x轴和y轴旋转,而不是物体空间的x轴和y轴,怎样计算新坐标系统世界的x轴和y轴?

答案是简单的:如果_rotMatrix转换一个物体向量到它应该在世界空间的位置,_rotMatrix反变换转换一个世界向量到世界空间。

你可以看下面的等式来帮助理解:

_rotMatrix * object vector = world vector

两边同时乘以 (_rotMatrix)-1 (与 _rotMatrix相反), 结果如下:

(_rotMatrix)-1 * _rotMatrix * object vector = (rotMatrix-1) * world vector

由于(_rotMatrix)-1 * _rotMatrix = 1, 结果为:

object vector = (_rotMatrix)-1 * world vector

让我们试着实现!将touchesMoved修改成下面这样

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
 
    UITouch * touch = [touches anyObject];
    CGPoint location = [touch locationInView:self.view];    
    CGPoint lastLoc = [touch previousLocationInView:self.view];
    CGPoint diff = CGPointMake(lastLoc.x - location.x, lastLoc.y - location.y);
 
    float rotX = -1 * GLKMathDegreesToRadians(diff.y / 2.0);
    float rotY = -1 * GLKMathDegreesToRadians(diff.x / 2.0);
 
    bool isInvertible;
    GLKVector3 xAxis = GLKMatrix4MultiplyVector3(GLKMatrix4Invert(_rotMatrix, &isInvertible), 
        GLKVector3Make(1, 0, 0));
    _rotMatrix = GLKMatrix4Rotate(_rotMatrix, rotX, xAxis.x, xAxis.y, xAxis.z);
    GLKVector3 yAxis = GLKMatrix4MultiplyVector3(GLKMatrix4Invert(_rotMatrix, &isInvertible), 
        GLKVector3Make(0, 1, 0));
    _rotMatrix = GLKMatrix4Rotate(_rotMatrix, rotY, yAxis.x, yAxis.y, yAxis.z);
 
}

编译运行,现在是按照预期运行了!

四元数弧球旋转

概述

另外一种流行的易于旋转3D对象的方式是由Ken Shoemake普及的弧球旋转算法

让我来告诉你一种简单的关于弧球旋转算法的基本信息:

1.映射到球体。创建一个你想要旋转对象的虚拟球体。当使用者触摸时,你要计算出所处范围内最近的点,与触摸的点相一致(开始),且和触摸移动的点(当前)相似。

2.计算当前的旋转。从开始到当前范围内的一个旋转的点,你可以认为它是通过转旋坐标轴或者一些角度而得到的。计算旋转/坐标轴从开始/当前的位置。

3.更新全部的旋转。更新全部由第三步得到当前旋转的对象

步骤3最神奇的地方是把数学概念里的四元数引入进来。我不打算在后面讨论它们,但是我想指出3个四元数的高等级属性:

1. Quaternion=axis+angle of rotation.四元数可以代表一个坐标轴和一个旋转的角度。一个四元数可以代表任何旋转。

2. Multiply quaternion = combine rotations.如果你要使2个四元数相乘,它们代表旋转的联合

3. Can convert quaternion to matrices.通过一些数学方法你可以使一个四元数转换到一个旋转矩阵里去。

一个关于GLKit的有意思的事情是你可以使用四元数而不需要明白它的后台是如何工作和运转的。你可以象下面这样使用它的功能:

GLKQuaternionMakeWithAngleAndVector3Axis: 

创建一个坐标轴或旋转角度的quaternion

 

GLKQuaternionMultiply:

一个四元数乘以另外一个四元数(联合旋转)

 

GLKMatrix4MakeWithQuaternion:

quaternion转换成一个旋转矩阵

如果你仍然有一些小困惑关于它是如何工作的,不用担心,我们将在下面的章节里一步一步的告诉你!

1) 映射到球体.

假设在我们的对象周围有一个虚拟的范围,半径有屏幕的1/3之宽。这个范围的中心就是我们选中对象的中心。我们想让使用者“grab and drag”这些范围来旋转对象。所以第一步是要计算出将一个2D的点转换成一个虚拟3D范围内的点。这里有一个最简单的方式。首先,让我们来学习一些基础的东西。

我们知道x和y的位置是用户点击的那个点,所以我们能够通过Pythagorean Theorem计算出斜边的长度


我们现在知道范围的中心到用户点击的x和y坐标的矢量距离,但是不知道z坐标。

然后,我们知道z坐标必须和范围相交,如果你绘制一条线段从范围的中心到任意一点,它会有一条半径。


目前,我们有另外一个正确的三角形可以通过pythagorean thorem来解决它!我们可以得到:

r^2 = (sqrt(p.x^2 + p.y^2))^2 + z^2

r^2 = (p.x^2 + p.y^2) + z^2

r^2 - (p.x^2 + p.y^2) = z^2

这里有个小技巧是如果用户点击到了半径的范围之外,如果真的发生了,那么我们只能用离它最近的点去替代它。

让我们看下面代码里的方法!并添加正确的方法在touchesBegan之前:

- (GLKVector3) projectOntoSurface:(GLKVector3) touchPoint
{
    float radius = self.view.bounds.size.width/3; 
    GLKVector3 center = GLKVector3Make(self.view.bounds.size.width/2, self.view.bounds.size.height/2, 0);
    GLKVector3 P = GLKVector3Subtract(touchPoint, center);
 
    // Flip the y-axis because pixel coords increase toward the bottom.
    P = GLKVector3Make(P.x, P.y * -1, P.z);
 
    float radius2 = radius * radius;
    float length2 = P.x*P.x + P.y*P.y;
 
    if (length2 <= radius2)
        P.z = sqrt(radius2 - length2);
    else
    {
        P.x *= radius / sqrt(length2);
        P.y *= radius / sqrt(length2);
        P.z = 0;
    }
 
    return GLKVector3Normalize(P);
}

这个数学方法和我们上面讨论的一样,注意在结尾我们要使用规格化的矢量,因为当计算旋转时方向是大于长度的。现在我们开始移动它。在HelloGLKitViewController.m里做如下的改变:

// Add to the private interface
GLKVector3 _anchor_position;
GLKVector3 _current_position;
 
// Add to bottom of touchesBegan
UITouch * touch = [touches anyObject];
CGPoint location = [touch locationInView:self.view];
 
_anchor_position = GLKVector3Make(location.x, location.y, 0);
_anchor_position = [self projectOntoSurface:_anchor_position];
 
_current_position = _anchor_position;
 
// Add to bottom of touchesMoved
_current_position = GLKVector3Make(location.x, location.y, 0);
_current_position = [self projectOntoSurface:_current_position];

现在我们开始规则化开始和结束范围内的点以此来和用户鼠标移动的动作保持一致。现在我们使用他们来计算坐标和旋转的角度

2) 计算当前旋转

计算坐标和旋转的角度,我们可以使用我们的dot product和cross product。如果这里你没有弄太明白,我强烈建议你去读David Rosen写的《Linear Algebra for Game Developers》。这篇文章介绍了大量的基础东西。但是如果你不想这么做,我可以给你在这里做一个简单的总结:

corss product允许你有2各矢量,它会给矢量一个垂直矢量。这将是旋转的坐标,但是现在我们需要计算出从一个矢量到另外一个旋转的数量。

长话短说,你可以使用dot product来帮助你决定2个矢量之间的角度。这个角度是acos(如果A和B不是相同矢量的话)。并且请记住在projectOntoSurface里规格化它们。

一旦我们决定了坐标和角度,我们就可以创造一个四元数来存储它们通过使用GLKit GLKQuaternionMakeWithAngleAndVector3Axis功能。让我们来试试吧,在HelloGLKitViewController.m里做如下修改:

// Add new method above touchesBegan
- (void)computeIncremental {
 
    GLKVector3 axis = GLKVector3CrossProduct(_anchor_position, _current_position);
    float dot = GLKVector3DotProduct(_anchor_position, _current_position);    
    float angle = acosf(dot);
 
    GLKQuaternion Q_rot = GLKQuaternionMakeWithAngleAndVector3Axis(angle * 2, axis);
    Q_rot = GLKQuaternionNormalize(Q_rot);
 
    // TODO: Do something with Q_rot...
 
}
 
// Call it at end of touchesEnded
[self computeIncremental];

现在我们有一个四元数来代替旋转的对象,从开始的触摸点到当前的触摸点

3) 更新全部旋转

最后一步,我们需要应用旋转的对象,这么做时我们需要追踪2个旋转:原始的旋转对象和当前的旋转对象

这是非常简单的,在HelloGLKitViewController.m里做如下改变:

// Add new variables to private interface
GLKQuaternion _quatStart;
GLKQuaternion _quat;
 
// Initialize them at bottom of setupGL
_quat = GLKQuaternionMake(0, 0, 0, 1);
_quatStart = GLKQuaternionMake(0, 0, 0, 1);
 
// Set _quat at bottom of computeIncremental
_quat = GLKQuaternionMultiply(Q_rot, _quatStart);
 
// Set _quatStart at bottom of touchesBegan
_quatStart = _quat;
 
// In update, replace GLKMatrix4Multiply(modelViewMatrix, rotation) line with this:
GLKMatrix4 rotation = GLKMatrix4MakeWithQuaternion(_quat);
modelViewMatrix = GLKMatrix4Multiply(modelViewMatrix, rotation);

这里_quatStart代表原始的旋转对象(在用户没有触碰之前),_quat时当前的旋转对象。

在computeIncremental的最后,我们把原始的与当前的进行相乘(点的数据是2者相乘的乘积)。

我们把四元数转成一个旋转矩阵,把它应用到视觉模型矩阵里。编译并运行,现在可以旋转这个立方体了

额外奖励: 另一个可选择的projectOntoSurface方法

注意,按照目前的实现,如果你在球体外部拖拽,它会映射到在球体上最近的点,而不是继续旋转。为了使旋转可以继续,将projectOntoSurface方法中的else部分替换成下面的代码:

P.z = radius2 / (2.0 * sqrt(length2));
float length = sqrt(length2 + P.z * P.z);
P = GLKVector3DivideScalar(P, length);

额外奖励: 四元数的更多乐趣

你可能会想弄明白用四元数能做其它什么事情。一件很酷的事情是:它们可以提供一种简单的方法在两个旋转位置间随着时间推移进行插值。

为了弄清楚我说的,让我们来试试看,加一些代码令我们双击后,让立方体执行动画旋转回最初的方向。按照下面修改HelloGLKitViewController.m:

// Add new private instance variables
BOOL _slerping;
float _slerpCur;
float _slerpMax;
GLKQuaternion _slerpStart;
GLKQuaternion _slerpEnd;
 
// Add to bottom of setupGL
UITapGestureRecognizer * dtRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)];
    dtRec.numberOfTapsRequired = 2;
    [self.view addGestureRecognizer:dtRec];
 
// Add new method
- (void)doubleTap:(UITapGestureRecognizer *)tap {
 
    _slerping = YES;
    _slerpCur = 0;
    _slerpMax = 1.0;
    _slerpStart = _quat;
    _slerpEnd = GLKQuaternionMake(0, 0, 0, 1);
 
}
 
// Add inside update method, right before declaration of modelViewMatrix
if (_slerping) {
 
    _slerpCur += self.timeSinceLastUpdate;
    float slerpAmt = _slerpCur / _slerpMax;
    if (slerpAmt > 1.0) {
        slerpAmt = 1.0;
        _slerping = NO;
    }
 
    _quat = GLKQuaternionSlerp(_slerpStart, _slerpEnd, slerpAmt);
}

当用户双击后,我们将起始旋转位置设置为当前方向(_quat),将最终旋转位置设置为恒等式旋转(没有任何旋转)。

然后在update方法中,判断如果当前状态是睡眠,我们先获得动画目前所处的进度。随后,我们使用内置的GLK四元数lerp方法,根据当前时间计算出slerpStart和slerpEnd之间的合适的旋转角度。

编译运行,旋转物体,然后双击使其旋转回初始位置。这个技术通常用于3D物体的关键帧动画和其它类似的地方。

从这里可以去什么地方?

这里有一个示例工程,包含了此教程中所有的代码。

希望这篇教程可以帮助到那些想学习一些触摸旋转3D物体方面的知识,以及复习一些3D数学概念的人。

如果任何人有疑问,更正或是更好更简单的办法来解释事情,请加入下面的论坛讨论!

标签: opengl

?>