LayaAir鼠標(手指)控制相機旋轉,限制角度

切換天空盒

腳本掛載到相機身上

const { regClass, property } = Laya;@regClass()
export class SmoothCameraController extends Laya.Script {declare owner: Laya.Camera;// 旋轉靈敏度@property({ type: Number, name: "旋轉靈敏度" })public rotationSensitivity: number = 0.5; // 每像素旋轉角度// 平滑度 (0-1, 值越小越平滑)@property({ type: Number, name: "平滑度" })public smoothness: number = 0.08;// 角度限制@property({ type: Number, name: "最小俯仰角" })public minPitchDegrees: number = -80; // 最小俯仰角(X軸)@property({ type: Number, name: "最大俯仰角" })public maxPitchDegrees: number = 80;  // 最大俯仰角(X軸)// Y軸不限制,可以360度自由旋轉private minYawDegrees: number = -Infinity;  // 最小偏航角(無限制)private maxYawDegrees: number = Infinity;   // 最大偏航角(無限制)// 目標位置@property({ type: Laya.Vector3, name: "目標位置" })public targetPosition: Laya.Vector3 = new Laya.Vector3(0, 0, 0);@property({ type: Laya.Sprite3D, name: "目標物體" })public targetTransform: Laya.Sprite3D | null = null;// 是否阻止在UI上操作@property({ type: Boolean, name: "UI上阻止操作" })public blockOnUI: boolean = true;// 是否阻止在3D物體上操作@property({ type: Boolean, name: "3D物體上阻止操作" })public blockOn3D: boolean = false;// 內部狀態private isDragging: boolean = false;private lastPointerX: number = 0;private lastPointerY: number = 0;// 當前和目標角度public currentYawDeg: number = 0;public currentPitchDeg: number = 0;private targetYawDeg: number = 0;private targetPitchDeg: number = 0;// 距離目標點的距離private distanceToTarget: number = 5;// 臨時變量private tempQuat: Laya.Quaternion = new Laya.Quaternion();private tempUp: Laya.Vector3 = new Laya.Vector3(0, 1, 0);private tempPos: Laya.Vector3 = new Laya.Vector3();private tempTarget: Laya.Vector3 = new Laya.Vector3();// UI根節點引用private uiRoot2D: Laya.Sprite | null = null;onAwake(): void {// 緩存2D UI根節點,避免在UI上拖拽時旋轉相機try {const scene2D = Laya.stage.getChildAt(1)?.getChildByName("Scene2D");if (scene2D instanceof Laya.Sprite) {this.uiRoot2D = scene2D;}} catch (e) {this.uiRoot2D = null;}}onStart(): void {// 從當前相機位置初始化球面坐標this.initializeFromCurrentPosition();// 設置輸入事件this.setupInputEvents();;}private initializeFromCurrentPosition(): void {const eye = this.owner.transform.position.clone();const target = this.getTargetPosition();this.distanceToTarget = Laya.Vector3.distance(eye, target);// 計算從目標到相機的偏移向量const offset = new Laya.Vector3(eye.x - target.x,eye.y - target.y,eye.z - target.z);// 計算偏航角(繞Y軸)和俯仰角(繞X軸)const yawRad = Math.atan2(offset.x, offset.z);const horizontalLen = Math.sqrt(offset.x * offset.x + offset.z * offset.z);const pitchRad = Math.atan2(offset.y, horizontalLen);// 轉換為角度并設置初始值(Z軸始終為0)this.currentYawDeg = this.targetYawDeg = yawRad * 180 / Math.PI;this.currentPitchDeg = this.targetPitchDeg = this.clampPitch(pitchRad * 180 / Math.PI);// 立即應用旋轉,確保Z軸為0this.applyRotationWithZeroRoll();}private setupInputEvents(): void {// 鼠標和觸摸事件(LayaAir統一處理)Laya.stage.on(Laya.Event.MOUSE_DOWN, this, this.onPointerDown);Laya.stage.on(Laya.Event.MOUSE_MOVE, this, this.onPointerMove);Laya.stage.on(Laya.Event.MOUSE_UP, this, this.onPointerUp);Laya.stage.on(Laya.Event.MOUSE_OUT, this, this.onPointerUp);}private onPointerDown(evt?: Laya.Event): void {// 檢查是否在UI上if (this.blockOnUI && this.pointerOnUI(Laya.stage.mouseX, Laya.stage.mouseY)) {console.log("在UI上,阻止相機操作");return;}// 檢查是否在3D物體上if (this.blockOn3D && this.hitAny3D(Laya.stage.mouseX, Laya.stage.mouseY)) {console.log("在3D物體上,阻止相機操作");return;}this.isDragging = true;this.lastPointerX = Laya.stage.mouseX;this.lastPointerY = Laya.stage.mouseY;}private onPointerMove(evt?: Laya.Event): void {if (!this.isDragging) return;const dx = Laya.stage.mouseX - this.lastPointerX;const dy = Laya.stage.mouseY - this.lastPointerY;// 更新目標角度this.targetYawDeg += dx * this.rotationSensitivity;this.targetPitchDeg = this.clampPitch(this.targetPitchDeg - dy * this.rotationSensitivity);// Y軸不限制,可以360度自由旋轉// this.targetYawDeg = this.clampYaw(this.targetYawDeg);// 更新上一幀位置this.lastPointerX = Laya.stage.mouseX;this.lastPointerY = Laya.stage.mouseY;}private onPointerUp(evt?: Laya.Event): void {this.isDragging = false;}private clampPitch(pitchDeg: number): number {return Math.max(this.minPitchDegrees, Math.min(this.maxPitchDegrees, pitchDeg));}private clampYaw(yawDeg: number): number {// Y軸不限制,直接返回原值,允許360度自由旋轉return yawDeg;}onUpdate(): void {// 平滑插值到目標角度const lerpFactor = 1 - Math.max(0, Math.min(1, this.smoothness));this.currentYawDeg = this.lerpAngle(this.currentYawDeg, this.targetYawDeg, lerpFactor);this.currentPitchDeg = this.lerpAngle(this.currentPitchDeg, this.targetPitchDeg, lerpFactor);// 從球面坐標重新計算相機位置this.updateCameraPosition();}private updateCameraPosition(): void {const yawRad = this.currentYawDeg * Math.PI / 180;const pitchRad = this.currentPitchDeg * Math.PI / 180;const cosPitch = Math.cos(pitchRad);const sinPitch = Math.sin(pitchRad);const sinYaw = Math.sin(yawRad);const cosYaw = Math.cos(yawRad);const target = this.getTargetPosition();// 計算新的相機位置this.tempPos.setValue(target.x + this.distanceToTarget * sinYaw * cosPitch,target.y + this.distanceToTarget * sinPitch,target.z + this.distanceToTarget * cosYaw * cosPitch);// 更新相機位置this.owner.transform.position = this.tempPos;// 應用旋轉,確保Z軸旋轉為0this.applyRotationWithZeroRoll();}// 應用旋轉,確保Z軸旋轉為0private applyRotationWithZeroRoll(): void {const eulerAngles = new Laya.Vector3();eulerAngles.setValue(-this.currentPitchDeg,  // X軸旋轉(俯仰)this.currentYawDeg,     // Y軸旋轉(偏航)0                       // Z軸旋轉固定為0(禁止翻滾));// 將歐拉角轉換為四元數Laya.Quaternion.createFromYawPitchRoll(eulerAngles.y * Math.PI / 180,  // YaweulerAngles.x * Math.PI / 180,  // PitcheulerAngles.z * Math.PI / 180,  // Roll (始終為0)this.tempQuat);this.owner.transform.rotation = this.tempQuat;// 調試信息:驗證Z軸旋轉是否為0if (this.isDragging) {const currentEuler = this.owner.transform.localRotationEuler;}}private lerpAngle(a: number, b: number, t: number): number {// 將角度差值包裝到[-180,180]范圍內,實現最短路徑插值let delta = ((b - a + 540) % 360) - 180;return a + delta * t;}private getTargetPosition(): Laya.Vector3 {if (this.targetTransform) {return this.targetTransform.transform.position.clone();}return this.targetPosition;}private pointerOnUI(stageX: number, stageY: number): boolean {if (!this.uiRoot2D) return false;return this.uiRoot2D.hitTestPoint(stageX, stageY);}private hitAny3D(stageX: number, stageY: number): boolean {const scene3D = this.owner.scene as Laya.Scene3D;if (!scene3D) return false;const ray = new Laya.Ray(new Laya.Vector3(), new Laya.Vector3());const sp = new Laya.Vector2(stageX, stageY);this.owner.viewportPointToRay(sp, ray);const hr = new Laya.HitResult();return scene3D.physicsSimulation.rayCast(ray, hr, 1000);}// 公共方法:重置相機到初始位置public resetToInitialPosition(): void {this.initializeFromCurrentPosition();}// 公共方法:設置新的目標位置public setTargetPosition(position: Laya.Vector3): void {this.targetPosition = position.clone();}// 公共方法:設置新的目標變換public setTargetTransform(transform: Laya.Sprite3D): void {this.targetTransform = transform;}// 公共方法:設置距離public setDistance(distance: number): void {this.distanceToTarget = Math.max(0.1, distance);}// 公共方法:設置角度限制(僅X軸)public setAngleLimits(minPitch: number, maxPitch: number): void {this.minPitchDegrees = minPitch;this.maxPitchDegrees = maxPitch;// 立即應用限制(僅X軸)this.targetPitchDeg = this.clampPitch(this.targetPitchDeg);// Y軸不限制,保持原值}// 公共方法:設置平滑度public setSmoothness(smoothness: number): void {this.smoothness = Math.max(0, Math.min(1, smoothness));}// 公共方法:設置旋轉靈敏度public setRotationSensitivity(sensitivity: number): void {this.rotationSensitivity = Math.max(0, sensitivity);}// 公共方法:設置相機旋轉角度public setRotation(eulerAngles: Laya.Vector3): void {// 設置目標角度(使用歐拉角)this.targetYawDeg = eulerAngles.y;     // Y軸旋轉(偏航)this.targetPitchDeg = -eulerAngles.x;  // X軸旋轉(俯仰),取負值以匹配坐標系// 立即更新當前角度(無平滑過渡)this.currentYawDeg = this.targetYawDeg;this.currentPitchDeg = this.clampPitch(this.targetPitchDeg);// 立即更新相機位置this.updateCameraPosition();}onDisable(): void {this.detachEvents();}onDestroy(): void {this.detachEvents();}private detachEvents(): void {Laya.stage.off(Laya.Event.MOUSE_DOWN, this, this.onPointerDown);Laya.stage.off(Laya.Event.MOUSE_MOVE, this, this.onPointerMove);Laya.stage.off(Laya.Event.MOUSE_UP, this, this.onPointerUp);Laya.stage.off(Laya.Event.MOUSE_OUT, this, this.onPointerUp);}
}

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

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

