阅读视图

发现新文章,点击刷新页面。

ShaderToy-山峦+蓝天+白云

知识点

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

课前必备

用noise 绘制山峦的算法:wolfram详解山峦算法

课程内容

1.杂色。

image-20241106141239814

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

02

3.栅格平滑过度。

03

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

04

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

白云

1-杂色

杂色的实现原理就是随机数。

// 坐标系缩放系数
#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.);
}

效果如下:

08

现在就是一个毫无章法的杂色效果。

2-栅格

我们可以将点位取整,从而画出大块的杂色。

float Noise(vec2 pos){
  vec2 i=floor(pos);
  return Random(i);
}

效果如下:

image-20250523155540077

我们可以将coord*5.,使色块更大。

void mainImage(out vec4 fragColor,in vec2 fragCoord){
  vec2 coord=Coord(fragCoord);
  float noise=Noise(coord*5.);
  fragColor=vec4(vec3(noise),1.);
}

效果如下:

09

3-山峦的平滑过度

山峦的平滑过度的原理在wolfram详解山峦算法中有详解。

公式如下:

image-20241127151550726

e=a+(b-a)*fx*(1-fy)+(c-a)*fy*(1-fx)+(d-a)*fx*fy

代码如下:

// 坐标系缩放系数
#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.);
}

效果如下:

14

4-山峦透视图

image-20260221100929091

山峦透视图涉及以下知识点:

  • RayMarching 光线推进
  • 山峦的SDF模型
  • 山峦的法线计算
  • 根据法线和平行光计算山峦颜色

整体代码如下:

// 坐标系缩放
#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.定义相机。

根据相机的视点、目标点和上方向可以计算视图旋转矩阵。

代码如下:

// 相机视点位
#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 坐标转换为一种原点在屏幕中心的屏幕坐标。

// 坐标系缩放
#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.用相机的视图旋转矩阵旋转初始推进方向,便可得到世界坐标系中的推进方向。

// 光线推进
RayMarchData rayMarch(vec2 coord, vec3 ro){
  // 光线推进方向  
  vec3 rd=normalize(RotateMatrix()*vec3(coord,2.));
  //...
}

5.RayMarchData 是自定义的光线推进数据的结构体,以便于数据管理。

// RayMarch 数据的结构体
struct RayMarchData {
   // 射线碰撞到的着色点位置
  vec3 ro;
  // 推进距离
  float t;
  // 推进方向
  vec3 rd;
};

山峦SDF模型

山峦SDF模型的距离判断原理:在每次光线推进时,计算推进点的高度位置到其正下方的山峦距离。

1.根据推进点的x、z 值,可以算出相应位置的山峦高度。

代码如下:

// 二维随机
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 值的距离,若距离小于某个精度值,便认为射线碰到了山体。

否则,根据此距离推进光线。

代码如下:

// 山峦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;
}

当然,这种光线推进的方法并不是绝对严谨的,因为这距离并不是推进点到山体的垂直距离,但其优点是计算快捷。

为了尽量避免距离误差,我们将推进距离缩小。

t+=0.1*h;

山峦的法线计算

山峦的法线可以用于逐片元计算受光程度。

山峦法线的计算原理是:以当前片元为中心,在一个极小范围内计算法线,假设此极小范围是一个平面。

算法示例

假设y=2x+3 是山峦函数,求它x=1处的法线。

image-20260318165526677

理解斜截式的同学肯定能看出它的斜率跟3没关系,且任意位置的法线都是一样的。

它在任意位置的法线都是y=2x上任一非零的点旋转90°的归一化。

如(1,2)旋转90°后的(-2,1)的归一化,即(−0.894, 0.447)。

image-20260318170032428

假设我们不知道斜截式的规律,我们可以换个思路计算其法线。

设精度为0.001。

取x=(1-0.001)和x=(1+0.001)处的y值,即:

