本項目參考自《Ray Tracing in One Weekend》系列。
上接《用兩天學習光線追蹤》,繼續學習光線追蹤。
項目鏈接:https://github.com/maijiaquan/ray-tracing-with-imgui
目錄:
《用兩天學習光線追蹤》1.項目介紹和ppm圖片輸出
《用兩天學習光線追蹤》2.射線、簡單相機和背景輸出
《用兩天學習光線追蹤》3.球體和表面法向量
《用兩天學習光線追蹤》4.封裝成類
《用兩天學習光線追蹤》5.抗鋸齒
《用兩天學習光線追蹤》6.漫反射材質
《用兩天學習光線追蹤》7.反射向量和金屬材質
《用兩天學習光線追蹤》8.折射向量和電介質
《用兩天學習光線追蹤》9.可放置相機
《用兩天學習光線追蹤》10.散焦模糊
《用一週學習光線追蹤》1.動態模糊
《用一週學習光線追蹤》2.BVH樹、AABB相交檢測
《用一週學習光線追蹤》3.純色紋理和棋盤紋理
《用一週學習光線追蹤》4.柏林噪聲
《用一週學習光線追蹤》5.球面紋理貼圖
《用一週學習光線追蹤》6.光照和軸對齊矩形
《用一週學習光線追蹤》7.長方體和平移旋轉
實現長方體
首先,實現一個軸對齊的長方體box,同樣繼承自hittable,軸對齊長方體有6個面,塞到一個hittable_list數組裏面,這6個面的數組構成了一個box。
class box: public hittable {
public:
box() {}
box(const vec3& p0, const vec3& p1, material *ptr);
virtual bool hit(const ray& r, float t0, float t1, hit_record& rec) const;
virtual bool bounding_box(float t0, float t1, aabb& box) const {
box = aabb(pmin, pmax);
return true; }
vec3 pmin, pmax;
hittable *list_ptr;
};
box::box(const vec3& p0, const vec3& p1, material *ptr) {
pmin = p0;
pmax = p1;
hittable **list = new hittable*[6];
list[0] = new xy_rect(p0.x(), p1.x(), p0.y(), p1.y(), p1.z(), ptr);
list[1] = new flip_normals(new xy_rect(p0.x(), p1.x(), p0.y(), p1.y(), p0.z(), ptr));
list[2] = new xz_rect(p0.x(), p1.x(), p0.z(), p1.z(), p1.y(), ptr);
list[3] = new flip_normals(new xz_rect(p0.x(), p1.x(), p0.z(), p1.z(), p0.y(), ptr));
list[4] = new yz_rect(p0.y(), p1.y(), p0.z(), p1.z(), p1.x(), ptr);
list[5] = new flip_normals(new yz_rect(p0.y(), p1.y(), p0.z(), p1.z(), p0.x(), ptr));
list_ptr = new hittable_list(list,6);
}
bool box::hit(const ray& r, float t0, float t1, hit_record& rec) const {
return list_ptr->hit(r, t0, t1, rec);
}
場景中增加兩個軸對齊長方體:
list[i++] = new box(vec3(130, 0, 65), vec3(295, 165, 230), white);
list[i++] = new box(vec3(265, 0, 295), vec3(430, 330, 460), white);
效果如下:
平移
平移是相對的,如果要射線採樣一個平移的box,只需要在射線命中檢測之前,將射線反向平移。
舉個例子:如果要對一個box在x軸上平移2個單位,從(1,1)平移到(3,1),並不需要老老實實地平移box,只需要在檢查射線是否命中之前,將射線反向平移,然後再做真正的射線命中判斷。若命中,則將命中點正向平移。
代碼中,需要包多一層translate,用反向平移後的射線moved_r
來進行命中檢測:
class translate : public hittable {
public:
translate(hittable *p, const vec3& displacement)
: ptr(p), offset(displacement) {}
virtual bool hit(
const ray& r, float t_min, float t_max, hit_record& rec) const;
virtual bool bounding_box(float t0, float t1, aabb& box) const;
hittable *ptr;
vec3 offset;
};
bool translate::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
ray moved_r(r.origin() - offset, r.direction(), r.time());
if (ptr->hit(moved_r, t_min, t_max, rec)) {
rec.p += offset;
return true;
}
else
return false;
}
bool translate::bounding_box(float t0, float t1, aabb& box) const {
if (ptr->bounding_box(t0, t1, box)) {
box = aabb(box.min() + offset, box.max() + offset);
return true;
}
else
return false;
}
旋轉
接下來實現旋轉,旋轉的原理跟平移非常相似。下面以繞z軸旋轉爲例:
繞z軸逆時針旋轉,則x和y分量的變化公式爲:
同理,繞y軸旋轉公式爲:
繞x軸旋轉公式爲:
如果反向旋轉,則只需要將 替換成 ,同時運用 和 即可。
如果要旋轉射線,則同樣要在射線命中檢測之前,同時反向旋轉 射線的起點和方向。若命中,則跟平移類似,變換命中點的座標,此外,還需要額外對法線執行旋轉。繞y軸旋轉寫成代碼如下:
class rotate_y : public hittable {
public:
rotate_y(hittable *p, float angle);
virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const;
virtual bool bounding_box(float t0, float t1, aabb& box) const {
box = bbox; return hasbox;}
hittable *ptr;
float sin_theta;
float cos_theta;
bool hasbox;
aabb bbox;
};
rotate_y::rotate_y(hittable *p, float angle) : ptr(p) {
float radians = (M_PI / 180.) * angle;
sin_theta = sin(radians);
cos_theta = cos(radians);
hasbox = ptr->bounding_box(0, 1, bbox);
vec3 min(FLT_MAX, FLT_MAX, FLT_MAX);
vec3 max(-FLT_MAX, -FLT_MAX, -FLT_MAX);
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
for (int k = 0; k < 2; k++) {
float x = i*bbox.max().x() + (1-i)*bbox.min().x();
float y = j*bbox.max().y() + (1-j)*bbox.min().y();
float z = k*bbox.max().z() + (1-k)*bbox.min().z();
float newx = cos_theta*x + sin_theta*z;
float newz = -sin_theta*x + cos_theta*z;
vec3 tester(newx, y, newz);
for ( int c = 0; c < 3; c++ )
{
if ( tester[c] > max[c] )
max[c] = tester[c];
if ( tester[c] < min[c] )
min[c] = tester[c];
}
}
}
}
bbox = aabb(min, max);
}
bool rotate_y::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
vec3 origin = r.origin();
vec3 direction = r.direction();
origin[0] = cos_theta*r.origin()[0] - sin_theta*r.origin()[2];
origin[2] = sin_theta*r.origin()[0] + cos_theta*r.origin()[2];
direction[0] = cos_theta*r.direction()[0] - sin_theta*r.direction()[2];
direction[2] = sin_theta*r.direction()[0] + cos_theta*r.direction()[2];
ray rotated_r(origin, direction, r.time());
if (ptr->hit(rotated_r, t_min, t_max, rec)) {
vec3 p = rec.p;
vec3 normal = rec.normal;
p[0] = cos_theta*rec.p[0] + sin_theta*rec.p[2]; //命中點座標需要旋轉
p[2] = -sin_theta*rec.p[0] + cos_theta*rec.p[2];
normal[0] = cos_theta*rec.normal[0] + sin_theta*rec.normal[2]; //命中點法線也要旋轉
normal[2] = -sin_theta*rec.normal[0] + cos_theta*rec.normal[2];
rec.p = p;
rec.normal = normal;
return true;
}
else
return false;
}
平移和旋轉的結合
在場景中,應用剛剛實現的平移和變換:
hittable *box1 = new box(vec3(0,0,0), vec3(165,165,165), white);
hittable *box2 = new box(vec3(0,0,0), vec3(165,330,165), white);
list[i++] = new translate(new rotate_y(box1, -18), vec3(130,0,65));
list[i++] = new translate(new rotate_y(box2, 15), vec3(265,0,295));
注意:上述旋轉是對物體的局部座標的旋轉,要等局部變換執行完之後才平移到世界座標。(也就是說,旋轉和平移的順序不能搞反,作者沒有提到這個潛規則,特此補充)
平移和旋轉的效果如下:
參考資料:https://raytracing.github.io/books/RayTracingTheNextWeek.html