跳到主内容
Playbook
2026-10-29 发布· 约 23 分钟

2.5Dhelper 2.5D效果实现

恒选和瑞昱等平台项目都多少使用了2.5D特效,例如:coverflow、rollinglist、立方体、六面柱等效果。其实现原理主要使用了旋转矩阵计算顶点反推变换矩阵等算法。 本章节需要读者对线性代数、矩阵变换相关知识有一些了解。

前提

恒选和瑞昱等平台项目都多少使用了2.5D特效,例如:coverflow、rollinglist、立方体、六面柱等效果。其实现原理主要使用了旋转矩阵计算顶点反推变换矩阵等算法。   本章节需要读者对线性代数、矩阵变换相关知识有一些了解。

快速了解矩阵变换 Matrix transformation

矩阵变换是记录并控制一个矩形图像在二维平面上的位置、大小、旋转、缩放等效果的数学模型。


3x3矩阵可以实现多种线性变换,包括但不限于以下几种:

  1. 缩放(Scaling):

    通过在矩阵的对角线上设置不同的值来实现对x、y、z轴的缩放。例如,一个简单的缩放矩阵可以是: [s_x  0  0 0  s_y  0 0  0  s_z ]\begin{bmatrix} s\_x & 0 & 0 \\ 0 & s\_y & 0 \\ 0 & 0 & s\_z \\ \end{bmatrix}

    其中,(s_x)、(s_y)、(s_z)分别是x、y、z轴的缩放因子。

  2. 旋转(Rotation): 通过特定的矩阵来实现绕x、y、z轴的旋转。例如,绕z轴旋转(\theta)角的矩阵是: [cos⁡θ  −sin⁡θ  0 sin⁡θ  cos⁡θ  0 0  0  1 ]\begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix}

  3. 剪切(Shearing): 通过在矩阵的非对角线上设置特定值来实现剪切变换。例如,一个简单的剪切矩阵可以是: [1  h_x  0 h_y  1  0 0  0  1 ]\begin{bmatrix} 1 & h\_x & 0 \\ h\_y & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix}

    其中,(h_x)和(h_y)是剪切因子。

  4. 反射(Reflection): 通过在矩阵的对角线上设置负值来实现对某个轴的反射。例如,对x轴反射的矩阵是: [−1  0  0 0  1  0 0  0  1 ]\begin{bmatrix} -1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix}

  5. 投影(Projection): 通过特定的矩阵来实现投影变换,例如透视投影或正交投影。

这些变换可以通过组合多个3x3矩阵来实现更复杂的变换。例如,可以通过先将一个物体缩放,然后旋转,最后再进行剪切来实现复杂的变换效果。

仿射变换 Affine transformation

仿射变换是一种保持直线和平行线性质的线性变换。它可以通过一个3x3的矩阵来表示,但通常简化为一个2x3的矩阵,因为仿射变换不涉及深度信息。仿射变换包括以下几种基本变换:

  1. 平移(Translation)

  2. 旋转(Rotation) (沿Z轴的旋转)

  3. 缩放(Scaling)

  4. 剪切(Shearing)

透视变换 Perspective transformation

透视变换(也称为投影变换)是一种更复杂的变换,它不仅改变物体的位置和方向,还会改变物体的形状,使得远处的物体看起来更小。透视变换通常通过一个3x3的矩阵来表示,并且涉及到深度信息。

透视变换的一个典型应用是图像的透视校正,例如将一个倾斜的图像校正为正面视角。透视变换可以通过给定的四个点(通常是图像的四个角)来计算变换矩阵。

在恒选平台使用的湃心OS中的矩阵变换,关于X轴Y轴的旋转操作属于透视变换。

区别

  • 仿射变换保持平行线性质,即平行线在变换后仍然平行。

  • 透视变换不保持平行线性质,平行线在变换后可能会相交。

在实际应用中,仿射变换通常用于简单的几何变换,而透视变换则用于更复杂的场景,如图像校正和3D视角变换。

应用示例

假设我们有一个图像,我们希望对其进行以下变换:

  1. 先将图像放大2倍。

  2. 然后绕原点旋转45度。

  3. 最后向右平移100像素,向下平移50像素。

