raytracing.github.io实战案例:构建 Cornell Box 经典场景

【免费下载链接】raytracing.github.io Main Web Site (Online Books) 【免费下载链接】raytracing.github.io 项目地址: https://gitcode.com/GitHub_Trending/ra/raytracing.github.io

引言:光线追踪领域的"Hello World"

你是否还在为光线追踪场景搭建而烦恼?是否想快速验证材质渲染效果却苦于场景复杂度?Cornell Box(康奈尔盒)作为计算机图形学领域的标准测试场景,以其结构简洁、光照可控的特点,成为验证光线追踪算法和材质表现的理想选择。本文将基于raytracing.github.io项目,从零开始构建完整的Cornell Box场景,通过200行核心代码实现包含漫反射、光源、折射等效果的物理精确渲染。读完本文后,你将掌握:

  • 经典Cornell Box的几何结构与光照原理
  • 平面与立方体的程序化构建方法
  • 材质系统与光源设置的最佳实践
  • 相机参数优化与渲染质量控制
  • 常见视觉 artifacts(如阴影 acne)的解决方案

技术背景:Cornell Box的科学价值

Cornell Box由康奈尔大学计算机图形学实验室于1980年代提出,其设计初衷是为了创建一个具有可预测光照条件的标准化测试环境。该场景包含:

  • 封闭立方体空间(通常尺寸为555x555x555单位)
  • 两个互补色墙面(红/绿)
  • 中性色地面与天花板
  • 可控区域光源
  • 标准测试物体(通常为两个不同尺寸的白色立方体)

这种结构能够精确测试:

  • 间接光照与色彩反弹效果
  • 软阴影生成质量
  • 材质BRDF模型准确性
  • 全局光照算法效率

mermaid

环境准备与项目结构

项目文件组织

raytracing.github.io项目采用模块化结构设计,与Cornell Box实现相关的核心文件分布如下:

文件路径 功能描述
src/TheNextWeek/main.cc 场景定义与主函数入口
src/TheNextWeek/quad.h 平面几何实现
src/TheNextWeek/material.h 材质系统定义
src/TheNextWeek/camera.h 相机模型与渲染控制
src/TheNextWeek/hittable.h 可命中物体接口

编译与运行

通过以下命令克隆仓库并编译项目:

git clone https://gitcode.com/GitHub_Trending/ra/raytracing.github.io
cd raytracing.github.io
mkdir build && cd build
cmake ..
make -j4
./raytracer  # 执行时通过命令行参数选择场景

核心实现:从平面到完整场景

1. 平面几何基础(Quad类)

Cornell Box的墙体由六个平面(quad)构成,每个平面通过三点定义:原点Q和两个方向向量u、v:

class quad : public hittable {
public:
    quad(const point3& Q, const vec3& u, const vec3& v, shared_ptr<material> mat)
        : Q(Q), u(u), v(v), mat(mat) {
        auto n = cross(u, v);
        normal = unit_vector(n);
        D = dot(normal, Q);
        w = n / dot(n,n);  // 用于平面坐标计算的权重向量
        set_bounding_box();
    }

    bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
        auto denom = dot(normal, r.direction());
        
        // 处理与平面平行的光线
        if (std::fabs(denom) < 1e-8)
            return false;

        // 计算光线与平面交点
        auto t = (D - dot(normal, r.origin())) / denom;
        if (!ray_t.contains(t))
            return false;

        // 检查交点是否在平面内部
        auto intersection = r.at(t);
        vec3 planar_hitpt_vector = intersection - Q;
        auto alpha = dot(w, cross(planar_hitpt_vector, v));
        auto beta = dot(w, cross(u, planar_hitpt_vector));

        if (!is_interior(alpha, beta, rec))
            return false;

        // 设置命中记录
        rec.t = t;
        rec.p = intersection;
        rec.mat = mat;
        rec.set_face_normal(r, normal);
        return true;
    }
    // ... 省略边界盒计算等辅助函数
};

2. 材质系统配置

Cornell Box需要四种基础材质,在material.h中定义:

// 漫反射材质(用于墙面与物体)
class lambertian : public material {
public:
    lambertian(const color& albedo) : tex(make_shared<solid_color>(albedo)) {}
    
    bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
    const override {
        auto scatter_direction = rec.normal + random_unit_vector();
        scattered = ray(rec.p, scatter_direction, r_in.time());
        attenuation = tex->value(rec.u, rec.v, rec.p);
        return true;
    }
private:
    shared_ptr<texture> tex;
};

// 发光材质(用于光源)
class diffuse_light : public material {
public:
    diffuse_light(shared_ptr<texture> tex) : tex(tex) {}
    diffuse_light(const color& emit) : tex(make_shared<solid_color>(emit)) {}

    color emitted(double u, double v, const point3& p) const override {
        return tex->value(u, v, p);
    }
private:
    shared_ptr<texture> tex;
};

3. 场景构建核心代码

Cornell Box场景构建在main.cc的cornell_box()函数中实现,完整流程分为五步:

void cornell_box() {
    hittable_list world;

    // 1. 材质定义
    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<diffuse_light>(color(15, 15, 15));  // 高亮度光源

    // 2. 墙体构建
    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(0,0,0), vec3(555,0,0), vec3(0,0,555), white));  // 地面
    world.add(make_shared<quad>(point3(555,555,555), 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));  // 后墙

    // 3. 光源设置 (343x105mm的矩形光源)
    world.add(make_shared<quad>(point3(343, 554, 332), vec3(-130,0,0), vec3(0,0,-105), light));

    // 4. 物体添加
    shared_ptr<hittable> box1 = box(point3(0,0,0), point3(165,330,165), white);
    box1 = make_shared<rotate_y>(box1, 15);  // 旋转15度
    box1 = make_shared<translate>(box1, vec3(265,0,295));  // 平移到指定位置
    world.add(box1);

    shared_ptr<hittable> box2 = box(point3(0,0,0), point3(165,165,165), white);
    box2 = make_shared<rotate_y>(box2, -18);  // 旋转-18度
    box2 = make_shared<translate>(box2, vec3(130,0,65));  // 平移到指定位置
    world.add(box2);

    // 5. 相机配置与渲染
    camera cam;
    cam.aspect_ratio      = 1.0;
    cam.image_width       = 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.render(world);
}

4. 立方体构建辅助函数

quad.h中提供了box()函数,通过六个quad面构建立方体:

inline shared_ptr<hittable_list> box(const point3& a, const point3& b, shared_ptr<material> mat) {
    auto sides = make_shared<hittable_list>();
    
    // 计算最小/最大点
    auto min = point3(std::fmin(a.x(),b.x()), std::fmin(a.y(),b.y()), std::fmin(a.z(),b.z()));
    auto max = point3(std::fmax(a.x(),b.x()), std::fmax(a.y(),b.y()), std::fmax(a.z(),b.z()));
    
    auto dx = vec3(max.x() - min.x(), 0, 0);
    auto dy = vec3(0, max.y() - min.y(), 0);
    auto dz = vec3(0, 0, max.z() - min.z());
    
    // 添加六个面
    sides->add(make_shared<quad>(point3(min.x(), min.y(), max.z()),  dx,  dy, mat)); // 前面
    sides->add(make_shared<quad>(point3(max.x(), min.y(), max.z()), -dz,  dy, mat)); // 右面
    sides->add(make_shared<quad>(point3(max.x(), min.y(), min.z()), -dx,  dy, mat)); // 后面
    sides->add(make_shared<quad>(point3(min.x(), min.y(), min.z()),  dz,  dy, mat)); // 左面
    sides->add(make_shared<quad>(point3(min.x(), max.y(), max.z()),  dx, -dz, mat)); // 顶面
    sides->add(make_shared<quad>(point3(min.x(), min.y(), min.z()),  dx,  dz, mat)); // 底面
    
    return sides;
}

关键技术解析

1. 坐标系统与空间变换

Cornell Box的物体定位采用右手坐标系,通过复合变换(旋转+平移)实现物体的精确定位:

// 先旋转后平移的组合变换
auto transformed_box = make_shared<translate>(
    make_shared<rotate_y>(original_box, rotation_angle), 
    translation_vector
);

这种变换顺序遵循"先缩放旋转,后平移"的图形学原则,确保变换中心正确。

2. 光源设计与能量控制

场景中的光源采用高亮度漫反射材质实现:

auto light = make_shared<diffuse_light>(color(15, 15, 15));

