Unity相機控制

相機的控制無非移動和旋轉,每種操作各3個軸6個方向,一共12種方式。在某些需要快速驗證的項目或Demo里常常需要絲滑的控制相機調試效果。相機控制雖然不是什么高深的技術,但是要寫的好用還是很磨人的。

鎖定Z軸的旋轉

一個自由的相機可以繞 X,Y,Z 軸旋轉,正常情況下用6個按鍵加上 transform.Rotate api 就可以搞定了。這里要注意的是要使用本地坐標系,transform.Rotate 默認就是本地坐標系。比如我們可以用上下左右方向鍵和鼠標左鍵來控制相機的旋轉。

void Update()
{if (Input.GetMouseButton(0)){var axis_x = Input.GetAxis("Mouse X") * 10;var axis_y = Input.GetAxis("Mouse Y") * 10;transform.localRotation *= Quaternion.Euler(-axis_y, axis_x, 0);}if (Input.GetKey(KeyCode.UpArrow)){transform.Rotate(Vector3.left, Time.deltaTime * 100);}else if (Input.GetKey(KeyCode.DownArrow)){transform.Rotate(Vector3.right, Time.deltaTime * 100);}if (Input.GetKey(KeyCode.LeftArrow)){transform.Rotate(Vector3.down, Time.deltaTime * 100);}else if (Input.GetKey(KeyCode.RightArrow)){transform.Rotate(Vector3.up, Time.deltaTime * 100);}
}

但是在某些情況下我希望實現一種類似第一人稱的視角,既相機可以左右看,上下看,但是不能歪頭,也就是要鎖定 Z 軸的旋轉。即便上面我們沒有 Z 方向的旋轉,但是實際上 X 軸和 Y 軸的旋轉也會引入 Z 軸的旋轉,讓人感覺相機極難控制,在不添加 Z 軸旋轉的情況下,相機很容易就歪了,還很難正回來。比如我們按上左下右的順序旋轉相機,當相機回到原點時,鏡頭已經歪到姥姥家了。

在這里插入圖片描述

鎖定 Z 軸旋轉就是把 Z 向角度設置為0,我們添加一個鎖定 Z 軸的函數。

private void LockZRotate()
{var euler = transform.eulerAngles;euler.z = 0;transform.eulerAngles = euler;
}

然后在 Update 的最后調用 LockZRotate 即可。但是這樣也會有問題,繞 X 軸的旋轉在 ±90° 范圍內是正常的,一旦到達 90°,Z 軸向正上或正下,再繼續轉就轉不動了,視角會向電風扇一樣瘋轉旋轉。

在這里插入圖片描述

雖然我們沒有旋轉 Z 軸,但是 Unity 會根據旋轉重新解算歐拉角,這種情況下繼續旋轉,經過 Unity 的解算,Z 軸上的角度就不是 0 了,但是我們又立刻將 Z 軸的角度置 0 了,導致 Unity 無法繼續旋轉,最終變成了直升機效果。

要邁過這道坎我們可以用世界坐標系去旋轉。也就是給 Rotate 函數加上 Space.World 參數,對于鼠標旋轉的情況,只需要將四元數的乘法順序調換一下就可以了。

void Update()
{if (Input.GetMouseButton(0)){var axis_x = Input.GetAxis("Mouse X") * 10;var axis_y = Input.GetAxis("Mouse Y") * 10;transform.rotation = Quaternion.Euler(-axis_y, axis_x, 0) * transform.rotation;}if (Input.GetKey(KeyCode.UpArrow)){transform.Rotate(Vector3.left, Time.deltaTime * 100, Space.World);}else if (Input.GetKey(KeyCode.DownArrow)){transform.Rotate(Vector3.right, Time.deltaTime * 100, Space.World);}if (Input.GetKey(KeyCode.LeftArrow)){transform.Rotate(Vector3.down, Time.deltaTime * 100, Space.World);}else if (Input.GetKey(KeyCode.RightArrow)){transform.Rotate(Vector3.up, Time.deltaTime * 100, Space.World);}LockZRotate();
}

Rotate 函數 Space.World 參數是指定旋轉軸的坐標空間的,Vector3.up + Space.World 相當于 transform.up + Space.Self
在這里插入圖片描述
可以看到可以正常繞 X 軸旋轉超過 90°,而且相機始終是正的,天空始終在畫面上面。似乎是正常了,嚴格來說是當相機的 X 軸和世界的 X 軸重合的時候是正常的,也就是說其實還是不對。

