OpenGLTANotes

🕹️OpenGL丨魔方最终版! - 魔方作业(5)

by ERIN.Z, 2022-12-05


前文提要——

🕹️OpenGL丨RubikCube类搭建 - 魔方作业(1)

🕹️OpenGL丨Object Mouse Trackball - 魔方作业(2)

🕹️OpenGL丨利用模板缓冲实现鼠标拾取 - 魔方作业(3)

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

快交作业了,来最后把魔方作业完善一下,修了几个bug,然后再贴一下完整版的文档。

👇👇👇👇👇👇👇

要是播放不了戳这里

☝☝☝☝☝☝☝

交互相关

stencil残影

之前debug模板缓冲时,发现旋转会留下很多残影: stencil bug 这是因为在glClear()命令前没有将glStencilMask()设置为0xFF,所以清空命令没有起效。

更正后的效果如下~ stencil debug

trackball转动过程中突然消失

在半边结构的作业中加入了trackball类的时候,一定程度上解决了这个问题。

为啥说一定程度呢...一方面不知道为啥,另一方面现在鼠标操作时偶尔会出现黑屏的闪烁.....

主要就是额外引入一个4x4矩阵来保存之前的旋转。在mouseClick()时拷贝matrix,用于计算local旋转轴和计算本次操作的的旋转矩阵。

void TrackBall::mouseMove(glm::vec2 pos2D) {
    glm::vec3 curpos = glm::vec3(0.0f);
    mapToSphere(pos2D, curpos);
    glm::vec3 axis_w = glm::cross(lastpos, curpos);
    glm::vec3 axis_l = glm::vec3(glm::inverse(lastmat) * glm::vec4(axis_w, 1));
    float angle = glm::length(lastpos - curpos);

    matrix = glm::rotate(lastmat, angle, axis_l);
}

歪门邪道网点shading

由于一个多月之前的我懒得读网格文件,这个魔方立方体的VAO是直接手写的,就没写法线的信息(而且直到刚刚才发现哦我没用indices...所以其实VAO里加一下就行.....)。最终是直接在着色器里歪门邪道地算了个world normal:

    vec3 N = step(vec3(0.499999),faceindex);
    N -= step(faceindex,vec3(-0.499999));
    N = normalize(N);
    N = (modelmat*vec4(N,0.0)).xyz;

emmm.....

没有任何参考价值。

Shaing根据光照和光线衰减大概分为三个区域,希望在中段的灰区绘制网点的效果: lighting 一通调参之后就可以得到这样有点漫画感的效果~ lighting 最终处理效果如图: lighting

完整文档

实验原理

矩阵变换

三维模型的缩放、平移、旋转等操作都可以通过矩阵进行运算。

在本次实验中,使用了GLM库进行矩阵的管理和实现。

将物体绘制到屏幕上时,需要通过多次的坐标转换,一般的变换过程是[局部坐标]->[世界坐标]->[观察坐标]->[裁剪坐标]->[屏幕坐标]

coordinate_systems_o.png

在魔方实验中,每个小立方体都是一组单独的网格,n x n x n个小立方体组成整个魔方。如果将整个魔方看为整体,那么我们还需要额外的一步坐标转换,将小立方体转换到"魔方空间"。 coordinate_systems.png CUBE MATRIX保存每个小立方体各自的变形,包括其相对位置和相对旋转,也包括层旋转时的动画效果。

MODEL MATRIX保存整体魔方的旋转。

Trackball

使用TrackBall来实现鼠标对于物体的自由旋转,我们在屏幕外虚构一个球形曲面,使鼠标在二维屏幕上的移动投影到球形曲面上,以实现更佳的用户体验。

The motivation behind the trackball (aka arcball) is to provide an intuitive user interface for complex 3D object rotation via a simple, virtual sphere - the screen space analogy to the familiar input device bearing the same name.

The sphere is a good choice for a virtual trackball because it makes a good enclosure for most any object; and its surface is smooth and continuous, which is important in the generation of smooth rotations in response to smooth mouse movements. Any smooth, continuous shape, however, could be used, so long as points on its surface can be generated in a consistent way.

假设我们的虚拟球面为x^2+y^2+z^2=1 ,即以屏幕中心为球心,半径为1的球。将屏幕坐标映射到(-1,1),由于映射点具有相同的x,y坐标,我们通过球面方程得到映射后的点坐标;若点在球面外,我们暂定映射到球面边缘的点。