我们可以通过以下步骤来实现:

  1. 缩放矩阵:

    S = [2  0  0 0  2  0 0  0  1 ]S = \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix}

  2. 旋转矩阵:

    R = [cos⁡(45∘)  −sin⁡(45∘)  0 sin⁡(45∘)  cos⁡(45∘)  0 0  0  1 ]= [22  −22  0 22  22  0 0  0  1 ]R = \begin{bmatrix} \cos(45^\circ) & -\sin(45^\circ) & 0 \\ \sin(45^\circ) & \cos(45^\circ) & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} = \begin{bmatrix} \frac{\sqrt{2}}{2} & -\frac{\sqrt{2}}{2} & 0 \\ \frac{\sqrt{2}}{2} & \frac{\sqrt{2}}{2} & 0 \\ 0 & 0 & 1 \\ \end{bmatrix}

  3. 平移矩阵:

    T = [1  0  100 0  1  50 0  0  1 ]T = \begin{bmatrix} 1 & 0 & 100 \\ 0 & 1 & 50 \\ 0 & 0 & 1 \\ \end{bmatrix}

  4. 组合变换矩阵:

M = T ⋅ R ⋅ SM = T \cdot R \cdot S

通过将图像的每个像素点乘以变换矩阵MM,就可以得到变换后的图像。该操作成为像素点映射。

快速了解旋转矩阵 Rotation matrix

旋转矩阵是一种用于描述二维或三维空间中物体绕某个轴旋转的线性变换矩阵。在二维空间中,旋转矩阵可以表示为:

R(θ) = [ cos⁡θ  −sin⁡θ  sin⁡θ  cos⁡θ  ]R(\theta) = \begin{bmatrix}  \cos\theta & -\sin\theta \\  \sin\theta & \cos\theta \\  \end{bmatrix}

其中,θ\theta是旋转角度。

在三维空间中,旋转可以用四元数、旋转矩阵、欧拉角等多种方式来描述。其中,欧拉角是最常用的描述方式。  使用欧拉角(Euler angles)来描述旋转,欧拉角定义了绕三个坐标轴(X、Y、Z轴)的旋转矩阵,再将三轴的旋转矩阵融合,就可以得到最终的旋转矩阵。

1.欧拉角的表示

绕z轴旋转的矩阵为:

R_z(θ) = [ cos⁡θ  −sin⁡θ  0  sin⁡θ  cos⁡θ  0  0  0  1  ]R\_z(\theta) = \begin{bmatrix}  \cos\theta & -\sin\theta & 0 \\  \sin\theta & \cos\theta & 0 \\  0 & 0 & 1 \\  \end{bmatrix}

绕x轴旋转的矩阵为:

R_x(θ) = [ 1  0  0  0  cos⁡θ  −sin⁡θ  0  sin⁡θ  cos⁡θ  ]R\_x(\theta) = \begin{bmatrix}  1 & 0 & 0 \\  0 & \cos\theta & -\sin\theta \\  0 & \sin\theta & \cos\theta \\  \end{bmatrix}

绕y轴旋转的矩阵为:

R_y(θ) = [ cos⁡θ  0  sin⁡θ  0  1  0  −sin⁡θ  0  cos⁡θ  ]R\_y(\theta) = \begin{bmatrix}  \cos\theta & 0 & \sin\theta \\  0 & 1 & 0 \\  -\sin\theta & 0 & \cos\theta \\  \end{bmatrix}

如果旋转顺序是XYZ,那么旋转矩阵 (R) 可以表示为:

R = R_z(θ_z) ⋅ R_y(θ_y) ⋅ R_x(θ_x)R = R\_z(\theta\_z) \cdot R\_y(\theta\_y) \cdot R\_x(\theta\_x)

其中,R_x(θ_x)R\_x(\theta\_x)、R_y(θ_y)R\_y(\theta\_y)和R_z(θ_z)R\_z(\theta\_z) 分别是绕X轴、Y轴和Z轴的旋转矩阵:

通过这些旋转矩阵,可以将图像或物体绕指定轴旋转一定的角度。

旋转矩阵可以用于描述物体在世界坐标系(World Coordinate System)或自身坐标系(Local Coordinate System)中的旋转。这两种坐标系的选择会影响旋转矩阵的表示和应用方式。

2.旋转矩阵的坐标系

-外旋(Extrinsic Rotation)世界坐标系中的旋转

在世界坐标系中,旋转矩阵都是相对于固定的世界坐标系来定义的,无论物体处于什么姿态,选择的方向总是以世界原点为参考。这样的旋转被称为:外部旋转(外旋)  例如:在立方体的旋转中,用户拖动立方体的某个面使其旋转时,实际上是在世界坐标系中进行旋转,无论立方体的姿态如何,它旋转的方向符合用户拖动的方向。

-内旋(Intrinsic Rotation)自身坐标系中的旋转

