
鼠标左键拖动能观察不同角度下的魔方。
鼠标右键拖动能拧动魔方。
关于观察魔方的各个角度,可以看我之前的“一个正方体“的程序,那里讲解比较详细,这个程序主要讲如何判断哪些面是要展示出来的以及如何判断点在哪个面的哪个小平面上,以及旋转时的逻辑。
要判断哪些面是要展示出来的,就需要投影面的法向量 m 和每个面的法向量 n,若 m * n 为负数,说明这两个向量夹角为钝角,也就是不用展示出来,反之就要展示出来。数学基础就这么两句话,但是要用代码实现出来得有几十行,这里我将魔方 8 个顶点固定住,6 个面的法向量也是固定的,略一计算,我们只需要根据投影面的法向量的 x,y,z 值的正负就能判断 6 个面中哪些面是要展示出来的。这里判断正负要注意,c 语言中的浮点数无法表示 0,最接近 0 的浮点数是 2^(-128),因此浮点数的 0 会比 0 略大或者略小,我这里用一个宏 ZERO 表示 0,只要浮点数比这个宏大就是大于 0。
同时在透视投影中,当观察点没有倾斜一定角度无法看到侧面,这个倾斜的角度的 cos 值为魔方边长的一半除以观察点到投影面的距离。也就是当观察点在某一个平面上,就只能看到距离最近的两个点形成的直线,看不到这个平面上的其他点。这时候 ZERO 值就表示能看到侧面的最小 cos 值。
不信的话可以吧 ZERO 值改小一点。

