Skip to content

Latest commit

 

History

History
793 lines (781 loc) · 33.6 KB

实验报告.markdown

File metadata and controls

793 lines (781 loc) · 33.6 KB
作业报告
包文鼎

###Assignment0   这个作业和图形学将要学习主要的知识关系不大,主要是让我们回顾向量、矩阵乘法等基础知识和图形库的使用。作业要求是通过迭代坐标变换生成IFS图形,并且为我们提供了比较详细的模板,我们只需要填鸭式的把内容补全。 ####代码介绍   首先是创建一个Ifs类,提供坐标变换的功能,这里我们将迭代次数设置为n,并且通过随机数选择迭代的矩阵,最后调用Matrix::Transform即可。由于这里我是直接以(size,size)为边界生成,导致在处理的时候出现了问题并且发现了Matrix库的BUG。这里由于在变换时候,对于低维度的向量和高维度的矩阵相乘,会自动在向量后面补1,但是我以(size,size)为边界导致没法找到不动点,最后我在Ifs类内增加一个size变量并且手动进行高维向量的转换

void render(Vec2f& v)
{
    for (int i = 0; i < n; i++)
    {
        float num = rand() % 100 * 1.0 / 100;
        float count = 0;
        for (int j = 0; j < possibiliy.size(); j++)
        {
            count += possibiliy[j];
            if (count > num)
            {
                Vec4f tmp(v.x(), v.y(), size, size);
                matrixs[j].Transform(tmp);
                v.Set(tmp.x(), tmp.y());
                break;
            }
        }
    }
}

  至于Matrix库的BUG,是他的代码在读取3*3矩阵的时候,continue的条件写错了,应该是x==3或者y==3时候continue,他这个bug导致第二行和第二列被赋值0,而第三行、第三列却是有值的

void Matrix::Write3x3(FILE* F) const {
    assert(F != NULL);
    for (int y = 0; y < 4; y++) {
        if (y == 2) continue;
        for (int x = 0; x < 4; x++) {
            if (x == 2) continue;
            float tmp = data[y][x];
            if (fabs(tmp) < 0.00001) tmp = 0;
            fprintf(F, "%12.6f ", tmp);
        }
        fprintf(F, "\n");
    }
}

  然后是主函数部分,一开始先把整个image设置为白色我们需要对点进行随机枚举,我这儿直接以size为边界进行随机,并且在写入的时候通过floor()函数保证数字不越界,将这些点设置为黑色,就能生成预期的图形。

for (int i = 0; i < num_points; i++)
{
    Vec2f pixel(rand() % size, rand() % size);
    ifs.render(pixel);
    //cout << tmp.x() << " " << tmp.y() << endl;
    image.SetPixel((int)floor(pixel.x()), (int)floor(pixel.y()), Vec3f(0, 0, 0));
}

####结果展示 #####测试1:谢宾斯基三角形 指令:ifs -input sierpinski_triangle.txt -points 10000 -iters 30 -size 200 -output sierpinski_triangle.tga 结果: Test #####测试2:铁杉 指令:ifs -input fern.txt -points 50000 -iters 30 -size 400 -output fern.tga 结果: Test ###Assignment1   这个作业要求我们用OrthographicCamera的方式实现Ray Cast, 并且拥有颜色和灰度两种显示方式。这次作业相比上一次,我们需要更多的发挥自己的能力和ppt中的相关知识解决问题,也拥有更高的自由度。 ####代码介绍 #####Sphere类 首先是构造函数,有radius,centermaterial三个参数

Sphere(Vec3f c, double r, Material* material)
{
    this->center = c;
    this->radius = r;
    this->material = material;
}

接着是实现Intersect方法,这里我使用了2种方法,第一个就是代数法:

Vec3f source = r.getOrigin();
source -= center; // 首先变换坐标系,让圆心处于(0,0,0),这样会方便接下来的计算
//接下来a,b,c为二元方程a * t^2 + b * t + c = 0的三个系数
float a = 1;
float b = r.getDirection().Dot3(source) * 2;
float c = source.Dot3(source) - radius * radius;
float d2 = b * b - 4 * a * c;
// 判断 b * b - 4 * a * c的正负,负代表没有交点
if (d2 < 0)
{
    //cout << "fuck";
    return false;
}
// 计算可行的t,即大于tmin的最小值
float t1 = (-b - (float)sqrt(d2)) / 2;
float t2 = (-b - (float)sqrt(d2)) / 2;
if (tmin < t1)
{
    h.set(t1, this->material, r);
    return true;
}
else if (tmin <= t2)
{
    h.set(t2, this->material, r);
    return true;
}
return false;

