ShaderToy-山峦+蓝天+白云

知识点

  • 杂色
  • 栅格
  • 山峦
  • 阳光
  • 补光
  • 雾效
  • 蓝天
  • 白云

课前必备

用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的渲染。

参考链接:www.bilibili.com/video/BV18P...

相关推荐
小码哥_常1 小时前
Android字体字重设置全攻略:XML黑科技+Kotlin动态实现,告别.ttf臃肿
前端
言萧凡_CookieBoty3 小时前
AI 编程省 Token 实战:从 Spec、上下文工程到模型分层的降本策略
前端·ai编程
DFT计算杂谈3 小时前
wannier90 参数详解大全
java·前端·css·html·css3
铁皮饭盒4 小时前
第2课:5分钟!用 Trae AI 生成你的第一个后端服务(Bunjs + Elysia)
前端·后端·全栈
之歆5 小时前
DAY13_CSS3进阶完全指南 —— 背景、边框、文本、渐变、滤镜与 Web 字体(下)
前端·css·css3
剑神一笑5 小时前
CSS 阴影生成器:从单层到多层叠加的艺术
前端·css·css3
lljss20205 小时前
1. NameServer 域名服务器---NS
linux·服务器·前端
anOnion6 小时前
构建无障碍组件之Tooltip Pattern
前端·html·交互设计