demoscene.jp Japanese demoscene portal

712月/120

全能感UP! GLSLで進めレイマーチング

こんにちは。tomohiroです。

概要

最近の4k/64k introでよく使われているレイマーチング(Ray marching)法について説明する。
レイマチーングとはレイトレーシング法の一種である。
レイマーチングではレイの始点から少しづつ進みながらシーン内のオブジェクトとの交点を求める。
シーン内のオブジェクトはDistance functionという関数で記述される。

なぜレイマーチング, Distance functionを使うのか

Distance functionは数行程度のコードによって実装できるので, 小さなデータで3D形状を表現する事ができる。
複雑な形状も関数をいくつか組み合わせる事によって作る事ができる。
しかし三角形ポリゴンほど汎用的に形状を表現するのは難しい。
レイマーチングを使うと Distance functionで表現された形状を少しのコードでレンダリングする事ができる。
また, 反射や影も簡単にレンダリングできる。
ラスタライズ法でレンダリングしする場合, Distance functionをポリゴン化する必要があるがそれを実装するには手間が掛かかる。
また, 滑らかな曲面を表現するにも多くのポリゴンが必要になる。

 

レイトレーシング法について

シーン内にカメラ, オブジェクト, 光源があるとする。
レイトレーシング法では、カメラから各ピクセルに対応するレイを飛ばし
レイがオブジェクトと交差するか計算する。
レイがオブジェクトと交差しない場合はピクセルに背景の色を設定する。
オブジェクトと交差した場合は, 交差した点でのオブジェクトの状態と光源を元にカメラに入射する色を計算(シェーディング)してピクセルに設定する。
このような計算を全ピクセルに対して繰り返す。

 

Raytracingの解説図

レイトレーシングはこんな感じ

 

Distance functionについて

Distance functionとは, 空間内の点の位置を受け取り, 図形と点との最短距離を返す関数である。
この関数の値が0となる点の集合が図形の表面となる。
例えば中心が原点, 半径rの球をDistance functionで表現すると以下のようになる。
DF(x, y, z) = sqrt(x*x + y*y + z*z) - r
図形の内部で値が負となるDistance functionをSigned distance function,
常に0以上の値となるものはUnsigned distance functionと呼ぶこともある.


画像をクリックすると1ステップづつ進む。

 

GLSLでレイマーチング

OpenGLでは画面に三角形をレンダリングするときに, 三角形内の各ピクセルに対してフラグメントシェーダを一回づつ実行してピクセルの色を決める。
(その後に画像を加工する処理等を入れる事もあるので、必ずしもフラグメントシェーダの実行結果の色がそのまま画面に表示されるとは限らない。)
フラグメントシェーダはGPUで実行され, GLSL言語によって自由にプログラミングする事ができる。
OpenGLでレイトレーシング法を実装するときは、画面全体を覆う四角形を描画し、
レイトレーシングを実装したフラグメントシェーダを画面内の全ピクセルに対して実行する。

 

GLSLでの実装

・以下のコードはGLSL SandboxLive Coderで実行できる。

画面の左下が(-w, -1), 右上が(w, 1)となるような座標系を作る。
座標が格納されている変数を色として出力しフラグメントシェーダが正しく動作している事を確認。

uniform vec2 resolution;
uniform float time;

void main() {
  vec2 pos = (gl_FragCoord.xy*2.0 -resolution) / resolution.y;

  gl_FragColor = vec4(pos, 0.0, 1.0);
}



 

カメラの位置, 向きと焦点距離を決めて, レイの位置と向きを決める。

uniform vec2 resolution;
uniform float time;

void main() {
  vec2 pos = (gl_FragCoord.xy*2.0 -resolution) / resolution.y;

  vec3 camPos = vec3(0.0, 0.0, 3.0);
  vec3 camDir = vec3(0.0, 0.0, -1.0);
  vec3 camUp = vec3(0.0, 1.0, 0.0);
  vec3 camSide = cross(camDir, camUp);
  float focus = 1.8;

  vec3 rayDir = normalize(camSide*pos.x + camUp*pos.y + camDir*focus);

  gl_FragColor = vec4(rayDir.xy*2.0, -rayDir.z, 1.0);
}



 