在自身坐标系中,旋转矩阵描述的是物体相对于其自身坐标系的旋转。也就是说,旋转是相对于物体当前的方向来定义的。

在自身坐标系中,物体绕Z旋转之后,X轴Y轴也会受到影响,同物体一起旋转,对于世界观察者(用户)来说,物体的坐标系已经发生了变化,之后再进行叠加旋转矩阵,就会作用在已经变化了的坐标系上。

然而,在从世界坐标系的角度来看,物体的自身坐标系并不是固定的,它随着物体的姿态而变化,因此,以世界的视角来使用自身坐标系的旋转矩阵是极其难以琢磨,且不合理的。确认好使用场景是非常重要的。  例如:飞行员处于飞机座舱中,飞行员对飞机的姿态进行控制时,实际上是在自身坐标系中进行旋转,飞行员以自身的视角来控制飞机的航向、横滚和俯仰。

3.欧拉角的旋转顺序

欧拉角中,旋转矩阵的顺序对最终的旋转结果有重要影响。旋转矩阵的顺序不同,会导致不同的旋转效果。常见的欧拉角旋转顺序有XYZ、XZY、YXZ、YZX、ZXY和ZYX等。

假设我们有一个点 PP,我们希望对其进行绕X轴、Y轴和Z轴的旋转。  如果旋转顺序是XYZ,那么旋转矩阵 RR可以表示为:

R = R_z(θ_z) ⋅ R_y(θ_y) ⋅ R_x(θ_x)R = R\_z(\theta\_z) \cdot R\_y(\theta\_y) \cdot R\_x(\theta\_x)

其中,R_x(θ_x)R\_x(\theta\_x)、R_y(θ_y)R\_y(\theta\_y)和 R_z(θ_z)R\_z(\theta\_z)分别是绕X轴、Y轴和Z轴的旋转矩阵。

如果旋转顺序是XZY,那么旋转矩阵RR 可以表示为:

  • XYZ内旋的组合旋转矩阵RR 为:

R = R_z(θ_z) ⋅ R_y(θ_y) ⋅ R_x(θ_x)R = R\_z(\theta\_z) \cdot R\_y(\theta\_y) \cdot R\_x(\theta\_x)

  • XYZ外旋的组合旋转矩阵 RR为:

R = R_x(θ_x) ⋅ R_y(θ_y) ⋅ R_z(θ_z)R = R\_x(\theta\_x) \cdot R\_y(\theta\_y) \cdot R\_z(\theta\_z)

4.旋转顺序和内旋外旋的关系

内旋和外旋,旋转的方向和角度相同,旋转的顺序相反情况下,内旋和外旋的效果是相同的。

2.5D模拟3D旋转的步骤(立方体实现)

初始化立方体

1.建立3D坐标系

在立方体的世界坐标系中,建立一个以原点为中心的坐标系,并确定三个坐标轴的方向。  坐标轴方向:

  • X轴:指向右侧,从屏幕左边指向右边

  • Y轴:指向下方,从屏幕上方指向下方

  • Z轴:垂直于屏幕,指向观察屏幕的用户

  • 原点:处于屏幕原点,即屏幕左上角

2.数据类型

创建所需的数据类型:

struct Icon // 立方体的每个面
{
   String name; //名称
   Image img; //显示的图片
   Pixmap *pixmap; //显示的图片的像素数据
   Pointer<Widget> widget; //显示的widget(每个面可以是容器)
   int id;    //ID 事件传递需要,辨别每个面
   int opacity; //透明度
   Transform transfrom; //变换矩阵
   Transform rotate; //旋转矩阵
   Vector3F normalVector; //法向量
};
 
struct Cube //立方体
{
    Icon icons[ICON_MAX]; //立方体的每个面
    Rect rects[ICON_MAX]; //立方体的每个面对应的矩形
    Rect3F unitRects[ICON_MAX]; //立方体的每个面对应的单位矩形
    Point3F position; //立方体的位置
    Vector3F angle; //立方体的旋转角度
    Vector3F selfAngle; //立方体自身的旋转角度
    TS_Helper2d5 *helperTrans; //2.5D变换帮助类(负责计算变换矩阵)
};
 
Cube m_cube; //立方体实例
Vector3F m_perspectiveVector; //透视视角向量
int m_cubeSize; //立方体大小
 

2.建立单位立方体初始状态

确定立方体的初始状态,就要确定其8个顶点坐标。

如图所示,定义立方体的中心坐标为原点P0,长宽高分别是2*a, 2*b, 2*c。