當我們先繞 Y 軸旋轉 90° 后,此時相機的 Z 軸與世界的 X 軸重合,此時當我們再想繞 X 軸旋轉時,但實際上面的代碼變成了繞相機的 Z 軸旋轉,但是 Z 軸的旋轉被我們鎖定了,根本轉不動,于是相機 X 方向的旋轉就被鎖死在這里了。這只是最極端的情況,事實上當相機的 X 軸偏離世界的 X 軸時,X 方向的旋轉就都不正常了。
在這里插入圖片描述
有一種辦法是把 X 軸的旋轉限制在 ±90° 范圍內,也就是不讓人“倒立”。可是妥協不是我想要的,我想要倒立,倒立過去之后還要保持鏡頭是正的。

回到最初按上左下右順序旋轉相機的例子,當我們在編輯模式下的 Inspector 面板中重復這個操作時,一切卻很正常,相機回到了原點,鏡頭也沒有歪。

唉?什么情況?

這并不是什么玄學,問題還是那個問題,Unity 會重新解算歐拉角。當我們在 Inspector 面板里面操作時,轉哪個軸就只轉那一個軸,不會重新解算,也不會動到其他軸,井水不犯河水。

這就意味這我們也可以模擬這個過程,手動記錄下相機的初始歐拉角,然后轉哪個軸就加減哪個角,最后將歐拉角賦值給相機就可以了。讓我們重新寫一個函數來專門負責旋轉。

public Vector3 euler;
private void RotateTransformAngle(float x = 0, float y = 0, float z = 0)
{euler.x = (euler.x + x) % 360;euler.y = (euler.y + y) % 360;euler.z = 0;transform.localEulerAngles = euler;
}

然后將旋轉也替換成這個函數,RotateTransformAngle 函數已經鎖定了 Z 軸,所以 LockZRotate 函數也不用再調用了。

void Update()
{if (Input.GetMouseButton(0)){var axis_x = Input.GetAxis("Mouse X") * 10;var axis_y = Input.GetAxis("Mouse Y") * 10;RotateTransformAngle(-axis_y, axis_x);}if (Input.GetKey(KeyCode.UpArrow)){RotateTransformAngle(x: -Time.deltaTime * 100);}else if (Input.GetKey(KeyCode.DownArrow)){RotateTransformAngle(x: Time.deltaTime * 100);}if (Input.GetKey(KeyCode.LeftArrow)){RotateTransformAngle(y: -Time.deltaTime * 100);}else if (Input.GetKey(KeyCode.RightArrow)){RotateTransformAngle(y: Time.deltaTime * 100);}//LockZRotate();
}

現在上左下右確實沒問題了,鏡頭不會再歪了,但是新的問題也出現了。當相機繞 X 軸旋轉 180° 時,我們真的“倒立”了,不能說沒有歪,簡直歪到極點了。
在這里插入圖片描述

要讓相機鏡頭始終是正的,實際上等價于讓相機的 Y 軸始終朝上,可以把 Y 軸想象成人的頭,所謂的“正”,也就是人頭沖上。有什么東西是始終朝上的嗎?當然有,那就是世界的 Y 軸。我們可以加一個判斷,當相機的 Y 軸和世界的 Y 軸反向時,將相機的 Y 軸反轉,我們可以使用點乘來實現這個判斷。

private void RotateTransformAngle(float x = 0, float y = 0, float z = 0)
{euler.x = (euler.x + x) % 360;euler.y = (euler.y + y) % 360;euler.z = 0;transform.localEulerAngles = euler;if (Vector3.Dot(transform.up, Vector3.up) < 0){transform.rotation = Quaternion.LookRotation(transform.forward, -transform.up);}
}

在這里插入圖片描述

嗯,現在我們會翻跟斗,但是不會倒立了。如果我們始終鎖定 Z 軸,到這里其實就可以結束了。但問題是并不是所有情況下我們都應該鎖定 Z 軸,萬一需要 Z 軸的旋轉呢?我們可以加一個開關來控制 Z 軸是否鎖定。

public bool lockz;
private void RotateTransformAngle(float x = 0, float y = 0, float z = 0)
{euler.x = (euler.x + x) % 360;euler.y = (euler.y + y) % 360;if (lockz){euler.z = 0;}else{euler.z = (euler.z + z) % 360;}transform.localEulerAngles = euler;if (lockz && (Vector3.Dot(transform.up, Vector3.up) < 0)){transform.rotation = Quaternion.LookRotation(transform.forward, -transform.up);}
}

然后我們還需要再添加兩個按鍵 QW 來旋轉 Z 軸,并在旋轉 Z 軸時,自動解鎖 Z 軸旋轉,順便加一個按鍵 z 來重新鎖定 Z 軸。

