零、写在前面
📌 本系列教程完整目录: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

操作说明:
- 拖动「时间」滑块(5:00-21:00),观察太阳位置变化及三个方块的阴影变化
- 点击「🌅 清晨 / ☀️ 正午 / 🌤 午后 / 🌇 傍晚」预设,快速切换时段
- 勾选/取消「开启直接阴影」观察阴影开关效果
- 使用「完整日期选择器」可指定具体年月日时分
- 三个方块(橙/蓝/绿)分别是 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即可恢复。