Assignment 1¶
这次作业完成的是正交投影的光线投射。难度不大,整体的思路就是任意像素垂直于屏幕产生一条光线与物体相交,得到最近的那个交点,然后直接将对应的颜色投射到对应的像素。于是整个程序就被分为了两个部分,一部分是产生光线,还有一部分就是与物体相交的部分
前一部分由 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 ¢er, 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 ¢er, 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
- 类的名字往往是大写开头的,关注一下书写的规范