Shadertoy 教程 Part 14 - 使用符號距離場函數

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的第十四篇教程。你以前有想過Shadertoy上的那些複雜的形狀是如何被繪製出來的呢?我們已經學會了如何繪製球和立方體,但是其他的一些複雜的形狀又該如何繪製呢?在本篇文章中,我們將通過Inigo Quilez大神,同時也是Shadertoy的聯合創始人提供的SDF操作方法,來學習如何繪製複雜的形狀。

初始化

下面我們創建了一份光線步進算法模板,該算法我們之前使用過,如果你是需要開發3D場景,它對你會非常有用的。我們下面就開始吧:

  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;
const float PI = 3.14159265359;
const vec3 COLOR_BACKGROUND = vec3(.741, .675, .82);
const vec3 COLOR_AMBIENT = vec3(0.42, 0.20, 0.1);

mat2 rotate2d(float theta) {
  float s = sin(theta), c = cos(theta);
  return mat2(c, -s, s, c);
}

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

float scene(vec3 p) {
  return sdSphere(p, 1., vec3(0, 0, 0));
}

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));
}

mat3 camera(vec3 cameraPos, vec3 lookAtPoint) {
	vec3 cd = normalize(lookAtPoint - cameraPos);
	vec3 cr = normalize(cross(vec3(0, 1, 0), cd));
	vec3 cu = normalize(cross(cd, cr));
	
	return mat3(-cr, cu, -cd);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
  vec2 mouseUV = iMouse.xy/iResolution.xy;
  
  if (mouseUV == vec2(0.0)) mouseUV = vec2(0.5); // trick to center mouse on page load

  vec3 col = vec3(0);
  vec3 lp = vec3(0);
  vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position
  
  float cameraRadius = 2.;
  ro.yz = ro.yz * cameraRadius * rotate2d(mix(-PI/2., PI/2., mouseUV.y));
  ro.xz = ro.xz * rotate2d(mix(-PI, PI, mouseUV.x)) + vec2(lp.x, lp.z);

  vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction

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

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

    vec3 lightPosition = vec3(0, 2, 2);
    vec3 lightDirection = normalize(lightPosition - p) * .65; // The 0.65 is used to decrease the light intensity a bit

    float dif = clamp(dot(normal, lightDirection), 0., 1.) * 0.5 + 0.5; // diffuse reflection mapped to values between 0.5 and 1.0

    col = vec3(dif) + COLOR_AMBIENT;    
  }

  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;
const float PI = 3.14159265359;
const vec3 COLOR_BACKGROUND = vec3(.741, .675, .82);
const vec3 COLOR_AMBIENT = vec3(0.42, 0.20, 0.1);

我們通過定義變量的方式存儲了背景顏色以及環境光照的顏色,這樣我們就能快速地更改3D物體的顏色。下一步,我們將會定義一個rotate2d函數,該函數是用來在2D平面上對物體進行旋轉的,我們在第10篇教程中討論過這點。我們將通過這個函數,使用鼠標移動3D模型。

  mat2 rotate2D(float theta) {
    float s = sin(theta), c = cos(theta);
    return mat2(c, -s, s, c);
  }

接下來使用的函數是創建3D場景的基本工具函數。我們在第六篇教程中首次學過它。sdSphere函數是一個用來創建球的符號距離場函數(SDF)。scene函數則是用來渲染場景中所有的物體。如果你讀過Shadertoy上的代碼,scene函數也被命名爲map函數。

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

float scene(vec3 p) {
  return sdSphere(p, 1., vec3(0));
}

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));
}

接下來,我們又創建了camera函數,通過一個觀察目標點來定義相機模型,關於這點,我們也在第10篇教程中提到過。使用目標觀察點相機聚焦到一個目標。

  mat3 camera(vec3 cameraPos, vec3 lookAtPoint) {
	vec3 cd = normalize(lookAtPoint - cameraPos);
	vec3 cr = normalize(cross(vec3(0, 1, 0), cd));
	vec3 cu = normalize(cross(cd, cr));
	
	return mat3(-cr, cu, -cd);
}

現在,我們分析一下mainImage函數。重新設置一下UV座標,這樣就能將像素座標控制在-0.5到0.5之間。我們也需要計算方位比例,這樣x軸的值將會是一些處在整數和負數之間的值。

  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;

由於我們使用鼠標旋轉3D物體,我們就需要設置mouseUV座標。當鼠標點擊屏幕的時候,我們將其座標設置在0到1之間。

  vec2 mouseUV = iMouse.xy/iResolution.xy;

這裏還有一個問題,當我們在Shadertoy上發佈着色器代碼,用戶首次加載我們的代碼時,座標會以初始值(0,0)作爲mouseUV的座標。我們可以使用一個小技巧,通過給它分配一個新的值來修復這個小缺陷。

  if (mouseUV == vec2(0.0)) mouseUV = vec2(0.5); //  trick to center mouse on page load

