海洋行星图形学 · 第 2 轨道
双星 G5 V + B8 V · 轨道半径 13.0 万 km · 2 颗卫星
用 Shader 画一颗程序化行星
不用一张贴图,只靠噪声函数和一点光照模型,在片元着色器里“捏”出一颗有海洋、大陆、云层和城市灯光的行星。
- 发布
- 字数
- 767
- 阅读
- 2 分钟
你在这个博客里看到的每一颗行星,都没有使用任何贴图。它们的地表、海洋、云层和夜晚的城市灯光,全部是在片元着色器里实时计算出来的。这篇文章拆解一下其中的思路。
第一步:噪声
一切的基础是梯度噪声(比如 Simplex Noise)。它是一个连续、平滑、看起来随机的函数:给它一个三维坐标,它返回一个大约在 之间的数。
单层噪声太“圆润”了,真实的地形有大尺度的大陆,也有小尺度的山脊。所以我们把不同频率的噪声叠加起来,这叫做分形布朗运动(fBm):
其中 是频率倍增系数(lacunarity,通常取 2), 是振幅衰减系数(gain,通常取 0.5)。
float fbm(vec3 p) {
float sum = 0.0;
float amp = 0.5;
for (int i = 0; i < 6; i++) {
sum += amp * snoise(p);
p *= 2.0; // lacunarity
amp *= 0.5; // gain
}
return sum;
}第二步:在球面上采样
关键技巧是:用球面上点的三维坐标去采样三维噪声,而不是用经纬度去采样二维噪声。这样就不会在两极出现拉伸,也不会在经线接缝处出现断层。
// vObjNormal 是单位球面上的点
float height = fbm(vObjNormal * 1.8 + uSeed);uSeed 是由文章网址计算出来的偏移量,不同的种子会采样到噪声空间的不同区域,于是每颗行星都独一无二。
第三步:从高度到颜色
有了高度,就可以划定海平面,再按高度和纬度分配颜色:
vec3 color;
if (height < seaLevel) {
float depth = seaLevel - height;
color = mix(shallowWater, deepWater, smoothstep(0.0, 0.3, depth));
} else {
float h = height - seaLevel;
color = mix(beach, grass, smoothstep(0.0, 0.02, h));
color = mix(color, rock, smoothstep(0.15, 0.3, h));
color = mix(color, snow, smoothstep(0.35, 0.45, h));
}
// 纬度越高越容易结冰
float ice = smoothstep(0.75, 0.85, abs(vObjNormal.y) + 0.1 * fbm(vObjNormal * 8.0));
color = mix(color, vec3(0.95), ice);第四步:光照
海洋需要镜面高光,陆地基本是漫反射。夜晚的一侧,在陆地、低海拔、并且噪声值恰好落在某个区间的地方,点亮暖黄色的城市灯光:
float night = smoothstep(0.1, -0.2, dot(normal, lightDir));
float city = smoothstep(0.62, 0.8, snoise(p * 40.0)) * landMask;
color += vec3(1.0, 0.7, 0.35) * city * night * 2.0;第五步:大气
最后在行星外面套一层略大的球壳,沿视线方向做若干步积分,近似计算瑞利散射。因为蓝光的散射系数比红光大得多(与波长的四次方成反比):
所以行星边缘会泛起一圈蓝色的光晕,而晨昏线附近则偏向橙红——和地球上看到的日出日落是同一个道理。
小结
噪声提供细节,数学提供规律,光照提供质感。这颗行星本身,就是这篇文章的插图。