y1=2*(1-0.001)+3=4.998
y2=2*(1+0.001)+3=5.002

则x=1处的法线为:

normalize(y1-y2,2*0.001)=normalize(-0.004,0.002)=(−0.894, 0.447)

其原理就是取任意不重合的两点,算一下其相对位置,然后旋转90°,做归一化。

代码实现

山峦法线的代码实现就是以极小范围采样的方式,将上面求二维直线的法线变成求三维平面的法线。

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 的大小,观察采样精度对渲染效果的影响。

根据法线和平行光计算山峦颜色

将光线方向与法线做点积运算,便可以得到片元的受光程度。

代码如下:

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 可以加强山峦的颜色的对比度。

效果分析

image-20260221100929091

通过当前的山峦效果,我们可以看到以下问题:

  • 山体形状太板
  • 缺少细节层次

接下来我们就解决这些问题。

5-山峦圆滑

山峦的圆滑的原理在wolfram详解山峦算法中有详解。

其基本原理就是将山峦平滑过度时的线性补间变成曲线补间。

山峦圆滑

圆滑代码如下:

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详解山峦算法中有详解。

山峦叠加

相关代码如下:

// 三维噪波
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-阳光

阳光可以理解为平行光,所以定义一个光线方向,打出投影既可。

阳光

相关代码如下:

// 光线推进的起始距离 
#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-补光

当前山体的投影是纯黑的,我们需要给它补光。

补光的方法有很多,最常见的环境光,但我这里图省事,就给了个垂直于地面的平行光。

相关代码如下:

// 环境光
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-雾效

雾效效果:离视点越近越清晰,越远越接近雾色。

雾效

相关代码如下:

// 相机视点位
#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-蓝天

蓝天的颜色是有渐变的,顶部更蓝,远方更白。

蓝天

相关代码如下:

// 天空
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 中画一个太阳。

// 天空
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 算法绘制。

白云

相关代码如下:

// 云彩高度
#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 推导的。

image-20260505120811048

已知:

  • h 是云彩高度
  • ro 是rayMarch 的原点
  • rd 是rayMarch 的方向

求:rayMarch 推进到云彩上的位置P

解:

P=ro+((h-ro.y)/rd.y)*rd

13-整体代码

整体代码如下:

// 坐标系缩放
#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…

wolfram详解山峦算法

代码链接:www.wolframcloud.com/obj/1051904…

知识点

  • noise 算法
  • noise 栅格
  • noise 栅格过度
  • 梯度

1-noise 绘制山峦的原理

noise 可以译作杂色,或者噪波,它可以理解为一种肌理,其表现形式有很多。

使用杂色可以绘制山峦、云海等。

noise 绘制山峦的原理如下:

1.杂色。

image-20260426114414985

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

image-20260426114511276

3.山峦:栅格平滑过度。

image-20260426170319591

4.山峦细节:对山峦进行多次变换叠加。

image-20260428110734479

利用这种算法,可以在shader 中渲染出云山云海的效果。

mount

2-随机数

杂色的实现原理就是随机数。

随机数的写法有很多。接下来,我会使用wolfram 语言演示其算法原理。

1.根据一维数据生成随机数的方法。

random[x_]:=Abs[FractionalPart[1000*Sin[x]]]
DiscretePlot[random[x],{x,0,10,0.1}]

效果如下:

image-20260423183548641

2.根据片元的二维位置生成随机数的方法。

vec2 = {_?NumericQ, _?NumericQ};
random[p:vec2]:=Module[{v},
v=FractionalPart[{p[[1]],p[[2]],p[[1]]}*0.1031];
v+=Dot[v,{v[[2]],v[[3]],v[[1]]}+33.33];
FractionalPart[(v[[1]]+v[[2]])*v[[3]]]
];
Plot3D[random[{x,y}],{x,0,10},{y,0,10},ColorFunction -> Function[{x, y, z}, GrayLevel[z]],Mesh->None]

效果如下:

image-20260426114414985

3-栅格

我们可以将点位取整,从而画出大块的杂色。

vec2 = {_?NumericQ, _?NumericQ};
random[p:vec2]:=Module[{v},
v=FractionalPart[{p[[1]],p[[2]],p[[1]]}*0.1031];
v+=Dot[v,{v[[2]],v[[3]],v[[1]]}+33.33];
FractionalPart[(v[[1]]+v[[2]])*v[[3]]]
];
noise[p:vec2]:=Module[{i,f,u,a,b,c,d,tx,ty},
i=Floor[p];
random[i]
]
plotSize=10;
Plot3D[noise[{x,y}],{x,0,plotSize},{y,0,plotSize},ColorFunction -> Function[{x, y, z}, GrayLevel[z]]]

效果如下:

image-20260426114511276

noise 栅格颜色的深浅代表了山的高度。但它现在是离散的,缺少过度。所以我们接下来要给栅格一个过度。

4-栅格过度

栅格过度的核心在于使用插值在相邻的栅格间做补间运算。

4-1-一行栅格的过度

一行没有过度的栅格高度图如下:

random[x_]:=Abs[FractionalPart[1000*Sin[x]]];
noise[x_]:=Module[{},
i=Floor[x];
a=random[i];
a
]
Plot[noise[x],{x,0,10}]

效果如下:

image-20260425002625971

我们可以以栅格位置的小数部分为插值,对相邻的两个栅格做补间。

random[x_]:=Abs[FractionalPart[1000*Sin[x]]];
mix[a_,b_,f_]:=Module[{},
(a+(b-a)*f)
];
noise[x_]:=Module[{},
i=Floor[x];
f=FractionalPart[x];
a=random[i];
b=random[i+1];
mix[a,b,f]
]
Plot[noise[x],{x,0,10}]

效果如下:

image-20260425003942467

mix[a_,b_,f_] 方法是基于f差值在a和b间做补间的方法。

在noise 方法中,a,b是相邻的2个栅格的值,f是栅格位置的小数部分,可以用作插值。

4-2-二维栅格的过度

过度算法

image-20241127151550726

已知:

  • 栅格尺寸为1

  • 栅格4个顶点:

    • 点P(px,py,a)
    • 点P右侧的点(px+1,py,b)
    • 点P上方的点(px,py+1,c)
    • 点P右上方的点(px+1,py+1,d)
  • 点F(px+fx,py+fy,e),px和py是整数,fx和fy是小数,e未知

求:e

思路:e 可以理解为b,c,d对a的加权

解:

b 对a 的影响力是:

(b-a)*fx*(1-fy)

(b-a)*fx 是b在x方向对a的影响,同时其影响力还会受到fy 的影响。

c 对a 的影响力是:

(c-a)*fy*(1-fx)

其原理与b同理。

d 对a 的影响力是:

(d-a)*fx*fy

最后把b,c,d 对a 的影响力合到a 上,就是着色点e 的颜色:

e=a+(b-a)*fx*(1-fy)+(c-a)*fy*(1-fx)+(d-a)*fx*fy

e 的颜色是对山的高度的可视化描述,方便大家理解。

算法可视化

vec2 = {_?NumericQ, _?NumericQ};
random[p:vec2]:=Module[
{
x=p[[1]],
y=p[[2]],
v
},
v=FractionalPart[{x,y,x}*0.1031];
v+=Dot[v,{v[[2]],v[[3]],v[[1]]}+33.33];
FractionalPart[(v[[1]]+v[[2]])*v[[3]]]
];
noise[p:vec2]:=Module[{i,f,u,a,b,c,d,tx,ty},
i=Floor[p];
f=FractionalPart[p];
a=random[i];
b=random[i+{1,0}];
c=random[i+{0,1}];
d=random[i+{1,1}];
tx=f[[1]];
ty=f[[2]];
a+(b-a)*tx*(1-ty)+(c-a)*ty*(1-tx)+(d-a)*tx*ty
]
plotSize=10;
Plot3D[noise[{x,y}],{x,0,plotSize},{y,0,plotSize},Mesh->None]