void Update() {...if (Input.GetKey(KeyCode.Q)){lockz = false;RotateTransformAngle(z: -Time.deltaTime * 100);}if (Input.GetKey(KeyCode.W)){lockz = false;RotateTransformAngle(z: Time.deltaTime * 100);}if (Input.GetKeyDown(KeyCode.Z)){lockz = true;}
}

這下總沒問題了,吧?當我們先繞 X 軸旋轉 180°,然后再轉動 Z 軸時,神奇的事情發生了,瞬間天地倒轉,又倒立了。

【圖】

這個問題的原因很簡單,因為當我們繞 X 軸旋轉超過 90° 時,Y 軸發生了一次反轉,也就是相機繞 Z 軸旋轉了 180°,但是這個信息并未被記錄到我們手動管理的歐拉角中。此時當我們繞 Z 軸旋轉時,其實是基于未反轉的 Z 方向角度在修改,所以鏡頭會突然倒轉。

當鎖定 Z 軸時,Z 方向的歐拉角只有可能是 0° 或 180°,要解決這個問題,我們需要一個只有 0 和 1 兩種狀態的變量來記錄相機 Y 軸的翻轉狀態。1 bit 二進制數就剛好滿足我們的需求,只需要不斷的加一,它就會在 0 和 1 之間不斷翻轉。之所以要記錄下這個狀態,是因為當我們重新鎖定相機時,需要將 Z 向歐拉角恢復到解鎖前的狀態,而不是簡單的直接置 0。

public bool lockz;
public byte flipz;
private void RotateTransformAngle(float x = 0, float y = 0, float z = 0)
{euler.x = (euler.x + x) % 360;euler.y = (euler.y + y) % 360;euler.z = 0;if (lockz){euler.z = (flipz & 0x1) * 180;}else{euler.z = (euler.z + z) % 360;}transform.localEulerAngles = euler;if (lockz && (Vector3.Dot(transform.up, Vector3.up) < 0)){transform.rotation = Quaternion.LookRotation(transform.forward, -transform.up);euler.z = (flipz++ & 0x1) * 180;}
}

在這里插入圖片描述

好了,這次是真的沒有問題了。

移動速度加成

相機移動相比于旋轉要簡單的多,直接使用 transform.Translate 函數就可以了,而且每個方向都可以自由移動。

與旋轉不同的是移動的范圍要更廣闊,對于旋轉,每個軸的旋轉角度只會在 0 ~ 360° 之間,但是移動的范圍幾乎是無限的。這就帶來了一個問題,當我們想移動很遠的距離時,要“走”很久才能到。

簡單,走快點就好了。但是也不能一開始就走很快,因為我們并不能確定當用戶按下移動鍵時,是想去很遠的地方,還是只想湊近一點。因此啟動速度不能太快,否則我們很難準確控制相機到達想去的地方。

解決這個問題我們可以記錄下用戶按下移動鍵的時長,然后根據按鍵按下的時間計算一個移動速度加成。剛開始時沒有任何加成,如果用戶一直按著鍵盤不撒手,那就逐漸給一個更大加成,讓相機移動的越來越快。最后當加成到達一個上限時,保持住不在變大。

