跳转至

Assignment 1

文本统计:约 780 个字 • 203 行代码

这次作业完成的是正交投影的光线投射。难度不大,整体的思路就是任意像素垂直于屏幕产生一条光线与物体相交,得到最近的那个交点,然后直接将对应的颜色投射到对应的像素。于是整个程序就被分为了两个部分,一部分是产生光线,还有一部分就是与物体相交的部分

前一部分由 camera.h 和 camera.C 完成

//定义了相机类
class Camera {
public:
    virtual Ray generateRay(Vec2f point) = 0;
    virtual float getTMin() const = 0;
};

//定义派生类正交相机
class OrthographicCamera : public Camera {
    public:
        OrthographicCamera(const Vec3f &center, const Vec3f &direction, const Vec3f &up, float size)
        {
            this->center = center;

            this->direction = direction;
            this->direction.Normalize();

            Vec3f::Cross3(this->horizontal, direction, up);
                this->horizontal.Normalize();

            Vec3f::Cross3(this->up, this->horizontal, direction);
                this->up.Normalize();

            this->size = size;
        }
        Ray generateRay(Vec2f point) override;
        float getTMin() const override;

    private:
        Vec3f center,up,direction,horizontal;
        float size;
};

在写正交相机构造函数的时候,一开始没有注意到 up 和 direction 不一定是垂直的,所以需要用水平方向的向量去修正 up 向量

Ray OrthographicCamera::generateRay(Vec2f point)
{
    point -= Vec2f(0.5, 0.5);
    point *= size;
    return {center + point.x() * horizontal + point.y() * up, direction};
}

float OrthographicCamera::getTMin() const
{
    return -FP_INFINITE ;
}

generateRay(Vec2f point) 只要注意一下这里 point 给的是 (0,0) 到 (1,1) 的一个比值量。

getTMin() 用来返回一个理论的最小值,然后这里的 \(t\) 一定是大于0的,所以这个其实无所谓。用这个函数的用处主要是为后面作业作铺垫的

getmin

我的理解可能不太对,先放上作业的原话

The getTMin() method will be useful when tracing rays through the scene. For an orthographic camera, rays always start at infinity, so tmin will be a large negative value. However, in the next assignment you will implement a perspective camera and the value of tmin will be zero to correctly clip objects behind the viewpoint.

后一部分是通过 sphere.h 和sphere.C 以及 group.h 和 group.C 来完成的

sphere.h 和sphere.C 用来实现光线与球的交

bool Sphere::intersect(const Ray &r, Hit &h, float tmin) {
    Vec3f vec_oc =  center -r.getOrigin();
    float a = r.getDirection().Dot3(r.getDirection());
    float b = vec_oc.Dot3(r.getDirection());
    float c = vec_oc.Dot3(vec_oc) - radius * radius;

    float delta = b * b - c;  
    if (delta < -EPSILON) {
        return false;
    }
    float t0 =  b - sqrt(delta);

    //把光线理解为直线,而不是射线
    /*if (t0 < 0)
        t0 = b + sqrt(delta);
    if (t0 < 0)
        return false;  
    */
    if (t0 < tmin-EPSILON) 
        return false;
    h.set(t0, material, r);
    return true;
}

同时我还注意到其他人的程序考虑了一些误差有关的东西,因为浮点数的运算并不是特别精确,所以会有一定的误差,要有一定的容忍度。还有注意一下这个 b 的符号,与 vec_oc 的方向有关的,a 和 c 倒是没什么关系。

bool Group::intersect(const Ray &r, Hit &h, float tMin) {
    Hit hTmp;
    float tTmp = std::numeric_limits<float>::infinity();
    bool flagHasInter = false;
    for (int i = 0; i < objectNum; i++) {
        auto obj = objects[i];
        if (obj->intersect(r, hTmp, tMin) && hTmp.getT() < tTmp + EPSILON) {
            flagHasInter = true;
            h = hTmp;
            tTmp = h.getT();
        }
    }
    return flagHasInter;
}
bool Group::intersect(const Ray &r, Hit &h, float tmin) {
    float tTmp = std::numeric_limits<float>::infinity();
    bool flag = false;
    for (int i = 0; i < objectNum; i++) {
        if (objects[i]->intersect(r, h, tmin) && h.getT() < tTmp) {
            flag = true;
            tTmp = h.getT();
        }
    }
    return flag;
}

辨析一下上面两个程序,为什么下面的结果并不对?

这是因为下面这个函数的 h 一直在更新,最后得到的也是最后那个物体的交点,无论是否比原来的更近。

