OpenGLTANotes

🕹️OpenGL丨魔方转动的动画与执行 - 魔方作业(4)

by ERIN.Z, 2022-10-26


工欲善其事必先利其器—— 我们的器已经都准备好了!来实现最重要的魔方转动功能吧~

Round 1

充分研究了一番参考的程序框架之后,发现给的参考代码和给的参考效果根本不是一回事哇!

参考代码框架是鼠标先操作,当右键松开时分析鼠标的动作并把操作压入栈;然后进入播动画模式,把前面的操作一帧一帧运算出来。 但参考的gif可是能拖着要转的层,即时更新想转到哪转到哪的呀

example.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度转动的位置。 Snipaste_2022-10-26_13-59-33.jpg

有几个要点需要唠唠:

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;
    }
}

basic move

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;
            }
        }
    }
}

Oct-27-2022 22-17-04.gif 这样方向锁定就准确多了,但还有一个问题是,每次选中层时,旋转角会直接从0跳到大约30~40的一个值,看了很久还没解决,病理不详。。 Oct-27-2022 23-51-48.gif

by ERIN.Z

2024 © typecho & elise