接下來,聲明瞭一個顏色變量,col,這個值可以任意設定。然後就需要設置目標觀察點,lp,以及射線源頭,ro,這些我們在第10篇教程中討論過。我們的球目前在場中沒有偏移,它的位置在vec2(0,0)。我們應該使目標觀察點也保持在這個位置上,當然也可以隨意調整它。

vec3 col = vec3(0);
vec3 lp = vec3(0); // lookat point
vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position

使用鼠標旋轉相機,但是要注意相機與3D物體之間的距離,我們在第10篇教程中學過,使用ratate2d函數讓相機在距離物體cameraRadius的間距上旋轉。

float cameraRadius = 2.;
ro.yz = ro.yz * cameraRadius * rotate2d(mix(-PI/2., PI/2., mouseUV.y));
ro.xz = ro.xz * rotate2d(mix(-PI, PI, mouseUV.x)) + vec2(lp.x, lp.z);

vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction

看起來差不多了!除此之外還有許多方式移動相機。不同的人使用的方式有所差別。你只需要選擇自己中意的方式即可。

3D物體的合併操作

我們已經理解了上面提供的代碼的含義了,現在開始做一些3D合併操作吧。我之前已經在第五篇教程中提到過關於2d的一些操作,3D操作其實與它們還是有一些相似的。我們會使用一些工具函數將物體結合在一起或者對它們進行裁剪。這些函數都可以 Inigo Quilez的3D 網頁上找到。讓我們在scene函數之上定義一些工具函數。

(結合)Union:把多個圖形合併在一起,或者在一塊屏幕上同時繪製多個圖形。我們應該對這個函數已經很熟悉了,我們之前就是用它在屏幕上繪製多個物體的。

float opUnion(float d1, float d2) { 
  return min(d1, d2);
}

float scene(vec3 p) {
  float d1 = sdSphere(p, 1., vec3(0, -1, 0));
  float d2 = sdSphere(p, 0.75, vec3(0, 0.5, 0));
  return opUnion(d1, d2);
}

Smooth Union: 使兩個物體平滑地結合在一起,然後通過參數k,來處理合並邊緣平滑程度。k表示等於0表示順滑度爲0,即正常的結合。

  float opSmoothUnion(float d1, float d2, float k) {
  float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 );
  return mix( d2, d1, h ) - k*h*(1.0-h);
}

float scene(vec3 p) {
  float d1 = sdSphere(p, 1., vec3(0, -1, 0));
  float d2 = sdSphere(p, 0.75, vec3(0, 0.5, 0));
  return opSmoothUnion(d1, d2, 0.2);
}

Interscetion: 取兩個圖形的相交部分

  float opIntersection(float d1, float d2) {
  return max(d1,d2);
}

float scene(vec3 p) {
  float d1 = sdSphere(p, 1., vec3(0, -1, 0));
  float d2 = sdSphere(p, 0.75, vec3(0, 0.5, 0));
  return opIntersection(d1, d2);
}

Smooth Intersection:結合兩個物體,並且使用k值來決定邊緣的融合程度。0表示不融合。

  float opSmoothIntersection(float d1, float d2, float k) {
  float h = clamp( 0.5 - 0.5*(d2-d1)/k, 0.0, 1.0 );
  return mix( d2, d1, h ) + k*h*(1.0-h);
}

float scene(vec3 p) {
  float d1 = sdSphere(p, 1., vec3(0, -1, 0));
  float d2 = sdSphere(p, 0.75, vec3(0, 0.5, 0));
  return opSmoothIntersection(d1, d2, 0.2);
}

裁剪(Subtraction): 用d1裁剪d2

  float opSubtraction(float d1, float d2 ) {
  return max(-d1, d2);
}

float scene(vec3 p) {
  float d1 = sdSphere(p, 1., vec3(0, -1, 0));
  float d2 = sdSphere(p, 0.75, vec3(0, 0.5, 0));
  return opSubtraction(d1, d2);
}

平滑裁剪(Smooth Subtraction): 用d1裁剪d2,使用平滑的邊緣參數k

  float opSmoothSubtraction(float d1, float d2, float k) {
  float h = clamp( 0.5 - 0.5*(d2+d1)/k, 0.0, 1.0 );
  return mix( d2, -d1, h ) + k*h*(1.0-h);
}

float scene(vec3 p) {
  float d1 = sdSphere(p, 1., vec3(0, -1, 0));
  float d2 = sdSphere(p, 0.75, vec3(0, 0.5, 0));
  return opSmoothSubtraction(d1, d2, 0.2);
}

反向裁剪2: 用d2裁剪d1.

  float opSubtraction2(float d1, float d2 ) {
  return max(d1, -d2);
}

