demoscene.jp Japanese demoscene portal

1712月/120

Raymarching始めてみた!

こんにちは、本日担当の@_keichi_です!今日まで「GLSLで流体シミュレーション!」という内容で書こうと思っていたんですが、ちょっとあまりにも趣旨違い過ぎね…?というのと、gyaboさんtomohiroさんのアドベントカレンダーで知ったレイマーチンすごい!ということで、急遽レイマーチンはもちろん、メガデモすらつくったことがない素人の僕がレイマーチンを始めてみました。

レイマーチン

普通のレイトレーシングであれば、レイと物体の交差点を方程式を解いて一発で出すところを、視点を少しずつレイの上を移動させながら、交差する点を探索する方式らしいです。このとき使用するのがdistance functionというやつで、任意の点と物体の最短距離を返す関数です。プリミティブのdistance functionの一覧は、Inigo Quilezさんのサイトにある、このページが参考になります。distance functionを使うと何が良いかと言うと、簡単に物体の変形・拡大・回転などができる他、繰り返しや融合なども従来のレイトレーシングに比べれば簡単にできるようになるのです。レイマーチンの詳細については、

がとても参考になります!これだけ読めばとりあえずレイマーチンはつくれるようになると思います。

環境について

実はマカーです。これだけ門前払いされそうです。マシンはMac Book Pro 15inch Retinaなので、大したことないマシンでも十分動くと思います。

まずはツールから

環境がMacなので、まずはGLSLを動かすツールからつくっていきます…

//
//  main.cpp
//  raymarch
//

#include 
#include 
#ifdef __APPLE__
#include <GLUT/glut.h>
#endif
#if defined _WIN32 || defined _WIN64
#include <gl/glew.h>
#include <gl/glut.h>
#endif

#include "shader_utils.h"

#define SCREEN_X    (1200)
#define SCREEN_Y    (900)

// GLSL shader program object
static GLuint program;
// Vertex array object
static GLuint buffer;

// Vertex array
float vertex[] = {
    // x, y, z
    -1.0, 1.0, 0.0,
    1.0, 1.0, 0.0,
    -1.0, -1.0, 0.0,
    1.0, -1.0, 0.0,
};

// Vertex array index
GLuint vtxindex[] = {
    0,1,2,3
};

void force_redraw(int value);
int make_resources(void);
void render(void);

int make_resources()
{
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW);

    return 1;
}

void render()
{
    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(program);
    GLint aspect_ratio = glGetUniformLocation(program, "aspect_ratio");
    glUniform1f(aspect_ratio, (double)SCREEN_X / SCREEN_Y);

    glEnableClientState(GL_VERTEX_ARRAY);

    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glVertexPointer(3, GL_FLOAT, 0, 0);    
    glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_INT, static_cast(vtxindex));

    glDisableClientState(GL_VERTEX_ARRAY);

    glutSwapBuffers();
}

void force_redraw(int value)
{
    glutPostRedisplay();
    glutTimerFunc(20, force_redraw, 0);
}

int main (int argc, const char * argv[])
{
    glutInit(&argc, const_cast(argv));
    // enable double buffering
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
    glutInitWindowSize(1200, 900);
    glutCreateWindow("Raymarching");
    glClearColor(1.0, 1.0, 1.0, 1.0);

#if defined _WIN32 || defined _WIN64
	GLuint error = glewInit();
	if (error != GLEW_OK) {
		std::cout << "Failed to initialize GLEW: " << glewGetErrorString(error) << std::endl;
		return 1;
	}
#endif

    // register display function
    glutDisplayFunc(render);
    // register timer event handler
    glutTimerFunc(20, force_redraw, 0);

    // load and compile vertex/fragment shader programs
    load_shader("raymarch.vert", "raymarch.frag", &program);

    if (!make_resources()) {
        std::cout << "Failed to prepare resources" << std::endl;
        return 1;
    }

    // start main loop
    glutMainLoop();

    return 0;
}