其八个顶点坐标分别为:

  • P1:(x, y, z) = (-a, -b, c)

  • P2:(x, y, z) = (a, -b, c)

  • P3:(x, y, z) = (a, b, c)

  • P4:(x, y, z) = (-a, b, c)

  • P5:(x, y, z) = (-a, -b, -c)

  • P6:(x, y, z) = (a, -b, -c)

  • P7:(x, y, z) = (a, b, -c)

  • P8:(x, y, z) = (-a, b, -c)

并选取每个面的四个点构成立方体的六个面,顶点的选取顺序在后续所有相关运算中应保持一致,即左上->右上->右下->左下。(如果在后续计算变换矩阵时,顶点顺序不一致,会导致计算结果不正确,最终面的图像显示的方向也会不正确。如果发现某个面的朝向不符合预期,可能是上述问题所致)

并且定义六个面的序号(序号只是方便理解逻辑和事件传递,并不影响实际运算):

如下面代码所示。

void CubeView::setUnitRectsSize(int size)
{
    float a = m_cubeSize / 2, b = m_cubeSize / 2, c = m_cubeSize / 2;
    Point3F vertices[8] =
    {
        {-a,    -b,     c},
        {a,     -b,     c},
        {a,     b,      c},
        {-a,    b,      c},
        {-a,    -b,     -c},
        {a,     -b,     -c},
        {a,     b,      -c},
        {-a,    b,      -c}
    };
 
    m_cube.unitRects[0] = { vertices[0], vertices[1], vertices[3], vertices[2] };
    m_cube.unitRects[1] = { vertices[1], vertices[5], vertices[2], vertices[6] };
    m_cube.unitRects[2] = { vertices[4], vertices[5], vertices[0], vertices[1] };
    m_cube.unitRects[3] = { vertices[3], vertices[2], vertices[7], vertices[6] };
    m_cube.unitRects[4] = { vertices[4], vertices[0], vertices[7], vertices[3] };
    m_cube.unitRects[5] = { vertices[5], vertices[4], vertices[6], vertices[7] };
}

3.初始化旋转矩阵和变换矩阵

立方体的每个面都是单独进行变换,他们都有各自的旋转矩阵和变换矩阵。   这些矩阵都需要初始化成单位矩阵(Identity Matrix)。

3*3单位矩阵表示为:

I_3 = [1  0  0 0  1  0 0  0  1 ]I\_3 = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix}

//初始化矩阵
matrix.m[0][0] = 1; matrix.m[0][1] = 0; matrix.m[0][2] = 0;
matrix.m[1][0] = 0; matrix.m[1][1] = 1; matrix.m[1][2] = 0;
matrix.m[2][0] = 0; matrix.m[2][1] = 0; matrix.m[2][2] = 1;

初始化的时机旋转矩阵和变换矩阵不同:

  • 旋转矩阵只需要在立方体创建之初,初始化一次即可,后续的每次旋转都是在上次的基础上进行叠加。

  • 变换矩阵是由旋转后的面顶点反推出来,所以他需要在每次计算之前都要清除为单位矩阵。

触摸事件:获取立方体旋转角度

这是外界输入控制立方体旋转的接口,通过触摸屏幕的位移坐标来计算旋转角度。

首先获取触摸滑动的位移,即 gest->delta(),并根据实际情况,将位移的XY轴分别映射到旋转角度的X轴和Y轴。触摸只有两个轴的位移,所以Z轴的旋转角度为0。

m_cube.selfAngle是立方体的外旋角度,而m_cube.angle是每次旋转的角度变化量,m_cube.selfAngle是m_cube.angle叠加而来。limTo2PI(&m_cube.selfAngle);将m_cube.selfAngle限制在 添加公式添加公式范围内。

CubeView::Vector3F CubeView::rotateMap(float rx, float ry)
{
    m_cube.angle.y = rx * 0.01f;
    m_cube.angle.x = -ry * 0.01f;
    m_cube.angle.z = 0;
 
    m_cube.selfAngle.x += m_cube.angle.x;
    m_cube.selfAngle.y += m_cube.angle.y;
    m_cube.selfAngle.z += m_cube.angle.z;
    limTo2PI(&m_cube.selfAngle);
    return m_cube.angle;
}
 
rotateMoveCube(rotateMap((float)gest->delta().x(), (float)gest->delta().y()));
 
void CubeView::limTo2PI(Vector3F *angle)
{
    if (angle->x > _PM_PI2)angle->x -= _PM_PI2;
    if (angle->x < 0)angle->x += _PM_PI2;
    if (angle->y > _PM_PI2)angle->y -= _PM_PI2;
    if (angle->y < 0)angle->y += _PM_PI2;
    if (angle->z > _PM_PI2)angle->z -= _PM_PI2;
    if (angle->z < 0)angle->z += _PM_PI2;
}
 

