Shadertoy 教程 Part 8 - 3D 旋轉

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教程的第八章。在本篇教程中,我們會學習到如何使用變換矩陣來旋轉3D物體。

初始化

我們新建一個着色器,使用上一篇教程中的代碼片段,然後把關於球的那部分代碼移除掉。

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

struct Surface {
    float sd; // 符號距離值
    vec3 col; // 顏色
};

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

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

Surface sdScene(vec3 p) {
  vec3 floorColor = vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0));
  Surface co = sdFloor(p, floorColor);
  return co;
}

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

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

vec3 calcNormal(in vec3 p) {
    vec2 e = vec2(1.0, -1.0) * 0.0005; // epsilon
    return normalize(
      e.xyy * sdScene(p + e.xyy).sd +
      e.yyx * sdScene(p + e.yyx).sd +
      e.yxy * sdScene(p + e.yxy).sd +
      e.xxx * sdScene(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); // 射線源頭即相機的位置
  vec3 rd = normalize(vec3(uv, -1)); // 射線方向

  Surface co = rayMarch(ro, rd, MIN_DIST, MAX_DIST); // 最近的物體

  if (co.sd > MAX_DIST) {
    col = backgroundColor; // ray didn't hit anything
  } else {
    vec3 p = ro + rd * co.sd; // 通過光線步進算法得到的立方體或者地板上的某個點r
    vec3 normal = calcNormal(p);
    vec3 lightPosition = vec3(2, 2, 7);
    vec3 lightDirection = normalize(lightPosition - p);

    float dif = clamp(dot(normal, lightDirection), 0.3, 1.); // 漫反射

    col = dif * co.col + backgroundColor * .2; //
  }

  // 輸出到屏幕
  fragColor = vec4(col, 1.0);
}

運行以上代碼,可以看到一個平鋪的格子地板,以及靛藍色的背景。

添加立方體

接下來,Inigo Quilez 網站上列出的3D SDFs列表中,有一段是關於如何繪製立方體的。在“Primitives”章節中的“BOX - exact”的標籤下,我們用這段代碼來創建一個立方體的。

  float sdBox( vec3 p, vec3 b )
{
  vec3 q = abs(p) - b;
  return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}

爲了讓這段代碼與我們現有的代碼兼容,我們可以使用上一節教程中學到的方式,將函數的返回值從float類型改爲Surface結構類型。這樣我們就能爲物體添加一個唯一的顏色。我們還需要爲函數添加兩個參數:偏移值和顏色。

  Surface sdBox( vec3 p, vec3 b, vec3 offset, vec3 col)
{
  p = p - offset;
  vec3 q = abs(p) - b;
  float d = length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
  return Surface(d, col);
}

第一個參數p,就是採樣的點,第二個參數b,是一個類型爲vec3的變量,表示盒子的邊界。使用xy和z元素控制盒子的長、寬、高。如果它們的值相同,就可以得到一個正方形盒子。

我們把立方體加入到3D場景中:

Surface sdScene(vec3 p) {
  vec3 floorColor = vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0));
  Surface co = sdFloor(p, floorColor);
  co = minWithColor(co, sdBox(p, vec3(1), vec3(0, 0.5, -4), vec3(1, 0, 0)));
  return co;
}

我們得到了一個111的紅色立方體,它所處的位置爲(0,0.5,-0.4)。

旋轉矩陣

在線性幾何中,變換矩陣通常用來執行一系列2D和3D圖形的變化:拉伸、擠壓、旋轉、裁剪和反射等。一個矩陣對應一種操作。

讓圖形上的某個點(GLSL代碼中的採樣點)乘以一個變換矩陣,我們就可以執行這些操作。我們同樣也可以讓多個矩陣相乘來得到一個新的矩陣,藉此來一次執行多種操作。

因爲矩陣的乘法運算是非交換的,所有矩陣在乘法中的位置就顯得很重要。對一個圖形進行旋轉之後又執行裁剪,得到的結果與先裁剪後再旋轉的結果完全不一樣。同樣地,圖形在x軸上旋轉之後再在z軸上進行旋轉,如果轉換旋轉順序,效果也是大不相同。

旋轉矩陣是變換矩陣的其中一種類型。我們來看看我們在本節教程中要使用到的[旋轉矩陣]
(https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations)吧:

維基百科(旋轉矩陣)

上圖中,我們有三個旋轉矩陣,分別對應3D場景中的每個軸。執行上面的矩陣會讓圖形沿着軸旋轉,就行體操健將繞着杆子旋轉一樣。

我們在代碼的頂部添加三個矩陣函數,它們分別圍繞着三條軸進行旋轉。我們同時需要添加一個返回 identity matrix 的函數,它表示不執行任何矩陣操作。

