ArcGIS JS 基础教程(10):Camera 相机控制

ArcGIS JS 基础教程(10):Camera 相机控制

零、写在前面

📌 本系列教程完整目录ArcGIS JS 系列基础教程(100个项目常用热门功能)

💡 在线示例 :完整可运行的 HTML 示例,无需任何环境配置,可直接在浏览器中打开体验

🗂️ 专栏导航 :收藏 + 关注,专栏文章第一时间送达

❤️ 一键三连:点赞(给教程充电)+ 评论(提问必回)+ 收藏(下次再看)


一、功能介绍

在 ArcGIS Maps SDK for JavaScript 的三维场景中,Camera(相机) 相当于用户的"眼睛"------它决定了你从哪个位置、以什么角度观察三维世界。Camera 类提供了四个核心属性来控制观察视角:

  • position(位置):经纬度 + 海拔高度(z 值,单位米)------你在哪里看
  • heading(朝向):水平旋转角度,0° 为正北,顺时针 0~360°------你朝哪个方向看
  • tilt(俯仰角):垂直倾斜角度,0° 为垂直俯视地面,90° 为水平平视 ------你以什么仰角看
  • fov(视场角):对角线视野角度,范围 1~170°,默认 55°------你看得多"广"

掌握 Camera API 是三维场景开发的基础------无论是设置初始视角、实现建筑漫游、环绕观察地标,还是保存和恢复书签视角,都离不开对相机的精准控制。


二、功能实现

核心 API: Camera 类(@arcgis/core/Camera.js),通过 view.camera 读取或设置。

2.1 Camera 对象基础

Camera 通过四个属性完整定义一个三维观察视角:

javascript 复制代码
const Camera = await $arcgis.import("@arcgis/core/Camera.js");
const Point = await $arcgis.import("@arcgis/core/geometry/Point.js");

// 创建相机实例
const camera = new Camera({
    position: new Point({
        longitude: 116.39,   // 经度
        latitude: 39.9,      // 纬度
        z: 500               // 海拔高度(米)
    }),
    heading: 0,    // 朝向:0=正北, 90=正东, 180=正南, 270=正西
    tilt: 45,      // 俯仰:0=垂直俯视, 45=倾斜观察, 90=水平平视
    fov: 55        // 视场角:越小越长焦,越大越广角
});

heading 方向对照:

heading 值 朝向 说明
正北 ↑ 默认朝向
90° 正东 → 顺时针旋转
180° 正南 ↓ ---
270° 正西 ← ---

tilt 角度对照:

tilt 值 视觉效果 典型场景
完全垂直俯视 二维地图视角、卫星图
30°~45° 倾斜鸟瞰 城市规划、园区总览
60°~75° 近地面视角 建筑立面展示、街景
90° 水平平视 第一人称漫游

2.2 设置相机位置

有两种方式设置相机:

javascript 复制代码
// 方式一:通过 SceneView 构造函数
const view = new SceneView({
    container: "mapContainer",
    map: map,
    camera: {                          // 直接传 camera 配置对象
        position: { longitude: 116.39, latitude: 39.9, z: 2000 },
        heading: 0,
        tilt: 45
    }
});

// 方式二:直接赋值 camera 对象(运行时切换视角)
view.camera = new Camera({
    position: { longitude: 116.39, latitude: 39.9, z: 500 },
    heading: 90,
    tilt: 65
});

💡 注意: 直接赋值 view.camera瞬间切换 (无动画),后文(第11课)将介绍使用 goTo() 实现平滑过渡。

2.3 相机旋转(heading)

heading 控制相机的水平朝向,范围 0~360°,顺时针旋转:

javascript 复制代码
// 看向正北
view.camera.heading = 0;

// 看向正东
view.camera.heading = 90;

// 看向正南
view.camera.heading = 180;

// 看向正西
view.camera.heading = 270;

// 每帧旋转 0.5°,实现环绕效果
setInterval(() => {
    view.camera.heading = (view.camera.heading + 0.5) % 360;
}, 16);

应用场景: 环绕观察地标建筑、旋转查看全景、无人机环绕航拍模拟。