计算每个面的旋转矩阵

立方体的旋转是使用rotateMoveCube函数来实现的。

void CubeView::rotateMoveCube(Vector3F angle)

它会遍历立方体的六个面,再分别计算每个面的四个点与旋转矩阵映射之后的坐标,步骤如下:

1.获取顶点坐标:

面的四个顶点坐标 v0, v1, v2, v3

   Point3F v0 =
   {
      m_cube.unitRects[i].topLeft.x,
      m_cube.unitRects[i].topLeft.y,
      m_cube.unitRects[i].topLeft.z
   };//vertex topLeft
 
   Point3F v1 =
   {
      m_cube.unitRects[i].topRight.x,
      m_cube.unitRects[i].topRight.y,
      m_cube.unitRects[i].topRight.z
   };//vertex topRight
 
   Point3F v2 =
   {
      m_cube.unitRects[i].bottomRight.x,
      m_cube.unitRects[i].bottomRight.y,
      m_cube.unitRects[i].bottomRight.z
   };//vertex bottomRight
 
   Point3F v3 =
   {
      m_cube.unitRects[i].bottomLeft.x,
      m_cube.unitRects[i].bottomLeft.y,
      m_cube.unitRects[i].bottomLeft.z
   };//vertex bottomLeft

2.面的缩放:

scale_3d 是将顶点坐标XYZ轴同时乘以一个系数,来控制面的大小

   //
   m_cube.helperTrans->scale_3d(&v0, 1.0f);
   m_cube.helperTrans->scale_3d(&v1, 1.0f);
   m_cube.helperTrans->scale_3d(&v2, 1.0f);
   m_cube.helperTrans->scale_3d(&v3, 1.0f);

3.计算旋转矩阵 :

angle.x, angle.y, angle.z 为外界输入的角度  rotate_3D 是旋转矩阵,也就是 compute_rotate 运算结果。   rotate_3D 是上一次的计算结果,compute_rotate 会将新的结果与上一次的叠加。

   //计算旋转矩阵
   m_cube.helperTrans->compute_rotate(angle.x, angle.y, angle.z, rotate_3D);

4.计算平移:

m_cube.position 为立方体的位置,m_cube.position.x 和 m_cube.position.y 则为屏幕中心坐标,m_cube.position.z 为立方体中心到屏幕的距离。   transfrom_rotate 的作用是计算得到平移后的顶点坐标 rv0, rv1, rv2, rv3,

  • 其中 rotate_3D 为旋转矩阵,

  • v0, v1, v2, v3 为面的四个顶点原始坐标,

  • rv0, rv1, rv2, rv3 为平移后的顶点坐标。

  • m_cube.position 为立方体的位置。

   if (!m_manualSetCubePos)
   {
      m_cube.position = { (float)w_width / 2, (float)w_height / 2, (float) - m_cubeSize / 2 };
   }
   m_cube.helperTrans->transfrom_rotate(rotate_3D, &v0, &rv0, m_cube.position.x, m_cube.position.y, m_cube.position.z);
   m_cube.helperTrans->transfrom_rotate(rotate_3D, &v1, &rv1, m_cube.position.x, m_cube.position.y, m_cube.position.z);
   m_cube.helperTrans->transfrom_rotate(rotate_3D, &v2, &rv2, m_cube.position.x, m_cube.position.y, m_cube.position.z);
   m_cube.helperTrans->transfrom_rotate(rotate_3D, &v3, &rv3, m_cube.position.x, m_cube.position.y, m_cube.position.z);

5.计算面的法向量:

该操作是为了获取面的法向量,方便后续计算面的朝向角度,以及用于后续的面的绘制,例如:判断面的法向量与屏幕的夹角大小,来确定哪些面需要绘制,哪些面需要隐藏

   Point3F center =
   {
      (rv0.x + rv1.x + rv2.x + rv3.x) / 4,
      (rv0.y + rv1.y + rv2.y + rv3.y) / 4,
      (rv0.z + rv1.z + rv2.z + rv3.z) / 4
   };
   icon->normalVector = m_cube.helperTrans->createVector(m_cube.position, center);

6.计算变换矩阵:

m_perspectiveVector 为透视向量,控制相机的视角和透视关系。   m_perspectiveVector.x 控制视角水平摇摆,m_perspectiveVector.y 控制视角垂直摇摆,m_perspectiveVector.z 控制视角与物体的距离。但是改变 m_perspectiveVector 并不会影响立方体的位置,只会影响立方体在视野里的透视关系。