这里的RGB值(15,15,15)远大于1.0,因为光线追踪中的颜色值是物理线性的,需要通过后续的gamma校正转换为显示值。光源面积控制软阴影边缘的模糊程度,本例中130x105mm的光源会产生自然的软阴影效果。

3. 相机参数优化

为获得最佳视角,Cornell Box的相机参数经过精心调整:

  • 位置(lookfrom): (278, 278, -800) - 位于盒子前方偏上位置,与地面成45度角
  • 目标(lookat): (278, 278, 0) - 对准盒子中心
  • 垂直视场角(vfov): 40度 - 确保完整容纳场景
  • 采样数(samples_per_pixel): 200 - 平衡噪点与渲染时间
  • 最大深度(max_depth): 50 - 足够捕捉多次反射

4. 抗锯齿与渲染质量

通过多重采样实现抗锯齿效果:

for (int sample = 0; sample < samples_per_pixel; sample++) {
    ray r = get_ray(i, j);
    pixel_color += ray_color(r, max_depth, world);
}
pixel_color /= samples_per_pixel;  // 平均采样结果

每个像素的颜色通过多次随机采样并平均得到,有效消除锯齿边缘。

常见问题与优化方案

1. 渲染时间过长

问题:200采样/像素的配置在普通PC上可能需要数小时渲染。

解决方案

  • 降低采样数(samples_per_pixel=100)
  • 减少最大深度(max_depth=30)
  • 启用BVH加速:world = hittable_list(make_shared<bvh_node>(world));

2. 阴影 acne问题

问题:物体表面出现黑白噪点。

解决方案:光线与物体相交时使用epsilon偏移:

// 在ray_color函数中使用0.001作为最小t值,避免自相交
if (!world.hit(r, interval(0.001, infinity), rec))
    return background;

3. 色彩溢出

问题:红色/绿色墙面的颜色过度污染其他物体。

解决方案

  • 降低墙面反射率(如red=color(.55, .05, .05))
  • 增加采样数减少噪点放大
  • 调整光源亮度平衡

4. 光源可见性

问题:光源本身在渲染结果中显示为白色矩形。

解决方案:在光源材质中禁用散射:

class diffuse_light : public material {
public:
    // ...
    bool scatter(...) const override { return false; }  // 光源不散射光线
};

扩展与进阶

1. 烟雾效果实现

通过constant_medium类为物体添加半透明效果:

// 在cornell_smoke()函数中
world.add(make_shared<constant_medium>(box1, 0.01, color(0,0,0)));  // 黑色烟雾
world.add(make_shared<constant_medium>(box2, 0.01, color(1,1,1)));  // 白色烟雾

2. 材质替换实验

尝试不同材质组合观察效果变化:

// 玻璃材质立方体
auto glass = make_shared<dielectric>(1.5);
world.add(make_shared<sphere>(point3(100, 165, 100), 50, glass));

// 金属材质立方体
auto metal = make_shared<metal>(color(0.8, 0.8, 0.9), 0.1);
world.add(make_shared<sphere>(point3(400, 165, 400), 50, metal));

3. HDR环境贴图

将后墙替换为环境贴图:

auto envmap = make_shared<image_texture>("envmap.jpg");
auto env_mat = make_shared<lambertian>(envmap);
world.add(make_shared<quad>(point3(0,0,555), vec3(555,0,0), vec3(0,555,0), env_mat));

总结与展望

Cornell Box作为光线追踪的标准测试场景,不仅是验证渲染算法正确性的工具,更是学习实时光照、全局光照和材质系统的绝佳案例。通过本文的实现,我们掌握了:

  • 基于平面几何的场景构建方法
  • 材质与光源的物理精确配置
  • 相机参数与视角优化技巧
  • 光线追踪渲染质量控制

未来可以进一步探索:

  • 双向路径追踪等高级算法实现
  • 体积雾与参与介质渲染
  • GPU加速与实时光线追踪
  • 光谱渲染与物理精确材质

希望本文能帮助你深入理解光线追踪技术,欢迎点赞、收藏、关注获取更多光线追踪实战教程!下一期我们将探讨"如何使用蒙特卡洛方法优化Cornell Box的渲染效率"。

【免费下载链接】raytracing.github.io Main Web Site (Online Books) 【免费下载链接】raytracing.github.io 项目地址: https://gitcode.com/GitHub_Trending/ra/raytracing.github.io

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