第05章 - 相机系统与视角控制

第05章:相机系统与视角控制

5.1 相机基础概念

5.1.1 相机坐标系

CesiumJS 中的相机使用多种坐标系来描述其位置和方向:

┌─────────────────────────────────────────────────────────────────┐
│                      相机坐标系统                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  世界坐标系 (World)                                              │
│  ├── 以地球中心为原点                                            │
│  ├── X 轴指向经度 0°、纬度 0° 方向                               │
│  ├── Y 轴指向经度 90°E、纬度 0° 方向                             │
│  └── Z 轴指向北极                                                │
│                                                                  │
│  相机坐标系 (Camera)                                             │
│  ├── position: 相机在世界坐标系中的位置                          │
│  ├── direction: 相机观察方向(单位向量)                         │
│  ├── up: 相机上方向(单位向量)                                  │
│  └── right: 相机右方向(单位向量)                               │
│                                                                  │
│  方位角系统 (Orientation)                                        │
│  ├── heading: 航向角(方位角,北向为 0,顺时针增加)             │
│  ├── pitch: 俯仰角(水平为 0,向下为负,向上为正)               │
│  └── roll: 翻滚角(绕视线轴旋转)                                │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

5.1.2 相机属性

const camera = viewer.camera;

// ===== 位置属性 =====
camera.position                    // Cartesian3 - 世界坐标位置
camera.positionWC                  // Cartesian3 - 世界坐标(只读)
camera.positionCartographic        // Cartographic - 经纬度位置

// ===== 方向属性 =====
camera.direction                   // Cartesian3 - 观察方向
camera.directionWC                 // Cartesian3 - 世界坐标观察方向(只读)
camera.up                          // Cartesian3 - 上方向
camera.upWC                        // Cartesian3 - 世界坐标上方向(只读)
camera.right                       // Cartesian3 - 右方向
camera.rightWC                     // Cartesian3 - 世界坐标右方向(只读)

// ===== 方位角属性 =====
camera.heading                     // Number - 航向角(弧度)
camera.pitch                       // Number - 俯仰角(弧度)
camera.roll                        // Number - 翻滚角(弧度)

// ===== 视锥体属性 =====
camera.frustum                     // PerspectiveFrustum - 透视视锥体
camera.frustum.fov                 // Number - 视场角
camera.frustum.aspectRatio         // Number - 宽高比
camera.frustum.near                // Number - 近裁剪面
camera.frustum.far                 // Number - 远裁剪面

// ===== 其他属性 =====
camera.defaultMoveAmount           // Number - 默认移动量
camera.defaultLookAmount           // Number - 默认旋转量
camera.defaultRotateAmount         // Number - 默认自转量
camera.defaultZoomAmount           // Number - 默认缩放量
camera.constrainedAxis             // Cartesian3 - 约束轴

5.2 相机定位方法

5.2.1 setView - 直接设置视角

// 基本用法
viewer.camera.setView({
    destination: Cesium.Cartesian3.fromDegrees(116.4, 39.9, 15000),
    orientation: {
        heading: Cesium.Math.toRadians(0),      // 朝北
        pitch: Cesium.Math.toRadians(-45),      // 向下倾斜 45°
        roll: 0
    }
});

// 使用 Rectangle 定位到区域
viewer.camera.setView({
    destination: Cesium.Rectangle.fromDegrees(
        116.0, 39.0,    // 西南角
        117.0, 40.0     // 东北角
    )
});

// 带偏移的定位
viewer.camera.setView({
    destination: Cesium.Cartesian3.fromDegrees(116.4, 39.9, 0),
    orientation: new Cesium.HeadingPitchRoll(
        Cesium.Math.toRadians(90),   // 朝东
        Cesium.Math.toRadians(-30),  // 向下 30°
        0
    )
});

// 使用方向向量
viewer.camera.setView({
    destination: Cesium.Cartesian3.fromDegrees(116.4, 39.9, 15000),
    orientation: {
        direction: new Cesium.Cartesian3(-0.04231243104240401, -0.20123236049443421, -0.97862924300734),
        up: new Cesium.Cartesian3(-0.47934589305293746, -0.8553216253114552, 0.1966022179118339)
    }
});

