切换天空盒
脚本挂载到相机身上
javascript
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轴为0
this.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轴旋转为0
this.applyRotationWithZeroRoll();
}
// 应用旋转,确保Z轴旋转为0
private 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, // Yaw
eulerAngles.x * Math.PI / 180, // Pitch
eulerAngles.z * Math.PI / 180, // Roll (始终为0)
this.tempQuat
);
this.owner.transform.rotation = this.tempQuat;
// 调试信息:验证Z轴旋转是否为0
if (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);
}
}