Fork me on GitHub

Note: This series blog was translated from Nathan Vaughn's Shaders Language Tutorial and has been authorized by the author. If reprinted or reposted, please be sure to mark the original link and description in the key position of the article after obtaining the author’s consent as well as the translator's. If the article is helpful to you, click this Donation link to buy the author a cup of coffee.
说明:该系列博文翻译自Nathan Vaughn着色器语言教程。文章已经获得作者翻译授权,如有转载请务必在取得作者译者同意之后在文章的重点位置标明原文链接以及说明。如果你觉得文章对你有帮助,点击此打赏链接请作者喝一杯咖啡。

朋友们,你们好!欢迎来到Shadertoy教程系列的第13章。在本次教程中,我们将学习如何在3D场景中添加阴影。

初始化

这次的初始化模板代码会和之前的有点区别,场景中只使用一种颜色,并且这次使用的相机将不会用到目标观察点。同时我也会将rayMarch函数简化:从原来的接收四个参数改成接收两个。剩下的两个参数我们都不会用到。

  const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;

float sdSphere(vec3 p, float r, vec3 offset)
{
  return length(p - offset) - r;
}

float sdFloor(vec3 p) {
  return p.y + 1.;
}

float scene(vec3 p) {
  float co = min(sdSphere(p, 1., vec3(0, 0, -2)), sdFloor(p));
  return co;
}

float rayMarch(vec3 ro, vec3 rd) {
  float depth = MIN_DIST;
  float d; // distance ray has travelled

  for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
    vec3 p = ro + depth * rd;
    d = scene(p);
    depth += d;
    if (d < PRECISION || depth > MAX_DIST) break;
  }
  
  d = depth;
  
  return d;
}

vec3 calcNormal(in vec3 p) {
    vec2 e = vec2(1, -1) * EPSILON;
    return normalize(
      e.xyy * scene(p + e.xyy) +
      e.yyx * scene(p + e.yyx) +
      e.yxy * scene(p + e.yxy) +
      e.xxx * scene(p + e.xxx));
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
  vec3 backgroundColor = vec3(0);

  vec3 col = vec3(0);
  vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position
  vec3 rd = normalize(vec3(uv, -1)); // ray direction

  float sd = rayMarch(ro, rd); // signed distance value to closest object

  if (sd > MAX_DIST) {
    col = backgroundColor; // ray didn't hit anything
  } else {
    vec3 p = ro + rd * sd; // point discovered from ray marching
    vec3 normal = calcNormal(p); // surface normal

    vec3 lightPosition = vec3(cos(iTime), 2, sin(iTime));
    vec3 lightDirection = normalize(lightPosition - p);

    float dif = clamp(dot(normal, lightDirection), 0., 1.); // diffuse reflection clamped between zero and one

    col = vec3(dif);
  }

  fragColor = vec4(col, 1.0);
}

运行以上的代码,我们就能看到基础的3D场景:球,地板以及漫反射。来自漫反射的光照会在黑色和白色的光照之间产生灰色的效果。

基础阴影

让我们从学习一个最简单的阴影开始吧。在开始编码之前,我们来参照下面的图片,用可视化的方式帮助我们理解阴影算法。

rayMarch函数执行的是光线步进算法。我们目前用它来计算场景中最先被光线击中的物体或者表面上的某个点。现在,我们将再次利用它生产出一条射线,然后将这条射线指回去。在上图中,就有一些阴影射线,它们就是从地板出发被投射到光源的光线。

我们将在代码中第二次使用光线步进算法,此时的射线起点等于p,这个点就是我们在首次调用光线步进后发现的球上和地板上的点。新的射线方向等于lightDirection。添加阴影很简单,在漫反射下面添加三行代码即可。

float dif = clamp(dot(normal, lightDirection), 0., 1.); // diffuse reflection clamped between zero and one

vec3 newRayOrigin = p;
float shadowRayLength = rayMarch(newRayOrigin, lightDirection); // 将阴影射线投射到光源 cast shadow ray to the light source
if (shadowRayLength < length(lightPosition - newRayOrigin)) dif *= 0.; //如果射线击中了球,漫反射的值为0,用以模拟阴影。 if the shadow ray hits the sphere, set the diffuse reflection to zero, simulating a shadow

