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;
};

各向同性的材质类Isotropicscatter()函数则返回一个均匀随机分布的方向:

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