效果如下:

image-20260426170319591

5-山体圆滑

当前的山体看起来比较凌厉,我可以使其圆滑一些。

5-1-线性补间与曲线补间

我们之前使用的补间算法是线性补间。

noise[x_]:=Module[{},
FractionalPart[x]
]
Plot[noise[x],{x,0,3}]

image-20260425004053941

我们可以曲线补间。

noise[x_]:=Module[{},
tx=FractionalPart[x];
FractionalPart[3*tx^2-2*tx^3]
]
Plot[noise[x],{x,0,3}]

image-20260425004145361

5-2-曲线补间的应用

曲线补间可以圆滑折线图。

random[x_]:=Abs[FractionalPart[1000*Sin[x]]];
mix[a_,b_,f_]:=Module[{},
(a+(b-a)*f)
];
noise[x_]:=Module[{},
i=Floor[x];
f=FractionalPart[x];
u=3*f^2-2*f^3;
a=random[i];
b=random[i+1];
mix[a,b,u]
]
Plot[noise[x],{x,0,10}]

效果如下:

image-20260425005014832

曲线补间也可以三维山峦。

vec2 = {_?NumericQ, _?NumericQ};
random[p:vec2]:=Module[
{
x=p[[1]],
y=p[[2]],
v
},
v=FractionalPart[{x,y,x}*0.1031];
v+=Dot[v,{v[[2]],v[[3]],v[[1]]}+33.33];
FractionalPart[(v[[1]]+v[[2]])*v[[3]]]
];
noise[p:vec2]:=Module[{i,f,u,a,b,c,d,tx,ty},
i=Floor[p];
f=FractionalPart[p];
u=3*f^2-2*f^3;
a=random[i];
b=random[i+{1,0}];
c=random[i+{0,1}];
d=random[i+{1,1}];
tx=u[[1]];
ty=u[[2]];
a+(b-a)*tx*(1-ty)+(c-a)*ty*(1-tx)+(d-a)*tx*ty
]
plotSize=10;
Plot3D[noise[{x,y}],{x,0,plotSize},{y,0,plotSize},Mesh->None]

效果如下:

image-20260426170413707

我们可以让z轴和x,y 轴等比,使之更接近现实。

Plot3D[noise[{x,y}],{x,0,plotSize},{y,0,plotSize},PlotRange -> {0, plotSize},BoxRatios -> {1, 1, 1},Mesh -> None]

效果如下:

image-20260426170732918

当前的山体有些简约,我们可以增加更多的细节。

6-增加山体细节

我可以对山体进行多次变换叠加,使其具有更能多细节。

原理如下图所示:

山势叠加

第一张c 图是由下面的b 图和a 图叠加而成。

按照此原理,对三维山峦进行变换叠加。

vec2 = {_?NumericQ, _?NumericQ};
random[p:vec2]:=Module[
{
x=p[[1]],
y=p[[2]],
v
},
v=FractionalPart[{x,y,x}*0.1031];
v+=Dot[v,{v[[2]],v[[3]],v[[1]]}+33.33];
FractionalPart[(v[[1]]+v[[2]])*v[[3]]]
];
noise[p:vec2]:=Module[{i,f,u,a,b,c,d,tx,ty},
i=Floor[p];
f=FractionalPart[p];
u=3*f^2-2*f^3;
a=random[i];
b=random[i+{1,0}];
c=random[i+{0,1}];
d=random[i+{1,1}];
tx=u[[1]];
ty=u[[2]];
a+(b-a)*tx*(1-ty)+(c-a)*ty*(1-tx)+(d-a)*tx*ty
];
mountainTF=2*{{0.6,-0.8},{0.8,0.6}};
mountain[p:vec2]:=Module[{p2=p,a,b,n},
a=0;
b=1;
For[i = 0, i < 4, i++,
n=1.65*noise[p2*0.5];
a+=b*n/(1+i);
p2=mountainTF.p2;
b*=0.5;
];
a
];
plotSize=10;
Plot3D[mountain[{x,y}],{x,0,plotSize},{y,0,plotSize},PlotRange -> {0, plotSize},BoxRatios -> {1, 1, 1},PlotPoints -> 30,Mesh -> None]

