ArcGIS JS 基础教程(8):环境光照与阴影

ArcGIS JS 基础教程(8):环境光照与阴影

零、写在前面

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

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

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

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


一、功能介绍

在三维场景中,光照是决定场景真实感的核心要素。ArcGIS Maps SDK for JavaScript 提供了 environment.lighting API,5.0 版本中有两种光照类型:

  • SunLightingtype: "sun"):模拟真实太阳,通过 date 属性指定日期时间,SDK 会根据场景地理位置自动计算太阳的方位角和高度角
  • VirtualLightingtype: "virtual"):虚拟光照,光方向跟随相机,适合无特定地理位置的场景

配合 directShadowsEnabled 属性和图层的 castShadows / receiveShadows 配置,还能开启真实的阴影投射------物体遮挡阳光后在地面或其他物体上留下阴影,大幅提升三维场景的视觉真实度。


二、功能实现

核心 API: view.environment.lighting,类型为 SunLighting | VirtualLighting

在 5.0 版本中,可以通过设置 date 属性让 SDK 自动计算太阳方向:

javascript 复制代码
// SunLighting:根据日期时间自动计算太阳位置
view.environment.lighting = {
    type: "sun",                              // 使用 SunLighting
    date: new Date(2026, 5, 1, 17, 0, 0),     // 2026年6月1日 17:00
    directShadowsEnabled: true                 // 开启阴影
};

SDK 会根据场景的地理位置(经纬度)和传入的 date 自动计算太阳的方位角和高度角。设置不同的时间就能模拟清晨、正午、傍晚等不同时段的真实光照效果。

💡 注意: SunLighting 没有 azimuth/altitude 属性,太阳方向只能通过 date 驱动。如果想手动控制光方向(非现实模拟),可以使用 VirtualLighting(光方向跟随相机)。

阴影配置(三层开关):

开启阴影需要三个条件同时满足

层级 配置 说明
全局 view.environment.lighting.directShadowsEnabled = true 场景整体阴影开关
图层投射 layer.castShadows = true 该图层的物体可以投射阴影
图层接收 layer.receiveShadows = true 该图层可以接收其他物体的阴影
javascript 复制代码
// 全局开关
view.environment.lighting.directShadowsEnabled = true;

// 图层级别
const graphicsLayer = new GraphicsLayer({
    castShadows: true,
    receiveShadows: true
});

搭配 3D 物体观察阴影:

仅使用地形(DEM)来观察阴影效果不明显,推荐在场景中放置 3D 物体 (如方块、柱子)来直观地观察阴影的方向、长度变化。可通过 Mesh.createBox() 创建立方体,放入 GraphicsLayer 中并开启阴影(Mesh.createBoxPoint.z 是底部中心,设为 0 即可贴地)。


三、功能应用

应用场景 光照策略 阴影需求 说明
日照分析(城市规划) SunLighting + date,模拟冬至/夏至 必须开启 评估新建建筑对周边采光的影响
实时阴影观察 SunLighting + 滑动时间轴 必须开启 直观展示不同时段建筑阴影变化
清晨/黄昏景观 SunLighting,date 设为 6:00 或 19:00 建议开启(阴影长) 烘托氛围的摄影视角
正午光照模拟 SunLighting,date 设为 12:00 关闭或减弱 阴影最短,适合平面观察
智慧城市大屏 SunLighting,date 驱动当前真实时间 选择性开启 随真实时间变化的光照效果
虚拟光照 VirtualLighting(光方向跟随相机) 可开启 无特定地理位置的演示场景

四、核心代码

