跳转至

Assignment 1

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

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

前一部分由 camera.hcamera.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;
};

在写正交相机构造函数的时候,一开始没有注意到 updirection 不一定是垂直的,所以需要用水平方向的向量去修正 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.hsphere.C 以及 group.hgroup.C 来完成的

sphere.hsphere.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 的方向有关的,ac 倒是没什么关系。

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
欢迎跟我探讨!!!