球のDistance function(distaceFunction)を定義。
マーチングループを追加する。
レイがヒットしたら白く表示。
Distance functionの返す距離が十分小さいならばヒットしたとみなす。

uniform vec2 resolution;
uniform float time;

float distaceFunction(vec3 pos)
{
  return length(pos) - 1.0;
}

void main() {
  vec2 pos = (gl_FragCoord.xy*2.0 -resolution) / resolution.y;

  vec3 camPos = vec3(0.0, 0.0, 3.0);
  vec3 camDir = vec3(0.0, 0.0, -1.0);
  vec3 camUp = vec3(0.0, 1.0, 0.0);
  vec3 camSide = cross(camDir, camUp);
  float focus = 1.8;

  vec3 rayDir = normalize(camSide*pos.x + camUp*pos.y + camDir*focus);

  float t = 0.0, d;
  vec3 posOnRay = camPos;

  for(int i=0; i<16; ++i)
  {
    d = distaceFunction(posOnRay);
    t += d;
    posOnRay = camPos + t*rayDir;
  }

  if(abs(d) < 0.001)
  {
    gl_FragColor = vec4(1.0);
  }else
  {
    gl_FragColor = vec4(0.0);
  }
}



 

getNormal関数でDistance functionの勾配を計算して法線ベクトルを求める。
ここではシェーディングの計算は省略し, 法線ベクトルをそのまま色として出力する。

uniform vec2 resolution;
uniform float time;

float distanceFunction(vec3 pos)
{
  return length(pos) - 1.0;
}

vec3 getNormal(vec3 p)
{
  const float d = 0.0001;
  return
    normalize
    (
      vec3
      (
        distanceFunction(p+vec3(d,0.0,0.0))-distanceFunction(p+vec3(-d,0.0,0.0)),
        distanceFunction(p+vec3(0.0,d,0.0))-distanceFunction(p+vec3(0.0,-d,0.0)),
        distanceFunction(p+vec3(0.0,0.0,d))-distanceFunction(p+vec3(0.0,0.0,-d))
      )
    );
}

void main() {
  vec2 pos = (gl_FragCoord.xy*2.0 -resolution) / resolution.y;

  vec3 camPos = vec3(0.0, 0.0, 3.0);
  vec3 camDir = vec3(0.0, 0.0, -1.0);
  vec3 camUp = vec3(0.0, 1.0, 0.0);
  vec3 camSide = cross(camDir, camUp);
  float focus = 1.8;

  vec3 rayDir = normalize(camSide*pos.x + camUp*pos.y + camDir*focus);

  float t = 0.0, d;
  vec3 posOnRay = camPos;

  for(int i=0; i<16; ++i)
  {
    d = distanceFunction(posOnRay);
    t += d;
    posOnRay = camPos + t*rayDir;
  }

  vec3 normal = getNormal(posOnRay);
  if(abs(d) < 0.001)
  {
    gl_FragColor = vec4(normal, 1.0);
  }else
  {
    gl_FragColor = vec4(0.0);
  }
}



 

球のDistance functionではlength関数を使って距離を計算している。
距離の計算方法を改造することによって, 丸まった立方体のような形状になる。

uniform vec2 resolution;
uniform float time;

float lengthN(vec3 v, float n)
{
  vec3 tmp = pow(abs(v), vec3(n));
  return pow(tmp.x+tmp.y+tmp.z, 1.0/n);
}

float distanceFunction(vec3 pos)
{
  return lengthN(pos, 4.0) - 1.0;
}

vec3 getNormal(vec3 p)
{
  const float d = 0.0001;
  return
    normalize
    (
      vec3
      (
        distanceFunction(p+vec3(d,0.0,0.0))-distanceFunction(p+vec3(-d,0.0,0.0)),
        distanceFunction(p+vec3(0.0,d,0.0))-distanceFunction(p+vec3(0.0,-d,0.0)),
        distanceFunction(p+vec3(0.0,0.0,d))-distanceFunction(p+vec3(0.0,0.0,-d))
      )
    );
}