📦 完整代码 已保存至 sample/lesson11_lighting_shadows.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>第8课:环境光照与阴影</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: 260px;
        }
        .control-panel h3 { margin: 0 0 12px 0; font-size: 14px; color: #333; }
        .form-group { margin-bottom: 12px; }
        .form-group label { display: block; font-size: 12px; color: #666; margin-bottom: 4px; }
        .form-group input[type="range"] { width: 100%; }
        .form-group input[type="checkbox"] { margin-right: 6px; }
        .form-group input[type="datetime-local"] { width: 100%; padding: 4px; }
        .preset-row { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
        .preset-row button {
            padding: 4px 10px; font-size: 12px;
            border: 1px solid #d9d9d9; border-radius: 4px;
            background: white; cursor: pointer;
        }
        .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: 12px; }
        .btn-row button {
            flex: 1; padding: 8px 0;
            border: 1px solid #d9d9d9; border-radius: 4px;
            background: white; cursor: pointer; font-size: 13px;
        }
        .btn-row button:hover { border-color: #1890ff; 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;
        }
    </style>
</head>
<body>
<h1 class="page-title">第8课:环境光照与阴影</h1>

<div class="control-panel">
    <h3>☀️ 太阳时间控制</h3>
    <div class="form-group">
        <label>时间(小时):<span id="valHour">14:00</span></label>
        <input type="range" id="sliderHour" min="5" max="21" value="14" step="0.5">
    </div>
    <div class="preset-row">
        <button data-hour="6">🌅 清晨</button>
        <button data-hour="12">☀️ 正午</button>
        <button data-hour="16">🌤 午后</button>
        <button data-hour="19">🌇 傍晚</button>
    </div>
    <div class="form-group" style="margin-top:12px;">
        <label>完整日期选择:</label>
        <input type="datetime-local" id="datetimePicker" style="width:100%;padding:4px;">
    </div>

    <h3 style="margin-top:16px;">🌑 阴影控制</h3>
    <div class="form-group">
        <label><input type="checkbox" id="chkDirectShadows" checked> 开启直接阴影(directShadowsEnabled)</label>
    </div>
    <div class="btn-row">
        <button id="btnResetLight">重置时间</button>
        <button id="btnToggleShadows">切换阴影</button>
    </div>
</div>

<div class="status-text" id="statusText">时间:14:00 | 阴影:开启</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 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 Point = await $arcgis.import("@arcgis/core/geometry/Point.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 view = new SceneView({
        container: "mapContainer", map: map,
        center: [116.39, 39.9], zoom: 17, tilt: 65
    });

    function addShadowObjects() {
        // Mesh.createBox: Point.z 是底部中心 = 0 贴地
        const boxGraphic = new Graphic({
            geometry: Mesh.createBox(
                new Point({ longitude: 116.392, latitude: 39.902, z: 0 }),
                { size: { width: 200, height: 200, depth: 200 } }
            ),
            symbol: { type: "mesh-3d", symbolLayers: [{ type: "fill", material: { color: [255, 165, 0, 0.95] } }] }
        });

        const smallBox = new Graphic({
            geometry: Mesh.createBox(
                new Point({ longitude: 116.3935, latitude: 39.901, z: 0 }),
                { size: { width: 100, height: 150, depth: 100 } }
            ),
            symbol: { type: "mesh-3d", symbolLayers: [{ type: "fill", material: { color: [70, 130, 180, 0.95] } }] }
        });

        const pillar = new Graphic({
            geometry: Mesh.createBox(
                new Point({ longitude: 116.391, latitude: 39.9005, z: 0 }),
                { size: { width: 30, height: 300, depth: 30 } }
            ),
            symbol: { type: "mesh-3d", symbolLayers: [{ type: "fill", material: { color: [50, 205, 50, 0.95] } }] }
        });

        map.add(new GraphicsLayer({
            graphics: [boxGraphic, smallBox, pillar],
            castShadows: true, receiveShadows: true, title: "阴影观察物体"
        }));
    }

    view.when(() => { addShadowObjects(); initLighting(); updateStatus(); });

    function initLighting() {
        applySunDate(getDateWithHour(14));

        document.getElementById("sliderHour").addEventListener("input", (e) => {
            const h = parseFloat(e.target.value);
            applySunDate(getDateWithHour(h));
            document.getElementById("valHour").textContent = formatHour(h);
            clearPresetActive(); updateStatus();
        });

        document.querySelectorAll(".preset-row button").forEach(btn => {
            btn.addEventListener("click", () => {
                const h = parseFloat(btn.dataset.hour);
                document.getElementById("sliderHour").value = h;
                document.getElementById("valHour").textContent = formatHour(h);
                applySunDate(getDateWithHour(h));
                clearPresetActive(); btn.classList.add("active"); updateStatus();
            });
        });

        const dp = document.getElementById("datetimePicker");
        dp.value = formatDateLocal(getDateWithHour(14));
        dp.addEventListener("change", (e) => {
            const d = new Date(e.target.value);
            applySunDate(d);
            const h = d.getHours() + d.getMinutes() / 60;
            document.getElementById("sliderHour").value = h;
            document.getElementById("valHour").textContent = formatHour(h);
            clearPresetActive(); updateStatus();
        });
    }

    function applySunDate(date) {
        view.environment.lighting = {
            type: "sun", date: date,
            directShadowsEnabled: document.getElementById("chkDirectShadows").checked
        };
    }

    function getDateWithHour(h) {
        const d = new Date();
        d.setHours(Math.floor(h), Math.round((h % 1) * 60), 0, 0);
        return d;
    }

    document.getElementById("chkDirectShadows").addEventListener("change", (e) => {
        view.environment.lighting.directShadowsEnabled = e.target.checked; updateStatus();
    });

    document.getElementById("btnResetLight").addEventListener("click", () => {
        const h = 14;
        document.getElementById("sliderHour").value = h;
        document.getElementById("valHour").textContent = formatHour(h);
        applySunDate(getDateWithHour(h)); clearPresetActive(); updateStatus();
    });

    document.getElementById("btnToggleShadows").addEventListener("click", () => {
        const c = view.environment.lighting.directShadowsEnabled;
        view.environment.lighting.directShadowsEnabled = !c;
        document.getElementById("chkDirectShadows").checked = !c; updateStatus();
    });

    function clearPresetActive() {
        document.querySelectorAll(".preset-row button").forEach(b => b.classList.remove("active"));
    }

    function formatHour(h) {
        const hh = Math.floor(h), mm = Math.round((h % 1) * 60);
        return String(hh).padStart(2, "0") + ":" + String(mm).padStart(2, "0");
    }

    function formatDateLocal(d) {
        const y = d.getFullYear(), m = String(d.getMonth() + 1).padStart(2, "0");
        const dd = String(d.getDate()).padStart(2, "0");
        const hh = String(d.getHours()).padStart(2, "0");
        const mm = String(d.getMinutes()).padStart(2, "0");
        return `${y}-${m}-${dd}T${hh}:${mm}`;
    }

    function updateStatus() {
        const l = view.environment.lighting;
        const hour = l.date ? formatHour(l.date.getHours() + l.date.getMinutes() / 60) : "--:--";
        document.getElementById("statusText").textContent =
            `时间:${hour}(UTC+8) | 阴影:${l.directShadowsEnabled ? "开启" : "关闭"}`;
    }
</script>
</body>
</html>

五、在线示例

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

操作说明

  1. 拖动「时间」滑块(5:00-21:00),观察太阳位置变化及三个方块的阴影变化
  2. 点击「🌅 清晨 / ☀️ 正午 / 🌤 午后 / 🌇 傍晚」预设,快速切换时段
  3. 勾选/取消「开启直接阴影」观察阴影开关效果
  4. 使用「完整日期选择器」可指定具体年月日时分
  5. 三个方块(橙/蓝/绿)分别是 200m³、150m³、300m 细高柱子,便于对比阴影

六、关键API说明

API 说明
SunLightingtype: "sun" 5.0 模拟太阳光照类,通过 date 自动计算太阳方向
VirtualLightingtype: "virtual" 5.0 虚拟光照类,光方向跟随相机移动
lighting.date Date 对象,SDK 根据地理位置自动计算太阳方位角/高度角
lighting.directShadowsEnabled 场景阴影开关(boolean),SunLighting/VirtualLighting 均支持
layer.castShadows 图层级:该图层物体能否投射阴影
layer.receiveShadows 图层级:该图层能否接收其他物体的阴影
Mesh.createBox(point, options) 创建立方体 Mesh,Point.z 为盒体底部中心

常见坑点

问题 原因 解决方案
azimuth/altitude 属性不存在 SunLighting 和 VirtualLighting 都没有这两个属性 使用 date 驱动 SunLighting 自动计算
阴影不显示 需同时满足:directShadowsEnabled + castShadows + receiveShadows 检查三层开关
Mesh.createBox 物体悬浮 Point.z底部中心,非几何中心 z=0 即可贴地
DirectionalLighting 类不存在 这是 4.x 的类,5.0 API 已移除 SunLightingVirtualLighting

七、系列导航

⬅️ 上一篇ArcGIS JS 基础教程(7):Global与Local场景模式


💡 小贴士directShadowsEnabled 可以单独设为 false 来关闭场景全局阴影,即使图层的 castShadowsreceiveShadowstrue 也不会渲染阴影------这在你需要临时提升性能时非常有用。

相关推荐
Highcharts.js1 天前
如何创建蛛网地图|气泡事件+全球发布+关联组合图表开发示例
arcgis·地图系列·highcharts·动态图表·可视化开发·气泡图·地图图表
城数派4 天前
2026年500米分辨率DEM地形数据(全球/全国/分省/分市)
数据库·arcgis·信息可视化·数据分析·excel
天青色等烟雨..6 天前
R+VIC模型融合实践技术应用及未来气候变化模型预测
大数据·人工智能·arcgis·语言模型·数据分析
玩大数据的龙威9 天前
【ArcGIS技巧】—防止注记压盖之引出标注
arcgis
逆境不可逃10 天前
【与我学 ClaudeCode】并发篇 之 Background Tasks :守护线程与异步通知队列
人工智能·arcgis·agent
中科GIS地理信息培训11 天前
【ArcGIS Pro 3.7新增功能】使用交互式等高线可视化栅格数据
arcgis·arcgis pro
非科班Java出身GISer11 天前
ArcGIS JS 基础教程(7):Global与Local场景模式
arcgis·arcgis js 教程·arcgis js 基础·arcgis js 场景模式·arcgis js 切换模式
中科GIS地理信息培训11 天前
【ArcGIS Pro 3.7新增功能2】新型高光谱图像工具:连续谱去除、PCA与 MNF 降低数据复杂性、使用波长直接计算、支持STAC等
人工智能·arcgis·目标跟踪
杨超越luckly11 天前
HTML应用指南:利用GET请求获取智己汽车门店位置信息
python·arcgis·html·汽车·数据可视化