とりあえず一枚板のポリゴンがあれば良いらしいので、左下(-1, -1, 0)右上(1, 1, 0)の長方形になるように三角形のポリゴンを2枚貼っています。

バーテックスシェーダ

#version 120

uniform float aspect_ratio;
varying vec2 pos;

void main()
{
    gl_Position = gl_Vertex;
    pos = vec2(gl_Vertex.x * aspect_ratio, gl_Vertex.y);
}

バーテックスシェーダは簡単です。posというuniformな変数に現在の画面上での位置を渡すだけです。

フラグメントシェーダ

#version 120

// FragmentProgram
varying vec2 pos;
const float eps = 0.001;

float scene(vec3 pos)
{
    float plane = dot(pos, vec3(0.0, 1.0, 0.0)) + 0.5;

    pos.xz = mod(pos.xz, 2) - vec2(1, 1);
    float box = length(max(abs(pos) - vec3(0.5, 0.5, 0.5), 0.0)) - 0.05;
    return min(plane, box);
}

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

void main() {
    vec3 camPos = vec3(0.0, 2.0, 5.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 rayDirection = normalize(camSide*pos.x + camUp*pos.y + camDir*focus);
    float distance;
    vec3 currentCamPos = camPos;

    for(int i = 0; i < 128; ++i) {
        distance = scene(
currentCamPos
);
        currentCamPos += rayDirection * scene(currentCamPos) * 0.95;
    }

    vec3 normal = get_normal(currentCamPos);
    if(abs(distance) < eps) {
        float c = dot(normalize(vec3(1.0, 1.0, 1.0)), normal) + length(currentCamPos - camPos) * 0.01;
        gl_FragColor = vec4(c, c, c, 1.0);
    } else {
        gl_FragColor = vec4(1.0);
    }
}

そしてフラグメントシェーダになるわけですが、実質ここだけでレイマーチンを行っています。tomohiroさんのコードをめっちゃ借りてます。すみません。

処理の内容は上記様々な資料にある通りなので、僕のひっかかったところを紹介させていただきます。

  • まずレイに沿って視点を近づけて行っている37-40行目の部分ですが、反復回数が少ないと、物体のエッジの部分がおかしくなったり、極端に遠い物体が上手く描画されません。考えてみれば当たり前ですが、はまりました……
  • 同じく39行目ですが、0.95をかけてやらないと、距離関数の誤差があったとき視点が移動しすぎ、物体内部まで入り込んでしまいます。はまりました……
  • シェーディングは法線・光源で計算していますが、レイトレーシングと異なってレイマーチングだと法線が自明には出てこないんですね。よって上記Inigo Quilezさんが提案されている距離関数の微小変化から法線ベクトルを推測する手法をとることになります。これがget_normalです。
  • 距離関数は地面の上に箱が無限に並ぶシーンを表現しています。ここでもまたまたInigoさんのサイトを参考に、プリミティブやその繰り返しや組み合わせ方を勉強させてもらいました。

完成!

というわけで無事に完成しました!やはり遠いところがおかしいのは否めないんですが、フラグメントシェーダだけでこんな絵ができるとは……感動です。やはりレイトレーシングと違って、簡単にシーンを拡張していけるのが魅力かなあ、と思いました。これから色々やりたいことは思いつくんですが、とりあえず、

  • 時間をuniformで受け取って動きのあるシーンへ
  • perlin-noiseとか生成してバンプマップしてみたら面白そう
  • プリミティブの変形とかやってみたい
  • ポストエフェクトもかけたい
  • シェーディングをもっともっとかっこよくする!(みなさんは一体どうやって考えているのでしょう……)
  • そしてTDF2013へ……

というわけですっかりレイマーチン&メガデモにはまってしまいました。みなさん今後ともよろしくお願いします。しかしまずはWindowsマシンを買います……

Comments (0) Trackbacks (0)

No comments yet.


Leave a comment

Trackbacks are disabled.