运行上面的代码,你会发现我们的屏幕几乎是全黑色的。这到底怎么回事儿呢?第一次执行步进函数时,我们从相机的位置发射了很多射线,如果射线击中了某个点pp点即表示靠近了地板或者球体,则符号距离值将会等于相机到地板的距离。当我们在第二次步进算法中使用同一个p值,我们已经知道了它靠近的是地板而非球,因此几乎所有的点都是在阴影中的,所以看来都是黑色。为了避免上述情况的出现,我们需要选择一个非常靠近的p点,在第二次光线步进中。通常,我们需要添加一个表面的法向量,再给p乘以一个很小的值,这样就得到了一个临近的点。我们将使用PRECISION作为这个值,将p点稍微的推向临近的点。

  vec3 newRayOrigin = p + normal * PRECISION;

运行以上代码,就可以看到在球附近出现了阴影。不过这里在球的中心的地方有些不自然的痕迹。

我们给结果再乘以2,让这个块黑色的东西消失掉。

   vec3 newRayOrigin = p + normal * PRECISION * 2.;

当我们在场景中添加阴影时,需要为newRayOrigin添加一个因子,这样才能使得它能正常的工作。创造出真实的阴影不是一件简单的事情,你需要时时调整查看你的作品,保证它达到最优的效果。最终的代码会看起来是下面这个样子。

  const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;

float sdSphere(vec3 p, float r, vec3 offset)
{
  return length(p - offset) - r;
}

float sdFloor(vec3 p) {
  return p.y + 1.;
}

float scene(vec3 p) {
  float co = min(sdSphere(p, 1., vec3(0, 0, -2)), sdFloor(p));
  return co;
}

float rayMarch(vec3 ro, vec3 rd) {
  float depth = MIN_DIST;
  float d; // distance ray has travelled

  for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
    vec3 p = ro + depth * rd;
    d = scene(p);
    depth += d;
    if (d < PRECISION || depth > MAX_DIST) break;
  }
  
  d = depth;
  
  return d;
}

vec3 calcNormal(in vec3 p) {
    vec2 e = vec2(1, -1) * EPSILON;
    return normalize(
      e.xyy * scene(p + e.xyy) +
      e.yyx * scene(p + e.yyx) +
      e.yxy * scene(p + e.yxy) +
      e.xxx * scene(p + e.xxx));
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
  vec3 backgroundColor = vec3(0);

  vec3 col = vec3(0);
  vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position
  vec3 rd = normalize(vec3(uv, -1)); // ray direction

  float sd = rayMarch(ro, rd); // signed distance value to closest object

  if (sd > MAX_DIST) {
    col = backgroundColor; // ray didn't hit anything
  } else {
    vec3 p = ro + rd * sd; // point discovered from ray marching
    vec3 normal = calcNormal(p); // surface normal

    vec3 lightPosition = vec3(cos(iTime), 2, sin(iTime));
    vec3 lightDirection = normalize(lightPosition - p);

    float dif = clamp(dot(normal, lightDirection), 0., 1.); // diffuse reflection clamped between zero and one
    
    vec3 newRayOrigin = p + normal * PRECISION * 2.;
    float shadowRayLength = rayMarch(newRayOrigin, lightDirection);
    if (shadowRayLength < length(lightPosition - newRayOrigin)) dif *= 0.;

    col = vec3(dif);
  }

  fragColor = vec4(col, 1.0);
}

为彩色的场景添加阴影

使用同样的技术,可以为之前的教程中的彩色场景添加阴影。

  const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;

struct Surface {
    float sd; // signed distance value
    vec3 col; // color
};

Surface sdFloor(vec3 p, vec3 col) {
  float d = p.y + 1.;
  return Surface(d, col);
}

Surface sdSphere(vec3 p, float r, vec3 offset, vec3 col) {
  p = (p - offset);
  float d = length(p) - r;
  return Surface(d, col);
}

Surface opUnion(Surface obj1, Surface obj2) {
  if (obj2.sd < obj1.sd) return obj2;
  return obj1;
}

Surface scene(vec3 p) {
  vec3 floorColor = vec3(0.1 + 0.7 * mod(floor(p.x) + floor(p.z), 2.0));
  Surface co = sdFloor(p, floorColor);
  co = opUnion(co, sdSphere(p, 1., vec3(0, 0, -2), vec3(1, 0, 0)));
  return co;
}

Surface rayMarch(vec3 ro, vec3 rd) {
  float depth = MIN_DIST;
  Surface co; // closest object

  for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
    vec3 p = ro + depth * rd;
    co = scene(p);
    depth += co.sd;
    if (co.sd < PRECISION || depth > MAX_DIST) break;
  }
  
  co.sd = depth;
  
  return co;
}