5.2.2 flyTo - 飞行动画

// 基本飞行
viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(116.4, 39.9, 15000),
    duration: 3  // 飞行时间(秒)
});

// 完整配置
viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(116.4, 39.9, 15000),
    orientation: {
        heading: Cesium.Math.toRadians(45),
        pitch: Cesium.Math.toRadians(-45),
        roll: 0
    },
    duration: 3,                              // 飞行时间
    complete: function() {                    // 完成回调
        console.log('飞行完成');
    },
    cancel: function() {                      // 取消回调
        console.log('飞行取消');
    },
    endTransform: Cesium.Matrix4.IDENTITY,    // 结束变换矩阵
    maximumHeight: 30000,                     // 最大飞行高度
    pitchAdjustHeight: 1000,                  // 俯仰调整高度
    flyOverLongitude: undefined,              // 飞越经度
    flyOverLongitudeWeight: 0.5,              // 飞越经度权重
    convert: true,                            // 是否转换坐标
    easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT // 缓动函数
});

// 飞行到矩形区域
viewer.camera.flyTo({
    destination: Cesium.Rectangle.fromDegrees(116.0, 39.0, 117.0, 40.0),
    duration: 2
});

// 使用 Promise
const flightPromise = viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(116.4, 39.9, 15000)
});

5.2.3 lookAt - 观察目标

// 观察一个点
const target = Cesium.Cartesian3.fromDegrees(116.4, 39.9, 0);
const offset = new Cesium.HeadingPitchRange(
    Cesium.Math.toRadians(45),    // 从东北方向观察
    Cesium.Math.toRadians(-30),   // 向下 30°
    10000                          // 距离目标 10km
);
viewer.camera.lookAt(target, offset);

// 使用 Cartesian3 偏移
const offsetCartesian = new Cesium.Cartesian3(5000, 5000, 10000);
viewer.camera.lookAt(target, offsetCartesian);

// 持续跟踪(锁定视角)
viewer.camera.lookAtTransform(
    Cesium.Transforms.eastNorthUpToFixedFrame(target),
    offset
);

// 解除锁定
viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);

5.2.4 flyToBoundingSphere - 飞行到边界球

// 计算边界球
const positions = Cesium.Cartesian3.fromDegreesArray([
    116.0, 39.0,
    117.0, 39.0,
    117.0, 40.0,
    116.0, 40.0
]);
const boundingSphere = Cesium.BoundingSphere.fromPoints(positions);

// 飞行到边界球
viewer.camera.flyToBoundingSphere(boundingSphere, {
    duration: 2,
    offset: new Cesium.HeadingPitchRange(0, -0.5, 0) // 0 表示自动计算距离
});

5.3 相机移动与旋转

5.3.1 移动方法

const camera = viewer.camera;

// ===== 平移移动 =====
camera.move(direction, amount);              // 沿指定方向移动
camera.moveForward(amount);                  // 向前移动
camera.moveBackward(amount);                 // 向后移动
camera.moveLeft(amount);                     // 向左移动
camera.moveRight(amount);                    // 向右移动
camera.moveUp(amount);                       // 向上移动
camera.moveDown(amount);                     // 向下移动

// 示例:向前移动 1000 米
camera.moveForward(1000);

// ===== 缩放移动 =====
camera.zoomIn(amount);                       // 放大
camera.zoomOut(amount);                      // 缩小

// 示例:放大 5000 米
camera.zoomIn(5000);

5.3.2 旋转方法

const camera = viewer.camera;

// ===== 环视旋转(改变观察方向)=====
camera.look(axis, angle);                    // 绕指定轴旋转
camera.lookLeft(amount);                     // 向左看
camera.lookRight(amount);                    // 向右看
camera.lookUp(amount);                       // 向上看
camera.lookDown(amount);                     // 向下看