并且我也尝试了Bonus的几何法,但是存在的问题是只判断了射线源在球外面的情况,且没有考虑t的方向(正负),所以还存在着一点问题,但是由于这个判断比较繁琐所以没有继续实现下去。

Vec3f dis_to_center = r.getOrigin() - center;//射线源到球心的矢量
//将dis_to_center投影到射线上
float dis_cast = (float)abs(dis_to_center.Dot3(r.getDirection()) / r.getDirection().Length());
//计算点到球心的距离
float ray_center_dis = (float)sqrt(dis_to_center.Length() * dis_to_center.Length() - dis_cast * dis_cast);//
if (ray_center_dis > radius) //如果球心到射线的距离大于半径,没有交点
    return false;
float dis_center_length = dis_to_center.Length();
float bias = (float)sqrt(radius * radius - ray_center_dis * ray_center_dis);
//通过几何法计算t并求出大于tmin的值
if (tmin < dis_cast - bias)
{
    h.set(dis_cast - bias, this->material, r);
    return true;
}
else if (tmin <= dis_cast + bias)
{
    h.set(dis_cast + bias, this->material, r);
    return true;
}
return false;

#####Group类 接着是Group类,我通过一个vector来存放里面的Object3D信息

vector<Object3D*> object_group;
int count;
public:
    Group(int count)
    {
        object_group.clear();
        this->count = count;
    }

    void addObject(int num, Object3D* object)
    {
        object_group.push_back(object);
    }

然后是Intersect,这个对容器里每一个元素进行检测并且记录一个最近的即可

bool intersect(const Ray& r, Hit& h, float tmin)
{
    bool mark = false;
    for (auto& object : object_group) //遍历每个Obejct3D
    {
        Hit hit;
        if (object->intersect(r, hit, tmin))
        {
            if (!mark || hit.getT() < h.getT())
            {
                h = hit;
            }
            mark = true;
        }
    }
    return mark;
}

#####OrthographicCamera实现 初始化中我们需要把direction进行单位化,并且up可能不是严格垂直于direction,所以需要通过2次叉乘获得垂直于direction的up向量

OrthographicCamera(Vec3f center, Vec3f direction, Vec3f up, float size)
{
    this->center = center;
    this->direction = direction;
    direction.Normalize();//获得单位向量
    Vec3f tmp;
    //两次叉乘
    Vec3f::Cross3(tmp, direction, up);
    Vec3f::Cross3(this->up, tmp, direction);
    this->size = size;
}

generateray中我们通过输入的偏移量乘以2个方向的向量,就可以获得源点的坐标,然后和direction构造Ray返回即可

Ray generateRay(Vec2f point)
{
    Vec3f hor, ver;
    Vec3f::Cross3(hor, direction, up);
    ver = up;
    hor.Normalize();
    ver.Normalize();
    //点的偏移量乘以方向向量
    float ch = size * point.x();
    float cw = size * point.y();
    hor.Scale(ch, ch, ch);
    ver.Scale(cw, cw, cw);
    //获得点的坐标
    Vec3f from_point = center + hor + ver;
    return Ray(from_point, direction);
}

#####主循环判断 主函数由2个for循环遍历Image所有像素点,然后对每个像素点转换成x,y轴偏移量放到camera中进行计算,最后根据返回hit的相关信息填写该像素内容