vec3 calcNormal(in vec3 p) {
    vec2 e = vec2(1, -1) * EPSILON;
    return normalize(
      e.xyy * scene(p + e.xyy).sd +
      e.yyx * scene(p + e.yyx).sd +
      e.yxy * scene(p + e.yxy).sd +
      e.xxx * scene(p + e.xxx).sd);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
  vec3 backgroundColor = vec3(0.835, 1, 1);

  vec3 col = vec3(0);
  vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position
  vec3 rd = normalize(vec3(uv, -1)); // ray direction

  Surface co = rayMarch(ro, rd); // closest object

  if (co.sd > MAX_DIST) {
    col = backgroundColor; // ray didn't hit anything
  } else {
    vec3 p = ro + rd * co.sd; // point discovered from ray marching
    vec3 normal = calcNormal(p);

    vec3 lightPosition = vec3(cos(iTime), 2, sin(iTime));
    vec3 lightDirection = normalize(lightPosition - p);
    
    float dif = clamp(dot(normal, lightDirection), 0., 1.); // diffuse reflection
    
    vec3 newRayOrigin = p + normal * PRECISION * 2.;
    float shadowRayLength = rayMarch(newRayOrigin, lightDirection).sd; // cast shadow ray to the light source
    if (shadowRayLength < length(lightPosition - newRayOrigin)) dif *= 0.0; // shadow

    col = dif * co.col; 
    
  }

  fragColor = vec4(col, 1.0); // Output to screen
}

运行以上代码,你会看到一个红色球,一个移动的光源(因此也有移动的阴影)。但是整个球看起来有些偏暗。

伽马修正

我们将在颜色被输出到屏幕上之前做一些伽马修正(gamma correction),来让深色部分变得更亮一些。

col = pow(col, vec3(1.0/2.2)); // Gamma correction

最终mainImage函数是下面这个样子的:

  void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
  vec3 backgroundColor = vec3(0.835, 1, 1);

  vec3 col = vec3(0);
  vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position
  vec3 rd = normalize(vec3(uv, -1)); // ray direction

  Surface co = rayMarch(ro, rd); // closest object

  if (co.sd > MAX_DIST) {
    col = backgroundColor; // ray didn't hit anything
  } else {
    vec3 p = ro + rd * co.sd; // point discovered from ray marching
    vec3 normal = calcNormal(p);

    vec3 lightPosition = vec3(cos(iTime), 2, sin(iTime));
    vec3 lightDirection = normalize(lightPosition - p);
    
    float dif = clamp(dot(normal, lightDirection), 0., 1.); // diffuse reflection
    
    vec3 newRayOrigin = p + normal * PRECISION * 2.;
    float shadowRayLength = rayMarch(newRayOrigin, lightDirection).sd; // cast shadow ray to the light source
    if (shadowRayLength < length(lightPosition - newRayOrigin)) dif *= 0.; // shadow

    col = dif * co.col; 
    
  }

  col = pow(col, vec3(1.0/2.2)); // Gamma correction
  fragColor = vec4(col, 1.0); // Output to screen
}

当你运行以上代码时,你应该可以看到整个场景看起来会更加亮一些.

阴影仍然有些暗。通过给漫反射乘以一个值来调整阴影的颜色,目前,我们的阴影颜色为0,我们可以将它调亮致0.2:

  if (shadowRayLength < length(lightPosition - newRayOrigin)) dif *= 0.2; // shadow

现在阴影看起来好一些了。可以通过漫反射的地板看见阴影。

软阴影

实际生活当中,阴影分为好几个部分,它们包括umbra,penumbra,以及antumbra。我们可以通过Inigo Quilez的网站上的算法创建软阴影,从而模拟真实世界中的效果。下面的代码是从Shadertoy中借鉴来的,这个算法叫做Raymarching Primitives Commentd。我对其进行了一些修改,来适用当下的代码场景。

  float softShadow(vec3 ro, vec3 rd, float mint, float tmax) {
  float res = 1.0;
  float t = mint;

  for(int i = 0; i < 16; i++) {
    float h = scene(ro + rd * t).sd;
      res = min(res, 8.0*h/t);
      t += clamp(h, 0.02, 0.10);
      if(h < 0.001 || t > tmax) break;
  }

  return clamp( res, 0.0, 1.0 );
}

我们将代码中的硬阴影替换成软阴影:

  float softShadow = clamp(softShadow(p, lightDirection, 0.02, 2.5), 0.1, 1.0);
  col = dif * co.col * softShadow;

将阴影限制在0.1-1.0之间,这样就不会让阴影太过于暗淡。

请注意软阴影的边缘,它在地板颜色中做了顺滑的渐变。

