切換天空盒
腳本掛載到相機身上
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);}
}