transfrom_blit 由平移后的顶点坐标和视角向量计算得到变换矩阵,并将其赋值给icon->transfrom。

  • m_cube.rects[i].width() 和 m_cube.rects[i].height() 为立方体的每个面的宽度和高度。

  • m_perspectiveVector 为透视向量。

  • rv0, rv1, rv2, rv3 为平移后的顶点坐标。

  • icon->transfrom 为变换矩阵结果。

这两步的代码如下:

if (!m_manualSetCubePers)
{
    m_perspectiveVector = { (float)w_width / 2, (float)w_height / 2, 4 * (float)m_cubeSize };
}
m_cube.helperTrans->transfrom_blit(m_cube.rects[i].width(),
                                   m_cube.rects[i].height(), &m_perspectiveVector,
                                   &rv0, &rv1, &rv2, &rv3,
                                   &icon->transfrom);

transfrom_blit 的具体实现:

   // p means the perspective point,  use this parameter can get four point in XY plane
void TS_Helper2d5::transfrom_blit(float w, float h, vector3_t* p, point3_t* v0, point3_t* v1, point3_t* v2,
   point3_t* v3, Transform* matrix)
{
   // Compute 3x3 image transform matrix to map a rectangle image (w,h) to
   // a parallelogram (x0,y0), (x1,y1), (x2,y2), (x3,y3) counterclock wise.
 
   float x0 = p->x - (p->x - v0->x) * p->z / (p->z - v0->z);
   float y0 = p->y - (p->y - v0->y) * p->z / (p->z - v0->z);
   float x1 = p->x - (p->x - v1->x) * p->z / (p->z - v1->z);
   float y1 = p->y - (p->y - v1->y) * p->z / (p->z - v1->z);
   float x2 = p->x - (p->x - v2->x) * p->z / (p->z - v2->z);
   float y2 = p->y - (p->y - v2->y) * p->z / (p->z - v2->z);
   float x3 = p->x - (p->x - v3->x) * p->z / (p->z - v3->z);
   float y3 = p->y - (p->y - v3->y) * p->z / (p->z - v3->z);
 
   point2_t src[4] = { {0, 0}, {w, 0}, {w, h}, {0, h} };
   point2_t dst[4] = { {x0, y0},{x1, y1},{x2, y2} ,{x3, y3} };
 
   for (int i = 0; i < 3; ++i)//初始化matrix_rotate_3D为单位矩阵
   {
      for (int j = 0; j < 3; ++j) matrix_rotate_3D[i][j] = (i == j ? 1.0f : 0.0f);
   }
 
   getPerspectiveTransform(src, dst, (float*)matrix_rotate_3D);
   Transform rotateT;
   Transform rotateR = *matrix;
   rotateT.setMatrix(
      matrix_rotate_3D[0][0], matrix_rotate_3D[1][0], matrix_rotate_3D[2][0],
      matrix_rotate_3D[0][1], matrix_rotate_3D[1][1], matrix_rotate_3D[2][1],
      matrix_rotate_3D[0][2], matrix_rotate_3D[1][2], matrix_rotate_3D[2][2]);
 
   matrix_multiply(rotateR, rotateT, rotateR);
   matrix->setMatrix(
   rotateR.m11(), rotateR.m12(), rotateR.m13(),
   rotateR.m21(), rotateR.m22(), rotateR.m23(),
   rotateR.m31(), rotateR.m32(), rotateR.m33());
 
}
 

其中:float x0 = p->x - (p->x - v0->x) * p->z / (p->z - v0->z); 这一行代码实际上是在进行透视投影操作。具体来说,它通过视角向量 p 和点 v0 来计算点 v0 在 XY 平面上的投影坐标 x0。

这个操作的步骤如下:

  1. 计算视角向量 **p** 和点 **v0** 在 X 轴上的差值:p->x - v0->x

  2. 计算视角向量 **p** 和点 **v0** 在 Z 轴上的差值:p->z - v0->z

  3. 计算透视比例:p->z / (p->z - v0->z)

  4. 计算透视投影后的 X 坐标:p->x - (p->x - v0->x) * p->z / (p->z - v0->z)

这个公式的作用是将三维空间中的点 v0 投影到 XY 平面上,考虑了视角向量 p 的影响。透视投影的目的是使得距离视角向量 p 更远的点在投影后显得更小,从而产生透视效果。

x1,x2,x3,y0,y1,y2,y3 同理。