// ===== 自转旋转(绕相机位置旋转)=====
camera.rotate(axis, angle);                  // 绕指定轴自转
camera.rotateLeft(angle);                    // 向左自转
camera.rotateRight(angle);                   // 向右自转
camera.rotateUp(angle);                      // 向上自转
camera.rotateDown(angle);                    // 向下自转

// ===== 扭转旋转(改变 heading/pitch/roll)=====
camera.twistLeft(amount);                    // 向左扭转
camera.twistRight(amount);                   // 向右扭转

// 示例:向右旋转 10°
camera.rotateRight(Cesium.Math.toRadians(10));

5.3.3 组合移动示例

// 实现键盘控制相机
class CameraKeyboardController {
    constructor(viewer) {
        this.viewer = viewer;
        this.camera = viewer.camera;
        this.flags = {
            moveForward: false,
            moveBackward: false,
            moveLeft: false,
            moveRight: false,
            moveUp: false,
            moveDown: false,
            lookLeft: false,
            lookRight: false,
            lookUp: false,
            lookDown: false
        };
        
        this.moveSpeed = 100;
        this.rotateSpeed = 0.01;
        
        this.initKeyEvents();
        this.startLoop();
    }
    
    initKeyEvents() {
        document.addEventListener('keydown', (e) => this.handleKeyDown(e));
        document.addEventListener('keyup', (e) => this.handleKeyUp(e));
    }
    
    handleKeyDown(e) {
        switch (e.key.toLowerCase()) {
            case 'w': this.flags.moveForward = true; break;
            case 's': this.flags.moveBackward = true; break;
            case 'a': this.flags.moveLeft = true; break;
            case 'd': this.flags.moveRight = true; break;
            case 'q': this.flags.moveUp = true; break;
            case 'e': this.flags.moveDown = true; break;
            case 'arrowleft': this.flags.lookLeft = true; break;
            case 'arrowright': this.flags.lookRight = true; break;
            case 'arrowup': this.flags.lookUp = true; break;
            case 'arrowdown': this.flags.lookDown = true; break;
        }
    }
    
    handleKeyUp(e) {
        switch (e.key.toLowerCase()) {
            case 'w': this.flags.moveForward = false; break;
            case 's': this.flags.moveBackward = false; break;
            case 'a': this.flags.moveLeft = false; break;
            case 'd': this.flags.moveRight = false; break;
            case 'q': this.flags.moveUp = false; break;
            case 'e': this.flags.moveDown = false; break;
            case 'arrowleft': this.flags.lookLeft = false; break;
            case 'arrowright': this.flags.lookRight = false; break;
            case 'arrowup': this.flags.lookUp = false; break;
            case 'arrowdown': this.flags.lookDown = false; break;
        }
    }
    
    startLoop() {
        this.viewer.clock.onTick.addEventListener(() => {
            if (this.flags.moveForward) this.camera.moveForward(this.moveSpeed);
            if (this.flags.moveBackward) this.camera.moveBackward(this.moveSpeed);
            if (this.flags.moveLeft) this.camera.moveLeft(this.moveSpeed);
            if (this.flags.moveRight) this.camera.moveRight(this.moveSpeed);
            if (this.flags.moveUp) this.camera.moveUp(this.moveSpeed);
            if (this.flags.moveDown) this.camera.moveDown(this.moveSpeed);
            if (this.flags.lookLeft) this.camera.lookLeft(this.rotateSpeed);
            if (this.flags.lookRight) this.camera.lookRight(this.rotateSpeed);
            if (this.flags.lookUp) this.camera.lookUp(this.rotateSpeed);
            if (this.flags.lookDown) this.camera.lookDown(this.rotateSpeed);
        });
    }
}

// 使用
const controller = new CameraKeyboardController(viewer);

5.4 相机动画

5.4.1 缓动函数

