1 前言
? ? ? ? 場景縮放、平移、旋轉(zhuǎn)有兩種實現(xiàn)方案,一種是對場景中所有物體進行同步變換,另一種方案是對相機的位置和姿態(tài)進行變換。
? ? ? ? 對于方案一,如果所有物體都在同一個根對象下(其子對象或?qū)O子對象),那么只需要對根對象施加變換就可以實現(xiàn)場景變換;如果有多個根對象,那就需要對所有根對象施加變換。該方案實現(xiàn)簡單,但是會破壞場景中對象的尺寸、位置、姿態(tài),不符合現(xiàn)實世界的規(guī)則。如:對場景施加縮放變換后,又新增了一個對象,但是該對象不是放在同一個根目錄下,就會讓用戶感覺新增對象的尺寸超出意外;如果有多個根對象,就會存在多個參考系(每個根對象一個參考系),增加場景中對象的控制難度。
? ? ? ? 對于方案二,通過變換相機的位置和姿態(tài),讓用戶感覺場景中所有對象在同步縮放、平移、旋轉(zhuǎn)。該方案實現(xiàn)較困難,但是不會破環(huán)場景中對象的尺寸、位置、姿態(tài),更貼近真實世界的規(guī)則,也不需要將所有對象都放在同一個根對象下。
? ? ? ? 方案二明顯優(yōu)于方案一,本文將詳細介紹其原理和實現(xiàn)。原理如下:
? ? ? ? 1)場景縮放原理
? ? ? ? 利用相機的透視原理(詳見→透視變換原理),即相機拍攝到的圖片呈現(xiàn)近大遠小的效果,將相機靠近和遠離場景,從而實現(xiàn)放大和縮小場景的效果。
? ? ? ? 2)場景平移原理
? ? ? ? 相機成像是在近平面上,如果擴展近平面的范圍,相機拍攝的范圍也就越大,將近平面平移到相機位置上,記為平面 S,將相機在 S 平面上平移,就會實現(xiàn)場景平移效果。
? ? ? ? 3)場景旋轉(zhuǎn)原理
? ? ? ? 在 Unity3D Scene 窗口,通過按 Alt 鍵 + 鼠標拖拽,可以旋轉(zhuǎn)場景。場景旋轉(zhuǎn)包含兩種情況,鼠標沿水平方向拖拽、鼠標沿豎直方向拖拽。
? ? ? ? 當鼠標沿水平方向拖拽時,筆者通過多次實驗觀察,發(fā)現(xiàn)如下規(guī)律:當場景縮放到某個值時,旋轉(zhuǎn)場景時,屏幕中心位置的物體(在相機的正前方)在場景旋轉(zhuǎn)過程中始終處在屏幕中心,并且旋轉(zhuǎn)軸的方向始終是 Y 軸方向。因此可以得出結(jié)論:旋轉(zhuǎn)中心在相機正前方(forward),旋轉(zhuǎn)軸沿 Y 軸方向。
? ? ? ? 旋轉(zhuǎn)中心的 y 值最好與地圖的 y 值相等,如果場景中沒有地圖,可以取旋轉(zhuǎn)中心為:cam.position + cam.forward * (nearPlan + 1 / nearPlan),當然,用戶也可以取其他值。已知旋轉(zhuǎn)中心的 y 值,可以按照以下公式推導(dǎo)出 x、z 值:
? ? ? ? 當鼠標沿豎直方向拖拽時,旋轉(zhuǎn)中心在相機位置,旋轉(zhuǎn)軸沿相機的左邊(-right)。
? ? ? ? 本文代碼資源見→縮放、平移、旋轉(zhuǎn)場景。
2 代碼實現(xiàn)
????????SceneController.cs
using UnityEngine;
public class SceneController : MonoBehaviour {
private Texture2D[] cursorTextures; // 鼠標樣式: 箭頭、小手、眼睛
private Transform cam; // 相機
private float nearPlan; // 近平面
private Vector3 preMousePos; // 上一幀的鼠標坐標
private int cursorStatus = 0; // 鼠標樣式狀態(tài)
private bool isDraging = false; // 是否在拖拽中
private void Awake() {
string[] mouseIconPath = new string[]{"MouseIcon/0_arrow", "MouseIcon/1_hand", "MouseIcon/2_eye"};
cursorTextures = new Texture2D[mouseIconPath.Length];
for(int i = 0; i < mouseIconPath.Length; i++) {
cursorTextures[i] = Resources.Load<Texture2D>(mouseIconPath[i]);
}
cam = Camera.main.transform;
Vector3 angle = cam.eulerAngles;
cam.eulerAngles = new Vector3(angle.x, angle.y, 0); // 使camp.right指向水平方向
nearPlan = Camera.main.nearClipPlane;
}
private void Update() {
cursorStatus = GetCursorStatus();
// 更新鼠標樣式, 第二個參數(shù)表示鼠標點擊位置在圖標中的位置, zero表示左上角
Cursor.SetCursor(cursorTextures[cursorStatus], Vector2.zero, CursorMode.Auto);
UpdateScene(); // 更新場景(Ctrl+Scroll: 縮放場景, Ctrl+Drag: 平移場景, Alt+Drag: 旋轉(zhuǎn)場景)
}
private int GetCursorStatus() { // 獲取鼠標狀態(tài)(0: 箭頭, 1: 小手, 2: 眼睛)
if (isDraging) {
return cursorStatus;
}
if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.LeftControl)) {
return 1;
}
if (Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.LeftAlt)) {
return 2;
}
return 0;
}
private void UpdateScene() { // 更新場景(Ctrl+Scroll: 縮放場景, Ctrl+Drag: 平移場景, Alt+Drag: 旋轉(zhuǎn)場景)
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (!isDraging && cursorStatus == 1 && Mathf.Abs(scroll) > 0) { // 縮放場景
ScaleScene(scroll);
} else if (Input.GetMouseButtonDown(0)) {
preMousePos = Input.mousePosition;
isDraging = true;
} else if (Input.GetMouseButtonUp(0)) {
isDraging = false;
} else if (Input.GetMouseButton(0)) {
Vector3 offset = Input.mousePosition - preMousePos;
if (cursorStatus == 1) { // 移動場景
MoveScene(offset);
} else if (cursorStatus == 2) { // 旋轉(zhuǎn)場景
RotateScene(offset);
}
preMousePos = Input.mousePosition;
}
}
private void ScaleScene(float scroll) { // 縮放場景
cam.position += cam.forward * scroll;
}
private void MoveScene(Vector3 offset) { // 平移場景
cam.position -= (cam.right * offset.x / 100 + cam.up * offset.y / 100);
}
private void RotateScene(Vector3 offset) { // 旋轉(zhuǎn)場景
Vector3 rotateCenter = GetRotateCenter(0);
cam.RotateAround(rotateCenter, Vector3.up, offset.x / 3); // 水平拖拽分量
cam.LookAt(rotateCenter);
cam.RotateAround(rotateCenter, -cam.right, offset.y / 5); // 豎直拖拽分量
}
private Vector3 GetRotateCenter(float planeY) { // 獲取旋轉(zhuǎn)中心
if (Mathf.Abs(cam.forward.y) < float.Epsilon || Mathf.Abs(cam.position.y) < float.Epsilon) {
return cam.position + cam.forward * (nearPlan + 1 / nearPlan);
}
float t = (planeY - cam.position.y) / cam.forward.y;
float x = cam.position.x + t * cam.forward.x;
float z = cam.position.z + t * cam.forward.z;
return new Vector3(x, planeY, z);
}
}
? ? ? ? 說明:SceneController 腳本組件掛在相機下,鼠標圖標如下,需要放在 Resouses/MouseIcon 目錄下, 并且需要在 Inspector 窗口將其 Texture Type 屬性調(diào)整為 Cursor。
3 運行效果
?????????通過 Ctrl+Scroll 縮放場景,Ctrl+Drag 平移場景,Alt+Drag 旋轉(zhuǎn)場景 ,效果如下:
4 優(yōu)化
? ? ? ? 第 2 節(jié)中場景變換存在以下問題,本節(jié)將對這些問題進行優(yōu)化。
- 豎直方向平移場景時,會抬高或降低相機高度;
- 豎直方向旋轉(zhuǎn)場景時,如果相機垂直朝向地面,就會出現(xiàn)窗口急速晃動問題,因為旋轉(zhuǎn)中心出現(xiàn)了跳變。
? ? ? ? 針對問題一,將相機的上方向量(camera.up)投影到水平面上,再用投影向量計算相機前后平移的偏移量。
? ? ? ? 針對問題二,使用一個全局變量實時保存并更新旋轉(zhuǎn)中心的位置,并通過相機周轉(zhuǎn)和自傳(兩者旋轉(zhuǎn)角度和方向相等)實現(xiàn)水平和豎直方向旋轉(zhuǎn)場景,避免使用 LookAt,因為相機不一定一直朝向旋轉(zhuǎn)中心(如:相機焦點不在地圖里)。文章來源:http://www.zghlxwxcb.cn/news/detail-421192.html
????????SceneController.cs文章來源地址http://www.zghlxwxcb.cn/news/detail-421192.html
using UnityEngine;
public class SceneController : MonoBehaviour {
private const float MAX_HALF_EDGE_X = 5f; // 地圖x軸方向半邊長
private const float MAX_HALF_EDGE_Z = 5f; // 地圖z軸方向半邊長
private Texture2D[] cursorTextures; // 鼠標樣式: 箭頭、小手、眼睛
private Transform cam; // 相機
private float planeY = 0f; // 地面高度
private Vector3 rotateCenter; // 旋轉(zhuǎn)中心
private Vector3 focusCenter; // 相機在地面上的焦點中心
private bool isFocusInMap; // 相機焦點是否在地圖里
private Vector3 preMousePos; // 上一幀的鼠標坐標
private int cursorStatus = 0; // 鼠標樣式狀態(tài)
private bool isDraging = false; // 是否在拖拽中
private void Awake() {
string[] mouseIconPath = new string[] { "MouseIcon/0_arrow", "MouseIcon/1_hand", "MouseIcon/2_eye" };
cursorTextures = new Texture2D[mouseIconPath.Length];
for (int i = 0; i < mouseIconPath.Length; i++) {
cursorTextures[i] = Resources.Load<Texture2D>(mouseIconPath[i]);
}
cam = Camera.main.transform;
Vector3 angle = cam.eulerAngles;
cam.eulerAngles = new Vector3(angle.x, angle.y, 0); // 使camp.right指向水平方向
rotateCenter = new Vector3(0, planeY, 0);
focusCenter = new Vector3(0, planeY, 0);
}
private void Update() {
cursorStatus = GetCursorStatus();
// 更新鼠標樣式, 第二個參數(shù)表示鼠標點擊位置在圖標中的位置, zero表示左上角
Cursor.SetCursor(cursorTextures[cursorStatus], Vector2.zero, CursorMode.Auto);
UpdateScene(); // 更新場景(Ctrl+Scroll: 縮放場景, Ctrl+Drag: 平移場景, Alt+Drag: 旋轉(zhuǎn)場景)
}
private int GetCursorStatus() { // 獲取鼠標狀態(tài)(0: 箭頭, 1: 小手, 2: 眼睛)
if (isDraging)
{
return cursorStatus;
}
if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.LeftControl))
{
return 1;
}
if (Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.LeftAlt))
{
return 2;
}
return 0;
}
private void UpdateScene() { // 更新場景(Ctrl+Scroll: 縮放場景, Ctrl+Drag: 平移場景, Alt+Drag: 旋轉(zhuǎn)場景)
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (!isDraging && cursorStatus == 1 && Mathf.Abs(scroll) > 0) { // 縮放場景
ScaleScene(scroll);
}
else if (Input.GetMouseButtonDown(0)) {
preMousePos = Input.mousePosition;
UpdateRotateCenter();
isDraging = true;
}
else if (Input.GetMouseButtonUp(0)) {
isDraging = false;
}
else if (Input.GetMouseButton(0)) {
Vector3 offset = Input.mousePosition - preMousePos;
if (cursorStatus == 1) { // 移動場景
MoveScene(offset);
}
else if (cursorStatus == 2) { // 旋轉(zhuǎn)場景
RotateScene(offset);
}
preMousePos = Input.mousePosition;
}
}
private void ScaleScene(float scroll) { // 縮放場景
cam.position += cam.forward * scroll;
}
private void MoveScene(Vector3 offset) { // 平移場景
Vector3 horVec = Vector3.ProjectOnPlane(cam.right, Vector3.up).normalized;
Vector3 verVec = Vector3.ProjectOnPlane(cam.up, Vector3.up).normalized;
cam.position -= (horVec * offset.x / 100 + verVec * offset.y / 100);
}
private void RotateScene(Vector3 offset) { // 旋轉(zhuǎn)場景
float hor = offset.x / 3;
float ver = -offset.y / 5;
cam.RotateAround(rotateCenter, Vector3.up, hor); // 相機繞旋轉(zhuǎn)中心水平旋轉(zhuǎn)
cam.RotateAround(rotateCenter, cam.right, ver); // 相機繞旋轉(zhuǎn)中心豎直旋轉(zhuǎn)
// 由于transform.RotateAround方法中已經(jīng)進行了物體姿態(tài)調(diào)整, 因此以下語句是多余的
// cam.RotateAround(cam.position, Vector3.up, hor); // 相機自轉(zhuǎn), 使其朝向旋轉(zhuǎn)中心
// cam.RotateAround(cam.position, cam.right, ver); // 相機自轉(zhuǎn), 使其朝向旋轉(zhuǎn)中心
}
private void UpdateRotateCenter() { // 更新旋轉(zhuǎn)中心
UpdateFocusStatus();
if (!isFocusInMap) {
return;
}
rotateCenter.x = Mathf.Clamp(focusCenter.x, -MAX_HALF_EDGE_X, MAX_HALF_EDGE_X);
rotateCenter.z = Mathf.Clamp(focusCenter.z, -MAX_HALF_EDGE_Z, MAX_HALF_EDGE_Z);
}
private void UpdateFocusStatus() { // 更新焦點狀態(tài)
isFocusInMap = true;
Vector3 vec1 = new Vector3(0, planeY - cam.position.y, 0);
Vector3 vec2 = cam.forward;
if (Mathf.Abs(vec1.y) < float.Epsilon || Mathf.Abs(vec2.y) < float.Epsilon) {
isFocusInMap = false;
return;
}
float angle = Vector3.Angle(vec1, vec2);
if (angle >= 90) { // 相機在地面以上并且朝天, 或在地面以下并且朝下
isFocusInMap = false;
return;
}
float t = (planeY - cam.position.y) / vec2.y;
focusCenter.x = cam.position.x + t * vec2.x;
focusCenter.z = cam.position.z + t * vec2.z;
if (Mathf.Abs(focusCenter.x) > MAX_HALF_EDGE_X || Mathf.Abs(focusCenter.z) > MAX_HALF_EDGE_Z) { // 相機焦點不在地圖區(qū)域內(nèi)
isFocusInMap = false;
}
}
}
到了這里,關(guān)于【Unity3D】縮放、平移、旋轉(zhuǎn)場景的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!