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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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 "搜索…