// Rotation matrix around the X axis.
mat3 rotateX(float theta) {
    float c = cos(theta);
    float s = sin(theta);
    return mat3(
        vec3(1, 0, 0),
        vec3(0, c, -s),
        vec3(0, s, c)
    );
}

// Rotation matrix around the Y axis.
mat3 rotateY(float theta) {
    float c = cos(theta);
    float s = sin(theta);
    return mat3(
        vec3(c, 0, s),
        vec3(0, 1, 0),
        vec3(-s, 0, c)
    );
}

// Rotation matrix around the Z axis.
mat3 rotateZ(float theta) {
    float c = cos(theta);
    float s = sin(theta);
    return mat3(
        vec3(c, -s, 0),
        vec3(s, c, 0),
        vec3(0, 0, 1)
    );
}

// Identity matrix.
mat3 identity() {
    return mat3(
        vec3(1, 0, 0),
        vec3(0, 1, 0),
        vec3(0, 0, 1)
    );
}

現在,我們需要調整sdbox函數,爲其新增一個參數來接收變換矩陣。給採樣的點乘以一個旋轉矩陣之後,我們的採樣點就會進行座標軸偏移值,運動到一個新的座標。

  Surface sdBox( vec3 p, vec3 b, vec3 offset, vec3 col, mat3 transform)
{
  p = (p - offset) * transform;
  vec3 q = abs(p) - b;
  float d = length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
  return Surface(d, col);
}

修改sdScene函數再調用sdBox函數是插入一個新的參數:

  Surface sdScene(vec3 p) {
  vec3 floorColor = vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0));
  Surface co = sdFloor(p, floorColor);
  co = minWithColor(co, sdBox(p, vec3(1), vec3(0, 0.5, -4), vec3(1, 0, 0), rotateX(iTime)));
  return co;
}

使用rotateXrotateY以及rotateZ繞三條軸進行獨立的旋轉。將角度設置爲iTime,這樣我們就能根據時間的維度來旋轉立方體。立方體的軸心點設置在自己的中心:

下圖就是一個例子,在sdBox函數裏面使用rotateX(iTime),讓立方體沿着x軸旋轉:

sdBox函數裏面使用rotateY(iTime),讓立方體沿着y軸旋轉:

sdBox函數裏面使用rotateZ(iTime),讓立方體沿着z軸旋轉:

如果不想任何旋轉效果,我們調用identity函數即可:

Surface sdScene(vec3 p) {
  vec3 floorColor = vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0));
  Surface co = sdFloor(p, floorColor);
  co = minWithColor(co, sdBox(p, vec3(1), vec3(0, 0.5, -4), vec3(1, 0, 0), identity())); // By using the identity matrix, the cube's orientation remains the same
  return co;
}

我們也可以通過將他們相乘的方式讓它們結合在一起。這樣,立方體就可以沿着三個軸同時旋轉了。

Surface sdScene(vec3 p) {
  vec3 floorColor = vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0));
  Surface co = sdFloor(p, floorColor);
  co = minWithColor(co, sdBox(
      p,
      vec3(1),
      vec3(0, 0.5, -4),
      vec3(1, 0, 0),
      rotateX(iTime) * rotateY(iTime) * rotateZ(iTime) // Combine rotation matrices
  ));
  return co;
}

下面是完整的代碼:

  // Rotation matrix around the X axis.
mat3 rotateX(float theta) {
    float c = cos(theta);
    float s = sin(theta);
    return mat3(
        vec3(1, 0, 0),
        vec3(0, c, -s),
        vec3(0, s, c)
    );
}

// Rotation matrix around the Y axis.
mat3 rotateY(float theta) {
    float c = cos(theta);
    float s = sin(theta);
    return mat3(
        vec3(c, 0, s),
        vec3(0, 1, 0),
        vec3(-s, 0, c)
    );
}

// Rotation matrix around the Z axis.
mat3 rotateZ(float theta) {
    float c = cos(theta);
    float s = sin(theta);
    return mat3(
        vec3(c, -s, 0),
        vec3(s, c, 0),
        vec3(0, 0, 1)
    );
}

// Identity matrix.
mat3 identity() {
    return mat3(
        vec3(1, 0, 0),
        vec3(0, 1, 0),
        vec3(0, 0, 1)
    );
}

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

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

Surface sdBox( vec3 p, vec3 b, vec3 offset, vec3 col, mat3 transform)
{
  p = (p - offset) * transform;
  vec3 q = abs(p) - b;
  float d = length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
  return Surface(d, col);
}

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

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

Surface sdScene(vec3 p) {
  vec3 floorColor = vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0));
  Surface co = sdFloor(p, floorColor);
  co = minWithColor(co, sdBox(
      p,
      vec3(1),
      vec3(0, 0.5, -4),
      vec3(1, 0, 0),
      rotateX(iTime)*rotateY(iTime)*rotateZ(iTime) // Combine rotation matrices
  ));
  return co;
}

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

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