void main() {
  vec2 pos = (gl_FragCoord.xy*2.0 -resolution) / resolution.y;

  vec3 camPos = vec3(0.0, 0.0, 3.0);
  vec3 camDir = vec3(0.0, 0.0, -1.0);
  vec3 camUp = vec3(0.0, 1.0, 0.0);
  vec3 camSide = cross(camDir, camUp);
  float focus = 1.8;

  vec3 rayDir = normalize(camSide*pos.x + camUp*pos.y + camDir*focus);

  float t = 0.0, d;
  vec3 posOnRay = camPos;

  for(int i=0; i<16; ++i)
  {
    d = distanceFunction(posOnRay);
    t += d;
    posOnRay = camPos + t*rayDir;
  }

  vec3 normal = getNormal(posOnRay);
  if(abs(d) < 0.001)
  {
    gl_FragColor = vec4(normal, 1.0);
  }else
  {
    gl_FragColor = vec4(0.0);
  }
}



 

Distance functionに入力される位置をmodでリピートすると同じ図形が繰り返し表示さ
れる。
繰り返し計算回数を増やすとより遠くまで繰り返しが見えるようになる。

uniform vec2 resolution;
uniform float time;

vec3 trans(vec3 p)
{
  return mod(p, 8.0)-4.0;
}

float lengthN(vec3 v, float n)
{
  vec3 tmp = pow(abs(v), vec3(n));
  return pow(tmp.x+tmp.y+tmp.z, 1.0/n);
}

float distanceFunction(vec3 pos)
{
  return lengthN(trans(pos), 4.0) - 1.0;
}

vec3 getNormal(vec3 p)
{
  const float d = 0.0001;
  return
    normalize
    (
      vec3
      (
        distanceFunction(p+vec3(d,0.0,0.0))-distanceFunction(p+vec3(-d,0.0,0.0)),
        distanceFunction(p+vec3(0.0,d,0.0))-distanceFunction(p+vec3(0.0,-d,0.0)),
        distanceFunction(p+vec3(0.0,0.0,d))-distanceFunction(p+vec3(0.0,0.0,-d))
      )
    );
}

void main() {
  vec2 pos = (gl_FragCoord.xy*2.0 -resolution) / resolution.y;

  vec3 camPos = vec3(0.0, 0.0, 3.0);
  vec3 camDir = vec3(0.0, 0.0, -1.0);
  vec3 camUp = vec3(0.0, 1.0, 0.0);
  vec3 camSide = cross(camDir, camUp);
  float focus = 1.8;

  vec3 rayDir = normalize(camSide*pos.x + camUp*pos.y + camDir*focus);

  float t = 0.0, d;
  vec3 posOnRay = camPos;

  for(int i=0; i<64; ++i)
  {
    d = distanceFunction(posOnRay);
    t += d;
    posOnRay = camPos + t*rayDir;
  }

  vec3 normal = getNormal(posOnRay);
  if(abs(d) < 0.001)
  {
    gl_FragColor = vec4(normal, 1.0);
  }else
  {
    gl_FragColor = vec4(0.0);
  }
}



 

 

参考文献

日本のレイマーチングの巨匠gyabo氏がTokyoDemoFest2012において講演されたときの資料
http://tokyo-demo-fest.jpn.org/2012/?page=event#seminar

スペイン生まれのエリート中のエリートデモシーナ I?go Quilez氏のサイト。(英語)
レイマーチングだけでなく様々な技術が紹介されている。
http://www.iquilezles.org/
どこにレイマーチングの情報があるかみつけられない人用:
http://www.iquilezles.org/www/articles/raymarchingdf/raymarchingdf.htm

pouetのBBSのレイマーチングスレッド.(英語)
Raymarching Beginners' Threadというタイトルだが, 初心者には難しい。
レイマーチングで困った事があればここで質問するとよいだろう。
http://pouet.net/topic.php?which=7920

Mikael Hvidtfeldt Christensen氏による3Dフラクタルをレイマーチングでレンダリングする方法の解説。(英語)
http://blog.hvidtfeldts.net/index.php/2011/06/distance-estimated-3d-fractals-part-i/

Comments (0) Trackbacks (0)

No comments yet.


Leave a comment

Trackbacks are disabled.