for (int i = 0; i < image.Width(); i++)
    {
        for (int j = 0; j < image.Height(); j++)
        {
            //计算x,y的偏移量,范围是[-1/2, 1/2]
            float x_bias = (i - image.Width() * 1.0 / 2) / image.Width();
            float y_bias = (j - image.Height() * 1.0 / 2) / image.Height();
            //输入偏移量构造Ray
            Ray ray = camera->generateRay(Vec2f(x_bias, y_bias));
            Hit hit;
            //检测碰撞
            if (group->intersect(ray, hit, tmin))
            {
                //out_image显示颜色,所以给这个像素设置颜色即可
                image.SetPixel(i, j, hit.getMaterial()->getDiffuseColor());
                //距离可视化的图片通过将距离变成(color,color,color)颜色,赋给image1
                float t = hit.getT();
                if (t < depth_min)
                    t = depth_min;
                if (t > depth_max)
                    t = depth_max;
                float color = (1 - (t - depth_min) / (depth_max - depth_min));
                image1.SetPixel(i, j, Vec3f(color , color, color));
            }
            // 如果没有碰撞,则赋予初值
            else
            {
                image.SetPixel(i, j, parser.getBackgroundColor());
                image1.SetPixel(i, j, Vec3f(0, 0, 0));
            }
                
        }
    }
    image.SaveTGA(output_file);//普通照片保存
    image1.SaveTGA(depth_file);//深度可视化照片保存

####结果展示 #####输入一 指令:raytracer -input scene1_01.txt -size 200 200 -output output1_01.tga -depth 9 10 depth1_01.tga 这个输入只有一个物体 透视图: Assignment1 距离可视化: Assignment1 #####输入2 指令:-input scene1_05.txt -size 200 200 -output output1_05.tga -depth 14.5 19.5 depth1_05.tga 这个输入多个武器,且摄像机的角度是(-1,-1,-1),并且up向量和direction不垂直,可以从图片看到我们的比例是正确的 透视图: Assignment1 距离可视化: Assignment1 #####输入三 指令:raytracer -input scene1_07.txt -size 200 200 -output output1_07.tga -depth -2 2 depth1_07.tga 这个输入有三个物体,且摄像机的中心在一个物体内,存在t < 0的情况,这个条件下我们能够正确的处理 透视图: Assignment1 距离可视化: Assignment1 其余输出也都验证过了,但是特别的地方就不赘述了 ###Assignment2 这个实验主要是分为4块:实现漫反射模式,法线可视化,透视相机实现,增加新的Object3D:Triangle(三角形), Plane(平面), Transform(变换),接下来我也针对这几个部分进行介绍,虽然这个实验有了上个实验的框架,但是实际写起来还是比较麻烦,仍然需要double click PPT和相关知识 ####Diffuse Shading 通过PPT中的公式我们可以获得$c_{pixel} = c_{ambient} * c_{object} + \sum_iclamped(L_i \cdot N) * c_{light_i} * c_{object} ]$,根据这个公式我们可以写出计算像素位置的diffuse color代码:

Vec3f get_diffuse_color(SceneParser& parser, Hit& hit, bool shade_back)
{
    Vec3f ambient = parser.getAmbientLight(), light_sum; 
    light_sum = ambient;//初值设置为ambient light
    for (int k = 0; k < parser.getNumLights(); k++)//对每个light进行计算
    {
        Light* light = parser.getLight(k);
        Vec3f p, dir, col
        light->getIllumination(p, dir, col);
        if (shade_back)//如果shade_back, 则需要用abs考虑dot结果是负数的情况,也就是光源照在表面的背面
            light_sum = light_sum + abs(dir.Dot3(hit.getNormal())) * col;
        else//否则只要正的结果
            light_sum = light_sum + max(0, dir.Dot3(hit.getNormal())) * col;
    }
    //最后统一点乘Material的Diffuse Color然后返回
    return light_sum * hit.getMaterial()->getDiffuseColor();
}

####Normal Visable Mode 这个比较简单,我们在Intersect()函数的hit中增加一个变量normal代表法线方向,然后color根据normal三个分量的绝对值即可,这里注意不能忘了normal.normalize(),这个我是在Intersect()中进行的,没必要放到外面的函数

Vec3f get_normal_color(Hit& hit)
{
    Vec3f normal = hit.getNormal();
    return Vec3f(abs(normal.x()), abs(normal.y()), abs(normal.z()));//取绝对值返回即可
}

####Perspective Camera 构造函数中除了记录必要的信息,通过叉乘算出horitontial矢量

PerspectiveCamera(Vec3f center, Vec3f& direction, Vec3f& up, float angle)
{
    this->center = center;
    this->direction = direction;
    this->up = up;
    Vec3f::Cross3(this->hor, this->direction, this->up);
    this->direction.Normalize();
    this->up.Normalize();
    this->hor.Normalize();
    this->angle = angle;
}