效果如下:

image-20260426185404625

这样山体就有了细节。我们还可以根据山峰的形成规律,使其看起来更加接近现实。

7-山的规律

观察现实中的山峰,我们不难发现一个基本规律:山峰陡峭的地方会更加平滑,山峰平缓的地方会有更多的山岩。

山峰

按照此规律,我们在叠加山体的时候,根据采样点的梯度判断陡峭度,在山峰陡峭的地方,让叠加山体更能矮。

使用wolfram的Grad 方法求noise函数的梯度。

Grad[a+(b-a)*u[x]*(1-u[y])+(c-a)*u[y]*(1-u[x])+(d-a)*u[x]*u[y],{x,y}] //Simplify

输出:

{(-a+b+(a-b-c+d)u[y])u'[x],(-a+c+(a-b-c+d)u[x])u'[y]}
  • u是曲线函数.
u=3*f^2-2*f^3;
  • u'[x]和u'[y] 是u 的导数,即
D[3*f^2 - 2*f^3, f]
du=6f-6

noise函数的梯度可以简化一下:

({b-a,c-a}+a-b-c+d)*{ty,yx})*du

我们可以根据梯度的点积确定山峰的陡峭程度,从而让陡峭的地方山体更矮。

整体程序如下:

vec2 = {_?NumericQ, _?NumericQ};
random[p:vec2]:=Module[
{
x=p[[1]],
y=p[[2]],
v
},
v=FractionalPart[{x,y,x}*0.1031];
v+=Dot[v,{v[[2]],v[[3]],v[[1]]}+33.33];
FractionalPart[(v[[1]]+v[[2]])*v[[3]]]
];
noise[p:vec2]:=Module[{i,f,u,du,a,b,c,d,tx,ty,x,yz},
i=Floor[p];
f=FractionalPart[p];
u=3*f^2-2*f^3;
du=6*f-6f*f*f;
a=random[i];
b=random[i+{1,0}];
c=random[i+{0,1}];
d=random[i+{1,1}];
tx=u[[1]];
ty=u[[2]];
x=a+(b-a)*tx*(1-ty)+(c-a)*ty*(1-tx)+(d-a)*tx*ty;
yz=({b-a,c-a}+(a-b-c+d)*{ty,tx})*du;
{x,yz[[1]],yz[[2]]}
];
mountainTF=2*{{0.6,-0.8},{0.8,0.6}};
mountain[p:vec2]:=Module[{p2=p,a,b,d,n,nx,nyz},
a=0;
b=1;
d={0,0};
For[i = 0, i < 4, i++,
n=noise[p2*0.5];
nx=n[[1]];
nyz={n[[2]],n[[3]]};
a+=b*nx/(1+nyz.nyz);
d+=nyx;
p2=mountainTF.p2;
b*=0.5;
];
a
];
plotSize=10;
Plot3D[mountain[{x,y}],{x,0,plotSize},{y,0,plotSize},PlotRange -> {0, plotSize},BoxRatios -> {1, 1, 1},PlotPoints -> 30,Mesh -> None]

效果如下:

image-20260428110734479

总结

这一章我们说了wolfram 绘制山体的基本过程,其基本原理是noise 的变换叠加,并使用梯度判断山峰陡峭程度,让陡峭的山峦更加平滑,更加复合自然界中的山峦规律。

后面我会利用此原理,在shader中写一篇在绘制云山云海的文章。

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

❌