知识点
- 杂色
- 栅格
- 山峦
- 阳光
- 补光
- 雾效
- 蓝天
- 白云
课前必备
用noise 绘制山峦的算法:wolfram详解山峦算法
课程内容
1.杂色。

2.栅格:降低采样频率,将杂色变成栅格。

3.栅格平滑过度。

4.云彩:对模糊后的图案进行多次变换叠加。

5.云与山:根据云彩的灰度值做起伏,可以画出云与山。

1-杂色
杂色的实现原理就是随机数。
scss
// 坐标系缩放系数
#define PROJECTION_SCALE 1.
// 坐标系
vec2 Coord(in vec2 pos) {
return PROJECTION_SCALE * 2. * (pos - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}
// 随机数
float Random(vec2 pos){
vec3 v=fract(vec3(pos.xyx)*.1031);
v+=dot(v,v.yzx+33.33);
return fract((v.x+v.y)*v.z);
}
// 杂色
float Noise(vec2 pos){
return Random(pos);
}
void mainImage(out vec4 fragColor,in vec2 fragCoord){
vec2 coord=Coord(fragCoord);
// float noise=Noise(coord);
// float noise=Noise(coord*10.);
float noise=Noise(coord*100.);
fragColor=vec4(vec3(noise),1.);
}
效果如下:

现在就是一个毫无章法的杂色效果。
2-栅格
我们可以将点位取整,从而画出大块的杂色。
scss
float Noise(vec2 pos){
vec2 i=floor(pos);
return Random(i);
}
效果如下:

我们可以将coord*5.,使色块更大。
ini
void mainImage(out vec4 fragColor,in vec2 fragCoord){
vec2 coord=Coord(fragCoord);
float noise=Noise(coord*5.);
fragColor=vec4(vec3(noise),1.);
}
效果如下:

3-山峦的平滑过度
山峦的平滑过度的原理在wolfram详解山峦算法中有详解。
公式如下:

css
e=a+(b-a)*fx*(1-fy)+(c-a)*fy*(1-fx)+(d-a)*fx*fy
代码如下:
scss
// 坐标系缩放系数
#define PROJECTION_SCALE 1.
// 坐标系
vec2 Coord(in vec2 pos) {
return PROJECTION_SCALE * 2. * (pos - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}
// 随机数
float Random(vec2 pos){
vec3 v=fract(vec3(pos.xyx)*.1031);
v+=dot(v,v.yzx+33.33);
return fract((v.x+v.y)*v.z);
}
// 杂色
float Noise(vec2 pos){
vec2 i=floor(pos);
vec2 f=fract(pos);
float a=Random(i);
float b=Random(i+vec2(1,0));
float c=Random(i+vec2(0,1));
float d=Random(i+vec2(1,1));
return a+(b-a)*f.x*(1.-f.y)+(c-a)*f.y*(1.-f.x)+(d-a)*f.x*f.y;
}
void mainImage(out vec4 fragColor,in vec2 fragCoord){
vec2 coord=Coord(fragCoord);
float noise=Noise(coord*5.);
fragColor=vec4(vec3(noise),1.);
}
效果如下:

4-山峦透视图

山峦透视图涉及以下知识点:
- RayMarching 光线推进
- 山峦的SDF模型
- 山峦的法线计算
- 根据法线和平行光计算山峦颜色
整体代码如下:
scss
// 坐标系缩放
#define PROJECTION_SCALE 1.
// 相机视点位
#define CAMERA_POS vec3(12,12,12)
// 相机目标点
#define CAMERA_TARGET vec3(0,0,0)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)
// 光线推进的最远距离
#define RAYMARCH_FAR 200.
// 光线推进次数
#define RAYMARCH_NUM 512
// 光线推进精度,当推进后的点位距离物体表面小于RAYMARCH_PRECISION时,默认此点为物体表面的点
#define RAYMARCH_PRECISION 0.001
// RayMarch 数据的结构体
struct RayMarchData {
// 射线碰撞到的着色点位置
vec3 ro;
// 推进距离
float t;
// 推进方向
vec3 rd;
};
// 坐标系
vec2 Coord(in vec2 fragColor) {
return PROJECTION_SCALE * 2. * (fragColor - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}
// 视图旋转矩阵
mat3 RotateMatrix() {
//基向量c,视线
vec3 c = normalize(CAMERA_TARGET-CAMERA_POS );
//基向量a,视线和上方向的垂线
vec3 a = cross(c, CAMERA_UP);
//基向量b,修正上方向
vec3 b = cross(a, c);
//正交旋转矩阵
return mat3(a, b, c);
}
// 二维随机
float Random(vec2 pos){
vec3 v=fract(vec3(pos.xyx)*.1031);
v+=dot(v,v.yzx+33.33);
return fract((v.x+v.y)*v.z);
}
// 三维平滑噪波
float Noise(vec2 pos){
vec2 i=floor(pos);
vec2 f=fract(pos);
float a=Random(i);
float b=Random(i+vec2(1,0));
float c=Random(i+vec2(0,1));
float d=Random(i+vec2(1,1));
return a+(b-a)*f.x*(1.-f.y)+(c-a)*f.y*(1.-f.x)+(d-a)*f.x*f.y;
}
// 山峦生成函数
float Mount(vec2 p){
return 2.* Noise(p);
}
// 山峦法线
vec3 MountNormal(vec3 p,float t){
// 极小值,受到推进距离t的加权
float epsilon=0.001*t;
// 采样
vec2 offsetX1=p.xz-vec2(epsilon,0);
vec2 offsetX2=p.xz+vec2(epsilon,0);
vec2 offsetZ1=p.xz-vec2(0,epsilon);
vec2 offsetZ2=p.xz+vec2(0,epsilon);
// 法线
return normalize(vec3(
Mount(offsetX1)-Mount(offsetX2),
2.0*epsilon,
Mount(offsetZ1)-Mount(offsetZ2)
));
}
// 山峦SDF
float MountSDF(vec3 pos){
return pos.y-Mount(pos.xz);
}
// 光线推进
RayMarchData rayMarch(vec2 coord, vec3 ro){
// 光线推进方向
vec3 rd=normalize(RotateMatrix()*vec3(coord,2.));
// 光线推进数据
RayMarchData rm = RayMarchData(vec3(0),0.,rd);
float t=0.;
for(int i=0;i<RAYMARCH_NUM;i++){
vec3 p=ro+t*rd;
float h=MountSDF(p);
rm.ro=p;
// RAYMARCH_PRECISION 受到推进距离t的加权
if(abs(h)<RAYMARCH_PRECISION*t||t>RAYMARCH_FAR){
break;
}
// 缩小推进距离
t+=0.1*h;
}
rm.t=t;
return rm;
}
// 渲染
vec3 render(vec2 coord){
RayMarchData rm=rayMarch(coord,CAMERA_POS);
vec3 color=vec3(0);
if(rm.t<RAYMARCH_FAR){
vec3 n=MountNormal(rm.ro,rm.t);
color=sqrt(vec3(dot(vec3(0,1,0),n)));
}
return color;
}
void mainImage(out vec4 fragColor,in vec2 fragCoord){
vec2 coord=Coord(fragCoord);
vec3 col=render(coord);
fragColor=vec4(col,1);
}
详细解释一下上面的代码。
RayMarching 光线推进
1.定义相机。
根据相机的视点、目标点和上方向可以计算视图旋转矩阵。
代码如下:
scss
// 相机视点位
#define CAMERA_POS vec3(12,12,12)
// 相机目标点
#define CAMERA_TARGET vec3(0,0,0)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)
// 视图旋转矩阵
mat3 RotateMatrix() {
//基向量c,视线
vec3 c = normalize(CAMERA_TARGET-CAMERA_POS );
//基向量a,视线和上方向的垂线
vec3 a = cross(c, CAMERA_UP);
//基向量b,修正上方向
vec3 b = cross(a, c);
//正交旋转矩阵
return mat3(a, b, c);
}
2.将fragCoord 坐标转换为一种原点在屏幕中心的屏幕坐标。
scss
// 坐标系缩放
#define PROJECTION_SCALE 1.
// 坐标系
vec2 Coord(in vec2 fragColor) {
return PROJECTION_SCALE * 2. * (fragColor - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}
void mainImage(out vec4 fragColor,in vec2 fragCoord){
vec2 coord=Coord(fragCoord);
//...
}
3.将相机的初始位置定义为(0,0,-2),则从相机向栅格图像中的每个栅格推进的初始方向就是vec3(coord,2.)
4.用相机的视图旋转矩阵旋转初始推进方向,便可得到世界坐标系中的推进方向。
scss
// 光线推进
RayMarchData rayMarch(vec2 coord, vec3 ro){
// 光线推进方向
vec3 rd=normalize(RotateMatrix()*vec3(coord,2.));
//...
}
5.RayMarchData 是自定义的光线推进数据的结构体,以便于数据管理。
arduino
// RayMarch 数据的结构体
struct RayMarchData {
// 射线碰撞到的着色点位置
vec3 ro;
// 推进距离
float t;
// 推进方向
vec3 rd;
};
山峦SDF模型
山峦SDF模型的距离判断原理:在每次光线推进时,计算推进点的高度位置到其正下方的山峦距离。
1.根据推进点的x、z 值,可以算出相应位置的山峦高度。
代码如下:
scss
// 二维随机
float Random(vec2 pos){
vec3 v=fract(vec3(pos.xyx)*.1031);
v+=dot(v,v.yzx+33.33);
return fract((v.x+v.y)*v.z);
}
// 三维平滑噪波
float Noise(vec2 pos){
vec2 i=floor(pos);
vec2 f=fract(pos);
float a=Random(i);
float b=Random(i+vec2(1,0));
float c=Random(i+vec2(0,1));
float d=Random(i+vec2(1,1));
return a+(b-a)*f.x*(1.-f.y)+(c-a)*f.y*(1.-f.x)+(d-a)*f.x*f.y;
}
// 山峦生成函数
float Mount(vec2 p){
return 2.* Noise(p);
}
Mount(vec2 p) 函数提升了山体的高度。
2.计算山峦高度到推进点的y 值的距离,若距离小于某个精度值,便认为射线碰到了山体。
否则,根据此距离推进光线。
代码如下:
ini
// 山峦SDF
float MountSDF(vec3 pos){
return pos.y-Mount(pos.xz);
}
// 光线推进
RayMarchData rayMarch(vec2 coord, vec3 ro){
// 光线推进方向
vec3 rd=normalize(RotateMatrix()*vec3(coord,2.));
// 光线推进数据
RayMarchData rm = RayMarchData(vec3(0),0.,rd);
float t=0.;
for(int i=0;i<RAYMARCH_NUM;i++){
vec3 p=ro+t*rd;
float h=MountSDF(p);
rm.ro=p;
// RAYMARCH_PRECISION*t:近处精度高,远处精度低
if(abs(h)<RAYMARCH_PRECISION*t||t>RAYMARCH_FAR){
break;
}
// 缩小推进距离
t+=0.1*h;
}
rm.t=t;
return rm;
}
当然,这种光线推进的方法并不是绝对严谨的,因为这距离并不是推进点到山体的垂直距离,但其优点是计算快捷。
为了尽量避免距离误差,我们将推进距离缩小。
ini
t+=0.1*h;
山峦的法线计算
山峦的法线可以用于逐片元计算受光程度。
山峦法线的计算原理是:以当前片元为中心,在一个极小范围内计算法线,假设此极小范围是一个平面。
算法示例
假设y=2x+3 是山峦函数,求它x=1处的法线。

理解斜截式的同学肯定能看出它的斜率跟3没关系,且任意位置的法线都是一样的。
它在任意位置的法线都是y=2x上任一非零的点旋转90°的归一化。
如(1,2)旋转90°后的(-2,1)的归一化,即(−0.894, 0.447)。

假设我们不知道斜截式的规律,我们可以换个思路计算其法线。
设精度为0.001。
取x=(1-0.001)和x=(1+0.001)处的y值,即:
ini
y1=2*(1-0.001)+3=4.998
y2=2*(1+0.001)+3=5.002
则x=1处的法线为:
scss
normalize(y1-y2,2*0.001)=normalize(-0.004,0.002)=(−0.894, 0.447)
其原理就是取任意不重合的两点,算一下其相对位置,然后旋转90°,做归一化。
代码实现
山峦法线的代码实现就是以极小范围采样的方式,将上面求二维直线的法线变成求三维平面的法线。
scss
vec3 MountNormal(vec3 p,float t){
// 极小值,受到推进距离t的加权
float epsilon=0.001*t;
// 采样
vec2 offsetX1=p.xz-vec2(epsilon,0);
vec2 offsetX2=p.xz+vec2(epsilon,0);
vec2 offsetZ1=p.xz-vec2(0,epsilon);
vec2 offsetZ2=p.xz+vec2(0,epsilon);
// 法线
return normalize(vec3(
Mount(offsetX1)-Mount(offsetX2),
2.0*epsilon,
Mount(offsetZ1)-Mount(offsetZ2)
));
}
大家可以调整epsilon 的大小,观察采样精度对渲染效果的影响。
根据法线和平行光计算山峦颜色
将光线方向与法线做点积运算,便可以得到片元的受光程度。
代码如下:
scss
vec3 render(vec2 coord){
RayMarchData rm=rayMarch(coord,CAMERA_POS);
vec3 color=vec3(0);
if(rm.t<RAYMARCH_FAR){
vec3 n=MountNormal(rm.ro,rm.t);
color=sqrt(vec3(dot(vec3(0,1,0),n)));
}
return color;
}
在当前的代码里,我们使用的平行光,光线是从正上方打下来的。
sqrt 可以加强山峦的颜色的对比度。
效果分析

通过当前的山峦效果,我们可以看到以下问题:
- 山体形状太板
- 缺少细节层次
接下来我们就解决这些问题。
5-山峦圆滑
山峦的圆滑的原理在wolfram详解山峦算法中有详解。
其基本原理就是将山峦平滑过度时的线性补间变成曲线补间。

圆滑代码如下:
scss
float Noise(vec2 pos){
vec2 i=floor(pos);
vec2 f=fract(pos);
vec2 u=3.*f*f-2.*f*f*f;
float a=Random(i);
float b=Random(i+vec2(1,0));
float c=Random(i+vec2(0,1));
float d=Random(i+vec2(1,1));
return a+(b-a)*u.x*(1.-u.y)+(c-a)*(1.-u.x)*u.y+(d-a)*u.x*u.y;
}
其它代码不变。
现在的山峦还有些单薄,我们对其进行多次变换叠加。
6-山峦叠加
山峦的叠加的原理在wolfram详解山峦算法中有详解。

相关代码如下:
scss
// 三维噪波
vec3 Noise(vec2 p){
vec2 i=floor(p);
vec2 f=fract(p);
vec2 u=3.*f*f-2.*f*f*f;
// u的偏导函数
vec2 du=6.*f-6.*f*f;
float a=Random(i);
float b=Random(i+vec2(1,0));
float c=Random(i+vec2(0,1));
float d=Random(i+vec2(1,1));
// 让半山腰的山凹下去
float x=a+(b-a)*u.x*(1.-u.y)+(c-a)*(1.-u.x)*u.y+(d-a)*u.x*u.y;
//x的偏导函数
vec2 yz=du*(vec2(b-a,c-a)+(a-b-c+d)*u.yx);
return vec3(x,yz);
}
// 山体变换矩阵
// 2可使山体每次迭代后更密集,增加山体细节;旋转矩阵可使山体更加随机、自然
mat2 mountainTF=2.*mat2(0.6,-0.8,0.8,0.6);
// 山峦生成器
float MountainFn(vec2 p,int len){
// 山峰高度
float a=0.;
// 山峰高度的增量,每次迭代后增量减半,使得山峰越高,增量越小
float b=1.;
// 山峰斜率的累积,越陡峭,斜率越大,增量越小。即山峰陡峭处更平滑。
vec2 d=vec2(0);
for(int i=0;i<len;i++){
// 1.65可使山体更高; p*0.5可使山体更平缓
vec3 n=1.65*Noise(p*0.5);
a+=b*n.x/(1.+dot(d,d));
// 累积斜率
d+=n.yz;
// 变换采样点,使得山体更自然
p=mountainTF*p;
// 减小山峰高度的增量
b*=0.56;
}
return a;
}
// 法线贴图
float MountainNormalFn(vec2 p){
return MountainFn(p,12);
}
// 山峦
float Mountain(vec2 p){
return MountainFn(p,6);
}
// 计算法线
vec3 MountainNormal(vec3 p,float t){
// 一种极小值
vec2 epsilon=vec2(0.001*t,0);
// 法线
return normalize(vec3(
MountainNormalFn(p.xz-epsilon.xy)-MountainNormalFn(p.xz+epsilon.xy),
2.0*epsilon.x,
MountainNormalFn(p.xz-epsilon.yx)-MountainNormalFn(p.xz+epsilon.yx)
));
}
山峦模型Mountain 和山峦法线MountainNormal 用了2种计算精度,这样可以在渲染效果和速度之间找一个平衡。
mountainTF 是缩放旋转矩阵,对每次叠加山峦进行变换,使之山峦更加自然。
Noise 中返回的yz数据是山峦的梯度,通过梯度可以确定山势的陡峭度,从而在山峰陡峭的地方,让叠加的山峦矮一些,从而更符合山峦的自然规律。
7-阳光
阳光可以理解为平行光,所以定义一个光线方向,打出投影既可。

相关代码如下:
scss
// 光线推进的起始距离
#define RAYMARCH_NEAR 0.
// 光线推进的最远距离
#define RAYMARCH_FAR 200.
// 阳光方向
#define SUNLIGHT_DIRECTION normalize(vec3(0.5, 0.2, 0.2))
// 软阴影
float SoftShadow(in vec3 ro, in vec3 rd, float k) {
float res = 1.;
for(float t = RAYMARCH_NEAR; t < RAYMARCH_FAR;) {
float h = MountainSDF(ro + rd * t);
if(h < RAYMARCH_PRECISION) {
return 0.;
}
res = min(res, k * h / t);
t += h;
}
return res;
}
// 阳光
void AddSunLight(out vec3 color,vec3 p,float t,vec3 n) {
// 阳光照明
float l=max(dot(SUNLIGHT_DIRECTION, n), 0.);
vec3 color1=vec3(0.02, 0.04, 0.11);
vec3 color2=vec3(1.0);
color=mix(color1,color2,l);
// 阳光投影
float f = SoftShadow(p, SUNLIGHT_DIRECTION, 8.);
vec3 color3=vec3(0.05, 0.0, 0.09);
vec3 color4=vec3(0.99, 0.96, 1.0);
vec3 shadow=mix(color3,color4,f);
color *= shadow;
}
// 渲染
vec3 render(vec2 coord){
RayMarchData rm=rayMarch(coord,CAMERA_POS);
vec3 color=vec3(0);
if(rm.t<RAYMARCH_FAR){
vec3 n=MountainNormal(rm.ro,rm.t);
vec3 basicColor=vec3(1.0);
AddSunLight(basicColor,rm.ro,rm.t,n);
color=basicColor;
}
return color;
}
8-补光
当前山体的投影是纯黑的,我们需要给它补光。
补光的方法有很多,最常见的环境光,但我这里图省事,就给了个垂直于地面的平行光。
相关代码如下:
scss
// 环境光
void AddEnvironmentLight(out vec3 color,vec3 n){
float l=sqrt(max(dot(vec3(0,1,0), n), 0.));
vec3 color1=vec3(0.29, 0.32, 0.36);
vec3 color2=vec3(1.0);
vec3 ambientColor=mix(color1,color2,l);
color=mix(color,ambientColor,0.3);
}
// 渲染
vec3 render(vec2 coord){
RayMarchData rm=rayMarch(coord,CAMERA_POS);
vec3 color=vec3(0);
if(rm.t<RAYMARCH_FAR){
vec3 n=MountainNormal(rm.ro,rm.t);
vec3 basicColor=vec3(1.0);
AddSunLight(basicColor,rm.ro,rm.t,n);
AddEnvironmentLight(basicColor,n);
color=basicColor;
}
return color;
}
效果如下:

把相机镜头降低,你会发现山峦远处有噪波:

我们可以使用雾效掩盖此问题。
9-雾效
雾效效果:离视点越近越清晰,越远越接近雾色。

相关代码如下:
scss
// 相机视点位
#define CAMERA_POS vec3(6.55,2.4,12.02)
// 相机目标点
#define CAMERA_TARGET vec3(6.5,1,0)
// 雾效近端距离
#define FOG_NEAR 20.
// 雾效远端距离
#define FOG_FAR FOG_NEAR+10.
// 雾效
void AddFog(out vec3 color,vec3 fogColor,float t){
color=mix(color,fogColor,smoothstep(FOG_NEAR,FOG_FAR,t));
}
// 渲染
vec3 render(vec2 coord){
RayMarchData rm=rayMarch(coord,CAMERA_POS);
vec3 color=vec3(0);
if(rm.t<RAYMARCH_FAR){
vec3 n=MountainNormal(rm.ro,rm.t);
vec3 basicColor=vec3(1.0);
AddSunLight(basicColor,rm.ro,rm.t,n);
AddEnvironmentLight(basicColor,n);
AddFog(basicColor,color,rm.t);
color=basicColor;
}
return color;
}
当前的远山并没有给一个单纯的白色,而是使其更接近底色,即color=vec3(0)。
接下来我会给底色color一个蓝天的颜色,当远山接近这个颜色的时候,也可以理解为接近了雾色。
10-蓝天
蓝天的颜色是有渐变的,顶部更蓝,远方更白。

相关代码如下:
scss
// 天空
vec3 Sky(vec3 rd){
// 基色
vec3 basicColor=vec3(0.3, 0.5, 0.85);
// 渐变色
vec3 gradientColor = basicColor - rd.y * rd.y * 0.8;
// 雾白
vec3 fog=vec3(1);
// 背景mix值
float backMountainMix=pow(1.0 - max(rd.y, 0.0), 4.0);
// 背景色
vec3 backMountainColor = mix(gradientColor, fog, backMountainMix);
return backMountainColor;
}
// 渲染
vec3 render(vec2 coord){
RayMarchData rm=rayMarch(coord,CAMERA_POS);
vec3 color=Sky(rm.rd);
if(rm.t<RAYMARCH_FAR){
vec3 n=MountainNormal(rm.ro,rm.t);
vec3 basicColor=vec3(1.0);
AddSunLight(basicColor,rm.ro,rm.t,n);
AddEnvironmentLight(basicColor,n);
AddFog(basicColor,color,rm.t);
color=basicColor;
}
return color;
}
天空的渐变插值就是RayMarch的光线推进方向rd 的y 值。
11-太阳
太阳在阳光洒下的方向,要从山的背面才能看见。

在天空的绘制方法Sky 中画一个太阳。
scss
// 天空
vec3 Sky(vec3 rd){
// ...
// 推进方向与太阳光线方向的点积
float sunDot = clamp(dot(rd, SUNLIGHT_DIRECTION), 0.0, 1.0);
float sun=pow(sunDot,4096.)*0.5;
return backMountainColor+sun;
}
太阳的位置可以用RayMarch 方向与阳光方向的点积确定。
12-白云
白云依旧可以用山峦的noise 算法绘制。

相关代码如下:
ini
// 云彩高度
#define CLOUD_HEIGHT 100.
// 白云
mat2 cloudMatrix=mat2(0.6,-0.8,0.8,0.6);
float FractalBrownianNoise(vec2 p){
float a=0.;
float fac=1.0;
float max=fac;
for(int i=0;i<4;i++){
a+=fac*Noise(p*0.015).x;
max+=fac;
p=2.*cloudMatrix*p;
fac*=0.5;
}
float n=smoothstep(0.5,max,a);
return n*3.;
}
void AddCloud(out vec3 color,RayMarchData rm){
vec3 ro=rm.ro;
vec3 rd=rm.rd;
vec3 cloudUV=ro+(CLOUD_HEIGHT-ro.y)/rd.y*rd;
float f=FractalBrownianNoise(cloudUV.xz);
color=mix(color,vec3(1,0.95,1),f);
}
vec3 render(vec2 coord){
RayMarchData rm=rayMarch(coord,CAMERA_POS);
vec3 color=Sky(rm.rd);
AddCloud(color,rm);
if(rm.t<RAYMARCH_FAR){
vec3 n=MountainNormal(rm.ro,rm.t);
vec3 basicColor=vec3(1.0);
AddSunLight(basicColor,rm.ro,rm.t,n);
AddEnvironmentLight(basicColor,n);
AddFog(basicColor,color,rm.t);
color=basicColor;
}
return color;
}
这种会绘制云彩noise 有个专门的名称叫Fractal Brownian Noise。
云彩着色点的位置cloudUV 是从rayMarch 的原点ro 推导的。

已知:
- h 是云彩高度
- ro 是rayMarch 的原点
- rd 是rayMarch 的方向
求:rayMarch 推进到云彩上的位置P
解:
ini
P=ro+((h-ro.y)/rd.y)*rd
13-整体代码
整体代码如下:
scss
// 坐标系缩放
#define PROJECTION_SCALE 1.
// 相机视点位
#define CAMERA_POS vec3(6.55,2.4,12.02)
// 相机目标点
#define CAMERA_TARGET vec3(6.5,1,0.)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)
// 光线推进的起始距离
#define RAYMARCH_NEAR 0.
// 光线推进的最远距离
#define RAYMARCH_FAR 200.
// 光线推进次数
#define RAYMARCH_NUM 512
// 光线推进精度,当推进后的点位距离物体表面小于RAYMARCH_PRECISION时,默认此点为物体表面的点
#define RAYMARCH_PRECISION 0.001
// 阳光方向
#define SUNLIGHT_DIRECTION normalize(vec3(0.5, 0.2, 0.2))
// 雾效近端距离
#define FOG_NEAR 20.
// 雾效远端距离
#define FOG_FAR FOG_NEAR+10.
// 云彩高度
#define CLOUD_HEIGHT 100.
// RayMarch 数据的结构体
struct RayMarchData {
// 射线碰撞到的着色点位置
vec3 ro;
// 推进距离
float t;
// 推进方向
vec3 rd;
};
// 坐标系
vec2 Coord(in vec2 fragColor) {
return PROJECTION_SCALE * 2. * (fragColor - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}
// 视图旋转矩阵
mat3 RotateMatrix() {
//基向量c,视线
vec3 c = normalize(CAMERA_TARGET-CAMERA_POS );
//基向量a,视线和上方向的垂线
vec3 a = cross(c, CAMERA_UP);
//基向量b,修正上方向
vec3 b = cross(a, c);
//正交旋转矩阵
return mat3(a, b, c);
}
// 二维随机
float Random(vec2 pos){
vec3 v=fract(vec3(pos.xyx)*.1031);
v+=dot(v,v.yzx+33.33);
return fract((v.x+v.y)*v.z);
}
// 三维噪波
vec3 Noise(vec2 p){
vec2 i=floor(p);
vec2 f=fract(p);
vec2 u=3.*f*f-2.*f*f*f;
// u的偏导函数
vec2 du=6.*f-6.*f*f;
float a=Random(i);
float b=Random(i+vec2(1,0));
float c=Random(i+vec2(0,1));
float d=Random(i+vec2(1,1));
// 让半山腰的山凹下去
float x=a+(b-a)*u.x*(1.-u.y)+(c-a)*(1.-u.x)*u.y+(d-a)*u.x*u.y;
//x的偏导函数
vec2 yz=du*(vec2(b-a,c-a)+(a-b-c+d)*u.yx);
return vec3(x,yz);
}
// 山体变换矩阵
// 2可使山体每次迭代后更密集,增加山体细节;旋转矩阵可使山体更加随机、自然
mat2 mountainTF=2.*mat2(0.6,-0.8,0.8,0.6);
// 山峦生成器
float MountainFn(vec2 p,int len){
// 山峰高度
float a=0.;
// 山峰高度的增量,每次迭代后增量减半,使得山峰越高,增量越小
float b=1.;
// 山峰斜率的累积,越陡峭,斜率越大,增量越小。即山峰陡峭处更平滑。
vec2 d=vec2(0);
for(int i=0;i<len;i++){
// 1.65可使山体更高; p*0.5可使山体更平缓
vec3 n=1.65*Noise(p*0.5);
a+=b*n.x/(1.+dot(d,d));
// 累积斜率
d+=n.yz;
// 变换采样点,使得山体更自然
p=mountainTF*p;
// 减小山峰高度的增量
b*=0.56;
}
return a;
}
// 法线贴图
float MountainNormalFn(vec2 p){
return MountainFn(p,12);
}
// 山峦
float Mountain(vec2 p){
return MountainFn(p,6);
}
// 计算法线
vec3 MountainNormal(vec3 p,float t){
// 一种极小值
vec2 epsilon=vec2(0.001*t,0);
// 法线
return normalize(vec3(
MountainNormalFn(p.xz-epsilon.xy)-MountainNormalFn(p.xz+epsilon.xy),
2.0*epsilon.x,
MountainNormalFn(p.xz-epsilon.yx)-MountainNormalFn(p.xz+epsilon.yx)
));
}
// 山峦SDF
float MountainSDF(vec3 pos){
return pos.y-Mountain(pos.xz);
}
// 光线推进
RayMarchData rayMarch(vec2 coord, vec3 ro){
// 光线推进方向
vec3 rd=normalize(RotateMatrix()*vec3(coord,2.));
// 光线推进数据
RayMarchData rm = RayMarchData(vec3(0),0.,rd);
float t=0.;
for(int i=0;i<RAYMARCH_NUM;i++){
vec3 p=ro+t*rd;
float h=MountainSDF(p);
rm.ro=p;
// RAYMARCH_PRECISION 受到推进距离t的加权
if(abs(h)<RAYMARCH_PRECISION*t||t>RAYMARCH_FAR){
break;
}
// 缩小推进距离
t+=0.1*h;
}
rm.t=t;
return rm;
}
// 软阴影
float SoftShadow(in vec3 ro, in vec3 rd, float k) {
float res = 1.;
for(float t = RAYMARCH_NEAR; t < RAYMARCH_FAR;) {
float h = MountainSDF(ro + rd * t);
if(h < RAYMARCH_PRECISION) {
return 0.;
}
res = min(res, k * h / t);
t += h;
}
return res;
}
// 阳光
void AddSunLight(out vec3 color,vec3 p,float t,vec3 n) {
// 阳光照明
float l=max(dot(SUNLIGHT_DIRECTION, n), 0.);
vec3 color1=vec3(0.02, 0.04, 0.11);
vec3 color2=vec3(1.0);
color=mix(color1,color2,l);
// 阳光投影
float f = SoftShadow(p, SUNLIGHT_DIRECTION, 8.);
vec3 color3=vec3(0.05, 0.0, 0.09);
vec3 color4=vec3(0.99, 0.96, 1.0);
vec3 shadow=mix(color3,color4,f);
color *= shadow;
}
// 环境光
void AddEnvironmentLight(out vec3 color,vec3 n){
float l=sqrt(max(dot(vec3(0,1,0), n), 0.));
vec3 color1=vec3(0.29, 0.32, 0.36);
vec3 color2=vec3(1.0);
vec3 ambientColor=mix(color1,color2,l);
color=mix(color,ambientColor,0.3);
}
// 雾效
void AddFog(out vec3 color,vec3 fogColor,float t){
color=mix(color,fogColor,smoothstep(FOG_NEAR,FOG_FAR,t));
}
// 天空
vec3 Sky(vec3 rd){
// 基色
vec3 basicColor=vec3(0.3, 0.5, 0.85);
// 渐变色
// rd.y∈[-1,1],rd.y * rd.y∈[0,1],rd.y * rd.y * 0.8∈[0,0.8]
vec3 gradientColor = basicColor - rd.y * rd.y * 0.8;
// 雾白
vec3 fog=vec3(1);
// 背景mix值
float backMountainMix=pow(1.0 - max(rd.y, 0.0), 4.0);
// 背景色
vec3 backMountainColor = mix(gradientColor, fog, backMountainMix);
// 推进方向与太阳光线方向的点积
float sunDot = clamp(dot(rd, SUNLIGHT_DIRECTION), 0.0, 1.0);
float sun=pow(sunDot,4096.)*0.5;
return backMountainColor+sun;
}
// 白云
mat2 cloudMatrix=mat2(0.6,-0.8,0.8,0.6);
float FractalBrownianNoise(vec2 p){
float a=0.;
float fac=1.0;
float max=fac;
for(int i=0;i<4;i++){
a+=fac*Noise(p*0.03).x;
max+=fac;
p=2.*cloudMatrix*p;
fac*=0.5;
}
float n=smoothstep(0.5,max,a);
return n*2.2;
}
void AddCloud(out vec3 color,RayMarchData rm){
vec3 ro=rm.ro;
vec3 rd=rm.rd;
vec3 cloudUV=ro+(CLOUD_HEIGHT-ro.y)/rd.y*rd;
float f=FractalBrownianNoise(cloudUV.xz);
color=mix(color,vec3(1,0.95,1),f);
}
// 渲染
vec3 render(vec2 coord){
RayMarchData rm=rayMarch(coord,CAMERA_POS);
vec3 color=Sky(rm.rd);
AddCloud(color,rm);
if(rm.t<RAYMARCH_FAR){
vec3 n=MountainNormal(rm.ro,rm.t);
vec3 basicColor=vec3(1.0);
AddSunLight(basicColor,rm.ro,rm.t,n);
AddEnvironmentLight(basicColor,n);
AddFog(basicColor,color,rm.t);
color=basicColor;
}
return color;
}
void mainImage(out vec4 fragColor,in vec2 fragCoord){
vec2 coord=Coord(fragCoord);
vec3 col=render(coord);
fragColor=vec4(col,1);,
}
总结
这一章我们说了杂色、栅格、山峦、阳光、补光、蓝天和白云的绘制,我们把更多的关注点都放在了山峦的形状上,而渲染效果并不是太真实。
后面我会再重点研究基于PBR的渲染。