2.4 相机俯仰(tilt)

tilt 控制相机向下倾斜的角度:

javascript 复制代码
// 垂直俯视(2D 地图风格)
view.camera.tilt = 0;

// 45° 鸟瞰(最常用)
view.camera.tilt = 45;

// 近地面观察建筑立面
view.camera.tilt = 75;

// 水平平视(第一人称视角)
view.camera.tilt = 90;

⚠️ 注意: tilt 的有效范围是 0~90°。0° 是垂直向下看;90° 是水平看。不存在大于 90° 的值(无法"仰头向上看")。

2.5 视野角度(fov)

fov 控制视场角,默认 55°,范围 1~170°:

javascript 复制代码
// 默认视场角(标准视角)
view.camera.fov = 55;

// 长焦效果(放大远处细节)
view.camera.fov = 20;

// 广角效果(更大视野)
view.camera.fov = 90;

// 鱼眼效果(极端广角)
view.camera.fov = 120;
fov 值 效果 类比
20°~30° 长焦,放大远处 望远镜
55° 默认标准视角 人眼自然视野
80°~100° 广角,视野更大 超广角镜头
120°+ 鱼眼,显著畸变 鱼眼镜头

2.6 获取当前相机状态

随时可以读取 view.camera 的当前属性:

javascript 复制代码
// 读取相机状态
const cam = view.camera;

console.log("位置:", cam.position.longitude, cam.position.latitude, cam.position.z);
console.log("朝向:", cam.heading, "°");
console.log("俯仰:", cam.tilt, "°");
console.log("视场角:", cam.fov, "°");

⚠️ 重要: view.camera 返回的是引用 ,修改 cam.heading = 90同步影响 view.camera。如需保存快照,使用 clone() 创建副本。

2.7 相机克隆与保存

javascript 复制代码
// 保存当前视角(深拷贝)
const savedCamera = view.camera.clone();

// ... 用户操作,视角改变 ...

// 恢复之前保存的视角
view.camera = savedCamera;

三、功能应用

应用场景 position.z (高度) heading tilt fov 说明
城市规划总览 2000~5000m 180°(南向) 30°~45° 55° 鸟瞰全城,俯瞰建筑肌理
建筑立面展示 200~500m 正对建筑 60°~75° 55° 近地观察建筑外观
环绕地标观察 500~1000m 动态旋转 45°~60° 55° heading 循环旋转
第一人称漫游 10~50m 行走方向 80°~90° 55°~70° 低空贴近地面
卫星图视角 10000m+ 任意 55° tilt=0 垂直俯视
远眺山脉 2000~5000m 朝向山脉 15°~30° 30°~40° 长焦拉近远景
视角书签保存 任意 任意 任意 任意 clone() 保存,随时恢复

四、核心代码