// CesiumJS 内置的缓动函数
const easingFunctions = {
    // 线性
    LINEAR_NONE: Cesium.EasingFunction.LINEAR_NONE,
    
    // 二次方
    QUADRATIC_IN: Cesium.EasingFunction.QUADRATIC_IN,
    QUADRATIC_OUT: Cesium.EasingFunction.QUADRATIC_OUT,
    QUADRATIC_IN_OUT: Cesium.EasingFunction.QUADRATIC_IN_OUT,
    
    // 三次方
    CUBIC_IN: Cesium.EasingFunction.CUBIC_IN,
    CUBIC_OUT: Cesium.EasingFunction.CUBIC_OUT,
    CUBIC_IN_OUT: Cesium.EasingFunction.CUBIC_IN_OUT,
    
    // 四次方
    QUARTIC_IN: Cesium.EasingFunction.QUARTIC_IN,
    QUARTIC_OUT: Cesium.EasingFunction.QUARTIC_OUT,
    QUARTIC_IN_OUT: Cesium.EasingFunction.QUARTIC_IN_OUT,
    
    // 五次方
    QUINTIC_IN: Cesium.EasingFunction.QUINTIC_IN,
    QUINTIC_OUT: Cesium.EasingFunction.QUINTIC_OUT,
    QUINTIC_IN_OUT: Cesium.EasingFunction.QUINTIC_IN_OUT,
    
    // 正弦
    SINUSOIDAL_IN: Cesium.EasingFunction.SINUSOIDAL_IN,
    SINUSOIDAL_OUT: Cesium.EasingFunction.SINUSOIDAL_OUT,
    SINUSOIDAL_IN_OUT: Cesium.EasingFunction.SINUSOIDAL_IN_OUT,
    
    // 指数
    EXPONENTIAL_IN: Cesium.EasingFunction.EXPONENTIAL_IN,
    EXPONENTIAL_OUT: Cesium.EasingFunction.EXPONENTIAL_OUT,
    EXPONENTIAL_IN_OUT: Cesium.EasingFunction.EXPONENTIAL_IN_OUT,
    
    // 圆形
    CIRCULAR_IN: Cesium.EasingFunction.CIRCULAR_IN,
    CIRCULAR_OUT: Cesium.EasingFunction.CIRCULAR_OUT,
    CIRCULAR_IN_OUT: Cesium.EasingFunction.CIRCULAR_IN_OUT,
    
    // 弹性
    ELASTIC_IN: Cesium.EasingFunction.ELASTIC_IN,
    ELASTIC_OUT: Cesium.EasingFunction.ELASTIC_OUT,
    ELASTIC_IN_OUT: Cesium.EasingFunction.ELASTIC_IN_OUT,
    
    // 回弹
    BACK_IN: Cesium.EasingFunction.BACK_IN,
    BACK_OUT: Cesium.EasingFunction.BACK_OUT,
    BACK_IN_OUT: Cesium.EasingFunction.BACK_IN_OUT,
    
    // 弹跳
    BOUNCE_IN: Cesium.EasingFunction.BOUNCE_IN,
    BOUNCE_OUT: Cesium.EasingFunction.BOUNCE_OUT,
    BOUNCE_IN_OUT: Cesium.EasingFunction.BOUNCE_IN_OUT
};

// 使用缓动函数
viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(116.4, 39.9, 15000),
    duration: 3,
    easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT
});

5.4.2 自定义相机动画

// 相机环绕动画
function orbitAnimation(viewer, target, duration = 10) {
    const startTime = viewer.clock.currentTime;
    const endTime = Cesium.JulianDate.addSeconds(startTime, duration, new Cesium.JulianDate());
    
    function tick() {
        const currentTime = viewer.clock.currentTime;
        const elapsed = Cesium.JulianDate.secondsDifference(currentTime, startTime);
        
        if (elapsed >= duration) {
            return;
        }
        
        const angle = (elapsed / duration) * 2 * Math.PI;
        const heading = Cesium.Math.toRadians(angle * 180 / Math.PI);
        
        viewer.camera.lookAt(
            target,
            new Cesium.HeadingPitchRange(heading, Cesium.Math.toRadians(-30), 10000)
        );
        
        requestAnimationFrame(tick);
    }
    
    tick();
}

