7 - 下周的光追Part4
开始学习大名鼎鼎的光追三部曲系列中的:Ray Tracing: The Next Week!希望我能坚持下去吧。
体渲染
可以让光追渲染器去渲染烟/雾气/薄雾一类的东西,它们被叫做体(Volume)或参与介质(Participating Media)。也还能让光追渲染器渲染次表面散射,看起来像物体里的浓雾。增加这些功能通常会使渲染器架构混乱,因为体与硬表面不同,但我们可以通过将体变为随机的表面来避免这个问题。
恒定密度的介质
首先从恒定密度的介质开始。光线进入介质后可能会在里面散射,也有可能直接穿过它。越接近透明的介质,光线越有可能从中穿过。
光线穿过介质后,可能朝任意方向散射,介质越密越有可能。光线在极小距离散射的可能性是:
恒定密度的介质类ConstantMedium
如下,它继承自Hittable
:
class ConstantMedium : public Hittable
{
public:
ConstantMedium(shared_ptr<Hittable> boundary, double density, shared_ptr<Texture> tex)
: boundary(boundary), neg_inv_density(-1 / density), phase_function(make_shared<Isotropic>(tex))
{}
ConstantMedium(shared_ptr<Hittable> boundary, double density, const Color& albedo)
: boundary(boundary), neg_inv_density(-1 / density), phase_function(make_shared<Isotropic>(albedo))
{}
bool hit(const Ray& r, Interval ray_t, HitRecord& rec) const override
{
HitRecord rec1, rec2;
// 找到入射点
if (!boundary->hit(r, Interval::universe, rec1))
return false;
// 找到出射点
if (!boundary->hit(r, Interval(rec1.t + 0.0001, infinity), rec2))
return false;
// 对入射点和出射点做clamp()操作
if (rec1.t < ray_t.min) rec1.t = ray_t.min;
if (rec2.t > ray_t.max) rec2.t = ray_t.max;
// 没有符合要求的入射&出射点
if (rec1.t >= rec2.t)
return false;
if (rec1.t < 0)
rec1.t = 0;
auto ray_length = r.direction().length();
auto distance_inside_boundary = (rec2.t - rec1.t) * ray_length;
auto hit_distance = neg_inv_density * std::log(random_double());
if (hit_distance > distance_inside_boundary)
return false;
rec.t = rec1.t + hit_distance / ray_length;
rec.position = r.at(rec.t);
rec.normal = Vec3(1, 0, 0); // 随意选的
rec.front_face = true; // 也是随意选的
rec.material = phase_function;
return true;
}
AABB bounding_box() const override { return boundary->bounding_box(); }
private:
shared_ptr<Hittable> boundary;
double neg_inv_density;
shared_ptr<Material> phase_function;
};
各向同性的材质类Isotropic
的scatter()
函数则返回一个均匀随机分布的方向:
class Isotropic : public Material
{
public:
Isotropic(const Color& albedo) : tex(make_shared<SolidColor>(albedo)) {}
Isotropic(shared_ptr<Texture> tex) : tex(tex) {}
bool scatter(const Ray& r_in, const HitRecord& rec, Color& attenuation, Ray& scattered) const override
{
scattered = Ray(rec.position, random_unit_vector(), r_in.time());
attenuation = tex->value(rec.u, rec.v, rec.position);
return true;
}
private:
shared_ptr<Texture> tex;
};
有关ConstantMedium
类中hit()
的严格判定,是为了要确保光源在介质里也能正常工作。比如云里就有很多光线在弹射。并且上面的代码还假设光线从介质中出去后继续传播,也就是说这个容器是闭包的,适用于盒子或球。
渲染有烟&雾的康奈尔盒子
接下来就能创建标题说的场景了:
void cornellSmokeScene(HittableList& world, Camera& cam)
{
auto red = make_shared<Lambertian>(Color(.65, .05, .05));
auto white = make_shared<Lambertian>(Color(.73, .73, .73));
auto green = make_shared<Lambertian>(Color(.12, .45, .15));
auto light = make_shared<DiffuseLight>(Color(7, 7, 7));
world.add(make_shared<Quad>(Point3(555, 0, 0), Vec3(0, 555, 0), Vec3(0, 0, 555), green));
world.add(make_shared<Quad>(Point3(0, 0, 0), Vec3(0, 555, 0), Vec3(0, 0, 555), red));
world.add(make_shared<Quad>(Point3(113, 554, 127), Vec3(330, 0, 0), Vec3(0, 0, 305), light));
world.add(make_shared<Quad>(Point3(0, 555, 0), Vec3(555, 0, 0), Vec3(0, 0, 555), white));
world.add(make_shared<Quad>(Point3(0, 0, 0), Vec3(555, 0, 0), Vec3(0, 0, 555), white));
world.add(make_shared<Quad>(Point3(0, 0, 555), Vec3(555, 0, 0), Vec3(0, 555, 0), white));
shared_ptr<Hittable> box1 = box(Point3(0, 0, 0), Point3(165, 330, 165), white);
box1 = make_shared<Rotate>(box1, RotateAxis::Y, 15);
box1 = make_shared<Translate>(box1, Vec3(265, 0, 295));
shared_ptr<Hittable> box2 = box(Point3(0, 0, 0), Point3(165, 165, 165), white);
box2 = make_shared<Rotate>(box2, RotateAxis::Y, -18);
box2 = make_shared<Translate>(box2, Vec3(130, 0, 65));
world.add(make_shared<ConstantMedium>(box1, 0.01, Color(0, 0, 0)));
world.add(make_shared<ConstantMedium>(box2, 0.01, Color(1, 1, 1)));
cam.aspectRadio = 1.0;
cam.imgWidth = 600;
cam.samples_per_pixel = 200;
cam.max_depth = 50;
cam.background = Color(0, 0, 0);
cam.vfov = 40;
cam.lookFrom = Point3(278, 278, -800);
cam.lookAt = Point3(278, 278, 0);
cam.vup = Vec3(0, 1, 0);
cam.defocus_angle = 0;
}
结果如下:
第二部完结
接下来让我们将所有东西放到一起。有一大片薄雾,一个蓝色的次表面反射球(使用介质材质Dielectric
包裹参与介质ConstantMedium
近似)。这个光追渲染器没有Shadow Rays
技术,但也让我们容易实现焦散和次表面效果,是一个双刃剑设计。
场景设计如下:
void book2Scene(HittableList& world, Camera& cam, int img_Width, int spp, int max_depth)
{
// 地面的盒子
auto ground = make_shared<Lambertian>(Color(0.48, 0.83, 0.53));
HittableList boxes1;
int boxes_per_size = 20;
for (int i = 0; i < boxes_per_size; ++i)
{
for (int j = 0; j < boxes_per_size; ++j)
{
auto w = 100.0;
auto x0 = -1000.0 + i * w;
auto y0 = 0.0;
auto z0 = -1000.0 + j * w;
auto x1 = x0 + w;
auto y1 = random_double(1, 101);
auto z1 = z0 + w;
boxes1.add(box(Point3(x0, y0, z0), Point3(x1, y1, z1), ground));
}
}
world.add(make_shared<BVHNode>(boxes1));
// 光源
auto light = make_shared<DiffuseLight>(Color(7, 7, 7));
world.add(make_shared<Quad>(Point3(123, 554, 147), Vec3(300, 0, 0), Vec3(0, 0, 265), light));
// 运动模糊球
auto center1 = Point3(400, 400, 200);
auto center2 = center1 + Vec3(30, 0, 0);
auto sphere_material = make_shared<Lambertian>(Color(0.7, 0.3, 0.1));
world.add(make_shared<Sphere>(center1, center2, 90, sphere_material));
// 普通球
world.add(make_shared<Sphere>(Point3(260, 150, 45), 50, make_shared<Dielectric>(1.5)));
world.add(make_shared<Sphere>(Point3(0, 150, 145), 50, make_shared<Metal>(Color(0.8, 0.8, 0.9), 1.0)));
// 次表面反射球
auto boundary = make_shared<Sphere>(Point3(360, 150, 145), 70, make_shared<Dielectric>(1.5));
world.add(boundary);
world.add(make_shared<ConstantMedium>(boundary, 0.2, Color(0.2, 0.4, 0.9)));
boundary = make_shared<Sphere>(Point3(0, 0, 0), 5000, make_shared<Dielectric>(1.5));
world.add(make_shared<ConstantMedium>(boundary, 0.0001, Color(1, 1, 1)));
// 地图球
auto emat = make_shared<Lambertian>(make_shared<ImageTexture>("resources/images/earthmap.jpg"));
world.add(make_shared<Sphere>(Point3(400, 200, 400), 100, emat));
// 柏林噪声球
auto perlin_tex = make_shared<NoiseTexture>(0.2);
world.add(make_shared<Sphere>(Point3(220, 280, 300), 80, make_shared<Lambertian>(perlin_tex)));
// 方形排列的球们
HittableList boxes2;
auto white = make_shared<Lambertian>(Color(0.73, 0.73, 0.73));
int ns = 1000;
for (int i = 0; i < ns; ++i)
{
boxes2.add(make_shared<Sphere>(Point3::random(0, 165), 10, white));
}
world.add(make_shared<Translate>(
make_shared<Rotate>(make_shared<BVHNode>(boxes2), RotateAxis::Y, 15)
, Vec3(-100, 270, 395))
);
// 摄像机设置
cam.aspectRadio = 1.0;
cam.imgWidth = img_Width;
cam.samples_per_pixel = spp;
cam.max_depth = max_depth;
cam.background = Color(0, 0, 0);
cam.vfov = 40;
cam.lookFrom = Point3(478, 278, -600);
cam.lookAt = Point3(278, 278, 0);
cam.vup = Vec3(0, 1, 0);
cam.defocus_angle = 0;
}
最终结果如下,渲染时长高达8小时:
参考资料
- Ray Tracing: The Next Week