float scene(vec3 p) {
  float d1 = sdSphere(p, 1., vec3(0, -1, 0));
  float d2 = sdSphere(p, 0.75, vec3(0, 0.5, 0));
  return opSubtraction2(d1, d2);
}

Smooth Subtraction 2:從d2裁剪d1,使用平滑值k

float opSmoothSubtraction2(float d1, float d2, float k) {
  float h = clamp( 0.5 - 0.5*(d2+d1)/k, 0.0, 1.0 );
  return mix( d1, -d2, h ) + k*h*(1.0-h);
}

float scene(vec3 p) {
  float d1 = sdSphere(p, 1., vec3(0, -1, 0));
  float d2 = sdSphere(p, 0.75, vec3(0, 0.5, 0));
  return opSmoothSubtraction2(d1, d2, 0.2);
}

3D定位

Inigo Quilez's 3D SDFs 的網頁上描述了一系列的3D SDF操作,能夠幫助我們在繪製3D物體時省下不少時間。有些操作還能提高性能,因爲我們不需要重複地運用光線步進函數。

我們之前學習過如果使用變換矩陣來旋轉一個圖形,同時將一個3D物體移動到一定距離。如果你需要縮放一個圖形,你可以簡單地修改SDF的維度即可。

如果你需要繪製對稱的場景,那麼你就需要使用opSymx操作。這個方法將會沿着X軸創建一個對稱的3D物體。如果你繪製的球在vec3(1,0,0)的位置,那麼在vec3(-1, 0, 0)的位置,我們會得到另外一個球;

  float opSymX(vec3 p, float r, vec3 o)
{
  p.x = abs(p.x);
  return sdSphere(p, r, o);
}

float scene(vec3 p) {
  return opSymX(p, 1., vec3(1, 0, 0));
}

如果想要沿着y軸或者z軸做對稱效果,那麼只需要用p.y或者p.z替換p.x即可。同時要記得同時調整你的偏移值。

如果你要沿着兩個軸而不是一個軸繪製球體,那麼你可以使用opSymXZ函數,它分別會在XZ平面上創建一個對象,結果就是出現了四個球。如果我們在vec3(1, 0, 1)的位置上繪製一個球,那麼在vec3(1,0,1), vec3(-1,0,1),vec3(1,0,-1)和vec3(-1, 0, -1)位置上都會出現一個球。

  float opSymXZ(vec3 p, float r, vec3 o)
{
  p.xz = abs(p.xz);
  return sdSphere(p, r, o);
}

float scene(vec3 p) {
  return opSymXZ(p, 1., vec3(1, 0, 1));
}

如果想要沿着多個軸創建一個無限數量的3D物體效果,可以使用opRep函數來實現這種效果。參數,c,用來控制在每條軸上物體在3D空間中的間距。

  float opRep(vec3 p, float r, vec3 o, vec3 c)
{
  vec3 q = mod(p+0.5*c,c)-0.5*c;
  return sdSphere(q, r, o);
}

float scene(vec3 p) {
  return opRep(p, 1., vec3(0), vec3(8));
}

如果想要在軸上創建出有限數量的3D物體,使用opRepLim函數。參數 c,仍然表示間距,參數 l,表示所在軸上的物體的數量。例如vec3(1,0,1)可以沿着x軸和z軸的正負方向繪製一個球體。

  float opRepLim(vec3 p, float r, vec3 o, float c, vec3 l)
{
  vec3 q = p-c*clamp(round(p/c),-l,l);
  return sdSphere(q, r, o);
}

float scene(vec3 p) {
  return opRepLim(p, 0.5, vec3(0), 2., vec3(1, 0, 1));
}

給SDF的計算結果添加p,並且任意修改p,就可以讓物體產生形變以及扭曲的效果。在opDisplace函數中,你可以任意的修改這個值來創建各種數學效果。

  float opDisplace(vec3 p, float r, vec3 o)
{
  float d1 = sdSphere(p, r, o);
  float d2 = sin(p.x)*sin(p.y)*sin(p.z) * cos(iTime);
  return d1 + d2;
}