光线的构造主要是direction部分的问题,这里我需要把[(-1/2, 1/2), (1/2, 1/2)]的offset映射到[(-tanθ/2,-tanθ/2), (tanθ/2, tanθ/2)],这个hardcode一下即可,这里我最开始把x,y轴方向弄反了,导致图形出现了y=x轴对称的情况,然而前几个样例都是球,所以没发现,知道后面样例出现平面才发现这个问题

virtual Ray generateRay(Vec2f point)
{
    Vec3f temp = direction + 2 * tan(angle / 2) * up * point.y() + 2 * tan(angle / 2) * hor * point.x();
    return Ray(center, temp);
}

Plane

Plane类需要记录normal和constant两个类型的值,这样可以唯一确定一个平面,根据这两个信息可以唯一确定一个平面

bool intersect(const Ray& r, Hit& h, float tmin)
{
    if (normal.Dot3(r.getDirection()) == 0) // 这里判断线与面平行的特殊情况
    {
        if (normal.Dot3(r.getOrigin()) != d)
            return false;
        else 
        {
            Vec3f tmp = normal;
            tmp.Normalize();
            h.set(0, this->material, tmp, r);
            return true;
        }

    }
    //通过点乘计算距离,判断t>=tmin
    Vec3f tmp = normal;
    tmp.Normalize();
    float t = (d - r.getOrigin().Dot3(normal)) / r.getDirection().Dot3(normal);
    h.set(t, this->material, tmp, r);
    return tmin <= t;
}

Triangle

Triangle类判断比较复杂,我是根据三条边a,b,c上的点满足 $P = R_0 + R_d \cdot t = \alpha \cdot a + \beta \cdot b + \gamma \cdot c$ $(\alpha + \beta + \gamma = 1)$, 然后根据矩阵运算确定这些的值包括t,这些是从PPT上搬过来hardcode的,具体原理没什么了解

bool intersect(const Ray& r, Hit& h, float tmin)
{
    Vec3f direction = r.getDirection(), origin = r.getOrigin();
    float A = det3x3(
        a.x() - b.x(), a.x() - c.x(), direction.x(),
        a.y() - b.y(), a.y() - c.y(), direction.y(),
        a.z() - b.z(), a.z() - c.z(), direction.z()
    );
    if (A == 0)
        return false;
    float beta = det3x3(
        a.x() - origin.x(), a.x() - c.x(), direction.x(),
        a.y() - origin.y(), a.y() - c.y(), direction.y(),
        a.z() - origin.z(), a.z() - c.z(), direction.z()
    ) / A;

    float gama = det3x3(
        a.x() - b.x(), a.x() - origin.x(), direction.x(),
        a.y() - b.y(), a.y() - origin.y(), direction.y(),
        a.z() - b.z(), a.z() - origin.z(), direction.z()
    ) / A;

    float t = det3x3(
        a.x() - b.x(), a.x() - c.x(), a.x() - origin.x(),
        a.y() - b.y(), a.y() - c.y(), a.y() - origin.y(),
        a.z() - b.z(), a.z() - c.z(), a.z() - origin.z()
    ) / A;
    bool res = false;
    if (beta >= 0 && gama >= 0 && beta + gama <= 1 && t >= tmin)
    {
        res = true;
    }
    if (res)
    {
        h.set(t, this->material, this->normal, r);
    }
    return res;
}

####结果展示

Case 1&2

这个样例是最简单的Diffuse Shading, 这两张分别是ambient light(0, 0, 0)和(0.5, 0.5, 0.5)

Case 3

这个样例有多个光线,并且有normal输出模式(图右)

Case 4

这个样例包括了shade_back选项,可以看到camera在球的里面却能看到效果 以下分别是output, depth 和 normal

Case 5

这个样例包含Plane

Case 6

这个样例包含最基础的Triangle, 右图是没有shade_back参数的

Case 7

这个样例是通过多个Triangle组成立体图形,包括cube, rabbit

Case 8