(魔方数学模型)
判断点在哪个面的哪个小平面上需要建一个模型,不然你不知道一个平面的起点在哪里,也就不能判断每个小平面的位置。魔方的 8 个顶点中,头四个表示底部,尾四个表示顶部,逆时针挨个排序,在空间直角坐标系中,魔方中心点为原点,x 轴正方向为右边平面的法向量,也就是 Right 表示的平面,y 轴正方向为后面平面的法向量,也就是 Back 表示的平面,z 轴正方向为上平面的法向量,也就是 Up 平面。用 8 个顶点来表示平面为
前面(Front):0、1、5、4
后面(Back):3、2、6、7
左面(Left):0、3、7、4
右面(Right):1、2、6、5
上面(Up):4、5、6、7
下面(Down):0、1、2、3
判断时先根据投影面得到每个魔方面的二维点值,对于魔方面的每个角,可以连接顶点与点中的地方构成一个二维向量 i,再从这个顶点往两边做两个二维向量求这两个二维向量与 i 的夹角余弦值,余弦值可以表示 180 度以内的角的大小,若这两个余弦值有一个比两边向量夹角的余弦值要小,说明点中的点与某条边所形成的夹角比这个顶点的夹角要大,也就是点在平面外。对任意多边形都可以用这个方法判断点是否在平面内,判断次数为多边形顶点的个数。
判断完点是否在平面内后判断在该平面的位置,每个平面的起点上面已经给出,根据每个平面的起点与点中的位置构成一个向量,起点与两边的向量的 1/3 长度为横纵坐标单位向量。这里做个简单的数学计算,a,b 是两个不平行的二维向量,m、n 为常数,m * a + n * b = c,c 为点中的位置与起点构成的二维向量,求出 m、n 就能得到点在平面内的位置。这方法只对平行投影有效,透视投影则将一个面的横纵方向各分为三个平面,判断点在第 m 个横平面的第 n 个纵平面,根据 m,n 值求出点在平面上的坐标。
旋转时的逻辑比以上两个都要复杂,我的做法是用一个数组保存六个面的颜色,旋转时先画出旋转时的效果,旋转结束后改变颜色数组。
-
- // 程序:魔方
- // 编译环境:Visual Studio 2019,EasyX_20211109
-
- #include
- #include
- #include
- #define WIDTH 640 // 窗口宽度
- #define HEIGHT 480 // 窗口高度
- #define PI 3.14159265 // π
- #define SIDE (min(WIDTH, HEIGHT) / 4) // 正方体边长
- #define GAMEPAD (SIDE / 2) // 手柄,控制面旋转幅度的量
- #define ZERO 0.1 // 对于浮点数来说的 0 值
- #define PIECE 180 // 将一个 π 分为 PIECE 份
- COLORREF DifferentColor = RGB(193, 181, 62);
-
- // 旋转时是否相反
- bool isOpposite(double Fi)
- {
- int degree = Fi / PI * PIECE;
- int judge = abs(degree) % (2 * PIECE);
- if (judge > (PIECE / 2) && judge <= (PIECE / 2 * 3))
- return true;
- return false;
- }
-
- enum Plane
- {
- Up, Down, Left, Right, Front, Back
- };
-
- // 六个面的颜色
- COLORREF SurfaceColor[6] =
- {
- RED, YELLOW, BLUE, GREEN, BROWN, MAGENTA
- };
-
- // 表示在正方体上的哪一面
- struct CubeIndex
- {
- Plane plane;
- int index;
- };
-
- // 判断旋转时是哪一层旋转
- struct RotationPoint
- {
- bool isAcrossAxis;
- bool isStraightAxis;
- bool isVerticalAxis;
- unsigned int Across_Judge;
- unsigned int Straight_Judge;
- unsigned int Vertical_Judge;
- };
-
- // 二维向量,也可以表示一个坐标
- struct Vec2
- {
- double x, y;
- };
- typedef struct Vec2;
-
- Vec2 operator + (Vec2 a, Vec2 b)
- {
- return { a.x + b.x, a.y + b.y };
- }
-
- Vec2 operator - (Vec2 a, Vec2 b)
- {
- return { a.x - b.x, a.y - b.y };
- }
-
-
- // 得到向量缩短 num 倍后的向量
- Vec2 operator / (Vec2 a, long double num)
- {
- Vec2 result;
- result.x = a.x / num;
- result.y = a.y / num;
- return result;
- }
-
- // 得到向量延长 num 倍后的向量
- Vec2 operator * (Vec2 a, long double num)
- {
- Vec2 result;
- result.x = a.x * num;
- result.y = a.y * num;
- return result;
- }
-
- double operator * (Vec2 a, Vec2 b)
- {
- return a.x * b.x + a.y * b.y;
- }
-
- // 三维向量,也可以表示一个坐标
- struct Vec3
- {
- double x, y, z;
- };
- typedef struct Vec3;
-
- // 求两向量相减
- Vec3 operator - (Vec3 a, Vec3 b)
- {
- return { a.x - b.x, a.y - b.y, a.z - b.z };
- }
-
- // 求两向量相加
- Vec3 operator + (Vec3 a, Vec3 b)
- {
- return { a.x + b.x, a.y + b.y, a.z + b.z };
- }
-
- // 得到两向量点乘的值
- double operator * (Vec3 a, Vec3 b)
- {
- return a.x * b.x + a.y * b.y + a.z * b.z;
- }
-
- // 得到向量缩短 num 倍后的向量
- Vec3 operator / (Vec3 a, long double num)
- {
- Vec3 result;
- result.x = a.x / num;
- result.y = a.y / num;
- result.z = a.z / num;
- return result;
- }
-
- // 得到向量延长 num 倍后的向量
- Vec3 operator * (Vec3 a, long double num)
- {
- Vec3 result;
- result.x = a.x * num;
- result.y = a.y * num;
- result.z = a.z * num;
- return result;
- }
-
- // 得到一个向量的模长
- double GetVec3Length(Vec3 vec)
- {
- return sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z);
- }
-
- double GetVec2Length(Vec2 vec)
- {
- return sqrt(vec.x * vec.x + vec.y * vec.y);
- }
-
- // 得到向量 a 与向量 b 的夹角余弦值
- double GetCosineOfTheAngle(Vec3 a, Vec3 b)
- {
- return a * b / GetVec3Length(a) / GetVec3Length(b);
- }
-
- double GetCosineOfTheAngle(Vec2 a, Vec2 b)
- {
- return a * b / GetVec2Length(a) / GetVec2Length(b);
- }
-
- // 得到向量 A 在向量 B 上的投影
- Vec3 GetProjectionAOntoB(Vec3 A, Vec3 B)
- {
- double num = GetCosineOfTheAngle(A, B); // 得到向量 A,B 的夹角余弦值
- double length = GetVec3Length(A) * num; // 向量 A 的模长乘 num 为向量 A 在向量 B 上投影的模长
- Vec3 result = B * (abs(length) / GetVec3Length(B)); // 向量 B 延长 length 倍再缩短 B 的模长倍就是向量 A 在向量 B 上的投影
- // 如果 length 比 0 小说明 num 小于 0,也就是两向量夹角大于 90 度,结果要变为相反向量
- if (length > 0)return result;
- return result * (-1.0);
- }
-
- // 根据投影面 x,y 轴正方向向量求出投影面法向量
- Vec3 getVerticalAxis(Vec3 AuxiliaryVector[2])
- {
- double x0 = AuxiliaryVector[0].x;
- double y0 = AuxiliaryVector[0].y;
- double z0 = AuxiliaryVector[0].z;
- double x1 = AuxiliaryVector[1].x;
- double y1 = AuxiliaryVector[1].y;
- double z1 = AuxiliaryVector[1].z;
- return { y0 * z1 - y1 * z0, x1 * z0 - x0 * z1, x0 * y1 - x1 * y0 };
- }
-
- // 将三维的点的值转换为在对应 xoy 面上的投影的坐标
- typedef Vec3 DoubleVec3[2];
- Vec2 Transform3DTo2D(Vec3 vertex, DoubleVec3 AuxiliaryVector)
- {
- Vec2 result;
- Vec3 tempX = GetProjectionAOntoB(vertex, AuxiliaryVector[0]); // 得到三维向量在 x 轴上的投影
- Vec3 tempY = GetProjectionAOntoB(vertex, AuxiliaryVector[1]); // 得到三维向量在 y 轴上的投影
- result.x = GetVec3Length(tempX); // 得到 tempX 的模长,模长就是结果的 x 值的绝对值
- result.y = GetVec3Length(tempY); // 得到 tempY 的模长,模长就是结果的 y 值的绝对值
- if (tempX * AuxiliaryVector[0] < 0)result.x *= -1; // 如果 tempX 向量与 x 轴正方向的向量夹角大于 90 度,也就是向量点乘为负数,那么结果的 x 值为负数
- if (tempY * AuxiliaryVector[1] < 0)result.y *= -1; // 如果 tempY 向量与 y 轴正方向的向量夹角大于 90 度,也就是向量点乘为负数,那么结果的 y 值为负数
- // 以下为透视投影所做的操作,不需透视投影只需直接返回 result 值
- Vec3 Vec_Z = getVerticalAxis(AuxiliaryVector) * SIDE * 5;
- Vec3 target = vertex - Vec_Z;
- return result * (SIDE * 5 / GetVec3Length(GetProjectionAOntoB(target, Vec_Z)));
- }
-
- // 得到当前投影面的法向量,用于判断哪些面要显示出来
- Vec3 getVerticalAxis(double Fi, double Th)
- {
- return { cos(Fi) * sin(Th), -cos(Fi) * cos(Th), sin(Fi) };
- }
-
- void Line_Vec2(Vec2 a, Vec2 b)
- {
- line(a.x, a.y, b.x, b.y);
- }
-
- // 画表面
- void drawSurface(Vec3 Surface[4], Vec3 AuxiliaryVector[2], Vec2 pericenter_Sur, COLORREF color[9])
- {
- setlinestyle(PS_SOLID, 2);
- Vec3 add_X = (Surface[1] - Surface[0]) / 3;
- Vec3 add_Y = (Surface[3] - Surface[0]) / 3;
- Vec3 add_All = add_X + add_Y;
- for (int i = 0; i < 3; i++)
- {
- for (int j = 0; j < 3; j++)
- {
- Vec3 position = Surface[0] + add_X * j + add_Y * i;
- // Vec2 pos_2d = Transform3DTo2D(position, AuxiliaryVector);
- setlinecolor(DifferentColor);
- Line_Vec2(Transform3DTo2D(position, AuxiliaryVector) + pericenter_Sur,
- Transform3DTo2D(position + add_X, AuxiliaryVector) + pericenter_Sur);
- Line_Vec2(Transform3DTo2D(position, AuxiliaryVector) + pericenter_Sur,
- Transform3DTo2D(position + add_Y, AuxiliaryVector) + pericenter_Sur);
- Line_Vec2(Transform3DTo2D(position + add_All, AuxiliaryVector) + pericenter_Sur,
- Transform3DTo2D(position + add_X, AuxiliaryVector) + pericenter_Sur);
- Line_Vec2(Transform3DTo2D(position + add_All, AuxiliaryVector) + pericenter_Sur,
- Transform3DTo2D(position + add_Y, AuxiliaryVector) + pericenter_Sur);
- Vec2 pericenter = Transform3DTo2D(position + add_All / 2, AuxiliaryVector) + pericenter_Sur;
- setfillcolor(color[i * 3 + j]);
- floodfill(pericenter.x, pericenter.y, DifferentColor);
- setlinecolor(WHITE);
- Line_Vec2(Transform3DTo2D(position, AuxiliaryVector) + pericenter_Sur,
- Transform3DTo2D(position + add_X, AuxiliaryVector) + pericenter_Sur);
- Line_Vec2(Transform3DTo2D(position, AuxiliaryVector) + pericenter_Sur,
- Transform3DTo2D(position + add_Y, AuxiliaryVector) + pericenter_Sur);
- Line_Vec2(Transform3DTo2D(position + add_All, AuxiliaryVector) + pericenter_Sur,
- Transform3DTo2D(position + add_X, AuxiliaryVector) + pericenter_Sur);
- Line_Vec2(Transform3DTo2D(position + add_All, AuxiliaryVector) + pericenter_Sur,
- Transform3DTo2D(position + add_Y, AuxiliaryVector) + pericenter_Sur);
- }
- }
- }
-
- // 画正方体
- void drawCube(Vec3 Vertex[8], Vec3 AuxiliaryVector[2], Vec2 pericenter, COLORREF ColorArray[6][9])
- {
- Vec2 Temp[8];
- Vec3 Vector_Z = getVerticalAxis(AuxiliaryVector);
- Vector_Z = Vector_Z / GetVec3Length(Vector_Z);
- Vec3 Surface[6][4] =
- {
- Vertex[1], Vertex[2], Vertex[6], Vertex[5],
- Vertex[0], Vertex[3], Vertex[7], Vertex[4],
- Vertex[3], Vertex[2], Vertex[6], Vertex[7],
- Vertex[0], Vertex[1], Vertex[5], Vertex[4],
- Vertex[4], Vertex[5], Vertex[6], Vertex[7],
- Vertex[0], Vertex[1], Vertex[2], Vertex[3],
- };
- if (Vector_Z.x > ZERO)drawSurface(Surface[0], AuxiliaryVector, pericenter, ColorArray[Right]);
- else if (Vector_Z.x < -ZERO)drawSurface(Surface[1], AuxiliaryVector, pericenter, ColorArray[Left]);
- if (Vector_Z.y > ZERO)drawSurface(Surface[2], AuxiliaryVector, pericenter