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