最后是一个测试Transform以及t是否正确变换的样例,由于我传入的direction在变换后没有经过normalize,所以在Sphere的Intersect函数中需要改进代码,具体就是把a = 1变成a = direction.dot3(direction),这样就能计算出t且这个t在外部不需要改变 ###Assignment3 这个实验主要是添加了Phong Model的Shader,通过对Diffuse, Specular和Ambient三个方面的反射光分量,使Shader更加真实,并且将OpenGL库加入工程,通过OpenGL快速渲染模型,并且实现可互动。这个作业虽然很多和OpenGL的代码在网站中都有给出,但是由于我需要通过阅读和查阅理解这部分内容。并且在Paint()函数中,对于Sphere类型的编写和Debug都比较困难,所以实际上花了比预期更多的时间 ####代码分析 #####GlCanvas类 首先增加GlCanvas类,这个类用于调用OpenGl库函数实现视角(相机)的移动,包括旋转,平移,缩放。由于这部分代码是作业提供的,并且提供的缩放功能存在问题。因此不贴出来了 #####PhongMaterial类 PhongMaterial类继承自Material类,它增加了两个成员函数,一个是为GlCanvas提供Material颜色的void glSetMaterial(void), 它主要是调用库函数设置了Phong Model的一些参数 然后是我们为camera提供的Phong Model Shader, 我们通过成员变量,计算pixel的颜色

virtual Vec3f Shade(const Ray& ray, const Hit& hit, const Vec3f& dirToLight, const Vec3f& lightColor) const
{
    // ray's direction should be normalize since it maybe transform, and it does matter
    Vec3f light_dir = dirToLight;
    light_dir.Normalize();
    Vec3f ray_dir = ray.getDirection();
    ray_dir.Normalize();
    //Diffuse Part
    Vec3f diffuse = this->getDiffuseColor() * max(hit.getNormal().Dot3(light_dir), 0);
    //Specular Part
    //In Blinn-Torrance' s solution, L = (l + v).normalize() dot hit.normal
    //But in our input the ray is from camera to object
    //So l - v is correct
    Vec3f beta = light_dir - ray_dir;
    beta.Normalize();
    Vec3f specular = this->getSpecularColor() * pow(max(beta.Dot3(hit.getNormal()), 0), exponent);
    //return part
    return (specular + diffuse) * lightColor;
}

#####Object3D::Paint() Paint()虚函数主要是调用OpenGl的相关函数,在Pre-Visualizer里绘制对应的图形 对于Group类,因为它实际上就是个容器,所以我们只需要recursively调用容器内每个元素的Paint()即可

void paint()
{
    for (auto& object : object_group)
        object->paint();
}

对于Triangle类比较简单,我们提供三个顶点的坐标即可

void paint()
{
    this->material->glSetMaterial(); //设置Material
    glBegin(GL_TRIANGLES); //绘制三角形
    glNormal3f(normal.x(), normal.y(), normal.z()); //提供法线
    //三个vertex坐标
    glVertex3f(a.x(), a.y(), a.z());
    glVertex3f(b.x(), b.y(), b.z());
    glVertex3f(c.x(), c.y(), c.z());
    glEnd();
}

对于Plane类,由于OpenGL没有提供这个primitive,所以我们选择绘制非常大的长方形,它的大小远大于窗口大小,用这张方式模拟平面

void paint()
{
    //首先我们需要通过Normal找到一条平行于平面的向量
    //所以我们随便找一个不和normal平行的向量进行叉乘即可,得到的记为b1
    //然后再用向量和b1
    Vec3f tmp = normal == Vec3f(1, 0, 0) ? Vec3f(0, 1, 0) : Vec3f(1, 0, 0);
    Vec3f b1, b2;
    Vec3f::Cross3(b1, normal, tmp);
    Vec3f::Cross3(b2, b1, normal);
    Vec3f center;
    //然后我们根据P dot N = d, 设定平面的中心
    if (normal.x() != 0)
        center = Vec3f(d / normal.x(), 0, 0);
    else if (normal.y() != 0)
        center = Vec3f(0, d / normal.y(), 0);
    else if (normal.z() != 0)
        center = Vec3f(0, 0, d / normal.z());
    else
    {
        cout << "The normal is wrongly set to (0, 0, 0)";
        assert(0);
    }
    //然后以INF上下左右延伸,获得4个vertex
    Vec3f a = center - INF * b1 - INF * b2;
    Vec3f b = center - INF * b1 + INF * b2;
    Vec3f c = center + INF * b1 + INF * b2;
    Vec3f d = center + INF * b1 - INF * b2;
    //调用函数绘制图形
    this->material->glSetMaterial();
    glBegin(GL_QUADS);
    glNormal3f(normal.x(), normal.y(), normal.z());
    glVertex3f(a.x(), a.y(), a.z());
    glVertex3f(b.x(), b.y(), b.z());
    glVertex3f(c.x(), c.y(), c.z());
    glVertex3f(d.x(), d.y(), d.z());
    glEnd();
}