vec3 calcNormal(in vec3 p) {
    vec2 e = vec2(1.0, -1.0) * 0.0005; // epsilon
    return normalize(
      e.xyy * sdScene(p + e.xyy).sd +
      e.yyx * sdScene(p + e.yyx).sd +
      e.yxy * sdScene(p + e.yxy).sd +
      e.xxx * sdScene(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, MIN_DIST, MAX_DIST); // closest object

  if (co.sd > MAX_DIST) {
    col = backgroundColor; // ray didn't hit anything
  } else {
    vec3 p = ro + rd * co.sd; // point on cube or floor we discovered from ray marching
    vec3 normal = calcNormal(p);
    vec3 lightPosition = vec3(2, 2, 7);
    vec3 lightDirection = normalize(lightPosition - p);

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

    col = dif * co.col + backgroundColor * .2; // Add a bit of background color to the diffuse color
  }

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

沿着一個點進行旋轉

如果我們想要立方體沿着某個外部的點而不是其中心軸進行旋轉,我們需要修改sdBox函數,讓它在旋轉操作之後移動一段距離。

Surface sdBox( vec3 p, vec3 b, vec3 offset, vec3 col, mat3 transform)
{
  p = (p - offset) * transform - vec3(3, 0, 0); // Move the cube as it is rotating
  vec3 q = abs(p) - b;
  float d = length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
  return Surface(d, col);
}

如果我們在sdScene函數中使用rotateY(iTime),則我們的立方體看起來就是沿着中心點以外的Y軸上的某個點進行旋轉。在這個例子當中,我們讓立方體旋轉的同時距離x軸一段距離,這個距離用vec3(3,0,0)表示。下面就是完整的代碼:

  // Rotation matrix around the X axis.
mat3 rotateX(float theta) {
    float c = cos(theta);
    float s = sin(theta);
    return mat3(
        vec3(1, 0, 0),
        vec3(0, c, -s),
        vec3(0, s, c)
    );
}

// Rotation matrix around the Y axis.
mat3 rotateY(float theta) {
    float c = cos(theta);
    float s = sin(theta);
    return mat3(
        vec3(c, 0, s),
        vec3(0, 1, 0),
        vec3(-s, 0, c)
    );
}

// Rotation matrix around the Z axis.
mat3 rotateZ(float theta) {
    float c = cos(theta);
    float s = sin(theta);
    return mat3(
        vec3(c, -s, 0),
        vec3(s, c, 0),
        vec3(0, 0, 1)
    );
}

// Identity matrix.
mat3 identity() {
    return mat3(
        vec3(1, 0, 0),
        vec3(0, 1, 0),
        vec3(0, 0, 1)
    );
}

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

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

Surface sdBox( vec3 p, vec3 b, vec3 offset, vec3 col, mat3 transform)
{
  p = (p - offset) * transform - vec3(3, 0, 0); // Move the cube as it is rotating
  vec3 q = abs(p) - b;
  float d = length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
  return Surface(d, col);
}

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

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

Surface sdScene(vec3 p) {
  vec3 floorColor = vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0));
  Surface co = sdFloor(p, floorColor);
  co = minWithColor(co, sdBox(p, vec3(1), vec3(0, 0.5, -4), vec3(1, 0, 0), rotateY(iTime)));
  return co;
}

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

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

vec3 calcNormal(in vec3 p) {
    vec2 e = vec2(1.0, -1.0) * 0.0005; // epsilon
    return normalize(
      e.xyy * sdScene(p + e.xyy).sd +
      e.yyx * sdScene(p + e.yyx).sd +
      e.yxy * sdScene(p + e.yxy).sd +
      e.xxx * sdScene(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, MIN_DIST, MAX_DIST); // closest object

  if (co.sd > MAX_DIST) {
    col = backgroundColor; // ray didn't hit anything
  } else {
    vec3 p = ro + rd * co.sd; // point on cube or floor we discovered from ray marching
    vec3 normal = calcNormal(p);
    vec3 lightPosition = vec3(2, 2, 7);
    vec3 lightDirection = normalize(lightPosition - p);

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

    col = dif * co.col + backgroundColor * .2; // Add a bit of background color to the diffuse color
  }

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

總結

本節教程,我們學會了如何在3D空間中讓立方體沿着每條軸旋轉。我們也學會了讓立方體沿着外部的一個軸點進行旋轉,就像他們是在一條軌道上旋轉一樣。我們在這裏選擇的是立方體而非球體,是因爲前者比起後者來要更容易觀察運動軌跡。

資源

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