trackball.png

当鼠标从P1移动到P2时,将P1,P2映射到球面上的p_1,p_2,叉乘\vec{op_1}\vec{op_2}即可得到旋转轴;\vec{op_1}\vec{op_2}的夹角即为旋转角。

只使用半球面作为映射面时,球面边缘处的突变较大,操作感不太自然。因此我们可以使用一个喇叭形的双曲面(x^2+y^2=r^2/2)与球面拼合,以得到更加平滑的拟合效果。

Trackball_composite2.png

基于模板缓冲的拾取

旋转魔方的层时,我们需要获知鼠标点击到的具体是哪一个小方块。

作业资料中提供的拾取方法,是基于固定管线GLUT环境下的Select Mode,通过glSelectBuffer获得集中列表。但由于作业使用的可编程管线,没有已包装好的拾取方法。因此借助帧缓冲来实现鼠标的拾取操作。帧缓冲的蒙版缓存附件颜色缓冲附件都可以实现物件ID的记录,区别是模板缓冲的格式一般为Depth24Stencil8,只有8位空间,最多只能保存255个不同ID;而颜色缓冲一般有32位空间,适用于更多场景物体。

在本实验中,最多有6×6×6=216个子物体(内部方块不会被选择,表面子物体最多152个需要有不同ID),使用默认帧缓冲的模板缓存就绰绰有余了。

由于模板缓冲是一种帧缓冲的渲染缓冲对象附件(Renderbuffer Object as Attachment),数据存储于GPU,需要使用glReadPixels()来读取物件ID数据回传到CPU。

由于渲染缓冲不是纹理,我们很难将它可视化出来以debug。在本实验中的debug是通过多次不同模板值的模板测试绘制全屏幕四边形,通过模板测试的区域即可被绘制。绘制的颜色反映物件ID转换得到的index值。

stencil

实验步骤

1.构建魔方类

魔方类的构建主要参考作业附件中的参考框架。N阶的RubikCube中应当包含N×N×N个cube对象和N×3个layer对象。

cube储存自身index序号,与相对变换的矩阵。

layer储存层内的cube序号,与旋转轴。

RubikCube储存cube表与layer表,其中layer表的第0,1,2行分别对应不同轴。

rubikclass

rubikclass

2.构建TrackBall类

TrackBall类将输入的x,y坐标映射到球面,计算旋转轴与旋转角并以矩阵形式保存。

核心方法如下:

基础功能:

void mapToSphere(const glm::vec2 pos2D, glm::vec3& pos3D)

  • 输入:重映射到(-1,1)的屏幕坐标
  • 输出:三维球面坐标
  • 功能:计算球面坐标

void mouseClick(glm::vec2 pos2D)

  • 输入:重映射到(-1,1)的屏幕坐标
  • 功能:鼠标按下时由回调函数调用。计算初始的鼠标球面位置,保存至lastpos

void mouseMove(glm::vec2 pos2D)

  • 输入:重映射到(-1,1)的屏幕坐标
  • 功能:鼠标移动时由回调函数调用。计算当前的鼠标球面位置,计算旋转轴与旋转角,将结果写入变换矩阵matrix

拓展功能:

Trackball可实现自由连续的球面角转动,但当右键旋转单层时,我们需要限制它仅沿自己的轴转动,为此设计了如下方法:

int calAxis(glm::vec2 _curPos, glm::mat4 global, bool& locked);

  • 输入:重映射到(-1,1)的屏幕坐标,整体魔方的变换矩阵,旋转轴是否已锁定
  • 输出:轴的index(0-x,1-y,2-z)
  • 功能:由于每一个方块同时位于不同方向的3个layer上,此方法计算当前转动操作的主要方向。由于鼠标刚刚按下时移动距离较小,不能完全确定本次操作的方向,因此设计了角度相关的阈值,当旋转角大于该阈值时,旋转轴锁定。

void restrictedMove(glm::vec2 pos2D, glm::vec3 axis, glm::mat4 global)

  • 输入:重映射到(-1,1)的屏幕坐标,旋转轴,整体魔方的变换矩阵

  • 功能:与void mouseMove(glm::vec2 pos2D)功能相似,但是限制特定旋转轴。