📦 完整代码 已保存至 sample/lesson10_camera_control.html,可直接在浏览器打开。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>第10课:Camera 相机控制</title>
    <link rel="stylesheet" href="https://js.arcgis.com/5.0/esri/themes/light/main.css">
    <script type="module" src="https://js.arcgis.com/5.0/"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: "Microsoft YaHei", sans-serif; }
        #mapContainer { width: 100vw; height: 100vh; }
        .page-title {
            position: absolute;
            top: 20px; left: 50%;
            transform: translateX(-50%);
            background: rgba(255,255,255,0.95);
            padding: 10px 24px; border-radius: 6px;
            font-size: 18px; font-weight: bold;
            z-index: 100;
            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
        }
        .control-panel {
            position: absolute;
            top: 80px; right: 20px;
            background: rgba(255,255,255,0.95);
            padding: 16px; border-radius: 8px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.15);
            z-index: 100; min-width: 320px; max-height: 85vh; overflow-y: auto;
        }
        .control-panel h3 { margin: 0 0 8px 0; font-size: 14px; color: #333; }
        .section { margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #eee; }
        .section:last-child { border-bottom: none; margin-bottom: 0; }
        .form-group { margin-bottom: 10px; }
        .form-group label { display: block; font-size: 12px; color: #666; margin-bottom: 4px; }
        .form-group label span { float: right; font-weight: bold; color: #1890ff; }
        .form-group input[type="range"] { width: 100%; }
        .preset-row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
        .preset-row button {
            padding: 5px 10px; font-size: 12px;
            border: 1px solid #d9d9d9; border-radius: 4px;
            background: white; cursor: pointer;
            transition: all 0.2s;
        }
        .preset-row button:hover { border-color: #1890ff; color: #1890ff; }
        .preset-row button.active { background: #1890ff; color: white; border-color: #1890ff; }
        .btn-row { display: flex; gap: 8px; margin-top: 8px; }
        .btn-row button {
            flex: 1; padding: 7px 0;
            border: 1px solid #d9d9d9; border-radius: 4px;
            background: white; cursor: pointer; font-size: 12px;
        }
        .btn-row button:hover { border-color: #1890ff; color: #1890ff; }
        .btn-row button.primary { background: #1890ff; color: white; border-color: #1890ff; }
        .info-card {
            margin-top: 10px; padding: 10px 12px;
            background: #f0f5ff; border-radius: 6px;
            border-left: 3px solid #1890ff;
        }
        .info-card .row { font-size: 12px; color: #333; margin-bottom: 2px; line-height: 1.6; }
        .info-card .row:last-child { margin-bottom: 0; }
        .info-card .val { font-weight: bold; color: #1890ff; }
        .status-text {
            position: absolute; bottom: 20px; left: 50%;
            transform: translateX(-50%);
            background: rgba(0,0,0,0.7); color: white;
            padding: 8px 20px; border-radius: 20px;
            font-size: 13px; z-index: 100; pointer-events: none;
            white-space: nowrap;
        }
    </style>
</head>
<body>
<h1 class="page-title">第10课:Camera 相机控制</h1>

<div class="control-panel">
    <div class="section">
        <h3>📍 位置预设</h3>
        <div class="preset-row">
            <button data-preset="beijing" class="active">🏯 北京故宫</button>
            <button data-preset="shanghai">🌆 上海外滩</button>
            <button data-preset="guangzhou">🗼 广州塔</button>
            <button data-preset="huangshan">⛰️ 黄山</button>
        </div>
    </div>

    <div class="section">
        <h3>🧭 朝向 heading(0~360°)</h3>
        <div class="form-group">
            <label>朝向:<span id="valHeading">0°</span></label>
            <input type="range" id="sliderHeading" min="0" max="360" value="0" step="1">
        </div>
        <div class="preset-row">
            <button data-heading="0">N 北</button>
            <button data-heading="90">E 东</button>
            <button data-heading="180">S 南</button>
            <button data-heading="270">W 西</button>
        </div>
    </div>

    <div class="section">
        <h3>📐 俯仰 tilt(0~90°)</h3>
        <div class="form-group">
            <label>俯仰:<span id="valTilt">45°</span></label>
            <input type="range" id="sliderTilt" min="0" max="90" value="45" step="1">
        </div>
        <div class="preset-row">
            <button data-tilt="0">0° 俯视</button>
            <button data-tilt="45">45° 倾斜</button>
            <button data-tilt="70">70° 近地</button>
            <button data-tilt="90">90° 平视</button>
        </div>
    </div>

    <div class="section">
        <h3>🔍 视场角 fov(1~170°)</h3>
        <div class="form-group">
            <label>视场角:<span id="valFov">55°</span></label>
            <input type="range" id="sliderFov" min="1" max="170" value="55" step="1">
        </div>
        <div class="preset-row">
            <button data-fov="20">20° 长焦</button>
            <button data-fov="55">55° 默认</button>
            <button data-fov="100">100° 广角</button>
            <button data-fov="150">150° 鱼眼</button>
        </div>
    </div>

    <div class="section">
        <h3>📏 高度 altitude(m)</h3>
        <div class="form-group">
            <label>海拔高度:<span id="valAltitude">2000m</span></label>
            <input type="range" id="sliderAltitude" min="50" max="10000" value="2000" step="10">
        </div>
    </div>

    <div class="btn-row">
        <button id="btnSave" class="primary">💾 保存视角</button>
        <button id="btnRestore">📂 恢复视角</button>
    </div>
    <div class="btn-row">
        <button id="btnReset">🔄 重置视角</button>
        <button id="btnAutoRotate">🔄 自动旋转</button>
    </div>

    <div class="info-card" id="infoCard">
        <div class="row">📍 位置:<span class="val" id="infoPos">经度 116.39, 纬度 39.91</span></div>
        <div class="row">📏 高度:<span class="val" id="infoAlt">2000 m</span></div>
        <div class="row">🧭 heading:<span class="val" id="infoHeading">0°</span></div>
        <div class="row">📐 tilt:<span class="val" id="infoTilt">45°</span></div>
        <div class="row">🔍 fov:<span class="val" id="infoFov">55°</span></div>
    </div>
</div>

<div class="status-text" id="statusText">Camera | 朝向:0° | 俯仰:45° | 高度:2000m | FOV:55°</div>

<div id="mapContainer"></div>

<script type="module">
    const Map = await $arcgis.import("@arcgis/core/Map.js");
    const SceneView = await $arcgis.import("@arcgis/core/views/SceneView.js");
    const Camera = await $arcgis.import("@arcgis/core/Camera.js");
    const Point = await $arcgis.import("@arcgis/core/geometry/Point.js");
    const GraphicsLayer = await $arcgis.import("@arcgis/core/layers/GraphicsLayer.js");
    const Graphic = await $arcgis.import("@arcgis/core/Graphic.js");
    const Mesh = await $arcgis.import("@arcgis/core/geometry/Mesh.js");
    const getTianditu = await $arcgis.import("https://openlayers.vip/examples/resources/tianditu.js");

    const vecLayers = getTianditu.default({ type: "vec_w" });
    const map = new Map({
        basemap: { baseLayers: [vecLayers.base, vecLayers.anno] },
        ground: {
            surface: {
                elevationLayers: [{
                    url: "https://www.geosceneonline.cn/image/rest/services/OpenData/ChinaTerrain3D/ImageServer/"
                }]
            }
        }
    });

    // ===== 位置预设 =====
    const presets = {
        beijing:   { lng: 116.397, lat: 39.917, zoom: 17, alt: 2000 },
        shanghai:  { lng: 121.491, lat: 31.240, zoom: 17, alt: 2000 },
        guangzhou: { lng: 113.324, lat: 23.109, zoom: 17, alt: 2000 },
        huangshan: { lng: 118.168, lat: 30.133, zoom: 16, alt: 3000 }
    };

    let currentPreset = "beijing";
    const p = presets[currentPreset];

    const view = new SceneView({
        container: "mapContainer",
        map: map,
        camera: {
            position: { longitude: p.lng, latitude: p.lat, z: p.alt },
            heading: 0,
            tilt: 45
        },
        zoom: p.zoom
    });

    window.view = view;

    let savedCamera = null;
    let autoRotateInterval = null;

    // ===== 添加 3D 参照物 =====
    function addReferenceObjects() {
        const layer = new GraphicsLayer({
            title: "参照物体",
            castShadows: true, receiveShadows: true
        });

        // 中心参照物
        const centerBox = new Graphic({
            geometry: Mesh.createBox(
                new Point({ longitude: presets[currentPreset].lng, latitude: presets[currentPreset].lat, z: 0 }),
                { size: { width: 200, height: 300, depth: 200 } }
            ),
            symbol: {
                type: "mesh-3d",
                symbolLayers: [{ type: "fill", material: { color: [220, 180, 100, 0.9] } }]
            }
        });

        // 方向指示物(东侧-红色)
        const eastBox = new Graphic({
            geometry: Mesh.createBox(
                new Point({ longitude: p.lng + 0.003, latitude: p.lat, z: 0 }),
                { size: { width: 80, height: 100, depth: 80 } }
            ),
            symbol: {
                type: "mesh-3d",
                symbolLayers: [{ type: "fill", material: { color: [255, 80, 80, 0.9] } }]
            }
        });

        // 方向指示物(南侧-绿色)
        const southBox = new Graphic({
            geometry: Mesh.createBox(
                new Point({ longitude: p.lng, latitude: p.lat - 0.003, z: 0 }),
                { size: { width: 80, height: 100, depth: 80 } }
            ),
            symbol: {
                type: "mesh-3d",
                symbolLayers: [{ type: "fill", material: { color: [80, 200, 80, 0.9] } }]
            }
        });

        layer.addMany([centerBox, eastBox, southBox]);
        map.add(layer);
    }

    // ===== 刷新参照物位置 =====
    function refreshReferences() {
        map.layers.filter(l => l.title === "参照物体").forEach(l => map.remove(l));
        addReferenceObjects();
    }

    // ===== 应用相机参数 =====
    function applyCamera(heading, tilt, fov, altitude) {
        const cam = view.camera.clone();
        cam.heading = heading;
        cam.tilt = tilt;
        cam.fov = fov;
        cam.position.z = altitude;
        view.camera = cam;
    }

    // ===== 初始化 =====
    view.when(() => {
        addReferenceObjects();
        initUI();
        updateInfo();
    });

    function initUI() {
        // 位置预设按钮
        document.querySelectorAll("[data-preset]").forEach(btn => {
            btn.addEventListener("click", () => {
                stopAutoRotate();
                currentPreset = btn.dataset.preset;
                const p = presets[currentPreset];

                view.camera = new Camera({
                    position: new Point({ longitude: p.lng, latitude: p.lat, z: p.alt }),
                    heading: parseInt(document.getElementById("sliderHeading").value),
                    tilt: parseInt(document.getElementById("sliderTilt").value)
                });

                document.querySelectorAll("[data-preset]").forEach(b => b.classList.remove("active"));
                btn.classList.add("active");
                refreshReferences();
                updateAll();
            });
        });

        // Heading 滑块
        initSlider("sliderHeading", "valHeading", "heading", (v) => {
            view.camera.heading = v; updateAll();
        });

        // Heading 预设按钮
        document.querySelectorAll("[data-heading]").forEach(btn => {
            btn.addEventListener("click", () => {
                const v = parseInt(btn.dataset.heading);
                document.getElementById("sliderHeading").value = v;
                document.getElementById("valHeading").textContent = v + "°";
                view.camera.heading = v;
                updateAll();
            });
        });

        // Tilt 滑块
        initSlider("sliderTilt", "valTilt", "tilt", (v) => {
            view.camera.tilt = v; updateAll();
        });

        document.querySelectorAll("[data-tilt]").forEach(btn => {
            btn.addEventListener("click", () => {
                const v = parseInt(btn.dataset.tilt);
                document.getElementById("sliderTilt").value = v;
                document.getElementById("valTilt").textContent = v + "°";
                view.camera.tilt = v;
                updateAll();
            });
        });

        // FOV 滑块
        initSlider("sliderFov", "valFov", "fov", (v) => {
            view.camera.fov = v; updateAll();
        });

        document.querySelectorAll("[data-fov]").forEach(btn => {
            btn.addEventListener("click", () => {
                const v = parseInt(btn.dataset.fov);
                document.getElementById("sliderFov").value = v;
                document.getElementById("valFov").textContent = v + "°";
                view.camera.fov = v;
                updateAll();
            });
        });

        // Altitude 滑块
        initSlider("sliderAltitude", "valAltitude", "altitude", (v) => {
            view.camera.position.z = v; updateAll();
        });

        // 保存视角
        document.getElementById("btnSave").addEventListener("click", () => {
            savedCamera = view.camera.clone();
            document.getElementById("statusText").textContent = "💾 视角已保存!";
            setTimeout(updateStatusText, 1500);
        });

        // 恢复视角
        document.getElementById("btnRestore").addEventListener("click", () => {
            stopAutoRotate();
            if (savedCamera) {
                view.camera = savedCamera;
                syncSlidersFromCamera();
                updateAll();
                document.getElementById("statusText").textContent = "📂 视角已恢复!";
                setTimeout(updateStatusText, 1500);
            } else {
                document.getElementById("statusText").textContent = "⚠️ 尚未保存视角,请先点击「保存视角」";
                setTimeout(updateStatusText, 2000);
            }
        });

        // 重置视角
        document.getElementById("btnReset").addEventListener("click", () => {
            stopAutoRotate();
            const p = presets[currentPreset];
            view.camera = new Camera({
                position: new Point({ longitude: p.lng, latitude: p.lat, z: 2000 }),
                heading: 0,
                tilt: 45,
                fov: 55
            });
            syncSlidersFromCamera();
            updateAll();
        });

        // 自动旋转
        document.getElementById("btnAutoRotate").addEventListener("click", () => {
            if (autoRotateInterval) {
                stopAutoRotate();
                document.getElementById("btnAutoRotate").textContent = "🔄 自动旋转";
                document.getElementById("btnAutoRotate").classList.remove("primary");
            } else {
                autoRotateInterval = setInterval(() => {
                    view.camera.heading = (view.camera.heading + 0.5) % 360;
                    document.getElementById("sliderHeading").value = Math.round(view.camera.heading);
                    document.getElementById("valHeading").textContent = Math.round(view.camera.heading) + "°";
                    updateInfo();
                    updateStatusText();
                }, 50);
                document.getElementById("btnAutoRotate").textContent = "⏸ 停止旋转";
                document.getElementById("btnAutoRotate").classList.add("primary");
            }
        });
    }

    function initSlider(sliderId, valId, type, callback) {
        const slider = document.getElementById(sliderId);
        const valSpan = document.getElementById(valId);
        slider.addEventListener("input", () => {
            const v = type === "altitude" ? parseInt(slider.value) : parseInt(slider.value);
            valSpan.textContent = v + (type === "altitude" ? "m" : "°");
            callback(v);
        });
    }

    function syncSlidersFromCamera() {
        const cam = view.camera;
        document.getElementById("sliderHeading").value = Math.round(cam.heading);
        document.getElementById("valHeading").textContent = Math.round(cam.heading) + "°";
        document.getElementById("sliderTilt").value = Math.round(cam.tilt);
        document.getElementById("valTilt").textContent = Math.round(cam.tilt) + "°";
        document.getElementById("sliderFov").value = Math.round(cam.fov);
        document.getElementById("valFov").textContent = Math.round(cam.fov) + "°";
        document.getElementById("sliderAltitude").value = Math.round(cam.position.z);
        document.getElementById("valAltitude").textContent = Math.round(cam.position.z) + "m";
    }

    function stopAutoRotate() {
        if (autoRotateInterval) {
            clearInterval(autoRotateInterval);
            autoRotateInterval = null;
            document.getElementById("btnAutoRotate").textContent = "🔄 自动旋转";
            document.getElementById("btnAutoRotate").classList.remove("primary");
        }
    }

    function updateAll() {
        updateInfo();
        updateStatusText();
    }

    function updateInfo() {
        const cam = view.camera;
        document.getElementById("infoPos").textContent =
            `经度 ${cam.position.longitude.toFixed(4)}, 纬度 ${cam.position.latitude.toFixed(4)}`;
        document.getElementById("infoAlt").textContent = Math.round(cam.position.z) + " m";
        document.getElementById("infoHeading").textContent = Math.round(cam.heading) + "°";
        document.getElementById("infoTilt").textContent = Math.round(cam.tilt) + "°";
        document.getElementById("infoFov").textContent = Math.round(cam.fov) + "°";
    }

    function updateStatusText() {
        const cam = view.camera;
        document.getElementById("statusText").textContent =
            `Camera | 朝向:${Math.round(cam.heading)}° | 俯仰:${Math.round(cam.tilt)}° | 高度:${Math.round(cam.position.z)}m | FOV:${Math.round(cam.fov)}°`;
    }
</script>
</body>
</html>

五、在线示例

🔗 在线体验地址(GitHub资源,等待时间较长,或者架梯子)https://southjor.github.io/arcgis-examples/lessons/lesson10.html

操作说明

  1. 点击位置预设按钮(北京故宫/上海外滩/广州塔/黄山),切换观察目标地点
  2. 拖动「朝向 heading」滑块 0~360°,或点击 N/E/S/W 快速设置方向(🔴红块在东、🟢绿块在南,帮助判断朝向)
  3. 拖动「俯仰 tilt」滑块 0~90°,对比俯视(0°) / 倾斜(45°) / 近地(70°) / 平视(90°) 的效果
  4. 拖动「视场角 fov」滑块 1~170°,体验长焦放大(20°) / 默认(55°) / 广角(100°) / 鱼眼(150°)
  5. 拖动「海拔高度」滑块 50~10000m,升空俯瞰或低空贴近
  6. 点击「💾 保存视角」保存当前相机,浏览后点击「📂 恢复视角」回到保存点
  7. 点击「🔄 自动旋转」让 heading 持续旋转 0.5°/帧,环绕观察地标

六、关键API说明

API 类型 默认值 说明
Camera.position Point --- 相机位置:经度 + 纬度 + z(海拔高度,米)
Camera.heading number 0 水平朝向角,0°=正北,顺时针 0~360°
Camera.tilt number 0 俯仰角,0°=垂直俯视,90°=水平平视
Camera.fov number 55 对角线视场角,1~170°,越小越长焦
view.camera Camera --- 获取/设置当前相机(直接赋值无动画)
camera.clone() Camera --- 深拷贝相机,用于保存/恢复视角

Camera vs goTo 对比

方式 动画 适用场景
view.camera = new Camera(...) ❌ 无动画,瞬间切换 初始设置、书签跳转、程序控制
view.goTo(...) ✅ 平滑过渡动画 用户交互、POI 缩放(见第11课)

常见坑点

问题 原因 解决方案
tilt 设为 120° 无效 tilt 有效范围是 0~90° 使用 0~90 之间的值
修改 cam.heading 没反应 view.camera 返回引用,需整体赋值 使用 view.camera = camview.camera.heading = 90
旋转 heading 后场景跳变 heading 从 360 回到 0 产生突变 用取模运算 heading % 360
保存的视角恢复后不一样 直接赋值引用而非 clone 使用 saved = view.camera.clone()

七、系列导航

⬅️ 上一篇ArcGIS JS 基础教程(9):天空盒与大气效果

➡️ 下一篇ArcGIS JS 基础教程(11):飞行定位 goTo


💡 小贴士 :Camera 的四个属性 positionheadingtiltfov 组合在一起,几乎可以表达任何三维观察视角。掌握它们就掌握了三维场景的"镜头语言":想看宏观总览就用高 altitude + 大 tilt;想看建筑细节就用低 altitude + 小 fov;想环绕观察就用动态 heading。下一课我们将学习 goTo() 方法,为 Camera 加上平滑的飞行动画。

相关推荐
码语智行17 小时前
Shapefile获取空间数据和中心点坐标
java·arcgis
码语智行19 小时前
地图上图、空间拓扑查询示例
java·arcgis
DXM05211 天前
第10期| 卷积神经网络CNN通俗详解:AI遥感的底层核心
人工智能·python·神经网络·机器学习·arcgis·cnn·文心一言
智航GIS2 天前
ArcGIS大师之路500技---078补零
arcgis
DXM05213 天前
第8期| 传统机器学习遥感解译:SVM & 随机森林分类全流程实操
人工智能·python·随机森林·机器学习·支持向量机·arcgis·自然语言处理
非科班Java出身GISer3 天前
ArcGIS JS 基础教程(9):天空盒与大气效果
arcgis·arcgis js 天空盒·arcgis js 大气效果·arcgis js 大气层·arcgis 场景背景
智航GIS4 天前
ArcGIS大师之路500技---078文件数据库的加密与解密
数据库·arcgis
步十人4 天前
【Vue3】前置知识简单概述(包括ES6核心语法,模块化ESM以及npm基础)
arcgis·npm·vue·es6
Lucky_云佳5 天前
ArcMap-去除底图水印
经验分享·arcgis·arcmap·google earth