鏡面反射

2009-07-17

レイトレーサーを作るシリーズ:

視線が鏡面にぶつかったとき、法線に対する視線の反射ベクトル(入射角=反射角)を次の式で求める:

求めた反射ベクトルと交差点で光線追跡を継続する:

boolean rendererd = false;
int renderY = 0;
int LineBuf[];

// 値を範囲内に収める
float clamp(float val, float min, float max) {
if (val < min) {
return min;
} else if (val > max) {
return max;
} else {
return val;
}
}

// 反射ベクトルを求める
vec reflection(vec v, vec normal) {
return v.add(normal.scale(-2 * v.dot(normal)));
}

int fcolor(float r, float g, float b) {
int ir = (int)(clamp(r, 0, 1) * 255.99);
int ig = (int)(clamp(g, 0, 1) * 255.99);
int ib = (int)(clamp(b, 0, 1) * 255.99);
return color(ir, ig, ib);
}

// シーン
class Scene {
ArrayList objs;
ArrayList lights;

Scene() {
objs = new ArrayList();
lights = new ArrayList();
}

void add_obj(Object obj) {
objs.add(obj);
}

void add_light(PointLight light) {
lights.add(light);
}

// 一番近くで交差するオブジェクトを探す
void find_nearest_intersect_with_ray(Intersect isect, vec orig, vec dir) {
isect.t = -1;
vec normal = new vec(0, 0, 0);
for (int i=objs.size(); --i>=0; ) {
Object obj = (Object)objs.get(i);
float t = obj.intersect_line(normal, orig, dir);
if (t >= 0 && (isect.t < 0 || t < isect.t)) {
isect.t = t;
isect.normal.set(normal);
isect.obj = obj;
}
}
}

// 目標物までの間に何かに当たるか?
boolean intersect_anything(vec orig, vec target) {
vec diff = target.sub(orig);
float len = diff.length();
vec dir = diff.scale(1.0 / len);
vec normal = new vec(0, 0, 0);
for (int i=objs.size(); --i>=0; ) {
Object obj = (Object)objs.get(i);
float t = obj.intersect_line(normal, orig, dir);
if (t >= 0 && t < len) {
return true;
}
}
return false;
}

// シェーディング
vec shading(vec pos, vec normal, Material material, vec raydir) {
vec col = new vec(0, 0, 0);
float a = material.reflection;
if (a < 1) {
col = col.add(calc_diffuse(pos, normal, material).scale(1 - a));
}
if (a > 0) {
col = col.add(calc_reflection(pos, normal, raydir).scale(a));
}
return col;
}
vec calc_diffuse(vec pos, vec normal, Material material) {
vec col = new vec(0, 0, 0);
for (int i=lights.size(); --i>=0; ) {
PointLight light = (PointLight)lights.get(i);
if (visible(pos, light.pos)) {
col = col.add(light.shading(pos, normal, material));
}
}
return col;
}
vec calc_reflection(vec pos, vec normal, vec raydir) {
vec refvec = reflection(raydir, normal);
return raytrace(pos, refvec);
}

vec raytrace(vec orig, vec raydir) {
Intersect isect = new Intersect();
find_nearest_intersect_with_ray(isect, orig, raydir);
if (isect.is_enable()) {
vec pos = orig.add(raydir.scale(isect.t));
return shading(pos, isect.normal, isect.obj.material, raydir);
} else {
return new vec(0, 0, 0);
}
}

// 見えるか?
boolean visible(vec pos, vec target) {
return !intersect_anything(pos, target);
}
}

vec eyepos = new vec(0, 0, -1);
Scene scene;

void setup() {
size(256, 256);
LineBuf = new int[width];

scene = new Scene();
// 球
{
Material material = new Material(1, 0, 0);
material.reflection = 0.5;
scene.add_obj(new Sphere(new vec(0, 0, 0), 0.5, material));
}
// 床
{
Material material = new Material(0, 1, 0);
material.grid = 0.25001;
material.diffuse2.set(0, 0, 1);
scene.add_obj(new Plane(0, 1, 0, 0.5, material));
}
scene.add_light(new PointLight(new vec(1000, 1000, -1000), new vec(2, 2, 2)));
}

void draw() {
if (rendererd) { return; }

render_line(LineBuf, width, height, renderY);
copy_line(LineBuf, renderY);
if (++renderY >= height) {
rendererd = true;
}
}