// 使用
const target = Cesium.Cartesian3.fromDegrees(116.4, 39.9, 0);
orbitAnimation(viewer, target, 20);

// 相机路径动画
async function flyAlongPath(viewer, waypoints, speed = 1000) {
    for (let i = 0; i < waypoints.length - 1; i++) {
        const start = waypoints[i];
        const end = waypoints[i + 1];
        const distance = Cesium.Cartesian3.distance(
            Cesium.Cartesian3.fromDegrees(start.lon, start.lat, start.height),
            Cesium.Cartesian3.fromDegrees(end.lon, end.lat, end.height)
        );
        const duration = distance / speed;
        
        await new Promise(resolve => {
            viewer.camera.flyTo({
                destination: Cesium.Cartesian3.fromDegrees(end.lon, end.lat, end.height),
                orientation: {
                    heading: Cesium.Math.toRadians(end.heading || 0),
                    pitch: Cesium.Math.toRadians(end.pitch || -30),
                    roll: 0
                },
                duration: duration,
                complete: resolve
            });
        });
    }
}

// 使用
const waypoints = [
    { lon: 116.4, lat: 39.9, height: 15000, heading: 0, pitch: -30 },
    { lon: 121.5, lat: 31.2, height: 10000, heading: 45, pitch: -45 },
    { lon: 113.2, lat: 23.1, height: 8000, heading: 90, pitch: -20 }
];
flyAlongPath(viewer, waypoints, 500);

5.4.3 平滑过渡动画

// 平滑缩放动画
function smoothZoom(viewer, targetHeight, duration = 1) {
    const camera = viewer.camera;
    const startHeight = camera.positionCartographic.height;
    const startTime = Date.now();
    
    function animate() {
        const elapsed = (Date.now() - startTime) / 1000;
        const progress = Math.min(elapsed / duration, 1);
        
        // 使用缓动函数
        const easedProgress = Cesium.EasingFunction.CUBIC_IN_OUT(progress);
        
        const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress;
        
        const cartographic = camera.positionCartographic;
        camera.setView({
            destination: Cesium.Cartesian3.fromRadians(
                cartographic.longitude,
                cartographic.latitude,
                currentHeight
            )
        });
        
        if (progress < 1) {
            requestAnimationFrame(animate);
        }
    }
    
    animate();
}

// 使用
smoothZoom(viewer, 5000, 2); // 缩放到 5km 高度,2秒完成

5.5 相机事件

5.5.1 相机事件监听

const camera = viewer.camera;

// ===== 移动事件 =====
camera.moveStart.addEventListener(function() {
    console.log('相机开始移动');
});

camera.moveEnd.addEventListener(function() {
    console.log('相机停止移动');
});

// ===== 变化事件 =====
camera.changed.addEventListener(function(percentage) {
    console.log('相机变化:', percentage);
    
    // 获取当前位置
    const position = camera.positionCartographic;
    console.log('当前位置:', {
        longitude: Cesium.Math.toDegrees(position.longitude),
        latitude: Cesium.Math.toDegrees(position.latitude),
        height: position.height
    });
});

// 设置变化阈值
camera.percentageChanged = 0.1; // 变化 10% 触发事件

5.5.2 相机状态监控

// 相机状态监控器
class CameraMonitor {
    constructor(viewer) {
        this.viewer = viewer;
        this.camera = viewer.camera;
        this.lastPosition = null;
        this.callbacks = {
            onMove: [],
            onZoom: [],
            onRotate: []
        };
        
        this.startMonitoring();
    }
    
