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

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

零、写在前面

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

💡 在线示例:完整可运行的 HTML 示例,无需任何环境配置,可直接在浏览器中打开体验
🗂️ 专栏导航:收藏 + 关注,专栏文章第一时间送达
❤️ 一键三连:点赞(给教程充电)+ 评论(提问必回)+ 收藏(下次再看)


一、功能介绍

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

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

配合 directShadowsEnabled 属性还能开启真实的阴影投射:物体遮挡阳光后,在地面或其他物体上留下阴影(物体是否投射阴影由其符号的 castShadows 控制,默认开启)。,大幅提升三维场景的视觉真实度。


二、功能实现

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

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

// 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(光方向跟随相机)。

阴影配置:

阴影由全局开关控制,物体是否投射由其符号的 castShadows 决定:

层级 配置 说明
全局 view.environment.lighting.directShadowsEnabled = true 场景整体阴影开关(默认开启)
物体 符号层的 castShadows(如 ObjectSymbol3DLayer.castShadows) 该物体是否投射阴影(默认开启)
// 全局开关
view.environment.lighting.directShadowsEnabled = true;

搭配 3D 物体观察阴影:

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


三、功能应用

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

四、核心代码

📦 完整代码 已保存至 sample/lesson11_lighting_shadows.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
    });

    // ===== 添加 3D 方块用于观察阴影 =====
    function addShadowObjects() {
    
        // Mesh.createBox 的 Point.z 是盒体底部中心(非几何中心)
        // 高 200m 的盒子,z=0 表示盒子底部贴在地面上
        const boxMesh = Mesh.createBox(
            new Point({
     longitude: 116.392, latitude: 39.902, z: 0 }),
            {
    
                size: {
     width: 200, height: 200, depth: 200 }
            }
        );
        const boxGraphic = new Graphic({
    
            geometry: boxMesh,
            symbol: {
    
                type: "mesh-3d",
                symbolLayers: [{
     type: "fill", material: {
     color: [255, 165, 0, 0.95] } }]
            }
        });

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

        // 细高柱子,高 300m,方便观察长阴影
        const pillarMesh = Mesh.createBox(
            new Point({
     longitude: 116.391, latitude: 39.9005, z: 0 }),
            {
    
                size: {
     width: 30, height: 300, depth: 30 }
            }
        );
        const pillarGraphic = new Graphic({
    
            geometry: pillarMesh,
            symbol: {
    
                type: "mesh-3d",
                symbolLayers: [{
     type: "fill", material: {
     color: [50, 205, 50, 0.95] } }]
            }
        });

        const boxLayer = new GraphicsLayer({
    
            graphics: [boxGraphic, smallBoxGraphic, pillarGraphic],
            title: "阴影观察物体"
        });

        map.add(boxLayer);
    }

    // ===== 等待视图就绪 =====
    view.when(() => {
    
        addShadowObjects();
        initLighting();
        updateStatus();
    });

    // ===== 初始日期(今天 14:00 北京时区 = UTC+8) =====
    const baseDate = new Date();
    baseDate.setHours(14, 0, 0, 0);

    // ===== 初始化光照 =====
    function initLighting() {
    
        // 初始:SunLighting, 今天14:00, 开启阴影
        applySunDate(getDateWithHour(14));

        // 时间滑块
        document.getElementById("sliderHour").addEventListener("input", (e) => {
    
            const hour = parseFloat(e.target.value);
            applySunDate(getDateWithHour(hour));
            document.getElementById("valHour").textContent = formatHour(hour);
            clearPresetActive();
            updateStatus();
        });

        // 时间预设按钮
        document.querySelectorAll(".preset-row button").forEach(btn => {
    
            btn.addEventListener("click", () => {
    
                const hour = parseFloat(btn.dataset.hour);
                document.getElementById("sliderHour").value = hour;
                document.getElementById("valHour").textContent = formatHour(hour);
                applySunDate(getDateWithHour(hour));

                clearPresetActive();
                btn.classList.add("active");
                updateStatus();
            });
        });

        // 日期选择器
        const datePicker = document.getElementById("datetimePicker");
        datePicker.value = formatDateLocal(baseDate);

        datePicker.addEventListener("change", (e) => {
    
            const selected = new Date(e.target.value);
            applySunDate(selected);
            // 同步滑块
            const h = selected.getHours() + selected.getMinutes() / 60;
            document.getElementById("sliderHour").value = h;
            document.getElementById("valHour").textContent = formatHour(h);
            clearPresetActive();
            updateStatus();
        });
    }

    // 用 date 构造 SunLighting 配置,设置光照时间
    function applySunDate(date) {
    
        view.environment.lighting = {
    
            type: "sun",
            date: date,
            directShadowsEnabled: document.getElementById("chkDirectShadows").checked
        };
    }

    // 获取指定时间(小时)的 Date 对象,保留今天的年月日
    function getDateWithHour(hour) {
    
        const d = new Date();
        d.setHours(Math.floor(hour), Math.round((hour % 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 current = view.environment.lighting.directShadowsEnabled;
        view.environment.lighting.directShadowsEnabled = !current;
        document.getElementById("chkDirectShadows").checked = !current;
        updateStatus();
    });

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

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

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

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

五、在线示例

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

在这里插入图片描述

操作说明:

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

六、关键API说明

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

常见坑点

问题 原因 解决方案
azimuth/altitude 属性不存在 SunLighting 和 VirtualLighting 都没有这两个属性 使用 date 驱动 SunLighting 自动计算
阴影不显示 directShadowsEnabled 被关闭,或物体符号的 castShadows 被关闭 依次检查全局开关与符号层 castShadows
Mesh.createBox 物体悬浮 Point.z 是底部中心,非几何中心 z=0 即可贴地

七、系列导航

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

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

](https://webgis.blog.csdn.net/article/details/161596897?spm=1011.2415.3001.5331)

💡 小贴士:directShadowsEnabled 设为 false 可关闭整个场景的阴影渲染,临时提升性能时很有用;重新设为 true 即可恢复。

相关文章
|
8月前
|
人工智能 数据可视化 IDE
上新!阿里云百炼Coding Plan全面支持Qoder直接调用
阿里云Coding Plan订阅服务全面接入Qoder编程平台,用户可免费调用Qwen 3.5、GLM-5、Kimi-K2.5等多模型,覆盖推理、编程、多模态与Agent能力。Qoder通过增强上下文工程、Quest智能体与知识可视化,显著提升模型输出质量与开发效率,大幅降低AI编程成本。
|
9月前
|
人工智能 自然语言处理 网络安全
2026年阿里云部署OpenClaw(Clawdbot)快速接入钉钉保姆级教程
2026年,AI自动化办公与企业IM协作深度融合,OpenClaw(前身为Clawdbot、Moltbot)作为轻量级开源AI代理平台,凭借“自然语言指令驱动、多工具协同、零编程门槛”的核心优势,成为个人与企业解锁“AI+IM”协同办公的首选工具。它无需复杂操作,仅需输入口语化指令,就能自动完成文档整理、邮件处理、日程规划、跨平台数据同步等重复性工作,而阿里云推出的OpenClaw一键部署方案,彻底解决了传统部署流程繁琐、环境配置复杂的痛点,新手10分钟即可完成云端部署。
7214 4
|
12天前
|
JavaScript 定位技术 API
ArcGIS JS 基础教程(4):地图中心点定位(指定经纬度/地址)
ArcGIS JS 基础教程(4):地图中心点定位(指定经纬度/地址)
|
4天前
|
JavaScript 数据可视化 物联网
ArcGIS JS 基础教程(7):Global与Local场景模式
ArcGIS JS 基础教程(7):Global与Local场景模式
|
21天前
|
前端开发 JavaScript 数据可视化
ArcGIS JSAPI 学习教程 - 初识 ArcGIS Maps SDK for JavaScript
本文为ArcGIS Maps SDK for JavaScript(JSAPI)入门首篇,涵盖概览、官方示例与API速览,完成4.28版HelloWorld实践,并简析其功能强大(二维/三维兼备、分析渲染丰富、免费闭源)与局限(社区小、扩展难、接口受限)。
|
19天前
|
JavaScript 前端开发 定位技术
ArcGIS Maps SDK for JavaScript 5.0 组件(Component)地图初始化示例
ArcGIS Maps SDK for JavaScript 5.0 组件(Component)地图初始化示例
|
18天前
|
JavaScript 前端开发 数据可视化
ArcGIS JS 基础教程(1):地图初始化(含AMD/ESM两种引入方式)
ArcGIS JS 基础教程(1):地图初始化(含AMD/ESM两种引入方式)
|
17天前
|
JavaScript 数据可视化 定位技术
ArcGIS JS 基础教程(2):地图切换底图(天地图)
ArcGIS JS 基础教程(2):地图切换底图(天地图)
|
14天前
|
JavaScript 数据可视化 定位技术
ArcGIS JS 基础教程(3):地图缩放、平移、旋转(基础交互)
ArcGIS JS 基础教程(3):地图缩放、平移、旋转(基础交互)
|
10天前
|
JavaScript 数据可视化 定位技术
ArcGIS JS 基础教程(5):地图限制缩放级别和显示范围
ArcGIS JS 基础教程(5):地图限制缩放级别和显示范围