void calSteps(int speed, int& steps, int& totalMoves)

  • 输入:单次旋转角步长
  • 输出:剩余步数、总旋转次数(以旋转90°为一次)
  • 功能:用于层动画。当用户右键松开时,可能层没有旋转到整90度的位置。本方法根据当前旋转的角度计算用于动画的旋转步数以及此次旋转总共旋转的次数(以旋转90°为一次)。

rotate

3.模板缓冲拾取

为利用模板缓冲储存cube ID,首先构建了工具函数以实现3维index与1维ID的相互转换:

unsigned int indexToStencil(int i, int j, int z)

  • 输入:cube的x/y/z方向索引
  • 输出:cube ID

bool stencilToIndex(unsigned int value, glm::ivec3& result);

  • 输入:cube ID
  • 输出:是否查找到cube,若有则写入cube的x/y/z方向索引

使用glEnable()启用模板测试,并配置glStencilOp(),glStencilMask(),glStencilFunc().

正常绘制模式下,所有魔方的片元都可以通过模板测试(GL_ALWAYS),其中亦通过了深度测试的部分片元会写入模板值。这保证了使用鼠标点击时,采样得到的模板值总是来自距离最近的物体。

当触发点击事件时,通过glReadPixel()可读取输入坐标的模板值。


//reset picked object
double keyx,keyy;
glfwGetCursorPos(window, &keyx, &keyy);
uint stencil;
glReadPixels(keyx*COORDSCALE, (SCR_HEIGHT-keyy)*COORDSCALE, 1, 1, GL_STENCIL_INDEX, GL_UNSIGNED_INT, &stencil);
if(!stencilToIndex(stencil, myCube.pickedCube)) return;

由于OpenGL的纹理采样零点在左下角,但窗口坐标的零点在左上角,注意采样时需要将y值反转(SCR_HEIGHT-keyy)。

select

4.鼠标监听

使用glfwSetInputMode()开启对鼠标时间的监听,并设置鼠标点击、移动的回调函数。

交互要求左键对整个魔方进行旋转,右键对单一层进行旋转。使用两个bool值记录左键和右键的按下状态,以便鼠标移动时计算对应的行为。

void click_callback(GLFWwindow* window, int button, int action, int mods)

  • 鼠标点击的回调函数,负责设置bool值状态,并记录当前鼠标位置。

void mouse_callback(GLFWwindow* window, double xpos, double ypos)

  • 鼠标移动的回调函数,根据bool值状态采取调用左键与右键不同的旋转函数。

5.交互逻辑

RubikCube增加两个TrackBall对象,分别管理整体和单层的旋转操作。

对于global trackball而言,它一直保存着整体变换的一个矩阵,并在绘制阶段设置到顶点着色器,相当于一个local到world的转换。

temp trackball在每一次右键操作中都会重置矩阵,它保存的矩阵会在右键松开前,通过RubikCube类的activeLayer()方法设置给被选中的layer的每一个小方块的变换matrix,再在绘制阶段设置到顶点着色器。

程序的运行逻辑如下:

functions

6.层动画实现

主要由如下方法实现:

RubikCube::startAnimation()

  • 当右键松开时,读取temp trackball总共旋转的角度,以90度为单位进行取整计算,分为rotateIterationrotateCountor两部分储存。rotateCounter是多少个整90度,rotateIteration是距离旋转至整90度还有多少个step步长。

RubikCube::rotateOnestep()

  • Iteration--,每次旋转一个step步长。

RubikCube::finishAnimation()

  • 动画运算完毕后,对rubikcube的结构进行更改,主要做了两个工作:
    • 复制layer所索引的n*n个Cube,将整90度的旋转写入到cube.pos
    • 修改magiccube的Cube[][][]数组

rotate

7.风格化漫画网点Shading

开头写了。略。

&.过程中遇到的一些问题&解决:

开头写了。略略略。

实验效果

详见demo.mp4.

  • 左键旋转整体魔方
  • 右键旋转单层
  • 中键滚动缩放魔方
  • 中键按下切换不同主题
  • 键盘数字键(2-6)切换魔方阶数
  • 键盘←键→键旋转光照

rubikcube

rubikcube

实验环境

Window 10. Visual Studio 2022.

依赖库:GLFW,GLAD, GLM,stb_image

shader.h参考learnOpengl框架。

by ERIN.Z

2024 © typecho & elise