Sphere::Paint()比较复杂,我们需要通过绘制多边形进行模拟,我将在注释中进行详细的说明

void Sphere::paint()
{
    material->glSetMaterial();
    vector<Vec3f> points;
    // 记录单位theta、phi的步长
    float unit_theta = M_PI * 2 / theta_num, unit_phi = M_PI / phi_num;
    // 生成phi_num * theta_num个点,放入容器中
    for (int phi = 0; phi < phi_num + 1; phi++)
        for (int theta = 0; theta < theta_num; theta++)
        {
            float phi_val = -M_PI_2 + unit_phi * phi;
            float theta_val = unit_theta * theta;
            points.push_back(
                Vec3f(radius * cos(phi_val) * sin(theta_val),
                    radius * cos(phi_val) * cos(theta_val),
                    radius * sin(phi_val)
                ) + center);
        }
    // 层序遍历用这些点画四边形
  
    int count = 0;
    glBegin(GL_QUADS);
    for (int phi = 0; phi < phi_num; phi++)
    {
        Vec3f p[4];
        for (int theta = 0; theta < theta_num; theta++)
        {
            p[0] = points[phi * theta_num + theta];
            p[1] = points[phi * theta_num + (theta + 1) % theta_num];
            p[2] = points[(phi + 1) * theta_num + (theta + 1) % theta_num];
            p[3] = points[(phi + 1) * theta_num + theta];
            Vec3f normal;
            // 通过叉乘获得normal
            // 注意由于在phi = -PI / 2 和 PI / 2时候有2个点重合,所以实际为三角形
            // 由于float的精度问题,所以直接计算normal可能会出错
            // 这时候我们需要特殊判断,正确处理法线的方向计算
            if (phi == phi_num - 1)
            {
                Vec3f::Cross3(normal, p[3] - p[0], p[1] - p[3]);
            }
            else
            {
                Vec3f::Cross3(normal, p[3] - p[0], p[2] - p[3]);
            }
            normal.Normalize();
            // 然后绘制四边形,在gouraud flag标记为true的时候,我们使用gouraud插值法
            // 为每个点设置不同的normal
            for (int i = 0; i < 4; i++)
            {
                if (gouraud_used)
                {
                    normal = p[i] - center;
                    normal.Normalize();
                }
                glNormal3f(normal.x(), normal.y(), normal.z());
                glVertex3f(p[i].x(), p[i].y(), p[i].z());
            }
            count++;
        }
        
    }
    glEnd();
}

对于Transform::Paint()比较简单,是通过入栈和出栈来存储变换矩阵,然后对他的child调用Paint()

void paint()
{
    glPushMatrix();
    GLfloat* glMatrix = m.glGet();
    glMultMatrixf(glMatrix);
    delete[] glMatrix;
    o->paint();
    glPopMatrix();
}

####结果展示 Cube和Rabbit本质上都是用Triangle模拟的,为了对比OpenGL和我们生成的tga文件验证Phong Model是否正确 #####Cube 正交相机 透视相机 #####Rabbit #####Transform 这个是测试OpenGL的Transform类是否正确,对比图片我们可以发现成功实现 #####Sphere 这个我认为是作业里最难的一部分,我查找Bug花费了很长时间,并且我最终我的成果与Assignment预期结果不同。 我通过对比样例图片,发现了camera视角是(0, 0, -1),而我用的是左手系,所以看到球的顶部,是由数个三角形拼接而成,而样例使用的是右手系,看到是球的正面,所以更为平滑。 但是由于Assignment中并没有明确要求我用右手系,所以我认为我的结果也是可行的

以下是没有进行插值的结果: 进行插值以后,可以看到还是达到了预期效果: 最后是调整镜面反射exponent参数的效果,由于没有进行specular fix,所以OpenGL的效果会差一点 ###Asssignment4 这个作业主要是通过为之前的Shader增加阴影、反射、折射,使它变成一个传统意义上的RayTracer,并且通过扩展GLCamera的功能,使能够在Pre-Visualizer上观察射向某点光线的折射、反射路径,做到光路可视化,也能够更好的帮助编写代码时候的Debug ####代码介绍 从局部光照到全局光照其实改动比较简单,我们只需要在原本group->intersect()的基础上,判断从这个点到所有光源的射线,是否会遇到其他物体,通过这种方式判断这个光源的光照是否有效 #####Shadows

