? 繪制魔方 中基于OpenGL ES 實現了魔方的繪制,實現較復雜,本文基于 Unity3D 實現了 2 ~ 10 階魔方的整體旋轉和局部旋轉。
? 本文完整代碼資源見→基于 Unity3D 的 2 ~ 10 階魔方實現。下載資源后,進入【Build/Windows】目錄,打開【魔方.exe】文件即可體驗產品。
? 詳細需求如下:
(資料圖片僅供參考)
? 1)魔方渲染模塊
用戶選擇魔方階數,渲染指定階數的魔方;? 2)魔方整體控制模塊
用戶 Scroll 或 Ctrl + Scroll,控制魔方放大和縮小;用戶 Drag 空白處(或右鍵 Drag),控制魔方整體連續旋轉;用戶點擊翻面按鈕(或方向鍵,或 Ctrl + Drag,或 Alt + Drag),控制魔方翻面;用戶點擊朝上的面按鈕,控制魔方指定面朝上;可以實時識別用戶視覺下魔方的正面、上面、右面;? 3)魔方局部控制模塊
用戶點擊刷新按鈕,打亂魔方;用戶 Drag 魔方相鄰的兩個方塊,控制該層旋轉,Drag 結束自動對齊魔方(局部旋轉);用戶輸入公式,提交后執行公式旋轉對應層;每次局部旋轉結束,檢驗魔方是否拼成,若拼成,彈出通關提示;? 4)魔方動畫模塊
魔方翻面動畫;魔方指定面朝上動畫;魔方打亂動畫;魔方局部旋轉對齊動畫;公式控制魔方旋轉動畫;通關彈窗動畫(漸變+縮放+平移);撤銷和逆撤銷動畫;整體旋轉和局部旋轉動畫互不干擾,可以并行;? 5)魔方撤銷和逆撤銷模塊
Drag 魔方連續整體旋轉支持撤銷和逆撤銷;魔方翻面支持撤銷和逆撤銷;魔方指定面朝上支持撤銷和逆撤銷;魔方局部旋轉支持撤銷和逆撤銷;公式控制魔方旋轉支持撤銷和逆撤銷(撤銷整個公式,而不是其中的一步);? 6)其他模塊
用戶點擊返回按鈕,可以返回到選擇階數界面;用戶每進行一次局部旋轉,記步加 1,公式每走一步,記步加1 ;顯示計時器;用戶點擊開始 / 暫停按鈕,可以控制計時器運行 / 暫停,暫停時,只能整體旋轉,不能局部旋轉;用戶異常操作,彈出 Toast 提示(主要是公式輸入合法性校驗);? 選擇階數界面如下:
? 魔方界面如下:
2 相關技術棧MonoBehaviour的生命周期Transform組件人機交互Input場景切換、全屏/恢復切換、退出游戲、截屏燈光組件Light碰撞體組件Collider發射(Raycast)物理射線(Ray)相機縮放、平移、旋轉場景UGUI概述UGUI之TextUGUI之Image和RawImageUGUI之ButtonUGUI之InputFieldUGUI回調函數UGUI之布局組件協同程序空間和變換3 原理介紹3.1 魔方編碼? 為方便計算,需要對魔方的軸、層序、小立方體、方塊、旋轉層進行編碼,編碼規則如下(假設魔方階數為 n):
軸:x、y、z 軸分別編碼為 0、1、2,x、y、z 軸分別指向 right、up、forward(由魔方的正面指向背面,左手坐標系);層序:每個軸向,由負方向到正方向分別編碼為 0 ~ (n-1);小立方體:使用僅包含 3 個元素的一維數組 loc 標記,loc[axis] 表示該小立方體在 axis 軸下的層序;方塊:紅、橙、綠、藍、粉、黃、黑色方塊分別編碼為:0、1、2、3、4、5、-1;旋轉層:旋轉層由旋轉軸 (axis) 和層序 (seq) 決定。3.2 渲染原理? 在 Hierarchy 窗口新建一個空對象,重命名為 Cube,在 Cube 下創建 6 個 Quad 對象,分別重命名為 0 (x = -0.5)、1 (x = 0.5)、2 (y = -0.5)、3 (y = 0.5)、4 (z = -0.5)、5 (z = 0.5) (方塊的命名標識了魔方所屬的面,在魔方還原檢測中會用到),調整位置和旋轉角度,使得它們圍成一個小立方體,將 Cube 拖拽到 Assets 窗口作為預設體。
? 在創建一個 n 階魔方時,新建一個空對象,重命名為 Rubik,復制 n^3 個 Cube 作為 Rubik 的子對象,調整所有 Cube 的位置使其拼成魔方結構,根據立方體和方塊位置,為每個方塊設置紋理圖片,如下:
? 說明:對于任意小方塊 Square,Square.forward 始終指向小立方體中心,該結論在旋轉層檢測中會用到;Inside.png 為魔方內部色塊,用粉紅色塊代替白色塊是為了凸顯白色線框。
? 每個小立方體的貼圖代碼如下:
? Cube.cs
private void GetTextures(){ // 獲取紋理textures = new Texture[COUNT];for (int i = 0; i < COUNT; i++){textures[i] = RubikRes.INSET_TEXTURE;squares[i].name = "-1";}for(int i = 0; i < COUNT; i++){int axis = i / 2; // loc為小立方體的位置序號(以魔方的左下后為坐標原點, 向右、向上、向前分別為x軸、y軸、z軸, 小立方體的邊長為單位刻度)if (loc[axis] == 0 && i % 2 == 0 || loc[axis] == Rubik.Info().order - 1 && i % 2 == 1){textures[i] = RubikRes.TEXTURES[i];squares[i].name = i.ToString();}squares[i].GetComponent().material.mainTexture = textures[i];}}
3.3 整體旋轉原理? 通過調整相機前進和后退,控制魔方放大和縮小;通過調整相機的位置和姿態,使得相機繞魔方旋轉,實現魔方整體旋轉。詳情見縮放、平移、旋轉場景。
? 使用相機繞魔方旋轉以實現魔方整體旋轉的好處主要有:
整體旋轉和局部旋轉可以獨立執行,互不干擾,方便實現整體旋轉和局部旋轉的動畫并行;魔方的姿態始終固定,其 x、y、z 軸始終與世界坐標系的 x、y、z 軸平行,便于后續計算,不用進行一系列的投影計算,也節省了性能;整體旋轉的誤差不會對局部旋轉造成影響,不會影響魔方結構,不會出現魔方崩塌問題。3.4 用戶視覺下魔方坐標軸檢測原理? 用戶翻面、選擇朝上的面等整體旋轉操作,會改變魔方的正面、右面、上面(即魔方朝上的面不一定是藍色面、朝右的面不一定是橙色面、朝前的面不一定是粉色面),用戶視覺下魔方的 x、y、z 軸也會發生變化。假設魔方的 x、y、z 軸正方向單位向量為 ox、oy、oz,用戶視覺下魔方的 x、y、z 軸正方向單位向量為 ux、uy、uz,相機的 right、up、forward 軸正方向單位向量分別為 cx、cy、cz,則 ux、uy、uz 的取值滿足以下關系:
? 相關代碼如下:
? AxisUtils.cs
using UnityEngine;/* * 坐標軸工具類 * 坐標軸相關計算 */public class AxisUtils{ private static Vector3[] worldAxis = new Vector3[] { Vector3.right, Vector3.up, Vector3.forward }; // 世界坐標軸 public static Vector3 Axis(int axis) { // 獲取axis軸向量 return worldAxis[axis]; } public static Vector3 NextAxis(int axis) { // 獲取axis的下一個軸向量 return worldAxis[(axis + 1) % 3]; } public static Vector3 Axis(Transform trans, int axis) { // 獲取trans的axis軸向量 if (axis == 0) { return trans.right; } else if (axis == 1) { return trans.up; } return trans.forward; } public static Vector3 NextAxis(Transform trans, int axis) { // 獲取trans的axis下一個軸向量 return Axis(trans, (axis + 1) % 3); } public static Vector3 FaceAxis(int face) { // 獲取face面對應的軸向量 Vector3 vec = worldAxis[face / 2]; if (face % 2 == 0) { vec = -vec; } return vec; } public static Vector3 GetXAxis() { // 獲取與相機right軸夾角最小的世界坐標軸 return GetXAxis(Camera.main.transform.right); } public static Vector3 GetYAxis() { // 獲取與相機up軸夾角最小的世界坐標軸 return GetYAxis(Camera.main.transform.up); } public static Vector3 GetZAxis() { // 獲取與相機forward軸夾角最小的世界坐標軸 return GetZAxis(Camera.main.transform.forward); } public static Vector3 GetXAxis(Vector3 right) { // 獲取與right向量夾角最小的世界坐標軸 int x = GetZAxisIndex(right); Vector3 xAxis = worldAxis[x]; if (Vector3.Dot(worldAxis[x], right) < 0) { xAxis = -xAxis; } return xAxis; } public static Vector3 GetYAxis(Vector3 up) { // 獲取與up向量軸夾角最小的世界坐標軸 int y = GetZAxisIndex(up); Vector3 yAxis = worldAxis[y]; if (Vector3.Dot(worldAxis[y], up) < 0) { yAxis = -yAxis; } return yAxis; } public static Vector3 GetZAxis(Vector3 forward) { // 獲取與forward向量夾角最小的世界坐標軸 int z = GetZAxisIndex(forward); Vector3 zAxis = worldAxis[z]; if (Vector3.Dot(worldAxis[z], forward) < 0) { zAxis = -zAxis; } return zAxis; } public static int GetAxis(int flag) { // 根據flag值, 獲取與相機坐標軸較近的軸 if (flag == 0) { return GetXAxisIndex(Camera.main.transform.right); } if (flag == 1) { return GetXAxisIndex(Camera.main.transform.up); } if (flag == 2) { return GetXAxisIndex(Camera.main.transform.forward); } return -1; } private static int GetXAxisIndex(Vector3 right) { // 獲取與right向量夾角最小的世界坐標軸索引 float[] dot = new float[3]; for (int i = 0; i < 3; i++) { // 計算世界坐標系的坐標軸在相機right軸上的投影 dot[i] = Mathf.Abs(Vector3.Dot(worldAxis[i], right)); } int x = 0; if (dot[x] < dot[1]) { x = 1; } if (dot[x] < dot[2]) { x = 2; } return x; } private static int GetYAxisIndex(Vector3 up) { // 獲取與up向量軸夾角最小的世界坐標軸索引 float[] dot = new float[3]; for (int i = 0; i < 3; i++) { // 計算世界坐標系的坐標軸在相機up軸上的投影 dot[i] = Mathf.Abs(Vector3.Dot(worldAxis[i], up)); } int y = 1; if (dot[y] < dot[2]) { y = 2; } if (dot[y] < dot[0]) { y = 0; } return y; } private static int GetZAxisIndex(Vector3 forward) { // 獲取與forward向量夾角最小的世界坐標軸索引 float[] dot = new float[3]; for (int i = 0; i < 3; i++) { // 計算世界坐標系的坐標軸在相機forward軸上的投影 dot[i] = Mathf.Abs(Vector3.Dot(worldAxis[i], forward)); } int z = 2; if (dot[z] < dot[0]) { z = 0; } if (dot[z] < dot[1]) { z = 1; } return z; }}
3.5 選擇朝上的面原理? 首先生成 24 個視覺方向(6 個面,每個面 4 個視覺方向),如下(不同顏色的線條代表該顏色的面對應的 4 個視覺方向),記錄相機在這些視覺方向下的 forward 和 right 向量,分別記為:forwardViews、rightViews(數據類型:Vector3[6][4])。
? 當選擇 face 面朝上時,需要在 forwardViews[face] 的 4 個向量中尋找與相機的 forward 夾角最小的向量,記該向量的索引為 index,旋轉相機,使其 forward 和 right 分別指向 forwardViews[face][index]、rightViews[face][index]。
3.6 旋轉層檢測原理? 1)旋轉軸檢測
? 假設屏幕射線檢測到的兩個相鄰方塊分別為 square1、square2。
如果 square1 與 square2 在同一個小立方體里,square1.forward 與 square2.forward 叉乘的向量就是旋轉軸方向向量;如果 square1 與 square2 在相鄰小立方體里,square1.forward 與 (square2.position - square1.position) 叉乘的向量就是旋轉軸方向向量;? 假設叉乘后的向量的單位向量為 crossDir,我們將 crossDir 與 3 個坐標軸的單位方向向量進行點乘(記為 project),如果 Abs(project) > 0.99(夾角小于 8°),就選取該軸作為旋轉軸,如果每個軸的點乘絕對值結果都小于 0.99,說明屏幕射線拾取的兩個方塊不在同一旋轉層,舍棄局部旋轉。補充:project 在 3)中會再次用到。
? 2)層序檢測
? 坐標分量與層序的映射關系如下,其中 order 為魔方階數,seq 為層序,pos 為坐標分量,cubeSide 為小立方體的邊長。由于頻繁使用到 pos 與 seq 的映射,建議將 0 ~ (order-1) 層的層序 seq 對應的 pos 存儲在數組中,方便快速查找。
? square1 與 square2 在旋轉軸方向上的坐標分量一致,假設為 pos(如果旋轉軸是 axis,pos = square1.position[axis]),由上述公式就可以推導出層序 seq。
? 3)拖拽正方向
? 拖拽正方向用于確定局部旋轉的方向,計算如下,project 是 1)中計算的點乘值。
? SquareUtils.cs
private static Vector2 GetDragDire(Transform square1, Transform square2, int project){ // 獲取局部旋轉拖拽正方向的單位方向向量Vector2 scrPos1 = Camera.main.WorldToScreenPoint(square1.position);Vector2 scrPos2 = Camera.main.WorldToScreenPoint(square2.position);Vector2 dire = (scrPos2 - scrPos1).normalized;return -dire * Mathf.Sign(project);}
3.7 局部旋轉原理? 1)待旋轉的小立方體檢測
? 對于每個小立方體,使用數組 loc[] 存儲了小立方體在 x、y、z 軸方向上的層序,每次旋轉結束后,根據小立方體的中心坐標可以重寫計算出 loc 數組(3.6 節中公式)。
? 假設檢測到的旋轉軸為 axis,旋轉層為 seq,所有 loc[axis] 等于 seq 的小立方體都是需要旋轉的小立方體。
? 2)局部旋轉
? 在 Rubik 對象下創建一個空對象,重命名為 RotateLayer,將 RotateLayer 移至坐標原點,旋轉角度全部置 0。
? 將處于旋轉層的小立方體的 parent 都設置為 RotateLayer,對 RotateLayer 進行旋轉,旋轉結束后,將這些小立方體的 parent 重置為 Rubik,RotateLayer 的旋轉角度重置為 0,根據小立方體中心的 position 更新 loc 數組。
3.8 還原檢測原理? 對于魔方的每個面,通過屏幕射線射向每個 Square 的中心,獲取檢測到的 Square 的 name,如果存在兩個 Square 的 name 不一樣,則魔方未還原,否則繼續檢測下一個面,如果每個面都還原了,則魔方已還原。
? SuccessDetector.cs
public void Detect(){ // 檢測魔方是否已還原for (int i = 0; i < squareRays.squareRays.Length - 1; i++){ // 檢測每個面(只需檢查5個面)string name = GetSquareName(i, 0);for (int j = 1; j < squareRays.squareRays[i].Length; j++){ // 檢測每個方塊if (!name.Equals(GetSquareName(i, j))){return;}}}Success();}private string GetSquareName(int face, int index){ // 獲取方塊名if (Physics.Raycast(squareRays.squareRays[face][index], out hitInfo)){return hitInfo.transform.name;}return "-1";}
? 說明:squareRays 里存儲了每個方塊對應的射線,這些射線由方塊的外部垂直指向方塊中心。
4 運行效果? 1)2 ~ 10 階魔方渲染效果
? 2)魔方打亂動畫
? 說明:在打亂的過程中可以縮放和整體旋轉,體現了局部控制和整體控制相互獨立,互不干擾。
? 3)按鈕翻面動畫
? 4)Ctrl + Drag 翻面動畫
? 5)選擇朝上的面動畫
? 6)局部旋轉動畫
? 7)公式控制局部旋轉動畫
? 說明:在公式執行過程中,不影響魔方的整體旋轉和縮放。
? 8)通關動畫
? 聲明:本文轉自【Unity3D】魔方
關鍵詞:
版權與免責聲明:
1 本網注明“來源:×××”(非商業周刊網)的作品,均轉載自其它媒體,轉載目的在于傳遞更多信息,并不代表本網贊同其觀點和對其真實性負責,本網不承擔此類稿件侵權行為的連帶責任。
2 在本網的新聞頁面或BBS上進行跟帖或發表言論者,文責自負。
3 相關信息并未經過本網站證實,不對您構成任何投資建議,據此操作,風險自擔。
4 如涉及作品內容、版權等其它問題,請在30日內同本網聯系。