拿到一份 DEM 数据,想要把它变成一张能放在报告封面、论文配图甚至社交媒体上的立体地形图,R 语言的 rayshader 包是目前我试过最顺手、效果也最惊艳的方案。它能在几分钟内把平淡的栅格高程数据渲染成有光影、有景深、可以自由旋转视角的 3D 模型,而且全程不用离开 R 环境。这篇指南里我会把从数据获取、预处理到 3D 渲染、导出成图的完整流程拆开讲清楚,包含我踩过的坑和一些能直接抄作业的参数配置,适合刚接触地形可视化、或者已经在 R 里处理过空间数据但想进一步提升出图质量的读者。
1. 项目概述与核心思路
1.1 这到底在解决什么问题
做地理数据分析的人应该都有这种体会:DEM 数据本身只是一个二维的数值矩阵,每一个像元记录的是该位置的海拔高度,虽然可以拉伸成灰度图或者用色带渲染成平面图,但人眼对三维形态的感知能力远比对颜色的感知能力敏锐得多。一个河谷的切割深度、一座火山的锥形轮廓、一条山脉的走向趋势,在等高线图或者平面渲染图里往往要盯着看半天才能浮现出来,而一旦转成 3D 透视图,整个地形的结构关系能在一秒钟之内被大脑捕获。rayshader 解决的就是这个“从数字到形态”的关键一步,它把高程矩阵映射为三维曲面,再通过模拟太阳光照、添加阴影纹理、叠加或影像配色,生成接近真实地貌视觉效果的可视化成果。
1.2 为什么选择 R 语言加 rayshader 的组合
做地形 3D 渲染其实有好几条技术路线:Blender 加 GIS 插件、QGIS 的 qgis2threejs、Cesium 或 Mapbox 这类 WebGIS 方案、还有 Python 的 pyvista 等。这些工具我多少都碰过,但回到日常数据分析工作流里,它们多多少少存在割裂感。数据清洗、统计建模在 R 里完成,换个工具做可视化就免不了导出导入和格式转换的麻烦。rayshader 最大的优势是直接嵌在 R 的生态里,能用 tidyverse、sf、raster、terra 处理完数据之后立刻渲染,整个环节不需要跳出 RStudio。另外一个原因在于 rayshader 本身的可控性相当高:光源角度、阴影强度、水面反射、雾效、相机位置都可以一一调整,相比黑盒式的在线工具,它给使用者保留了非常充足的创作空间。
1.3 从 DEM 到 3D 渲染的整体管线
可以把完整流程拆成四个环节:数据准备、数据预处理、3D 场景构建、渲染输出。数据准备阶段解决“DEM 从哪里来”的问题,常见选择包括本地已有的 GeoTIFF 文件,或者通过 elevatr 包直接下载全球范围的开源高程数据。预处理阶段负责统一坐标系、裁剪范围、处理缺失值,必要时可以做轻微的平滑或重采样,让数据更利于后续渲染。3D 场景构建是 rayshader 的主场,包括把高度矩阵转成纹理贴图、设置相机视角、调整光照阴影、叠加水体图层等。最后是渲染输出,可以选择快速出图用于预览,也可以启用高质量渲染获得适合打印或出版的超大分辨率图片。
2. 环境准备与数据获取
2.1 安装 rayshader 及依赖环境的注意事项
rayshader 的安装整体不难,但如果你用的是 Windows 且 R 版本较旧,个别依赖包可能需要预先配置。基础安装只需要一条命令:
install.packages("rayshader")建议同时安装配套的空间数据处理包,后面做数据裁剪和投影转换时都会用到:
install.packages(c("elevatr", "terra", "sf", "magrittr"))Windows 用户容易忽略的一个坑是 Rtools。如果你的计划是从 GitHub 安装开发版 rayshader,或者有些依赖包需要从源码编译,没有 Rtools 会直接报错。Rtools 的安装版本需要与当前 R 版本对应,这一点可以在 CRAN 的 Rtools 页面确认。我首次装的时候就是 R 4.3 配了旧版 Rtools,排查半天才发现版本不匹配。
Linux 和 macOS 用户则需要额外留意系统层面的图形库。这是因为 rayshader 底层会调用一些 C++ 图形处理代码,如果系统缺少相关的依赖库,编译阶段会抛出找不到头文件的错误。Ubuntu 系的话,建议提前安装:
sudo apt install libgdal-dev libproj-dev libssl-dev2.2 获取 DEM 数据:elevatr 包下载与本地数据读取
本地已经有 GeoTIFF 格式的 DEM 数据的话,用 terra 包读取即可:
library(terra) dem <- rast("your_dem.tif")没有现成数据也不想自己去各个数据平台翻的话,elevatr 包提供了一条便捷通道。它在后端调用 AWS Terrain Tiles 提供的开源高程数据,可以根据你定义的地理范围自动裁剪和拼接,默认使用的是 Mapzen 的高程服务。核心代码非常简洁:
library(elevatr) library(sf) # 定义一个范围,比如某个研究区域的边界框 bounds <- st_as_sfc(st_bbox(c(xmin = 116.3, ymin = 39.8, xmax = 116.6, ymax = 40.2), crs = 4326)) dem_elevatr <- get_elev_raster(bounds, z = 10)其中 z 参数控制数据分辨率,z 值越高瓦片越精细、数据量也越大。z = 10 大约对应 30 米级别的分辨率,适合展示大范围地形;如果关注的是一个小流域或者山体,可以调高到 z = 12 或 13。不过要注意下载时间会随 z 值快速上升,第一次使用建议从 z = 9 或 10 开始测试流程。
还有一点值得提醒:get_elev_raster 返回的是 RasterLayer 对象,在 terra 体系下可以用 rast() 转换一下统一数据类型。同时,不同渠道拿到的 DEM 数据坐标系可能不同,渲染本身不要求投影坐标系,但经纬度坐标系(WGS84)在后续涉及距离或面积计算时会有变形问题,建议提前用 project() 转换到合适的投影坐标系。
2.3 DEM 数据的预处理:裁剪、重采样、缺失值处理
拿到原始 DEM 之后,不要急着渲染,先做三件小事。第一是裁剪到目标区域,把无关的周边地形去掉,这样既能减少内存占用,也能避免无关地形抢走视觉焦点。可以用 terra 的 crop 和 mask 操作完成。第二是检查缺失值。SRTM 这类数据在陡峭山区和水体区域偶尔会出现空值,直接渲染的话这些地方会呈现异常的凹陷或尖刺,需要用 focal 函数做平滑插值或用邻近像元填充。第三是确认数据范围是否需要重采样。如果 DEM 分辨率太高、范围又很大,意味着矩阵规模可能是上万乘上万,不仅渲染慢,还有可能导致显卡或内存溢出。可以先用聚合降低分辨率,比如用 aggregate 做因子为 2 或 3 的聚合,牺牲一点细节换取流程的顺畅。这一点在实际项目中非常常用,我一般预览阶段用低分辨率数据,最终出图再换回全分辨率。
3. 核心渲染实现:从高度矩阵到 3D 视图
3.1 数据格式的转换逻辑
rayshader 的核心渲染函数接收的不是 Spatial 对象或 RasterLayer,而是普通的数值矩阵(matrix)。这个矩阵的每一个值对应一个像元的高度,行和列则对应地物的平面坐标。所以渲染前的关键一步是把 terra 的 SpatRaster 转成矩阵。这个过程本身很简单,但有一个方向问题需要注意:矩阵的行方向和地理坐标的南北方向可能相反,渲染出来的地形会出现上下颠倒的情况。处理方法是:
library(rayshader) library(terra) # 读取 DEM dem <- rast("your_dem.tif") # 转换为矩阵 mat <- matrix( extract(dem, ext(dem), cells = TRUE)[, 3], nrow = nrow(dem), ncol = ncol(dem), byrow = TRUE ) # 如果地形上下颠倒,翻转矩阵 mat <- apply(mat, 2, rev)实际上 rayshader 社区更常见的做法是直接使用raster_to_matrix()函数,它会把 RasterLayer 对象转成矩阵并自动处理方向问题。配合 elevatr 下载的数据,整个过程可以写成:
dem_raster <- raster::raster(dem_elevatr) mat <- rayshader::raster_to_matrix(dem_raster)3.2 第一次 3D 渲染:玩转 plot_3d 的关键参数
矩阵准备好之后,最简单的 3D 渲染只需要一行核心函数:
mat %>% height_shade(texture = texture_desert()) %>% plot_3d(heightmap = mat, zscale = 100, solid = TRUE, shadow = TRUE)这行代码做了两件事:height_shade 根据高度生成一张色彩纹理图,相当于给地形“涂上颜色”,然后 plot_3d 把这个带有纹理的高度地图渲染成 3D 场景。关键在于 zscale 参数,它控制垂直方向的比例系数。由于 DEM 数据中一个像元的平面距离动辄几十上百米,而海拔变化可能只有几百米,如果不做垂直拉伸,渲染出来的地形会平坦得像一块板子;zscale 设置得越小,垂直方向的拉伸越明显。经验值是:平坦区域、大范围场景用 200 到 500;山地、小范围场景用 20 到 80。可以多试几个值找到视觉效果最舒服的倍数。
plot_3d 还有一些值得调整的视觉参数。solid 控制是否在底部生成实体块,可以让地形看起来像悬浮的模型;shadow 控制是否生成阴影,打开后会在地形底部产生一个深色投影,空间感更强。threejs 或 rgl 窗口打开后,你可以用鼠标拖拽旋转视角、滚轮缩放,从不同角度检查地形的形态。
3.3 让地形更真实:光照、阴影和视角控制
第一次渲染出来可能只是“有色地形”,还不够“3D”。这里的关键在于 add_shadow 和光照角度。可以这样操作:
mat %>% height_shade(texture = texture_desert()) %>% add_shadow(lamb_shade(mat, zscale = 100, sunaltitude = 30, sunangle = 45), 0.5) %>% plot_3d(heightmap = mat, zscale = 100, windowsize = c(800, 600))lamb_shade 函数模拟的是朗伯体漫反射光照,sunaltitude 是太阳高度角(单位是度),数值越小光线越倾斜、地形的阴影和立体感越强;sunangle 是太阳方位角,改变它会改变阴影的方向,相当于移动太阳的位置,让山脉的明暗关系发生变化。处理完光照之后,用 add_shadow 把阴影叠加到纹理图上,第二个参数控制阴影的透明度。
视角控制同样重要。plot_3d 的默认视角是 45 度左右的俯视,但很多场景下低角度视角更有冲击力。可以通过 render_camera 函数精细控制:
render_camera(phi = 35, theta = 30, zoom = 0.6)phi 是相机与水平面的夹角,可以理解为俯仰角;theta 是相机绕地形旋转的角度,相当于圆周方向的位置;zoom 是缩放系数。建议在 RStudio 的 rgl 窗口里先手动旋转找到一个心仪的角度,再把对应的 phi/theta 值填进代码,保证后期重新渲染时视角一致。
4. 视觉增强与配色方案选择
4.1 如何根据地形类型选择合适的纹理色带
配色是决定一张地形图质感的核心因素。rayshader 内置了不少纹理函数,比如 texture_desert() 适合干旱区,texture_imagination() 偏艺术化,texture_water() 适合水文主题。但内置纹理有时候满足不了需求,好在这个包提供了非常灵活的配色机制,核心是使用 colorRampPalette 自定义色带:
custom_palette <- colorRampPalette(c("#285942", "#4B8C5A", "#A8B85A", "#E8D9A0", "#D9B36C", "#A67B5B"))(256) mat %>% height_shade(texture = custom_palette) %>% plot_3d(heightmap = mat, zscale = 100)自定义色带的逻辑并不复杂:想一想这个地区的地貌特征,低海拔是河谷常绿植被就用偏深的绿色,中山区是草地或裸岩就过渡到黄褐色,高海拔是裸岩或积雪就用灰色到白色收尾。这样渲染出来的图不仅能表达高度,还能让读者一眼理解地表覆盖的大致类型。需要注意 colorRampPalette 生成的色带是离散的颜色向量,256 个级别足够平滑,少于 64 会看到明显的分层色带。
4.2 叠加卫星影像或专题数据作为纹理
除了用颜色表达高度之外,rayshader 还允许把其他栅格数据作为纹理贴到 3D 地形上。最典型的是叠加卫星影像,这样做出来的效果是“真实地球”风格:地形的高程来自 DEM,颜色来自遥感影像,两者结合之后非常接近航拍视角。操作上也很直接:
# 假设 img 是与 DEM 范围一致的卫星影像,已转成矩阵 mat %>% sphere_shade(texture = "desert") %>% add_overlay(img, alphalayer = 1) %>% plot_3d(heightmap = mat, zscale = 100)add_overlay 可以把任意 RGB 矩阵作为贴图覆盖在地形表面,透明度由 alphalayer 控制。这个功能同样适用于叠加土壤类型图、土地覆盖分类图、人口密度图等专题数据,相当于让三维地形成为一个画布,上面展示的内容可以根据研究目的自由替换。我自己的经验是,叠加高分辨率影像时注意检查两个栅格数据的范围、分辨率、投影是否一致,不一致的话要先重采样和裁剪对齐,否则贴图位置会发生偏移,产生“张冠李戴”的错误。
4.3 添加水面效果和雾效:做出场景感
很多地形可视化项目都涉及河流、湖泊、水库一类的水体要素。ray shader 的水面模拟是我非常喜欢的功能,它并不是简单地把某块区域涂成蓝色,而是渲染出带反射效果的水面质感。前提是准备一个水体掩膜矩阵,水体区域值为 1,非水体区域为 0,然后用 render_water 添加到场景中:
water_mask <- matrix(0, nrow = nrow(mat), ncol = ncol(mat)) water_mask[湖泊对应的行, 湖泊对应的列] <- 1 plot_3d(heightmap = mat, zscale = 100) render_water(heightmap = mat, waterheight = 80, watermask = water_mask, wateralpha = 0.7)waterheight 是水面的海拔高度,只有当 DEM 值低于这个高度的区域才会被水覆盖;watermask 是可选的,用来限定哪些位置可以出现水。wateralpha 控制水的透明度,越大水体越不透明,反射感越强。如果你只有一个固定的水面高度(比如模拟海平面上升),可以不传 watermask,直接让所有低于 waterheight 的位置变成水面。
雾效的加入则能大大增强场景的纵深感。render_atmosphere 可以在远处添加大气散射效果,让地形边缘自然淡出,降低人工感:
render_atmosphere(altitude = 1000, sunangle = 45, haze = 0.3)altitude 模拟相机距离地面的高度,这个值越小雾效越浓;haze 控制大气的浑浊程度。实际使用中,我在展示大范围山脉的时候喜欢加一点雾效,能让远山有一种朦胧感,更接近真实航拍。
5. 高质量渲染与结果导出
5.1 预览图与最终图的差异
rgl 窗口里旋转查看的场景是实时渲染的,受限于显示器分辨率和显卡性能,画面往往比较粗糙,有锯齿、阴影不够柔和。真正用于报告或出版的图片需要用 render_snapshot 或 render_highquality 单独输出。两者的区别可以这样理解:render_snapshot 捕获的是当前 3D 窗口的画面,速度非常快,适合做快速预览;render_highquality 则是重新启动一个离屏渲染管线,逐像素计算每一束光线的颜色和阴影,效果接近电影级画面,但耗时显著增加。一张 2000 像素宽的图,在普通电脑上可能要等一两分钟;如果用 4K 分辨率、开抗锯齿和软阴影,等上十几分钟也不奇怪。
5.2 输出参数详解:分辨率、抗锯齿、文件名
高质量渲染的调用方式很直接:
render_highquality( filename = "terrain_3d.png", width = 3000, height = 2000, samples = 16, antialias = TRUE, clear = FALSE )samples 控制在每个像素上采样光线的次数,数值越高画质越细腻,噪点越少,但渲染时间成倍增加。入门建议从 4 开始,效果不满意逐步上调到 16。antialias 开启后可以显著消除边缘锯齿,代价同样是时间。实际渲染前,先把相机角度调整到理想位置,再调用导出函数;如果发现导出后视角不对,可以直接在 rgl 窗口拖拽调整,然后重新执行 render_highquality,不需要重复前面的 3D 场景构建代码。
另外值得一提的还有 render_snapshot 的一个实用场景:分享到社交媒体、嵌入网页或快速给同事看一版效果时,完全没必要动用高画质渲染,一张快图能大大提高迭代速度。我的习惯是先用 snapshot 确认构图和配色,最后一步才上 highquality 出最终图。
5.3 动起来:让视角环绕地形的 GIF 与视频
静态图之外,rayshader 还提供了一种非常有传播力的输出方式——旋转视角动画。render_movie 可以让相机围绕地形旋转 360 度,最终输出一个 MP4 视频或者一系列帧图片,配合 ffmpeg 还能转成 GIF。代码模板如下:
render_movie(filename = "terrain_rotate.mp4", phi = 35, theta = seq(0, 360, length.out = 180))theta 参数传入一个序列,表示每一帧相机所处的角度。180 帧是一个比较流畅的配置,如果机器性能一般,可以减少到 120 帧。实际体验中,这种旋转动画在学术汇报、项目展示和社交媒体上都非常讨喜,几分钟的模型演示视频比十张静态图更能直观传达地形全貌。我自己做区域规划项目的汇报时,经常把主导航图和一段 15 秒的三维旋转动画放在一起,听众的专注度会明显上一个台阶。
6. 渲染过程中的常见问题与避坑指南
6.1 高频错误和排查方案速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 安装 rayshader 报错,提示缺少 Rtools | Windows 环境未配置 Rtools | 到 CRAN 下载与当前 R 版本匹配的 Rtools 并完成安装 |
| 加载包时提示找不到 GDAL | 系统缺少 GDAL 库 | Linux 安装 libgdal-dev,macOS 用 Homebrew 安装 gdal |
| 渲染出来的地形上下颠倒 | 矩阵行顺序与坐标方向不一致 | 用 apply(mat, 2, rev) 翻转矩阵 |
| 地形过于平坦,几乎没有立体感 | zscale 设置太大 | 试 50 到 100 之间的值,按比例缩小 |
| 叠加影像后纹理位置偏移 | 两个栅格数据的投影或范围不一致 | 用 project 和 resample 统一坐标系与分辨率 |
| 内存不足,R 会话直接崩溃 | DEM 范围太大或分辨率太高 | 用 aggregate 降低矩阵规模,或裁剪小范围测试 |
| render_highquality 渲染很久没结果 | samples 或分辨率设置过高 | 先用小图和 samples=4 测试,再逐步加码 |
| 窗口能显示图像,但导出 PNG 全黑 | rgl 窗口被遮挡或显卡驱动兼容问题 | 尝试调用 rgl.clear() 后重建场景,或更新显卡驱动 |
6.2 关于数据范围和分辨率匹配的经验
规划一个地形可视化项目时,最先确定的不是配色或视角,而是“主角”的范围和尺度。范围决定了地形的叙事尺度,同一座山,放进十公里的大场景里它是山脉的一部分,放进两公里的小场景里它是独立的视觉主体。相应地,分辨率也要匹配范围:大范围用高分辨率数据,渲染矩阵会非常庞大,普通笔记本可能直接卡死;不如主动降低分辨率先把构图跑通,之后再用局部放大或者提高分辨率产出终稿。这一步看起来很简单,但很多人容易忽略,结果浪费了大量时间在等待渲染上。
实际使用中,我还有一个习惯:把范围边界多向外扩一点,留出视觉缓冲空间,避免地形模型的边缘正好切在山顶或山谷这种视觉焦点位置。好比拍照时在主体周围留白,画面的构图会松弛很多。
6.3 投影与坐标那点事,别等到出图才发现
由于 rayshader 本身只是在画数值矩阵,它对坐标的依赖其实很低——你完全可以把 DEM 当成一张普通的灰度图像来处理。但这不代表可以忽略投影问题。如果后续需要把渲染图与矢量边界、标注点、比例尺等要素合成,或者导出坐标信息供 GIS 软件读取,那么从源数据开始就使用统一的投影坐标系会省很多事。更实际的一点是,球面坐标(经纬度)在高纬度地区的面积变形非常严重,如果一个研究区纬度较高,直接渲染会让地形在东西方向被拉伸变形,看起来非常别扭。这种情况下应该在预处理阶段用 project() 转换到适合当地的投影坐标系,比如 UTM 分区投影。
6.4 配色与光照的视觉错觉陷阱
最后分享一个比较容易踩坑的地方:配色和光照叠加后可能产生视觉误导。例如,当使用从低海拔绿色到高海拔白色的渐变时,如果光线恰好从某个角度照射,白色雪顶区域会反射大量光线,导致过曝,丢失纹理细节。这在渲染高海拔、多雪区域时尤其明显。解决办法是把光线角度调得低一些,或者使用更柔和的阴影叠加层。另外,自定义色带时注意避免使用红绿这一类色盲用户难以区分的颜色组合。我通常会在渲染后把图给身边同事瞄一眼,确认没有明显的视觉歧义再定稿。
7. 从静态图到交互应用:一些扩展玩法
rayshader 并不只停留在静态出图这一步。借助另一个包,你可以把渲染结果导出为交互式 3D 场景,发布到网页上让读者自己拖拽旋转,这个功能在做数据新闻、在线报告或教学材料时非常加分。核心操作是用save_3d相关的函数,将 R 中的 3D 模型保存为 HTML 格式,之后用浏览器打开就能直接交互浏览。步骤上并不复杂,关键是先把 3D 场景构建好,再调一次导出函数:
library(rayshader) library(htmlwidgets) plot_3d(heightmap = mat, zscale = 100) # 这里调整好视角后,导出为交互式网页 save_3d(filename = "terrain_interactive.html")打开这个 HTML 文件后,读者可以像操纵一个真实模型一样旋转、缩放地形,从任意角度观察。这种做法相比静态图最大的优势在于,它把“探索感”交给了读者,而不是由作者固定一个观看视角。我拿一个流域的地形做过测试,地理专业的同事说,这种交互方式在讲解地形与水文关系的时候比 PPT 里的静态图直观太多了。
另外,rayshader 也可以与 Shiny 应用结合,把渲染过程封装成一个交互式应用,让使用者在网页端自行选择渲染区域、调整 zscale 和配色。当然这会涉及更多的工程开发内容,通常适合需要面向非技术用户交付成果的团队。
8. 实操中的几点体会
用 rayshader 做地形可视化,上手周期其实很短,但想要出真正养眼、信息传达准确的图,还是需要在使用中慢慢积累感觉。我自己总结了三条经验:第一,数据预处理花的时间永远值得,裁剪、投影、缺失值处理这些前期工作决定了最终渲染质量的上限;第二,zscale 和视角是地形表现力的灵魂,花时间试不同组合一定会有回报;第三,配色要克制,避免过于花哨的颜色分带,让地形本身的形态和光影表达主要的信息,颜色只是辅助。
另外,在没有特殊要求的情况下,建议始终按照“小图快测,大图精渲”的节奏推进。先用较低分辨率和较小的导出尺寸确认画面构图和参数是否合适,再把数据与分辨率升到最高做最终渲染。这样既能把试错成本保持在几秒钟,又能在最终出图时获得最好的画质。
你手上如果有现成的 DEM 数据,不管是一片山地还是一个城市周边,完全可以从今天这篇文章里的代码开始,花十分钟跑通第一张三维地形图。等到跑通了,再回来慢慢调整配色和光影,相信你会发现,原来从数据到触手可及的立体山川,距离就这么近。