void copy_line(int[] img, int y) {
loadPixels();
for (int i = 0; i < width; ++i) {
pixels[y * width + i] = img[i];
}
updatePixels();
}

void render_line(int[] img, int width, int height, int iy) {
float w2 = width / 2;
float h2 = height / 2;
float xofs = w2 - 0.5;
float yofs = h2 - 0.5;
float py = -((iy - yofs) / h2); // flip Y

vec normal = new vec(0, 0, 0);
for (int j=0; j<width; ++j) {
float px = (j - xofs) / w2;
vec raydir = (new vec(px, py, 1)).normal();
vec col = scene.raytrace(eyepos, raydir);
img[j] = fcolor(col.x, col.y, col.z);
}
}
// 線分と球の交差判定
float intersect_line_sphere(vec orig, vec dir, vec sphere_center, float sphere_radius) {
vec c = sphere_center;
vec s = orig;
vec d = dir;
float r = sphere_radius;
vec v = s.sub(c);

float dd = sq(v.dot(d)) - (v.dot(v) - sq(r));
if (dd < 0) {
return -1; // not intersect
} else {
float sqdd = sqrt(dd);
float t1 = -v.dot(d) - sqdd;
return t1; // maybe intersect
}
}

// 線分と無限平面の交差判定
float intersect_line_plane(vec orig, vec dir, float a, float b, float c, float d) {
float h = orig.x * a + orig.y * b + orig.z * c + d;
if (h < 0) {
return -1;
} else {
float inner = dir.x * a + dir.y * b + dir.z * c;
if (inner >= 0) {
return -1;
} else {
return h / (-inner);
}
}
}

// マテリアル
class Material {
vec diffuse = new vec(0, 0, 0);
vec emissive = new vec(0, 0, 0);
vec diffuse2 = new vec(0, 0, 0);
float grid = 0;
float reflection = 0;

Material(float difr, float difg, float difb) {
diffuse.set(difr, difg, difb);
}

vec get_diffuse(vec pos) {
if (grid == 0) {
return diffuse;
} else {
float v = floor(pos.x/grid) + floor(pos.y/grid) + floor(pos.z/grid);
int i = (int)v;
if ((i & 1) == 0) {
return diffuse;
} else {
return diffuse2;
}
}
}
};

// 物体
class Object {
Material material;

float intersect_line(vec pNrm, vec orig, vec dir) {
return -1;
}
}

// 球
class Sphere extends Object {
vec center;
float radius;

Sphere(vec _center, float _radius, Material _material) {
center = _center;
radius = _radius;
material = _material;
}

float intersect_line(vec normal, vec orig, vec dir) {
float t = intersect_line_sphere(orig, dir, center, radius);
if (t >= 0) {
vec pos = new vec(orig.add(dir.scale(t)));
normal.set(pos.sub(center).normal());
}
return t;
}
}

// 無限平面
class Plane extends Object {
float a, b, c, d; // 平面方程式の係数

Plane(float _a, float _b, float _c, float _d, Material _material) {
a = _a;
b = _b;
c = _c;
d = _d;
material = _material;
}

float intersect_line(vec normal, vec orig, vec dir) {
float t = intersect_line_plane(orig, dir, a, b, c, d);
if (t >= 0) {
normal.set(a, b, c);
}
return t;
}
}

// 点光源
class PointLight {
vec pos;
vec col;

PointLight(vec _pos, vec _col) {
pos = _pos;
col = _col;
}

vec shading(vec hitpos, vec normal, Material material) {
vec lv = pos.sub(hitpos).normal();
float f = max(normal.dot(lv), 0);
vec diffuse = material.get_diffuse(hitpos);
return new vec(f * diffuse.x * col.x, f * diffuse.y * col.y, f * diffuse.z * col.z);
}
}

// 交差情報
class Intersect {
float t = -1;
vec normal = new vec(0, 0, 0);
Object obj = null;

boolean is_enable() { return t >= 0; }
}
  • 市松模様をするためにマテリアルに2色目を入れたり
  • 反射率0.5で暗くなってしまうので光源の強さを2にしてみたり
  • 謎のノイズが乗るので悩んでいたが市松模様が原因だったとか
  • 球面を反射後もZの直線がまっすぐ続いているのが不思議な感じ
  • 反射の回数を制限してないため、シーンによっては無限ループに陥るかも