Shadertoy 教程 Part 13 - 陰影

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章