for (int i = 0; i < parser->getNumLights(); i++)
{
    Light* light = parser->getLight(i);
    Vec3f p, dir, col;
    Vec3f flag(1, 1, 1);//用flag代表这个光线的光照是否有效
    p = hit.getIntersectionPoint();
    float dis;
    light->getIllumination(p, dir, col, dis);
    //如果全局光照的选项打开,则进行判断
    if (shadows)
    {
        Hit light_hit;
        Ray light_ray(p, dir); //做一条从交点到光源的射线
        if (group->intersect(light_ray, light_hit, EPSILON)) //如果有交点
        {
            if (dis > light_hit.getT()) //交点在光源之前
            {
                flag = Vec3f(0, 0, 0); //标记无效
                RayTree::AddShadowSegment(light_ray, 0, light_hit.getT()); //射线可视化
            }
            else //如果交点在光源之后,实际上并没有影响
            {
                RayTree::AddShadowSegment(light_ray, 0, dis);
            }
        }
        else // 如果没交点,则该光线必定有效
        {
            RayTree::AddShadowSegment(light_ray, 0, dis);
        }
    }
    // 累计每条光线的颜色
    color += flag * hit.getMaterial()->Shade(ray, hit, dir, col);
}

#####Reflection 首先是通过入射光线和法线计算反射光线

Vec3f mirrorDirection(const Vec3f& normal, const Vec3f& incoming)
{
    Vec3f n = normal, v = incoming;
    n.Normalize();
    v.Normalize();
    Vec3f mirror = v - 2 * n.Dot3(v) * n;
    mirror.Normalize();
    return	mirror;
}

然后如果Material的反射系数大于零,则考虑反射光线

if (hit.getMaterial()->getReflectiveColor().Length() > 0)
{
    Vec3f reflect = mirrorDirection(hit.getNormal(), ray.getDirection());//获得反射光线的方向
    Ray reflect_ray = Ray(hit.getIntersectionPoint(), reflect);
    Hit reflect_hit;
    //递归的调用traceRay,对新的Ray进行计算
    //这里的EPSILON设置为0.01,主要是避免因为浮点数的精度导致碰撞检测到自己的失误
    color += traceRay(reflect_ray, EPSILON, bounces + 1, weight, indexOfRefraction, reflect_hit) 
    * hit.getMaterial()->getReflectiveColor();
    RayTree::AddReflectedSegment(reflect_ray, 0, reflect_hit.getT());//反射光线可视化
}

#####Refration 折射的处理比较麻烦,主要有两个难点,第一个是折射光线方向的计算比反射难,第二个是由于介质的折射率不同,需要考虑全反射现象。 首先是折射光线方向计算

bool transmittedDirection(const Vec3f& normal, const Vec3f& incoming,
    float index_i, float index_t, Vec3f& transmitted)
{
    //对入射方向和法线进行矢量化
    Vec3f n = normal, i = incoming;
    n.Normalize();
    i.Normalize();
    float cos_theta = n.Dot3(i);//入射光线和法线夹角的cos值 	
    Vec3f m = -cos_theta * n + i;//入射光线在法线垂直方向的投影
    float in_sin = m.Length();//入射夹角的sin值
    float out_sin = in_sin * index_i / index_t;
    //考虑全反射的特殊情况
    if (out_sin > 1)
        return false;
    float out_cos = sqrt(1 - out_sin * out_sin);
    m.Normalize();
    //由于这儿out_cos没有考虑方向,所以需要
    //从material中射出或者射入material中两种情况分别考虑
    if (cos_theta < 0)
        transmitted = -out_cos * n + out_sin * m;
    else
        transmitted = out_cos * n + out_sin * m;
    return true;
}

然后是rayTracer中的处理部分,如果material的TransparentColor不为0,则考虑折射