    startMonitoring() {
        this.viewer.scene.postRender.addEventListener(() => {
            const currentPosition = this.camera.positionCartographic.clone();
            
            if (this.lastPosition) {
                const heightChange = Math.abs(currentPosition.height - this.lastPosition.height);
                const latChange = Math.abs(currentPosition.latitude - this.lastPosition.latitude);
                const lonChange = Math.abs(currentPosition.longitude - this.lastPosition.longitude);
                
                if (heightChange > 1) {
                    this.callbacks.onZoom.forEach(cb => cb(currentPosition.height));
                }
                
                if (latChange > 0.0001 || lonChange > 0.0001) {
                    this.callbacks.onMove.forEach(cb => cb({
                        longitude: Cesium.Math.toDegrees(currentPosition.longitude),
                        latitude: Cesium.Math.toDegrees(currentPosition.latitude)
                    }));
                }
            }
            
            this.lastPosition = currentPosition;
        });
    }
    
    onMove(callback) {
        this.callbacks.onMove.push(callback);
    }
    
    onZoom(callback) {
        this.callbacks.onZoom.push(callback);
    }
    
    getCurrentState() {
        const cartographic = this.camera.positionCartographic;
        return {
            longitude: Cesium.Math.toDegrees(cartographic.longitude),
            latitude: Cesium.Math.toDegrees(cartographic.latitude),
            height: cartographic.height,
            heading: Cesium.Math.toDegrees(this.camera.heading),
            pitch: Cesium.Math.toDegrees(this.camera.pitch),
            roll: Cesium.Math.toDegrees(this.camera.roll)
        };
    }
}

// 使用
const monitor = new CameraMonitor(viewer);
monitor.onMove(pos => console.log('移动到:', pos));
monitor.onZoom(height => console.log('高度:', height));

5.6 视角控制高级功能

5.6.1 视角限制

// 限制相机移动范围
function limitCameraExtent(viewer, extent) {
    const west = Cesium.Math.toRadians(extent.west);
    const south = Cesium.Math.toRadians(extent.south);
    const east = Cesium.Math.toRadians(extent.east);
    const north = Cesium.Math.toRadians(extent.north);
    
    viewer.scene.postRender.addEventListener(function() {
        const camera = viewer.camera;
        const position = camera.positionCartographic;
        
        let needsUpdate = false;
        let newLon = position.longitude;
        let newLat = position.latitude;
        
        if (position.longitude < west) {
            newLon = west;
            needsUpdate = true;
        } else if (position.longitude > east) {
            newLon = east;
            needsUpdate = true;
        }
        
        if (position.latitude < south) {
            newLat = south;
            needsUpdate = true;
        } else if (position.latitude > north) {
            newLat = north;
            needsUpdate = true;
        }
        
        if (needsUpdate) {
            camera.setView({
                destination: Cesium.Cartesian3.fromRadians(
                    newLon, newLat, position.height
                ),
                orientation: {
                    heading: camera.heading,
                    pitch: camera.pitch,
                    roll: camera.roll
                }
            });
        }
    });
}

// 使用:限制在中国范围内
limitCameraExtent(viewer, {
    west: 73.0,
    south: 3.0,
    east: 135.0,
    north: 54.0
});

// 限制缩放范围
const controller = viewer.scene.screenSpaceCameraController;
controller.minimumZoomDistance = 100;      // 最近 100 米
controller.maximumZoomDistance = 20000000; // 最远 20000 公里

5.6.2 第一人称视角

// 第一人称漫游模式
class FirstPersonController {
    constructor(viewer) {
        this.viewer = viewer;
        this.camera = viewer.camera;
        this.moveSpeed = 10;
        this.turnSpeed = 0.002;
        this.height = 1.7; // 人眼高度
        
        this.enabled = false;
        this.handler = null;
        
        this.initMouseHandler();
    }
    
    enable() {
        this.enabled = true;
        
        // 禁用默认控制
        const controller = this.viewer.scene.screenSpaceCameraController;
        controller.enableRotate = false;
        controller.enableTranslate = false;
        controller.enableZoom = false;
        controller.enableTilt = false;
        controller.enableLook = false;
        
        // 设置初始视角
        const cartographic = this.camera.positionCartographic;
        this.camera.setView({
            destination: Cesium.Cartesian3.fromRadians(
                cartographic.longitude,
                cartographic.latitude,
                this.height
            ),
            orientation: {
                heading: this.camera.heading,
                pitch: 0,
                roll: 0
            }
        });
    }
    
