工欲善其事必先利其器—— 我们的器已经都准备好了!来实现最重要的魔方转动功能吧~
Round 1
充分研究了一番参考的程序框架之后,发现给的参考代码和给的参考效果根本不是一回事哇!
参考代码框架是鼠标先操作,当右键松开时分析鼠标的动作并把操作压入栈;然后进入播动画模式,把前面的操作一帧一帧运算出来。 但参考的gif可是能拖着要转的层,即时更新想转到哪转到哪的呀
看来还是得上TrackBall! 开始前把结构稍微调整了一下,把trackball对象从main函数里移到了rubikcube类中,分为global和temp分管左键和右键的旋转,这样的好处是右键旋转的同时,左键也可以对整体进行旋转。
对于global trackball而言,它一直保存着整体变换的一个矩阵,并在绘制阶段设置到顶点着色器,相当于一个local到world的转换。
而temp trackball在每一次右键操作中都会重置矩阵,它保存的矩阵会在右键松开前,通过RubikCube类的activeLayer()
方法设置给被选中的layer的每一个小方块的变换matrix,再在绘制阶段设置到顶点着色器。
这时就不得不说一下(可能是暂时的)Cube Struct改动。在之前不涉及旋转时,每个Cube只需要通过vec3的形式保存中心位置坐标,再通过glm::translate()进行平移即可。但既然我们用了纹理,就还要处理cube转动后的朝向,以应对正确的纹理坐标。
因此,现在每个Cube会保存两个matrix:
- 每一次完整旋转后更新的pos矩阵 初识化时即为translate的平移矩阵,之后完整90度的旋转会存入这个矩阵。
- 用于动画和绑定层转动的matrix矩阵 会在完成90度旋转时清空。
//Sub-cube structure
struct Cube
{
//just used for debug
int index;
//model-view matrix, used to save finished opration
glm::mat4 pos = glm::mat4(1.0f);
//model-view matrix, used to display an animation
glm::mat4 matrix = glm::mat4(1.0f);
};
整体交互的程序逻辑就是这样的啦:rightkey和leftkey记录着鼠标键位的操作,isRotating用于控制动画,以保证用户每次转动后魔方都会回到整90度转动的位置。
有几个要点需要唠唠:
layer的选择 strategy1
之前cube的选择还是比较顺利的(虽然偶尔精度上会出错),而一个cube可以锁定三个可转的layer。
在最初我尝试的方法是用右键按下的第一帧和第二帧进行运算,把鼠标位移转换到local坐标后,求旋转轴并与三个轴进行点乘,从而选择最接近的旋转轴。
由此便锁定这个层,在右键松开前的所有位移操作都仅对该层有效。
IN Main.cpp
// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xposIn, double yposIn)
{
float keyx = static_cast<float>(xposIn);
float keyy = static_cast<float>(yposIn);
if (leftkey) {
myCube.global_transform.mouseMove(screenPosRemap(keyx,keyy));
}
else if (rightkey)
{
if(initRotation){
myCube.findLayer(screenPosRemap(keyx,keyy));
initRotation = false;
}
myCube.activateLayer(screenPosRemap(keyx,keyy));
}
}
IN RUBIKCUBE.cpp
void RubikCube::findLayer(glm::vec2 curPos){
int axis = temp_transform.calAxis(curPos,global_transform.matrix);
pickedLayer = glm::ivec2(axis,pickedCube[axis]);
}
void RubikCube::activateLayer(glm::vec2 curPos){
Layer* layer = &layers[pickedLayer.x][pickedLayer.y];
//apply mouse move to temp trackball
temp_transform.restrictedMove(curPos, layer->axis, global_transform.matrix);
//apply transformation to cubes
for(int i = 0;i<n;i++){
for(int j= 0;j<n;j++){
glm::ivec3 cubeIndex = layer->cubes[i][j];
Cube* curCube = &magicCube[cubeIndex.x][cubeIndex.y][cubeIndex.z];
curCube->matrix = temp_transform.matrix;
}
}
}
IN TRACKBALL.cpp
int TrackBall::calAxis(glm::vec2 _curPos,glm::mat4 global){
glm::vec3 curpos = glm::vec3(0.0f);
mapToSphere(_curPos, curpos);
glm::vec3 axis_w = glm::cross(lastpos, curpos);//world space axis
glm::vec3 axis_l = glm::vec3(glm::inverse(global) * glm::vec4(axis_w, 1));//local space axis
//use dot product to find final axis
float Xcomp = glm::dot(axis_l, glm::vec3(1.0f,0.0f,0.0f));
float Ycomp = glm::dot(axis_l, glm::vec3(0.0f,1.0f,0.0f));
float Zcomp = glm::dot(axis_l, glm::vec3(0.0f,0.0f,1.0f));
int axis;
if(abs(Xcomp)>abs(Ycomp)){
if(abs(Xcomp)>abs(Zcomp)) axis = 0;//x
else axis = 2;//z
}
else if (abs(Ycomp)>abs(Zcomp)) axis = 1;//y
else axis = 2;//z
return axis;
}
(好多弯弯绕,我觉得我在如何设计类和方法上还有好多路要走.........
layer动画
主要写了以下三个方法:
-
startAnimation()
当右键松开时,读取temp trackball总共旋转的角度,以90度为单位进行取整计算,分为rotateIteration和rotateCountor两部分储存。rotateCounter是多少个整90度,rotateIteration是距离旋转至整90度还有多少个step步长。
-
rotateOnestep()
Iteration--,每次旋转一个step步长。
-
finishAnimation()
动画运算完毕后,对rubikcube的结构进行更改,主要做了两个工作:
1.复制layer所索引的n*n个Cube,将整90度的旋转写入到cube.pos
2.修改magiccube的Cube[][][]数组
主要就是一些好麻烦的下标计算啦,不知道能不能优雅一点用一两行公式表达....脑子不够了。
void RubikCube::startAnimation(){
temp_transform.calSteps(rotateSpeed,rotateIteration,rotateCountor);
}
void TrackBall::calSteps(int speed,int& remainingSteps,int& totalMoves){
if(restrictedAngle > 0){
if(restrictedAngle%90 > 15){
totalMoves = restrictedAngle/90 + 1;
remainingSteps =( 90 - restrictedAngle%90)/speed;
}
else{
totalMoves = restrictedAngle/90;
remainingSteps = -1 * restrictedAngle%90/speed;
}
}
else{
if(restrictedAngle%90 < -15){
totalMoves = restrictedAngle/90 - 1;
remainingSteps = (-90 - restrictedAngle%90)/speed;
}
else{
totalMoves = restrictedAngle/90;
remainingSteps = -1 * restrictedAngle%90/speed;
}
}
}
void RubikCube::finishAnimation(){
//save current transformation
//这里用countor*90而不是先前的matrix来计算最终的旋转,以防speed不能整除导致动画结束后的layer位置有偏差
Cube changedcubes[n][n];
Layer* layer = &layers[pickedLayer.x][pickedLayer.y];
for(int i = 0;i<n;i++){
for(int j= 0;j<n;j++){
glm::ivec3 cubeIndex = layer->cubes[i][j];
changedcubes[i][j] = magicCube[cubeIndex.x][cubeIndex.y][cubeIndex.z];
changedcubes[i][j].pos = glm::rotate(glm::mat4(1.0f),glm::radians(90.f*rotateCountor),layer->axis)*changedcubes[i][j].pos;
changedcubes[i][j].matrix = glm::mat4(1.0f);
}
}
//reconstruct index system
//只有旋转过的layer所对应的cubes需要重构index
int rotationCase = rotateCountor%4<0?rotateCountor%4+4:rotateCountor%4 ;
switch (pickedLayer.x) {
case 0://绕x轴
for(int i = 0;i<n;i++){
for(int j= 0;j<n;j++){
switch(rotationCase){
case 3: magicCube[pickedLayer.y][j][n-1-i] = changedcubes[n-1-j][i];continue;
case 2: magicCube[pickedLayer.y][j][n-1-i] = changedcubes[n-1-i][n-1-j]; continue;
case 1: magicCube[pickedLayer.y][j][n-1-i] = changedcubes[j][n-1-i]; continue;
case 0: magicCube[pickedLayer.y][j][n-1-i] = changedcubes[i][j];
default:continue;
}
}
}
break;
case 1://绕y轴
for(int i = 0;i<n;i++){
for(int j= 0;j<n;j++){
switch(rotationCase){
case 3: magicCube[i][pickedLayer.y][n-1-j] = changedcubes[n-1-j][i]; continue;
case 2: magicCube[i][pickedLayer.y][n-1-j] = changedcubes[n-1-i][n-1-j]; continue;
case 1: magicCube[i][pickedLayer.y][n-1-j] = changedcubes[j][n-1-i]; continue;
case 0: magicCube[i][pickedLayer.y][n-1-j] = changedcubes[i][j];
default:continue;
}
}
}
break;
case 2://绕z轴
for(int i = 0;i<n;i++){
for(int j= 0;j<n;j++){
switch(rotationCase){
case 3: magicCube[i][j][pickedLayer.y] = changedcubes[n-1-j][i]; continue;
case 2: magicCube[i][j][pickedLayer.y] = changedcubes[n-1-i][n-1-j]; continue;
case 1: magicCube[i][j][pickedLayer.y] = changedcubes[j][n-1-i]; continue;
case 0: magicCube[i][j][pickedLayer.y] = changedcubes[i][j];
default:continue;
}
}
}
break;
default:
break;
}
cubeDebug();
rotateCountor = 0;
}
bool RubikCube::rotateOneStep(){
if(rotateIteration == 0) return false;
else{
int dir = rotateIteration/abs(rotateIteration);
Layer* layer = &layers[pickedLayer.x][pickedLayer.y];
for(int i = 0;i<n;i++){
for(int j= 0;j<n;j++){
glm::ivec3 cubeIndex = layer->cubes[i][j];
Cube* curCube = &magicCube[cubeIndex.x][cubeIndex.y][cubeIndex.z];
curCube->matrix = glm::rotate(curCube->matrix, glm::radians(rotateSpeed*dir*1.f), layer->axis);
}
}
rotateIteration -= dir;
return true;
}
}
emmmm,凑活能用,但还有几个问题——
1.cube映射不准。有时候明明瞄着右边点的,却是左边的layer转了...可能还是试试射线检测?
2.layer映射不准。点击后的两帧的位移量太小了,可能考虑点击一个层的时候先进入一个待选状态(三个轴都有可能),在一个轴转动超过阈值后再锁定轴?
3.内侧的面、里部的cube不用渲染。其实每个面都是六面着色的,转开就穿帮啦,得区分一下,也节省一点渲染量。
4.切换魔方时也许可以有一个快速旋转的帅气特效.....?
5.还没做光照。
今天先把前两个问题解决一下,毕竟用户交互可是这个作业的重点~
Round 2
先来改比较简单的layer判定问题,我们加一个布尔值axislocked
来判断状态,只有当temp旋转角大于5度,才会使axislocked变为true,锁定layer层并写入cube。
void RubikCube::activateLayer(glm::vec2 curPos){
if(!axislocked){
axislocked = findLayer(curPos);
}
Layer* layer = &layers[pickedLayer.x][pickedLayer.y];
//apply mouse move to temp trackball
temp_transform.restrictedMove(curPos, layer->axis, global_transform.matrix);
if(axislocked){
//apply transformation to cubes
for(int i = 0;i<n;i++){
for(int j= 0;j<n;j++){
glm::ivec3 cubeIndex = layer->cubes[i][j];
Cube* curCube = &magicCube[cubeIndex.x][cubeIndex.y][cubeIndex.z];
curCube->matrix = temp_transform.matrix;
}
}
}
}
这样方向锁定就准确多了,但还有一个问题是,每次选中层时,旋转角会直接从0跳到大约30~40的一个值,看了很久还没解决,病理不详。。