if (hit.getMaterial()->getTransparentColor().Length() > 0)
{
    bool flag;
    Vec3f transmitted;
    float to_index;
    //考虑两种情况,如果是从material中射出,则外面介质的折射率设置为1
    //否则设置为material的折射率
    if (ray.getDirection().Dot3(hit.getNormal()) < 0)
        to_index = hit.getMaterial()->getIndexOfRefraction();
    else
        to_index = 1;
    //通过flag标记是否全反射
    flag = transmittedDirection(hit.getNormal(), ray.getDirection(), indexOfRefraction, to_index, transmitted);
    //如果没有发生全反射,则递归的考虑折射光线的影响
    if (flag)
    {
        Ray transmitted_ray(hit.getIntersectionPoint(), transmitted);
        Hit transmitted_hit;
        Vec3f tmp = traceRay(transmitted_ray, EPSILON, bounces + 1, weight, to_index, transmitted_hit) * hit.getMaterial()->getTransparentColor();//记录tmp主要是为了输出debug
        color += tmp;
        RayTree::AddTransmittedSegment(transmitted_ray, 0, transmitted_hit.getT());
    }
}

对于折射、反射次数上限的处理比较简单,判断bounces和max_bounces的大小即可

if (bounces > max_bounces)
            return Vec3f(0, 0, 0);

#####Pre-Visualizer交互 最后是在Pre-Visualizer中,按T实现该点的光线,我们是增加了如下的函数

void trace(float x, float y)
{
    Camera* camera = parser->getCamera();
    float tmin = camera->getTMin();
    //这里我们是手动生成了一条光线
    RayTracer raytracer(parser, max_bounces, cutoff_weight, shadows);
    //由于我的camera的输入范围是[-0.5, -0.5]到[0.5, 0.5],而输入是[0, 0]到[1, 1]
    //所以需要手动转换
    Vec2f point(x - 0.5, y - 0.5);
    Ray ray = camera->generateRay(point);
    Hit hit_result;
    //进行traceRay函数,用他的功能进行可视化
    //返回的Color并不被我们需要
    Vec3f Color = raytracer.traceRay(ray, camera->getTMin(), 0, cutoff_weight, 0, hit_result);
}

####结果展示 #####Shadows 可以看到由于全局的原因,Plane的阴影正确处理 #####光线可视化 由于这是个实心的球体,所以没折射和反射光,我们可以看到3个光源的射线,有2个被球体本身挡住,最后一个则没有被遮挡,因此这个像素呈现了颜色 而对于下面的阴影部分,则是3个光源都被挡住 #####Reflection 这个样例的Plane支持反射,所以可以看到Plane的交点有一条反射光线射到红色柱子上 因此我们生成的tga文件也有反射的效果 #####Bounces 这个样例是体现Bounces影响反射 首先是bounces为0的情况,可以看到不允许反射,显示物体本身的颜色 然后是bounces为1,2,3的情况,可以看到反射效果越来越好(仔细看还有一种递归的感觉) #####Point Light 然后是对于新的类型,点光源,通过这个例子我们可以看到点光源是向四周放出光线,但是有direction的因素影响光照,这个在提供的代码里已经处理了,不需要我们在Shader里实现 #####Reflaction 折射的情况我们直接放最复杂的一种,即对于diamond的折射 最开始的时候bounces设置为0,无法折射,这时候体现的就是光线的颜色 然后随着bounces变大,钻石变得璀璨起来 但是这里其实我和网站的sample有些区别,因为在rayTracer的参数中有一个设置为0.01的weight,是和光线的权重有关的,但是我不清楚这个weight具体该怎么使用,实际效果出来我的钻石会比他的稍微亮一点,因为相差不是很大所以就不纠结在这儿了 ###总结   在两周内,我完成了MIT图形学Assignment0~4总共5个作业的内容。一开始我认为我能够在两周内完成整个作业,但是在实际写代码的时候,发现由于之前我并没有修读过图形学,因此对于理解PPT和Asignment中内容的理解花费了不少时间,我在看PPT和Assignment以外,还去查阅包括wiki和其他的很多资料,才将课程内容绝大部分都理解了。因此两周也只完成了一半的内容。接下来我的目标是在国庆期间将剩余的部分完成。   但是在完成作业的途中,我也一步步从最开始的camera,到自己编写图形类,最后到raytracer和OpenGL库的使用,我一步一步实现了我两周以前不能理解的内容,并且内容将他们以图片和Pre-Visualizer的形式实现,我认为这是挺有成就感的一件事。