    disable() {
        this.enabled = false;
        
        // 恢复默认控制
        const controller = this.viewer.scene.screenSpaceCameraController;
        controller.enableRotate = true;
        controller.enableTranslate = true;
        controller.enableZoom = true;
        controller.enableTilt = true;
        controller.enableLook = true;
    }
    
    initMouseHandler() {
        this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.canvas);
        
        let lastX = 0;
        let lastY = 0;
        
        this.handler.setInputAction((movement) => {
            if (!this.enabled) return;
            
            const deltaX = movement.endPosition.x - movement.startPosition.x;
            const deltaY = movement.endPosition.y - movement.startPosition.y;
            
            // 水平旋转
            this.camera.setView({
                destination: this.camera.position,
                orientation: {
                    heading: this.camera.heading - deltaX * this.turnSpeed,
                    pitch: Math.max(
                        Math.min(this.camera.pitch - deltaY * this.turnSpeed, Math.PI / 2),
                        -Math.PI / 2
                    ),
                    roll: 0
                }
            });
        }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
    }
    
    moveForward(distance = this.moveSpeed) {
        if (!this.enabled) return;
        
        const direction = this.camera.direction;
        const horizontalDirection = new Cesium.Cartesian3(
            direction.x, direction.y, 0
        );
        Cesium.Cartesian3.normalize(horizontalDirection, horizontalDirection);
        
        const movement = Cesium.Cartesian3.multiplyByScalar(
            horizontalDirection, distance, new Cesium.Cartesian3()
        );
        
        Cesium.Cartesian3.add(this.camera.position, movement, this.camera.position);
    }
    
    moveBackward(distance = this.moveSpeed) {
        this.moveForward(-distance);
    }
    
    moveLeft(distance = this.moveSpeed) {
        if (!this.enabled) return;
        
        const right = this.camera.right;
        const horizontalRight = new Cesium.Cartesian3(right.x, right.y, 0);
        Cesium.Cartesian3.normalize(horizontalRight, horizontalRight);
        
        const movement = Cesium.Cartesian3.multiplyByScalar(
            horizontalRight, -distance, new Cesium.Cartesian3()
        );
        
        Cesium.Cartesian3.add(this.camera.position, movement, this.camera.position);
    }
    
    moveRight(distance = this.moveSpeed) {
        this.moveLeft(-distance);
    }
}

// 使用
const fpController = new FirstPersonController(viewer);
fpController.enable();

5.6.3 跟踪实体

// 基本跟踪
const entity = viewer.entities.add({
    name: '移动目标',
    position: Cesium.Cartesian3.fromDegrees(116.4, 39.9, 100),
    model: {
        uri: 'model.glb',
        scale: 1.0
    }
});

// 设置跟踪
viewer.trackedEntity = entity;

// 带偏移的跟踪
viewer.trackedEntity = entity;
viewer.camera.setView({
    destination: viewer.camera.position,
    orientation: new Cesium.HeadingPitchRange(
        Cesium.Math.toRadians(45),  // 从侧后方观察
        Cesium.Math.toRadians(-20), // 略微向下
        500                          // 距离 500 米
    )
});

// 自定义跟踪逻辑
function customTracking(viewer, entity, offset) {
    viewer.scene.postRender.addEventListener(function() {
        const position = entity.position.getValue(viewer.clock.currentTime);
        if (position) {
            viewer.camera.lookAt(position, offset);
        }
    });
}

customTracking(viewer, entity, new Cesium.HeadingPitchRange(0, -0.5, 1000));

// 停止跟踪
viewer.trackedEntity = undefined;
viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);

5.7 相机工具类

// 完整的相机工具类
class CameraUtils {
    constructor(viewer) {
        this.viewer = viewer;
        this.camera = viewer.camera;
    }
    