正确的程序应该是和每个物体相交得到的交点,比较谁更近,然后再选择是否更新。

最后介绍一下主函数

int main(int argc, char *argv[]) {
    char *input_file = nullptr;
    int width = 100;
    int height = 100;
    char *output_file = nullptr;
    float depth_min = 0;
    float depth_max = 1;
    char *depth_file = nullptr;

    // sample command line:
    // raytracer -input scene1_1.txt -size 200 200 -output output1_1.tga -depth 9 10 depth1_1.tga

    for (int i = 1; i < argc; i++) {
        if (!strcmp(argv[i], "-input")) {
            i++;
            assert(i < argc);
            input_file = argv[i];
        } else if (!strcmp(argv[i], "-size")) {
            i++;
            assert(i < argc);
            width = atoi(argv[i]);
            i++;
            assert(i < argc);
            height = atoi(argv[i]);
        } else if (!strcmp(argv[i], "-output")) {
            i++;
            assert(i < argc);
            output_file = argv[i];
        } else if (!strcmp(argv[i], "-depth")) {
            i++;
            assert(i < argc);
            depth_min = atof(argv[i]);
            i++;
            assert(i < argc);
            depth_max = atof(argv[i]);
            i++;
            assert(i < argc);
            depth_file = argv[i];
        } else {
            printf("whoops error with command line argument %d: '%s'\n", i, argv[i]);
            assert(0);
        }
    }

    //得到相机和物体的信息
    auto parser = new SceneParser(input_file);
    auto camera = parser->getCamera();
    auto group = parser->getGroup();

    const float EPSILON = 1e-8;
    //对image进行初始化
    auto colorImg = new Image(width, height);
    colorImg->SetAllPixels(parser->getBackgroundColor());
    auto depthImg = new Image(width, height);
    depthImg->SetAllPixels(Vec3f(0, 0, 0));

    //对每个像素进行染色
    for (int i = 0; i < width; i++) {
        for (int j = 0; j < height; j++) {
            auto ray = camera->generateRay(Vec2f((float) i / width,
                                                 (float) j / height));
            Hit hit;
            auto interRes = group->intersect(ray, hit, camera->getTMin());
            if (interRes) { // has intersection
                //给像素染上交点的颜色
                colorImg->SetPixel(i, j, hit.getMaterial()->getDiffuseColor());
                float t = hit.getT();
                //染上灰度图,一定程度反映了远近
                if (t > depth_min - EPSILON && t < depth_max + EPSILON) {
                    auto depthCol = (depth_max - t) / (depth_max - depth_min);
                    depthImg->SetPixel(i, j, Vec3f(depthCol, depthCol, depthCol));
                }
            }
        }
    }

    //存储相应的文件
    colorImg->SaveTGA(output_file);
    depthImg->SaveTGA(depth_file);

    return 0;
}

之前写 GAMES101 相关程序的时候,往往只需要写函数就可以了,大部分框架都帮你搭好了,而这份作业需要完成的内容更多,需要完成相关头文件的书写,定义相关的类。

总结一下一些经验:

  • 一个类中往往将成员变量放在私有 (private) 成员中,然后函数放在公有成员内。而对于需要派生的类,成员变量可以放在受保护 (protected) 中。
  • 函数一般再头文件中先声明,然后在 .C 文件中写相关程序。
  • 对于派生类,需要重写父类的某一函数,写上 override
  • 这次作业也增加我对虚函数的理解,比如说下面的 Object3D 类与 Sphere 类,可以加以体会一下
class Object3D {
public:
    explicit Object3D(Material *material) : material(material) {}

    Object3D() : Object3D(nullptr) {}

    virtual bool intersect(const Ray &r, Hit &h, float tMin) = 0;

protected:
    constexpr static const float EPSILON = 1e-8;
    Material *material;
};

#endif //ASSIGNMENT1_OBJECT3D_H
class Sphere : public Object3D {
public:
    Sphere(const Vec3f &center, float radius,Material *m) : 
            center(center), radius(radius),Object3D(m) {}
    Sphere(): Sphere(Vec3f(),0,nullptr){}

    Vec3f getCenter() const { return center; }
    float getRadius() const { return radius; }

    bool intersect(const Ray &r, Hit &h, float tmin) override;

private:
    Vec3f center;
    float radius;
};
#endif
  • 类的名字往往是大写开头的,关注一下书写的规范

评论区

对你有帮助的话请给我个赞和 star => GitHub stars
欢迎跟我探讨!!!