src[4] 为原始的矩形四个顶点坐标,dst[4] 为变换之后的矩形四个顶点坐标。

getPerspectiveTransform 函数使用了线性代数中的透视变换矩阵求解算法。具体来说,它通过已知的原始矩形顶点坐标 src 和变换后的矩形顶点坐标 dst,来求解一个 3x3 的透视变换矩阵。这个透视变换矩阵可以用来将原始矩形的任意点映射到变换后的矩形上。   在实现上,getPerspectiveTransform 函数使用了以下步骤:

  1. 构建线性方程组:通过已知的原始矩形顶点坐标和变换后的矩形顶点坐标,构建一个包含 8 个方程的线性方程组。每个方程对应一个顶点的变换关系。

    假设透视变换矩阵为: [a  b  c d  e  f g  h  1]\begin{bmatrix} a & b & c \\ d & e & f \\ g & h & 1 \end{bmatrix}

    对于每个顶点 (x_i, y_i)(x\_i, y\_i)变换到(u_i, v_i)(u\_i, v\_i),我们有:

    [x_i  y_i  1  0  0  0  −x_i u_i  −y_i u_i 0  0  0  x_i  y_i  1  −x_i v_i  −y_i v_i][a b c d e f g h]=[u_i v_i]\begin{bmatrix} x\_i & y\_i & 1 & 0 & 0 & 0 & -x\_i u\_i & -y\_i u\_i \\ 0 & 0 & 0 & x\_i & y\_i & 1 & -x\_i v\_i & -y\_i v\_i \end{bmatrix} \begin{bmatrix} a \\ b \\ c \\ d \\ e \\ f \\ g \\ h \end{bmatrix}= \begin{bmatrix} u\_i \\ v\_i \end{bmatrix}

    8 个方程的线性方程组如下:

    x_0 a + y_0 b + c − x_0 u_0 g − y_0 u_0 h = u_0x_1 a + y_1 b + c − x_1 u_1 g − y_1 u_1 h = u_1x_2 a + y_2 b + c − x_2 u_2 g − y_2 u_2 h = u_2x_3 a + y_3 b + c − x_3 u_3 g − y_3 u_3 h = u_3x_0 d + y_0 e + f − x_0 v_0 g − y_0 v_0 h = v_0x_1 d + y_1 e + f − x_1 v_1 g − y_1 v_1 h = v_1x_2 d + y_2 e + f − x_2 v_2 g − y_2 v_2 h = v_2x_3 d + y_3 e + f − x_3 v_3 g − y_3 v_3 h = v_3x\_0 a + y\_0 b + c - x\_0 u\_0 g - y\_0 u\_0 h = u\_0\\ x\_1 a + y\_1 b + c - x\_1 u\_1 g - y\_1 u\_1 h = u\_1\\ x\_2 a + y\_2 b + c - x\_2 u\_2 g - y\_2 u\_2 h = u\_2\\ x\_3 a + y\_3 b + c - x\_3 u\_3 g - y\_3 u\_3 h = u\_3\\ x\_0 d + y\_0 e + f - x\_0 v\_0 g - y\_0 v\_0 h = v\_0\\ x\_1 d + y\_1 e + f - x\_1 v\_1 g - y\_1 v\_1 h = v\_1\\ x\_2 d + y\_2 e + f - x\_2 v\_2 g - y\_2 v\_2 h = v\_2\\ x\_3 d + y\_3 e + f - x\_3 v\_3 g - y\_3 v\_3 h = v\_3

  2. 矩阵表示:将这些方程表示为一个 8x9 的矩阵 A,其中前 8 列表示系数,最后一列表示结果。

    float A[8][9] =
    {
       { x0, y0, 1, 0, 0, 0, -x0 * u0, -y0 * u0, u0 },
       { x1, y1, 1, 0, 0, 0, -x1 * u1, -y1 * u1, u1 },
       { x2, y2, 1, 0, 0, 0, -x2 * u2, -y2 * u2, u2 },
       { x3, y3, 1, 0, 0, 0, -x3 * u3, -y3 * u3, u3 },
       { 0, 0, 0, x0, y0, 1, -x0 * v0, -y0 * v0, v0 },
       { 0, 0, 0, x1, y1, 1, -x1 * v1, -y1 * v1, v1 },
       { 0, 0, 0, x2, y2, 1, -x2 * v2, -y2 * v2, v2 },
       { 0, 0, 0, x3, y3, 1, -x3 * v3, -y3 * v3, v3 }
    };
  3. 求解线性方程组:使用高斯消元法来求解这个 8x9 的矩阵,得到透视变换矩阵的 8 个参数。

  4. 构建透视变换矩阵:将求解得到的 8 个参数填充到一个 3x3 的矩阵中,形成最终的透视变换矩阵。