你可能已經想到了,這不就是一個分段函數嗎?的確,我們需要的確實是一個分段函數。
y={1x<axa<x<b10x>by = \begin{cases} 1 & x < a \\ x & a < x < b \\ 10 & x > b \end{cases} y=????1x10?x<aa<x<bx>b?
在這里插入圖片描述

但是分段函數是不平滑的,而且我們還想讓變化有一些非線性。有這么一個函數,在 x=0x=0x=0 附近函數值為 111,隨著 xxx 的增大,函數值逐漸增大,最后在 +∞+\infty+ 處趨于 111
y=e?1x2y=e^{-\frac{1}{x^2}} y=e?x21?
在這里插入圖片描述
因為函數值在 [0,1][0,1][0,1] 范圍內,因此我們很容易把他縮放到 [1,M][1, M][1,M] 范圍內。同時我們還可以加一些參數來調整函數的增長速度和底部寬度。
y=1+(M?1)e?txry=1+(M-1)e^{-\frac{t}{x^r}} y=1+(M?1)e?xrt?
除了鍵盤操作,用鼠標滾輪來前后移動相機也是實用的操作,此時我們在算加成時,不是用滾動時間,而是用滾輪連續同方向滾動的距離。

goto與環繞

除了移動與旋轉,我們還可以實現一些快捷操作,比如用鼠標點擊一個點,讓相機看向并移動到這個點“前面”,或者移動相機變成環繞這個點移動。這些功能的實現并不難,使用 LookAtRotateAround 就能實現了。需要注意的是要讓相機平滑的看向并移動到目標點,需要進行插值,否則鏡頭會生硬的跳過去。

最終我們會實現下面的功能:

鼠標按鍵功能
左鍵上下左右旋轉相機
上下左右旋轉相機
滾輪X Y Z繞指定軸旋轉相機
左鍵A D L-Shift Space相機上下左右環繞點擊的點
W S A D L-Shift Space相機前后左右上下移動
滾輪相機前后移動
中鍵相機上下左右移動
右鍵相機Goto點擊的點
Z鎖定 Z 軸旋轉

源碼

源碼以 .unitypackage 的形式放到了CSDN,可以直接導入使用。

https://download.csdn.net/download/puss0/91565511

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/917207.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/917207.shtml
英文地址,請注明出處:http://en.pswp.cn/news/917207.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

vue2 使用liveplayer加載視頻

vue2 使用liveplayer加載視頻 官網: https://www.liveqing.com/docs/manuals/LivePlayer.html支持WebRTC/MP4播放;支持m3u8/HLS播放;支持HTTP-FLV/WS-FLV/RTMP播放;支持直播和點播播放;支持播放器快照截圖;支持點播多清晰度播放;支持全屏或比例顯示;自動檢測IE瀏覽器兼容播放;支…

JavaScript語法樹簡介:AST/CST/詞法/語法分析/ESTree/生成工具

AST簡介 在平時的開發中&#xff0c;經常會遇到對JavaScript代碼進行檢查或改動的工具&#xff0c;例如ESLint會檢查代碼中的語法錯誤&#xff1b;Prettier會修改代碼的格式&#xff1b;打包工具會將不同文件中的代碼打包在一起等等。這些工具都對JavaScript代碼本身進行了解析…

Java函數式編程之【基本數據類型流】

一、基本數據類型與基本數據的包裝類 在Java編程語言中&#xff0c;int、long和double等基本數據類型都各有它們的包裝類型Integer、Long和Double。 基本數據類型是Java程序語言內置的數據類型&#xff0c;可直接使用。 而包裝類型則歸屬于普通的Java類&#xff0c;是對基本數據…

.NET Core部署服務器

1、以.NET Core5.0為例&#xff0c;在官網下載 下載 .NET 5.0 (Linux、macOS 和 Windows) | .NET 根據自己需求選擇x64還是x86&#xff0c;記住關鍵下載完成還需要下載 Hosting Bundel &#xff0c;否則不成功 2、部署https將ssl證書放在服務器上&#xff0c;雙擊導入&#…

YOLO---04YOLOv3

YOLOV3 論文地址&#xff1a;&#xff1a;【https://arxiv.org/pdf/1804.02767】 YOLOV3 論文中文翻譯地址&#xff1a;&#xff1a;【YOLO3論文中文版_yolo v3論文 中文版-CSDN博客】 YOLOv3 在實時性和精確性在當時都是做的比較好的&#xff0c;并在工業界得到了廣泛應用 …

Qt知識點3『自定義屬性的樣式表失敗問題』

問題1&#xff1a;自定義類中的自定義屬性&#xff0c;如何通過樣式表來賦值除了QT自帶的屬性&#xff0c;我們自定義的類中如果有自定義的靜態屬性&#xff0c;也可以支持樣式表&#xff0c;如下 &#xff1a; Q_PROPERTY(QColor myBorderColor READ getMyBorderColor WRITE s…

RDQS_c和RDQS_t的作用及區別

&#x1f501; LPDDR5 中的 RDQS_t 和 RDQS_c — 復用機制詳解 &#x1f4cc; 基本角色 引腳名 讀操作&#xff08;READ&#xff09;作用 寫操作&#xff08;WRITE&#xff09;作用&#xff08;當啟用Link ECC&#xff09; RDQS_t Read DQS True&#xff1a;與 RDQS_c…

測試分類:詳解各類測試方式與方法

前言&#xff1a;為什么要將測試進行分類呢&#xff1f;軟件測試是軟件生命周期中的?個重要環節&#xff0c;具有較高的復雜性&#xff0c;對于軟件測試&#xff0c;可以從不同的角度加以分類&#xff0c;使開發者在軟件開發過程中的不同層次、不同階段對測試工作進行更好的執…

新手docker安裝踩坑記錄

最近在學習docker&#xff0c;安裝和使用折騰了好久&#xff0c;在這里記錄一下。下載# 依賴安裝 sudo apt update sudo apt install -y \ca-certificates \curl \gnupg \lsb-release# 使用清華鏡像源&#xff08;Ubuntu 24.04 noble&#xff09; echo \"deb [arch$(dpkg …

TOGAF指南1

1.TOGAF標準簡介 TOGAF&#xff08;The Open Group Architecture Framework&#xff09;就像是一個企業架構的“操作手冊”。它幫助企業設計、搭建和維護自己的“系統地圖”&#xff0c;確保不同部門、技術、業務目標能像齒輪一樣協調運轉。 它的核心是&#xff1a; 用迭代的方…

[Linux入門] Linux 防火墻技術入門:從 iptables 到 nftables

目錄 一、防火墻基礎&#xff1a;netfilter 與 iptables 的關系 1??什么是 netfilter&#xff1f; 2??什么是 iptables&#xff1f; 二、iptables 核心&#xff1a;五鏈四表與規則體系 1??什么是 “鏈”&#xff08;Chain&#xff09;&#xff1f; 2?? 什么是 “…

函數fdopendir的用法

以下是關于 fdopendir 函數的詳細解析&#xff0c;結合其核心功能、參數說明及典型應用場景&#xff1a;&#x1f50d; ?一、函數功能與原型??核心作用?將已打開的目錄文件描述符&#xff08;fd&#xff09;轉換為目錄流指針&#xff08;DIR*&#xff09;&#xff0c;用于后…

[源力覺醒 創作者計劃]_文心4.5開源測評:國產大模型的技術突破與多維度能力解析

聲明&#xff1a;文章為本人真實測評博客&#xff0c;非廣告&#xff0c;并沒有推廣該平臺 &#xff0c;為用戶體驗文章 一起來輕松玩轉文心大模型吧&#x1f449; 文心大模型免費下載地址 一、引言&#xff1a;文心4.5開源——開啟多模態大模型新時代 2025年6月30日&#x…

微信小程序無法構建npm,可能是如下幾個原因

安裝位置的問題&#xff0c;【npm安裝在cd指定位置】小程序緩存的問題退出小程序&#xff0c;重新構建即可

從 MyBatis 到 MyBatis - Plus:@Options 注解的那些事兒

在 MyBatis 以及 MyBatis - Plus 的開發過程中&#xff0c;注解的使用是提升開發效率和實現特定功能的關鍵。今天我們就來聊聊 Options 注解&#xff0c;以及在 MyBatis - Plus 中它的使用場景和替代方案。 一、MyBatis 中的 Options 注解 在 MyBatis 框架中&#xff0c;Option…

轉換圖(State Transition Diagram)和時序圖(Sequence Diagram)畫圖流程圖工具

針對程序員繪制狀態轉換圖&#xff08;State Transition Diagram&#xff09;和時序圖&#xff08;Sequence Diagram&#xff09;的需求&#xff0c;以下是一些好用的工具推薦&#xff0c;涵蓋在線工具、桌面軟件和基于文本的工具&#xff0c;適合不同場景和偏好。這些工具在易…

基于php的在線酒店管理系統(源代碼+文檔+PPT+調試+講解)

課題摘要在旅游住宿行業數字化轉型的背景下&#xff0c;傳統酒店管理存在房態更新滯后、預訂渠道分散等問題。基于 PHP 的在線酒店管理系統&#xff0c;憑借其開發高效、兼容性強的特點&#xff0c;構建集客房管理、預訂處理、客戶服務于一體的綜合性管理平臺。 系統核心功能包…

視頻質量檢測中卡頓識別準確率↑32%:陌訊多模態評估框架實戰解析

原創聲明本文為原創技術解析&#xff0c;核心技術參數與架構設計引用自《陌訊技術白皮書》&#xff0c;禁止未經授權的轉載與改編。一、行業痛點&#xff1a;視頻質量檢測的現實挑戰在實時流媒體、在線教育、安防監控等領域&#xff0c;視頻質量直接影響用戶體驗與業務可信度。…

流式輸出阻塞原因及解決辦法

流式輸出不懂可看這篇文章&#xff1a;流式輸出&#xff1a;概念、技巧與常見問題 正常情況&#xff0c;如下代碼所示&#xff1a; async def event_generator():# 先輸出數字1yield "data: 1\n\n"# 然后每隔2秒輸出數字2&#xff0c;共輸出10次for i in range(10):…

linux系統----Ansible中的playbook簡單應用

目錄 Playbooks中tasks語法使用 1、file 創建文件&#xff1a;touch 創建目錄&#xff1a;directory 2、lineinfile 修改文件某一行文本 3、replace 根據正則表達式替換文件內容&#xff08;指定換字符串&#xff09; 5、template/copy 模板作用類似于copy&#xff0…