float scene(vec3 p) {
  return opDisplace(p, 1., vec3(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;
const float PI = 3.14159265359;
const vec3 COLOR_BACKGROUND = vec3(.741, .675, .82);
const vec3 COLOR_AMBIENT = vec3(0.42, 0.20, 0.1);

mat2 rotate2d(float theta) {
  float s = sin(theta), c = cos(theta);
  return mat2(c, -s, s, c);
}

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

float opUnion(float d1, float d2) { 
  return min(d1, d2);
}

float opSmoothUnion(float d1, float d2, float k) {
  float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 );
  return mix( d2, d1, h ) - k*h*(1.0-h);
}

float opIntersection(float d1, float d2) {
  return max(d1, d2);
}

float opSmoothIntersection(float d1, float d2, float k) {
  float h = clamp( 0.5 - 0.5*(d2-d1)/k, 0.0, 1.0 );
  return mix( d2, d1, h ) + k*h*(1.0-h);
}

float opSubtraction(float d1, float d2) {
  return max(-d1, d2);
}

float opSmoothSubtraction(float d1, float d2, float k) {
  float h = clamp( 0.5 - 0.5*(d2+d1)/k, 0.0, 1.0 );
  return mix( d2, -d1, h ) + k*h*(1.0-h);
}

float opSubtraction2(float d1, float d2) {
  return max(d1, -d2);
}

float opSmoothSubtraction2(float d1, float d2, float k) {
  float h = clamp( 0.5 - 0.5*(d2+d1)/k, 0.0, 1.0 );
  return mix( d1, -d2, h ) + k*h*(1.0-h);
}

float opSymX(vec3 p, float r, vec3 o)
{
  p.x = abs(p.x);
  return sdSphere(p, r, o);
}

float opSymXZ(vec3 p, float r, vec3 o)
{
  p.xz = abs(p.xz);
  return sdSphere(p, r, o);
}

float opRep(vec3 p, float r, vec3 o, vec3 c)
{
  vec3 q = mod(p+0.5*c,c)-0.5*c;
  return sdSphere(q, r, o);
}

float opRepLim(vec3 p, float r, vec3 o, float c, vec3 l)
{
  vec3 q = p-c*clamp(round(p/c),-l,l);
  return sdSphere(q, r, o);
}

float opDisplace(vec3 p, float r, vec3 o)
{
  float d1 = sdSphere(p, r, o);
  float d2 = sin(p.x)*sin(p.y)*sin(p.z) * cos(iTime);
  return d1 + d2;
}

float scene(vec3 p) {
  float d1 = sdSphere(p, 1., vec3(0, -1, 0));
  float d2 = sdSphere(p, 0.75, vec3(0, 0.5, 0));
  //return d1;
  //return d2;
  //return opUnion(d1, d2);
  //return opSmoothUnion(d1, d2, 0.2);
  //return opIntersection(d1, d2);
  //return opSmoothIntersection(d1, d2, 0.2);
  //return opSubtraction(d1, d2);
  //return opSmoothSubtraction(d1, d2, 0.2);
  //return opSubtraction2(d1, d2);
  //return opSmoothSubtraction2(d1, d2, 0.2);
  //return opSymX(p, 1., vec3(1, 0, 0));
  //return opSymXZ(p, 1., vec3(1, 0, 1));
  //return opRep(p, 1., vec3(0), vec3(8));
  //return opRepLim(p, 0.5, vec3(0), 2., vec3(1, 0, 1));
  return opDisplace(p, 1., vec3(0));
}

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));
}

mat3 camera(vec3 cameraPos, vec3 lookAtPoint) {
	vec3 cd = normalize(lookAtPoint - cameraPos);
	vec3 cr = normalize(cross(vec3(0, 1, 0), cd));
	vec3 cu = normalize(cross(cd, cr));
	
	return mat3(-cr, cu, -cd);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
  vec2 mouseUV = iMouse.xy/iResolution.xy;
  
  if (mouseUV == vec2(0.0)) mouseUV = vec2(0.5); // trick to center mouse on page load

  vec3 col = vec3(0);
  vec3 lp = vec3(0);
  vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position
  
  float cameraRadius = 2.;
  ro.yz = ro.yz * cameraRadius * rotate2d(mix(-PI/2., PI/2., mouseUV.y));
  ro.xz = ro.xz * rotate2d(mix(-PI, PI, mouseUV.x)) + vec2(lp.x, lp.z);

  vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction

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

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

    vec3 lightPosition = vec3(0, 2, 2);
    vec3 lightDirection = normalize(lightPosition - p) * .65; // The 0.65 is used to decrease the light intensity a bit

    float dif = clamp(dot(normal, lightDirection), 0., 1.) * 0.5 + 0.5; // diffuse reflection mapped to values between 0.5 and 1.0

    col = vec3(dif) + COLOR_AMBIENT;    
  }

  fragColor = vec4(col, 1.0);
}

總結

通過本篇教程,我們學習了各種3D物體的形變,例如unionsintersections,以及subtractions等操作。同時學會了使用“positional”方法來在不同的軸上繪製相同的圖形。下面的一些資源中,包含了我創建的一個光線步進的模板代碼,以及上文中提到的一些3D SDF函數操作。這裏討論的還只是一小部分SDF操作,還有其他的很多操作,你需要訪問Inigo Quilez的網站來學習。

資源

Ray Marching Template
3D SDF Operations
Combination
Elongation
Rounding
Onion
Metric
Repetition
Extrusion2D
Revolution2D
Ray Marching Primitives
Ray Marching Primitives Commented

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