具体的实现如下:

bool TS_Helper2d5::getPerspectiveTransform(point2_t* src, point2_t* dst, float* ret)
{
   float x0 = src[0].x, x1 = src[1].x, x2 = src[3].x, x3 = src[2].x;
   float y0 = src[0].y, y1 = src[1].y, y2 = src[3].y, y3 = src[2].y;
   float u0 = dst[0].x, u1 = dst[1].x, u2 = dst[3].x, u3 = dst[2].x;
   float v0 = dst[0].y, v1 = dst[1].y, v2 = dst[3].y, v3 = dst[2].y;
   float A[8][9] =
   {
      { x0, y0, 1, 0, 0, 0, -x0 * u0, -y0 * u0, u0 },
      { x1, y1, 1, 0, 0, 0, -x1 * u1, -y1 * u1, u1 },
      { x2, y2, 1, 0, 0, 0, -x2 * u2, -y2 * u2, u2 },
      { x3, y3, 1, 0, 0, 0, -x3 * u3, -y3 * u3, u3 },
      { 0, 0, 0, x0, y0, 1, -x0 * v0, -y0 * v0, v0 },
      { 0, 0, 0, x1, y1, 1, -x1 * v1, -y1 * v1, v1 },
      { 0, 0, 0, x2, y2, 1, -x2 * v2, -y2 * v2, v2 },
      { 0, 0, 0, x3, y3, 1, -x3 * v3, -y3 * v3, v3 },
   };
   if (Gauss(A, 8, 8, ret) == true)//使用高斯消元法求解线性方程组A[8][9]
   {
      *(ret + 8) = 1;
      return true;
   }
   return false;
}

面的隐藏和显示

立方体旋转的过程中,有些面会在前方,有些面会在后方,正常情况下后方的面会被遮挡住,因此需要根据面法向量和屏幕法向量的夹角来判断哪些面需要显示,哪些面需要隐藏。

  • center 是面的中点,由四个顶点的平均值计算得到。

  • icon->normalVector 是面的法向量。

  • screen_v 是屏幕的法向量,这里假设屏幕的法向量为 (0,0,1)。

  • a 是 icon->normalVector 和 screen_v 的点积。

  • dot_ret 是 icon->normalVector 和 screen_v 的点积最大值。

  • icon_find 是 dot_ret 对应的面索引。

Point3F center =
{
    (rv0.x + rv1.x + rv2.x + rv3.x) / 4,
    (rv0.y + rv1.y + rv2.y + rv3.y) / 4,
    (rv0.z + rv1.z + rv2.z + rv3.z) / 4
};
icon->normalVector = m_cube.helperTrans->createVector(m_cube.position, center);
 
...
 
Vector3F screen_v = {0, 0, 1}; //screen normal vector
float a = m_cube.helperTrans->dotProduct(screen_v, icon->normalVector);
if (a > 0)//find display icon
{
    icon->opacity = 255;//display icon
}
else
{
    icon->opacity = 0;//hide icon
}
if (a > dot_ret)//find top dispaly icon
{
    icon_find = i;
    dot_ret = a;
}

渲染绘制

绘制时,就和正常绘制图片流程一样,遍历立方体的六个面,当面的透明度不等于零,就绘制对应的图片。

void CubeView::paintEvent(PaintEvent *event)
{
    for (int i = 0; i < ICON_MAX; i++)
    {
        Painter p(this);
        Image p_img = m_cube.icons[i].img;
        p.setTransform(m_cube.icons[i].transfrom); //设置变换矩阵
        p.setOpacity(m_cube.icons[i].opacity); //设置透明度
        if (m_cube.icons[i].opacity != 0) //判断是否需要绘制
        {
            if (!p_img.isEmpty()) //判断是否有图片,否则绘制pixmap
            {
                p.drawImage(Point(0, 0), p_img);
            }
            else if (m_cube.icons[i].pixmap
                     && m_cube.icons[i].widget
                     && !m_cube.icons[i].widget->isVisible())
            {
                p.drawTexture(PointF(0, 0), *m_cube.icons[i].pixmap);
            }
        }
    }
}

阅读约 23 分钟

评论

0 条已通过审核

还没有评论,来写第一条吧。

写一条评论

评论需要审核后才会公开显示。

只用于识别,服务端仅保存哈希。

纯文本,不支持 Markdown 或 HTML。上限 500 字