ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++:Raylib 3D魔方

C++:Raylib 3D魔方 Raylib下载详情请进专栏上一篇文章// // 三阶魔方 · C raylib 修复版 // · 鼠标拖背景 / 右键拖自由旋转视角预览轨道相机 滚轮缩放 // · 鼠标拖动魔方上的色块按拖拽方向转动对应的一层 // · 键盘标准记号R L U D F BShift 逆时针、M E S 中层、X Y Z 整体转 // 空格打乱、Backspace 撤销、C 复位、A 自动旋转 // // 本版修复 // 1) X/Y/Z 整体转原先只转 layer0等价于 M/E/S会把中心块转到别的面。 // 现改为三层同步转动一次动画完成不再是逐层播放。 // 2) 空格打乱原先只往队列里塞 move静止状态下队列无人消费 —— 打乱完全不生效。 // 现改为入队后立即开播。 // 3) 撤销动作本身会被记录进撤销栈排队时无条件 push导致来回撤销死循环。 // 现给每个 move 带上 record 标志撤销不入栈、不计步。 // 4) isSolved() 原判据为「所有朝向矩阵 单位阵」整体转或中层转之后明明 // 六面纯色却显示 scrambled。现改为「每个面是否单色」。 // 5) 拖拽判定方向时用贴纸中心的真实速度 ω×r而不是只用面法线近似。 // 6) 打乱改用外层转动WCA 习惯且打乱不计入步数、不可撤销。 // // 编译Linux: g rubiks_cube.cpp -o cube -stdc17 -lraylib -lm -lpthread -ldl // 编译macOS: g rubiks_cube.cpp -o cube -stdc17 -lraylib -framework OpenGL -framework Cocoa -framework IOKit -framework CoreVideo // 编译MinGW: g rubiks_cube.cpp -o cube.exe -stdc17 -lraylib -lopengl32 -lgdi32 -lwinmm // #include raylib.h #include raymath.h #include cmath #include cstdio #include cstdlib #include vector // // 第一部分与渲染无关的核心数据小向量 / 魔方状态 / 转动逻辑 // struct V3 { float x, y, z; }; static inline V3 mk(float x, float y, float z) { return V3{ x, y, z }; } static inline V3 operator(V3 a, V3 b) { return V3{ a.x b.x, a.y b.y, a.z b.z }; } static inline V3 operator-(V3 a, V3 b) { return V3{ a.x - b.x, a.y - b.y, a.z - b.z }; } static inline V3 operator*(V3 a, float s) { return V3{ a.x * s, a.y * s, a.z * s }; } static inline float dot(V3 a, V3 b) { return a.x * b.x a.y * b.y a.z * b.z; } static inline V3 cross(V3 a, V3 b) { return V3{ a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x }; } static inline float len(V3 a) { return std::sqrt(dot(a, a)); } static inline V3 normalize(V3 a) { float l len(a); return (l 1e-6f) ? a * (1.0f / l) : mk(0, 0, 0); } // 三个坐标轴单位向量0X(R/L), 1Y(U/D), 2Z(F/B) static inline V3 axisVec(int a) { return mk(a 0 ? 1.0f : 0.0f, a 1 ? 1.0f : 0.0f, a 2 ? 1.0f : 0.0f); } // 绕 axis 轴的右手旋转ang0 为 90° 方向dir1 时与 rotVecI 完全一致 static inline V3 rotAxisF(V3 v, int axis, float ang) { float c std::cos(ang), s std::sin(ang); if (axis 0) return mk(v.x, v.y * c - v.z * s, v.y * s v.z * c); if (axis 1) return mk(v.x * c v.z * s, v.y, -v.x * s v.z * c); return mk(v.x * c - v.y * s, v.x * s v.y * c, v.z); } // 整数版 90° 旋转用于位置与朝向矩阵避免浮点误差累积 static void rotVecI(int v[3], int axis, int dir) { int x v[0], y v[1], z v[2]; if (axis 0) { // 绕 X if (dir 0) { v[1] -z; v[2] y; } else { v[1] z; v[2] -y; } } else if (axis 1) { // 绕 Y if (dir 0) { v[0] z; v[2] -x; } else { v[0] -z; v[2] x; } } else { // 绕 Z if (dir 0) { v[0] -y; v[1] x; } else { v[0] y; v[1] -x; } } } // 面索引0X(R) 1-X(L) 2Y(U) 3-Y(D) 4Z(F) 5-Z(B) static inline int faceAxis(int f) { return f / 2; } static inline int faceSign(int f) { return (f % 2 0) ? 1 : -1; } struct Cubie { int pos[3]; // 位置坐标取值 -1 / 0 / 1 int R[3][3]; // 朝向矩阵世界向量 R · 本地向量 int sticker[6]; // 本地方向上的贴纸颜色索引0..5-1 表示没有贴纸 }; // axis转轴dir1 / -1maskbit(layer1) 置位表示该层一起转整体转 0b111 struct Move { int axis, dir, mask; bool record; }; struct Cube { Cubie c[27]; void reset() { int i 0; for (int x -1; x 1; x) for (int y -1; y 1; y) for (int z -1; z 1; z) { Cubie b c[i]; b.pos[0] x; b.pos[1] y; b.pos[2] z; for (int a 0; a 3; a) for (int b2 0; b2 3; b2) b.R[a][b2] (a b2) ? 1 : 0; // 初始朝向为单位阵本地方向朝外的一面才有贴纸 for (int f 0; f 6; f) { int a faceAxis(f), s faceSign(f); b.sticker[f] (b.pos[a] * s 1) ? f : -1; } } } // 转动一层axis 轴、layer 层-1/0/1、dir1 右手 90° void applyMove(int axis, int layer, int dir) { for (int i 0; i 27; i) { Cubie b c[i]; if (b.pos[axis] ! layer) continue; rotVecI(b.pos, axis, dir); // 位置旋转 for (int j 0; j 3; j) { // 朝向矩阵左乘旋转矩阵 int col[3] { b.R[0][j], b.R[1][j], b.R[2][j] }; rotVecI(col, axis, dir); b.R[0][j] col[0]; b.R[1][j] col[1]; b.R[2][j] col[2]; } } } // 复原判定每个世界面上的贴纸同色。 // 这样整体转X/Y/Z或中层转之后只要六面纯色就仍算复原 // 也不会被「中心块自转」这种人眼看不出来的状态误判。 bool isSolved() const { int faceColor[6]; for (int g 0; g 6; g) faceColor[g] -1; for (int i 0; i 27; i) { const Cubie b c[i]; for (int f 0; f 6; f) { if (b.sticker[f] 0) continue; int nl[3] { 0, 0, 0 }; nl[faceAxis(f)] faceSign(f); int nw[3]; // 贴纸在世界中的朝向 for (int a 0; a 3; a) nw[a] b.R[a][0] * nl[0] b.R[a][1] * nl[1] b.R[a][2] * nl[2]; int g -1; // 该朝向对应哪个外面 for (int k 0; k 6; k) { int a faceAxis(k); if (nw[a] faceSign(k) nw[(a 1) % 3] 0 nw[(a 2) % 3] 0) { g k; break; } } if (g 0) return false; if (faceColor[g] 0) faceColor[g] b.sticker[f]; else if (faceColor[g] ! b.sticker[f]) return false; } } return true; } }; // // 第二部分渲染 交互 // static const float HALF 0.5f; // 小方块半边长 static const float SPACING 1.04f; // 小方块中心间距留出缝隙 static const float STICKER_K 0.84f; // 贴纸相对小方块的比例 static const float LIFT 0.006f; // 贴纸外凸避免与黑底 z-fighting // 六个面的标准配色R 红、L 橙、U 白、D 黄、F 绿、B 蓝 static const Color FACE_COLOR[6] { { 200, 32, 58, 255 }, // X R 红 { 255, 132, 0, 255 }, // -X L 橙 { 245, 245, 245, 255 }, // Y U 白 { 255, 214, 0, 255 }, // -Y D 黄 { 0, 158, 96, 255 }, // Z F 绿 { 0, 92, 190, 255 }, // -Z B 蓝 }; static const Color BODY_COLOR { 22, 22, 26, 255 }; // 生成本地坐标下某个面的四个顶点从外部看为逆时针法线朝外 static void faceQuad(int f, float half, float lift, V3 out[4]) { int a faceAxis(f), s faceSign(f); V3 n axisVec(a) * (float)s; V3 u axisVec((a 1) % 3) * half; V3 v axisVec((a 2) % 3) * half; V3 c n * (HALF lift); if (s 0) { out[0] c - u - v; out[1] c u - v; out[2] c u v; out[3] c - u v; } else { out[0] c - u - v; out[1] c - u v; out[2] c u v; out[3] c u - v; } } static inline Vector3 toRay(V3 a) { return Vector3{ a.x, a.y, a.z }; } static void drawQuad(V3 p0, V3 p1, V3 p2, V3 p3, Color col) { DrawTriangle3D(toRay(p0), toRay(p1), toRay(p2), col); DrawTriangle3D(toRay(p0), toRay(p2), toRay(p3), col); } // 本地坐标 → 世界坐标先乘朝向矩阵再平移到位最后叠加转动动画 static V3 toWorld(const Cubie b, V3 p, int animAxis, int animMask, float ang) { V3 q mk(b.R[0][0] * p.x b.R[0][1] * p.y b.R[0][2] * p.z, b.R[1][0] * p.x b.R[1][1] * p.y b.R[1][2] * p.z, b.R[2][0] * p.x b.R[2][1] * p.y b.R[2][2] * p.z); q q mk(b.pos[0] * SPACING, b.pos[1] * SPACING, b.pos[2] * SPACING); if (animAxis 0 (animMask (1 (b.pos[animAxis] 1)))) q rotAxisF(q, animAxis, ang); return q; } static void drawCube(const Cube cube, int animAxis, int animMask, float ang) { for (int i 0; i 27; i) { const Cubie b cube.c[i]; for (int f 0; f 6; f) { V3 q[4]; faceQuad(f, HALF, 0.0f, q); V3 w[4]; for (int k 0; k 4; k) w[k] toWorld(b, q[k], animAxis, animMask, ang); drawQuad(w[0], w[1], w[2], w[3], BODY_COLOR); // 黑色底块 if (b.sticker[f] 0) { // 彩色贴纸 faceQuad(f, HALF * STICKER_K, LIFT, q); for (int k 0; k 4; k) w[k] toWorld(b, q[k], animAxis, animMask, ang); drawQuad(w[0], w[1], w[2], w[3], FACE_COLOR[b.sticker[f]]); } } } } // 鼠标拾取结果命中哪个方块的哪个面 struct Pick { int idx -1; int face -1; V3 center { 0, 0, 0 }; V3 normal { 0, 1, 0 }; Vector2 screen { 0, 0 }; float radius 0.0f; }; static bool pickFace(const Cube cube, const Camera3D cam, Vector2 mouse, Pick *out) { bool found false; float bestScore 1e9f; for (int i 0; i 27; i) { const Cubie b cube.c[i]; for (int f 0; f 6; f) { if (b.sticker[f] 0) continue; // 只看有贴纸的外表面 V3 nl axisVec(faceAxis(f)) * (float)faceSign(f); V3 nw mk(b.R[0][0] * nl.x b.R[0][1] * nl.y b.R[0][2] * nl.z, b.R[1][0] * nl.x b.R[1][1] * nl.y b.R[1][2] * nl.z, b.R[2][0] * nl.x b.R[2][1] * nl.y b.R[2][2] * nl.z); V3 center mk(b.pos[0] * SPACING, b.pos[1] * SPACING, b.pos[2] * SPACING) nw * HALF; if (dot(nw, mk(cam.position.x, cam.position.y, cam.position.z) - center) 0.05f) continue; // 背面 Vector2 sc GetWorldToScreen(toRay(center), cam); // 用一条切边估算该面在屏幕上的半径 V3 t cross(nw, mk(0, 1, 0)); if (len(t) 0.1f) t cross(nw, mk(1, 0, 0)); t normalize(t); Vector2 sc2 GetWorldToScreen(toRay(center t * HALF), cam); float r std::sqrt((sc2.x - sc.x) * (sc2.x - sc.x) (sc2.y - sc.y) * (sc2.y - sc.y)); float d std::sqrt((mouse.x - sc.x) * (mouse.x - sc.x) (mouse.y - sc.y) * (mouse.y - sc.y)); float score d / (r * 1.35f 1.0f); if (score bestScore) { bestScore score; found true; out-idx i; out-face f; out-center center; out-normal nw; out-screen sc; out-radius r; } } } return found bestScore 1.0f; } static inline Vector2 v2sub(Vector2 a, Vector2 b) { return Vector2{ a.x - b.x, a.y - b.y }; } static inline float v2len(Vector2 a) { return std::sqrt(a.x * a.x a.y * a.y); } static inline Vector2 v2norm(Vector2 a) { float l v2len(a); return (l 1e-6f) ? Vector2{ a.x / l, a.y / l } : Vector2{ 0, 0 }; } static inline float v2dot(Vector2 a, Vector2 b) { return a.x * b.x a.y * b.y; } static float easeOutCubic(float t) { float u 1.0f - t; return 1.0f - u * u * u; } int main(void) { SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_WINDOW_RESIZABLE); InitWindow(1180, 760, Rubiks Cube 3x3 - C / raylib); SetTargetFPS(60); Cube cube; cube.reset(); Camera3D cam { 0 }; cam.up Vector3{ 0, 1, 0 }; cam.fovy 45.0f; cam.projection CAMERA_PERSPECTIVE; cam.target Vector3{ 0, 0, 0 }; float yaw 0.72f, pitch 0.45f, dist 8.6f; bool autoRotate false; // 转动动画 bool animating false; int animAxis -1, animDir 1, animMask 0; float animT 0.0f; const float ANIM_TIME 0.20f; // 单次转动时长秒 std::vectorMove queue, undoStack; int moveCount 0; // 播完一步后从队列取下一步保持 record 标志撤销不会再被记进撤销栈 auto pumpQueue []() { if (animating || queue.empty()) return; Move m queue.front(); queue.erase(queue.begin()); animating true; animT 0.0f; animAxis m.axis; animDir m.dir; animMask m.mask; if (m.record) { undoStack.push_back(m); moveCount; } }; // recordfalse 用于“撤销”和“打乱”本身不入撤销栈、不计步 auto startMove [](int axis, int dir, int mask, bool record) { Move m{ axis, dir, mask, record }; if (animating) { queue.push_back(m); return; } // 前一步还没播完先排队 animating true; animT 0.0f; animAxis axis; animDir dir; animMask mask; if (record) { undoStack.push_back(m); moveCount; } }; // 鼠标拖拽状态1转视角2转魔方层 bool dragging false; int dragMode 0; Pick pick; bool hasPick false; Vector2 dragStart { 0, 0 }; bool dragFired false; while (!WindowShouldClose()) { float dt GetFrameTime(); // ---------- 动画推进 ---------- if (animating) { float dur queue.empty() ? ANIM_TIME : ANIM_TIME * 0.45f; // 连续播放时提速 animT dt / dur; if (animT 1.0f) { for (int l -1; l 1; l) // 同步应用参与转动的所有层 if (animMask (1 (l 1))) cube.applyMove(animAxis, l, animDir); animating false; animAxis -1; animMask 0; pumpQueue(); // 接着播放队列里的下一步 } } float ang animating ? (animDir * (PI * 0.5f) * easeOutCubic(animT)) : 0.0f; // ---------- 相机 ---------- if (autoRotate !dragging) yaw dt * 0.25f; float cp std::cos(pitch); cam.position Vector3{ dist * cp * std::sin(yaw), dist * std::sin(pitch), dist * cp * std::cos(yaw) }; // ---------- 鼠标拖拽 ---------- Vector2 mouse GetMousePosition(); if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) || IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { dragging true; dragFired false; dragStart mouse; bool right IsMouseButtonPressed(MOUSE_BUTTON_RIGHT); hasPick (!right !animating) ? pickFace(cube, cam, mouse, pick) : false; dragMode hasPick ? 2 : 1; } if (dragging dragMode 1) { // 空白处拖动 转视角 Vector2 d v2sub(mouse, dragStart); yaw - d.x * 0.0065f; pitch d.y * 0.0065f; dragStart mouse; if (pitch 1.45f) pitch 1.45f; if (pitch -1.45f) pitch -1.45f; } if (dragging dragMode 2 !dragFired) { // 在魔方上拖动 转层 Vector2 delta v2sub(mouse, dragStart); if (v2len(delta) 12.0f) { int nAxis faceAxis(pick.face); int bestAxis -1; float bestScore 0.0f; // 对每个可能的转轴算出该贴纸的真实线速度方向 ω×r再比较屏幕方向 for (int a 0; a 3; a) { if (a nAxis) continue; V3 mv cross(axisVec(a), pick.center); // dir1 时贴纸的移动方向 if (len(mv) 1e-4f) continue; Vector2 s0 GetWorldToScreen(toRay(pick.center), cam); Vector2 s1 GetWorldToScreen(toRay(pick.center mv * 0.5f), cam); float sc v2dot(v2norm(delta), v2norm(v2sub(s1, s0))); if (std::fabs(sc) std::fabs(bestScore)) { bestScore sc; bestAxis a; } } if (bestAxis 0) { int layer cube.c[pick.idx].pos[bestAxis]; startMove(bestAxis, bestScore 0 ? 1 : -1, 1 (layer 1), true); dragFired true; } } } if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT) || IsMouseButtonReleased(MOUSE_BUTTON_RIGHT)) { dragging false; dragMode 0; hasPick false; } float wheel GetMouseWheelMove(); // 滚轮缩放 if (wheel ! 0.0f) { dist - wheel * 0.9f; if (dist 4.5f) dist 4.5f; if (dist 22.0f) dist 22.0f; } // ---------- 键盘标准魔方记号 ---------- // dir: 从该面外侧看顺时针 绕对应轴右手 -90°右侧/上层/前面为正层 bool inv IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); int sgn inv ? -1 : 1; auto faceTurn [](int key, int axis, int layer, int baseDir) { if (IsKeyPressed(key)) startMove(axis, baseDir * sgn, 1 (layer 1), true); }; auto wholeTurn [](int key, int axis, int baseDir) { // 整体转三层一起动 if (IsKeyPressed(key)) startMove(axis, baseDir * sgn, 0b111, true); }; faceTurn(KEY_R, 0, 1, -1); // R 右面顺时针 faceTurn(KEY_L, 0, -1, 1); // L 左面顺时针 faceTurn(KEY_U, 1, 1, -1); // U 上面顺时针 faceTurn(KEY_D, 1, -1, 1); // D 下面顺时针 faceTurn(KEY_F, 2, 1, -1); // F 前面顺时针 faceTurn(KEY_B, 2, -1, 1); // B 后面顺时针 faceTurn(KEY_M, 0, 0, 1); // M 中层跟随 L faceTurn(KEY_E, 1, 0, 1); // E 中层跟随 D faceTurn(KEY_S, 2, 0, -1); // S 中层跟随 F wholeTurn(KEY_X, 0, -1); // x 整体绕 R 方向 wholeTurn(KEY_Y, 1, -1); // y 整体绕 U 方向 wholeTurn(KEY_Z, 2, -1); // z 整体绕 F 方向 if (IsKeyPressed(KEY_SPACE)) { // 打乱外层转动不计步、不可撤销 undoStack.clear(); queue.clear(); moveCount 0; int lastAxis -1; for (int i 0; i 25; i) { int a; do { a GetRandomValue(0, 2); } while (a lastAxis); lastAxis a; int l GetRandomValue(0, 1) ? 1 : -1; queue.push_back(Move{ a, GetRandomValue(0, 1) ? 1 : -1, 1 (l 1), false }); } pumpQueue(); // 关键静止状态下也要开播 } if (IsKeyPressed(KEY_BACKSPACE)) { // 撤销一步 if (!queue.empty()) queue.pop_back(); else if (!undoStack.empty()) { Move m undoStack.back(); undoStack.pop_back(); startMove(m.axis, -m.dir, m.mask, false); if (--moveCount 0) moveCount 0; } } if (IsKeyPressed(KEY_C) || IsKeyPressed(KEY_ENTER)) { // 复位 cube.reset(); queue.clear(); undoStack.clear(); animating false; animAxis -1; animMask 0; moveCount 0; } if (IsKeyPressed(KEY_A)) autoRotate !autoRotate; // ---------- 绘制 ---------- BeginDrawing(); ClearBackground(Color{ 18, 20, 28, 255 }); BeginMode3D(cam); DrawPlane(Vector3{ 0, -3.2f, 0 }, Vector2{ 60, 60 }, Color{ 26, 29, 40, 255 }); drawCube(cube, animAxis, animMask, ang); EndMode3D(); DrawText(Drag a sticker - turn that layer Drag background / right-drag - rotate view Wheel - zoom, 16, 14, 18, Color{ 200, 210, 225, 255 }); DrawText(R L U D F B (Shift prime) M E S X Y Z Space: scramble Backspace: undo C: reset A: auto-rotate, 16, 38, 18, Color{ 150, 165, 190, 255 }); char info[128]; std::snprintf(info, sizeof(info), Moves: %d | %s | Auto-rotate: %s, moveCount, cube.isSolved() ? SOLVED : scrambled, autoRotate ? ON : OFF); DrawText(info, 16, GetScreenHeight() - 30, 20, cube.isSolved() ? Color{ 120, 230, 150, 255 } : Color{ 235, 200, 120, 255 }); if (hasPick dragging dragMode 2) DrawText(dragging..., 16, GetScreenHeight() - 56, 16, Color{ 130, 150, 180, 255 }); EndDrawing(); } CloseWindow(); return 0; }
返回列表