你应该注意到了,球体背面朝光的那一面仍然有些暗淡。我们可以为其添加0.5个单位的漫反射,diff

  float dif = clamp(dot(normal, lightDirection), 0., 1.) + 0.5; // diffuse reflection

运行以上的代码,球会看起来效果好一些。但是远处的地板部分会看起来有些怪异。

我们经常看见开发者利用雾来掩盖这样的一种情况,我们这里也用此方法。

  col = mix(col, backgroundColor, 1.0 - exp(-0.0002 * co.sd * co.sd * co.sd)); // fog

现在就看起来更加真实了!

最终的代码如下:

const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;

struct Surface {
    float sd; // signed distance value
    vec3 col; // color
};

Surface sdFloor(vec3 p, vec3 col) {
  float d = p.y + 1.;
  return Surface(d, col);
}

Surface sdSphere(vec3 p, float r, vec3 offset, vec3 col) {
  p = (p - offset);
  float d = length(p) - r;
  return Surface(d, col);
}

Surface opUnion(Surface obj1, Surface obj2) {
  if (obj2.sd < obj1.sd) return obj2;
  return obj1;
}

Surface scene(vec3 p) {
  vec3 floorColor = vec3(0.1 + 0.7*mod(floor(p.x) + floor(p.z), 2.0));
  Surface co = sdFloor(p, floorColor);
  co = opUnion(co, sdSphere(p, 1., vec3(0, 0, -2), vec3(1, 0, 0)));
  return co;
}

Surface rayMarch(vec3 ro, vec3 rd) {
  float depth = MIN_DIST;
  Surface co; // closest object

  for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
    vec3 p = ro + depth * rd;
    co = scene(p);
    depth += co.sd;
    if (co.sd < PRECISION || depth > MAX_DIST) break;
  }
  
  co.sd = depth;
  
  return co;
}

vec3 calcNormal(in vec3 p) {
    vec2 e = vec2(1, -1) * EPSILON;
    return normalize(
      e.xyy * scene(p + e.xyy).sd +
      e.yyx * scene(p + e.yyx).sd +
      e.yxy * scene(p + e.yxy).sd +
      e.xxx * scene(p + e.xxx).sd);
}

float softShadow(vec3 ro, vec3 rd, float mint, float tmax) {
  float res = 1.0;
  float t = mint;

  for(int i = 0; i < 16; i++) {
    float h = scene(ro + rd * t).sd;
      res = min(res, 8.0*h/t);
      t += clamp(h, 0.02, 0.10);
      if(h < 0.001 || t > tmax) break;
  }

  return clamp( res, 0.0, 1.0 );
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
  vec3 backgroundColor = vec3(0.835, 1, 1);

  vec3 col = vec3(0);
  vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position
  vec3 rd = normalize(vec3(uv, -1)); // ray direction

  Surface co = rayMarch(ro, rd); // closest object

  if (co.sd > MAX_DIST) {
    col = backgroundColor; // ray didn't hit anything
  } else {
    vec3 p = ro + rd * co.sd; // point discovered from ray marching
    vec3 normal = calcNormal(p);

    vec3 lightPosition = vec3(cos(iTime), 2, sin(iTime));
    vec3 lightDirection = normalize(lightPosition - p);

    float dif = clamp(dot(normal, lightDirection), 0., 1.) + 0.5; // diffuse reflection

    float softShadow = clamp(softShadow(p, lightDirection, 0.02, 2.5), 0.1, 1.0);

    col = dif * co.col * softShadow;
  }

  col = mix(col, backgroundColor, 1.0 - exp(-0.0002 * co.sd * co.sd * co.sd)); // fog
  col = pow(col, vec3(1.0/2.2)); // Gamma correction
  fragColor = vec4(col, 1.0); // Output to screen
}

总结

我们在本篇文章中学习了几个概念,它们分别是:硬阴影(hard shadows)、软阴影(soft shadows)、伽马修正(gamma correction)以及雾(fog)。如你所见,添加阴影需要一些小技巧。在本节教程中,我们只讨论了添加漫反射的阴影,同样的原则也是适用于其他类型的反射。你需要确定的是你的场景是怎么样被点亮的,并且预测阴影对你的场景会产生怎么样的影响。我们在本篇文章中只提到一种添加阴影的方式。如果你深入了解其他的一些着色器的教程,你会找到更多完全不同的光照手法。

资源

YouTube: Ray Marching For Dummies
Wikipedia: Gamma Correction
Shadertoy: Raymarching Primitives
Shadertoy: Raymarching Primitives Commented
Fog
Outdoors Lighting

posted on 2021-12-27 11:15  chen·yan  阅读(218)  评论(0编辑  收藏  举报