    // 获取当前视角状态
    getState() {
        const cartographic = this.camera.positionCartographic;
        return {
            longitude: Cesium.Math.toDegrees(cartographic.longitude),
            latitude: Cesium.Math.toDegrees(cartographic.latitude),
            height: cartographic.height,
            heading: Cesium.Math.toDegrees(this.camera.heading),
            pitch: Cesium.Math.toDegrees(this.camera.pitch),
            roll: Cesium.Math.toDegrees(this.camera.roll)
        };
    }
    
    // 恢复视角状态
    setState(state) {
        this.camera.setView({
            destination: Cesium.Cartesian3.fromDegrees(
                state.longitude, state.latitude, state.height
            ),
            orientation: {
                heading: Cesium.Math.toRadians(state.heading),
                pitch: Cesium.Math.toRadians(state.pitch),
                roll: Cesium.Math.toRadians(state.roll)
            }
        });
    }
    
    // 飞行到坐标
    flyTo(lon, lat, height, options = {}) {
        return new Promise(resolve => {
            this.camera.flyTo({
                destination: Cesium.Cartesian3.fromDegrees(lon, lat, height),
                orientation: {
                    heading: Cesium.Math.toRadians(options.heading || 0),
                    pitch: Cesium.Math.toRadians(options.pitch || -45),
                    roll: 0
                },
                duration: options.duration || 2,
                complete: resolve
            });
        });
    }
    
    // 获取视野范围
    getViewExtent() {
        const rectangle = this.camera.computeViewRectangle();
        if (rectangle) {
            return {
                west: Cesium.Math.toDegrees(rectangle.west),
                south: Cesium.Math.toDegrees(rectangle.south),
                east: Cesium.Math.toDegrees(rectangle.east),
                north: Cesium.Math.toDegrees(rectangle.north)
            };
        }
        return null;
    }
    
    // 获取中心点
    getCenter() {
        const canvas = this.viewer.scene.canvas;
        const centerScreen = new Cesium.Cartesian2(
            canvas.clientWidth / 2,
            canvas.clientHeight / 2
        );
        
        const ray = this.camera.getPickRay(centerScreen);
        const center = this.viewer.scene.globe.pick(ray, this.viewer.scene);
        
        if (center) {
            const cartographic = Cesium.Cartographic.fromCartesian(center);
            return {
                longitude: Cesium.Math.toDegrees(cartographic.longitude),
                latitude: Cesium.Math.toDegrees(cartographic.latitude)
            };
        }
        return null;
    }
    
    // 截图
    screenshot(filename = 'screenshot.png') {
        const canvas = this.viewer.scene.canvas;
        const link = document.createElement('a');
        link.download = filename;
        link.href = canvas.toDataURL('image/png');
        link.click();
    }
}

// 使用
const cameraUtils = new CameraUtils(viewer);

// 保存和恢复视角
const state = cameraUtils.getState();
console.log('当前视角:', state);

// 稍后恢复
cameraUtils.setState(state);

// 飞行到指定位置
await cameraUtils.flyTo(116.4, 39.9, 15000, {
    heading: 45,
    pitch: -30,
    duration: 3
});

5.8 本章小结

本章详细介绍了 CesiumJS 的相机系统:

  1. 相机基础:坐标系、属性说明
  2. 定位方法:setView、flyTo、lookAt
  3. 移动旋转:平移、缩放、旋转操作
  4. 相机动画:缓动函数、自定义动画
  5. 相机事件:事件监听、状态监控
  6. 高级功能:视角限制、第一人称、实体跟踪
  7. 工具类:完整的相机操作封装

在下一章中,我们将详细介绍 Entity API 实体管理。

5.9 思考与练习

  1. 实现一个相机漫游路径编辑器。
  2. 创建平滑的相机过渡动画效果。
  3. 实现地图双击放大功能。
  4. 开发第一人称漫游模式的完整实现。
  5. 实现视角书签功能(保存和恢复多个视角)。
posted @ 2026-01-08 11:13  我才是银古  阅读(9)  评论(0)    收藏  举报