相關文章

【數據結構入門】排序算法(4)歸并排序

目錄 1.排序的原理 1.1 保證子數組有序 1.2 時間復雜度 2. 遞歸實現 2.1 思路 2.2 代碼 3. 非遞歸實現 3.1 思路 3.2 代碼 4.面試題 4.1 題目 4.2 思路 1.排序的原理 歸并排序是外排序,所謂外排序就是說能夠對文件中的數據進行排序。 ①首先&#xff…

FLEXSPI_Init 硬件故障問題

使用官方例程發現FLEXSPI_Init會引起硬件故障,查閱相關帖子發現主要有兩個可能:1、外部閃存配置差異修改 LUT(查找表)命令:示例中擦除扇區命令為 0xD7,寫狀態寄存器命令為 0x01,需分別改為 閃存…

如何用 Rust 重寫 SQLite 數據庫(一):項目探索

要使用 Rust 重寫 SQLite 數據庫,我們需要實現一個簡化的關系型數據庫核心功能(如 SQL 解析、存儲引擎、事務管理)。以下是一個分步實踐指南,包含關鍵代碼示例。一、項目規劃 我們將實現一個超簡化數據庫 MiniSQL,支持…

JVM之堆(Heap)

一、堆的核心特性 唯一性與共享性 每個JVM實例僅有一個堆,所有線程共享,但可通過線程私有緩沖區(TLAB)減少多線程分配沖突。內存結構演變 JDK 7及之前:堆分為新生代(Young)、老年代(…

單片機的RAM與ROM概念

RAM與ROM1、RAM與ROM2、 bss、data、heap、stack、text詳細講解3、詳細探討 TCM、OCRAM 和 HBNRAM 之間的區別及其具體作用。3.1、TCM(Tightly Coupled Memory)3.2、 OCRAM(On Chip RAM)3.3、HBNRAM (Hibernate RAM)3.4、總結1、R…

實驗3:事件處理(2學時)

實驗目的(1)熟練掌握 v-on 指令的用法,學會使用 v-on 指令監聽 DOM 元素的事件,并通過該事件觸發調用事件處理程序。(2)掌握v-on 指令修飾符的基本用法。實驗內容實現購物車功能的拓展(商品數量…

商品庫存扣減方案

文章目錄1. Lua腳本 Redis(業界首選,綜合最優)2. Redis原子命令(DECRBY 結果校驗)3. Redis事務(MULTI/EXEC)4. 分布式鎖(基于Redis實現)5. Redisson客戶端封裝&#xf…

關于在阿里云DMS誤操作后如何恢復數據的記錄

前言 昨天因客戶員工操作錯誤,導致快遞單號和訂單互換。客戶員工那邊讓筆記修改數據。 于是筆者寫下如下SQL來操作,導致了災難性事故。 update t_order_fed_ex_record set tracking_number 884102170661, master_tracking_number 884102170661, push…

【操作系統核心知識梳理】線程(Thread)重點與易錯點全面總結

在多任務操作系統中,線程是比進程更輕量的執行單元,理解線程的特性和實現方式是掌握并發編程的基礎。本文系統梳理了線程相關的核心知識點和常見誤區,助你夯實操作系統基礎。一、線程的基本概念與引入目的 1.1 什么是線程? 線程是…

深入理解 Python 中的 `__call__` 方法

化身為可調用的對象:深入理解 Python 中的 __call__ 方法 引言:函數與對象的邊界模糊化 在 Python 中,我們最熟悉的概念莫過于函數(Function) 和對象(Object)。函數是可調用的(calla…

云服務器使用代理穩定與github通信方法

使用SSH反向隧道 (SSH Reverse Tunneling) 利用SSH連接在您的本地電腦和云服務器之間建立一個反向的加密通道。 原理: 從本地電腦發起一個SSH命令到您的云服務器,這個命令會告訴云服務器:“請監聽您自己的某個端口(例如&#xff1…

7.k8s四層代理service

Service的基本介紹 Cluster IP:每個 Service 都分配了一個Cluster IP,它是一個虛擬的內部IP地址,用于在集群內部進行訪問。這個虛擬IP是由Kubernetes自動分配的,并且與Service對象一一對應。 端口映射:Service可以映射…

Qt 工程中 UI 文件在 Makefile 中的處理

Qt 工程中 UI 文件在 Makefile 中的處理 在 Qt 工程中,.ui 文件(Qt Designer 界面文件)需要通過 uic(用戶界面編譯器)工具轉換為對應的頭文件。以下是幾種情況下如何處理 UI 文件:1. 使用 qmake 自動生成 M…

ZLMediaKit性能測試

一、環境 系統:虛擬機 Ubuntu22.04 64bit配置: 4核8G設置:ulimit -n 102400 二、安裝 依賴安裝sudo apt update sudo apt install ffmpeg sudo apt install nloadzlm服務安裝參考:https://blog.csdn.net/hanbo622/article/details/149064939?…

智能文檔處理業務,應該選擇大模型還是OCR專用小模型?

智能文檔處理業務中,最佳策略不是二選一,而是“大小模型協同”。用專用小模型處理高頻、標準化的核心文檔流,實現極致效率與成本控制;用大模型賦能非標、長尾文檔的靈活處理,加速業務創新。 OCR小模型會被大模型取代嗎…

android 如何判定底部導航欄顯示時 不是鍵盤顯示

在 Android 中判定底部導航欄是否顯示時,核心痛點是 區分 “導航欄的底部 Insets” 和 “軟鍵盤彈出的底部 Insets”—— 兩者都會導致 getSystemWindowInsetBottom() 返回非零值,直接判斷會誤將鍵盤彈出當成導航欄顯示。以下是基于 WindowInsets 類型區…

你知道服務器和電腦主機的區別嗎?

我們都知道服務器和臺式主機有著不同之處,但具體說出個一二三來很多人還是一頭霧水,也就是知其然不知其所以然,都是CPU主板 內存 硬盤 電源,撐死就差一個顯卡不同,但其實服務器和我們正常使用的臺式主機差距很大&#…

什么是包裝類

什么是包裝類 在Java中,包裝類(Wrapper Class)是為基本數據類型提供的對應的引用類型。Java中的基本數據類型(如int、char、boolean等)不是對象,為了在需要對象的場景中使用基本數據類型(如集合…

用Python打造專業級老照片修復工具:讓時光倒流的數字魔法

在這個數字化時代,我們手中珍藏著許多泛黃、模糊、甚至有劃痕的老照片。這些照片承載著珍貴的回憶,但時間的侵蝕讓它們失去了往日的光彩。今天,我將帶您一起用Python開發一個專業級的老照片修復工具,讓這些珍貴的記憶重現光彩。為…

linux中查找包含xxx內容的文件

linux中怎么查找哪個文件包含xxx內容 在Linux中查找包含特定內容的文件 在Linux系統中,有幾種常用方法來查找包含特定內容的文件。以下是幾種最有效的方法:1. 使用 grep 命令(最常用) 基本語法:bash grep -r "搜索…