跳转至

Assignment 4

文本统计:约 1117 个字 • 265 行代码

这次作业实现了光线追踪基本的内容,考虑了折射与反射的内容,并且加入了阴影的判断。

Tasks

(1)给 PhongMaterial 类中加入折射与反射相关的参数 relfectiveColor transparentColor 以及 indexOfRefraction 并添加相应的访问函数。

relfectiveColor transparentColor 的解释

可被认为是光线在 RGB 三个维度上的反射和折射后的结果,比如 reflectiveColor0.5 0.6 0.7 那么经过反射后,光线的 G 值就会变成原来的 0.6 倍。

然后题目中还引入了 weight 这个约束(在下一条中有所阐述),它的值与 relfectiveColor transparentColor 的模长 (magnitude of vector) 有关,占比变成 \(\text{weight}\times|\text{reflectiveColor}|\)

(2)实现光线追踪的部分,我们新增加一个类 RayTracer ,可被认为是一个光线追踪单元,包含了场景信息,最大弹射次数,cutoff ray weight (实在不知如何翻译),以及其中最重要的函数为

Vec3f traceRay(Ray &ray, float tmin, int bounces, float weight,float indexOfRefraction, Hit &hit) const;

射入光线 ray 以及给定一些参数,得到最近的碰撞点,并返回相应的颜色。

(3)注意光线追踪产生新光束的时候,为避免自遮挡,需要适当移动起始点

(4)为了方便反射和折射产生新光线,我们还需要写两个函数,分别是

Vec3f mirrorDirection(const Vec3f &normal, const Vec3f &incoming);

用于生成反射光线的方向,其中 incoming 是入射光线的方向。

bool transmittedDirection(const Vec3f &normal, const Vec3f &incoming, 
      float index_i, float index_t, Vec3f &transmitted);

一方面这个函数用来判断是否能发射折射,另一方面,如果发生了折射,可以生成相应的折射光线。

一些简化

我们假设所有的透明物体(即可发生折射的物体)均处在真空中,不存在折射率嵌套的情况。

同时我们在发生折射的时候,要判断一下是从物体内部射出,还是射入物体内部(之前用到的命令行指令 -shade_back 在这里就用到了),而这个判断方法就是将入射向量与法向量求点积,得到负的,说明是从外向内射入;得到正的,说明就是从内向外射出。

(5)同时题目还提供了相应的可视化工具用于清晰地观察出入射光线,反射折射光线,以及判断阴影的光线。要使用提供的函数,如下

void RayTree::SetMainSegment(const Ray &ray, float tstart, float tstop);
void RayTree::AddShadowSegment(const Ray &ray, float tstart, float tstop);
void RayTree::AddReflectedSegment(const Ray &ray, float tstart, float tstop);
void RayTree::AddTransmittedSegment(const Ray &ray, float tstart, float tstop);

glCanvas::initialize 需要增加第三个参数

void initialize(SceneParser *_scene, void (*_renderFunction)(void), void (*_traceRayFunction)(float, float));

第三个参数代表的函数是用于生成可视化光线的,回顾一下第二个参数,是用于生成相应的图片的

(7*)这个任务并不用完成,已经完成,简单介绍一下。添加了一个新的派生类 PointLight 代表点光源,点光源的光强会随着时间而衰减,尽管在物理上正确的衰减系数为 \(1/d^2\) 但是我们在光线追踪中并不会如此使用,因为它会导致高对比度的图像难以在低动态范围设备上正确显示。在这里我们使用的是 \(1/d\)

void getIllumination (const Vec3f &p, Vec3f &dir, Vec3f &col, float &distanceToLight) const {
    dir = position - p;
    // grab the length before the direction is normalized
    distanceToLight = dir.Length(); 
    dir.Normalize(); 
    float attenuation = 1 / (attenuation_1 + 
                             attenuation_2*distanceToLight + 
                             attenuation_3*distanceToLight*distanceToLight);
    if (attenuation < 0) attenuation = 0;
    col = color * attenuation;
}

Ray tracer

class RayTracer {
    public:
        RayTracer(SceneParser *s, int max_bounces, float cutoff_weight)
        {
            scene = s;
            group = s->getGroup();
            maxBounces = max_bounces;
            cutoffWeight = cutoff_weight;
        }

        Vec3f traceRay(Ray &ray, float tmin, int bounces, float weight, 
                 float indexOfRefraction, Hit &hit) const;

    private:
        SceneParser *scene;
        Group *group;
        int maxBounces;
        float cutoffWeight;
};

先是镜面反射的出射光线函数,比较容易,不多赘述

Vec3f mirrorDirection(const Vec3f &normal, const Vec3f &incoming)
{
    return incoming - 2 * normal.Dot3(incoming) * normal;
}

然后是有关折射的函数,这个内容在 GAMES101 的作业中已经阐述过了,也不再赘述

bool transmittedDirection(const Vec3f &normal, const Vec3f &incoming, float index_i, float index_t, Vec3f &transmitted)
{
    float cosi = -normal.Dot3(incoming);
    float eta = index_i / index_t;
    float k = 1 - eta * eta * (1 - cosi * cosi);

    //发生全反射的情况
    if (k < 0) {
        return false;
    }
    transmitted = eta * incoming + (eta * cosi - sqrtf(k)) * normal;
    return true;
}

然后到了这次作业的重点函数 traceRay 函数,注意一下,这里的indexOfRefraction 是当前光线所处环境的折射率。

Vec3f RayTracer::traceRay(Ray &ray, float tmin, int bounces, float weight, 
                 float indexOfRefraction, Hit & intersection) const
{
    // 递归截止条件
    if (bounces > maxBounces || weight < cutoffWeight) {
        return Vec3f(0, 0, 0);
    }

    //光线是否打到物体
    bool is_inter = group -> intersect(ray, intersection, tmin);
    auto material = dynamic_cast<PhongMaterial *>(intersection.getMaterial());

    //如果没有打到的话就返回背景色
    if (!is_inter) {
        return scene -> getBackgroundColor();
    }

    //当weight是1的时候,说明这条光线是入射光线
    if (weight == 1 )
        RayTree::SetMainSegment(ray, 0, intersection.getT());

    //创建变量color,这是我最后要返回的内容,先加上环境光
    Vec3f color = scene -> getAmbientLight()* material -> getDiffuseColor();

    //局部光照
    for (int ilight = 0; ilight < scene -> getNumLights(); ilight++)
    {
        Light *light = scene -> getLight(ilight);
        Vec3f dirToLight, lightColor;
        float disToLight;
        light -> getIllumination(intersection.getIntersectionPoint(), dirToLight, lightColor, disToLight);

        //略微改变光线起点位置,防止出现自遮挡的情况
        Ray lightray = Ray(intersection.getIntersectionPoint()-ray.getDirection()*0.001f, dirToLight);
        Hit lightHit;

        //判断阴影,并画出相应的线
        if (shadows && group -> intersect(lightray, lightHit, 0.001f) && lightHit.getT() < disToLight) {
            RayTree::AddShadowSegment(lightray, 0, lightHit.getT());
            continue;
        }
        if (shadows) {
            RayTree::AddShadowSegment(lightray, 0, disToLight);
        }

        //得到该点对应的局部光照结果
        color += material -> Shade(ray, intersection, dirToLight, lightColor);
    }

    Vec3f hitpoint = intersection.getIntersectionPoint();
    Vec3f normal = intersection.getNormal();
    Vec3f incoming = ray.getDirection();

    //计算反射项
    auto reflectionColor = material -> getreflectiveColor();
    if (reflectionColor.Length() > 0)   //这是判断该物体会不会反射
    {
        //计算反射光线的起始点与方向,得到相应的光线
        Vec3f reflectionDirection = mirrorDirection(normal, incoming);
        Vec3f reflectionRayOrig = intersection.getIntersectionPoint() - ray.getDirection() * 0.001;
        Ray reflectionRay(reflectionRayOrig, reflectionDirection);

        //进行递归操作
        Hit reflectionHit;
        Vec3f reflectColor = traceRay(reflectionRay, 0, bounces + 1, weight * material -> getreflectiveColor().Length(), indexOfRefraction, reflectionHit);
        float reflectT = EPSILON;
        if (reflectionHit.getMaterial()!= nullptr)
            reflectT = reflectionHit.getT();
        RayTree::AddReflectedSegment(reflectionRay, 0, reflectT);

        //注意一下 向量乘法在 vector.h 中的定义
        color += reflectColor*reflectionColor;
    }

    //计算折射项
    auto refractionColor = material -> gettransparentColor();
    if (refractionColor.Length()>0)     //这是判断该物体是否会发生折射
    {
        //设置对应的折射率
        float index_i = indexOfRefraction;
        float index_t = material -> getindexOfRefraction();

        //如果打到了背面,那么把法向量反向
        //同时说明是从里面往外面打的,外部的折射率定为1
        if (shadeback && normal.Dot3(incoming) > 0)
        {
            normal.Negate();
            index_t = 1;
        }

        //判断能否发生折射
        Vec3f transimittedDirection;
        bool is_refract = transmittedDirection(normal, incoming, index_i, index_t, transimittedDirection);

        if(is_refract)
        {
            //计算折射光线的起始点与方向,得到相应的光线
            Vec3f refractionRayOrig = intersection.getIntersectionPoint() + ray.getDirection() * 0.001;
            Ray refractionRay(refractionRayOrig, transimittedDirection);

            //进行相应的递归操作
            Hit refractionHit;
            Vec3f refractColor = traceRay(refractionRay, 0, bounces + 1, weight * material -> gettransparentColor().Length(), material -> getindexOfRefraction(), refractionHit);
            float refractT = EPSILON;
            if (refractionHit.getMaterial()!= nullptr)
                refractT = refractionHit.getT();
            RayTree::AddTransmittedSegment(refractionRay, 0, refractT);
            color += refractColor*refractionColor;     
        }
    }
    return color;
}

最后关注一下相应的渲染函数

void RayTracerRenderer::Render() {
    Renderer::Render(); // preparations

    //遍历每一个像素,使用光追进行渲染
    for (int i = 0; i < image->Width(); i++) {
        for (int j = 0; j < image->Height(); j++) {
            //产生光线
            auto ray = camera->generateRay(Vec2f((float) i / image->Width(),
                                                 (float) j / image->Height()));
            //生成光线追踪器
            auto tracer = new RayTracer(scene, maxBounces, cutoffWeight);
            Hit hit;
            //调用相应的函数得到像素的颜色
            //设置bounces为0,weight为1(主光线),折射率为1
            image->SetPixel(i, j, tracer->traceRay(ray, camera->getTMin(), 0, 1, 1, hit));
        }
    }
}

还有添加到 glCanvas::initialize 中的新的函数参数 void (*_traceRayFunction)(float, float)

先看一下在 glCanvas::initialize 中相应参数的调用,实际上是设置了相应的成员变量

void GLCanvas::initialize(SceneParser *_scene, void (*_renderFunction)(void), void (*_traceRayFunction)(float,float)) {
  scene = _scene;
  renderFunction = _renderFunction;
  traceRayFunction = _traceRayFunction;

  // Set global lighting parameters
  glEnable(GL_LIGHTING);
  glShadeModel(GL_SMOOTH);
  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE);


  // Set window parameters
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH | GLUT_RGB);
  glEnable(GL_DEPTH_TEST);
  // OPTIONAL: If you'd like to set the window size from 
  // the command line, do that here
  glutInitWindowSize(400,400);
  glutInitWindowPosition(100,100);
  glutCreateWindow("OpenGL Viewer");

  glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);
  glEnable(GL_NORMALIZE);

  // Ambient light
  Vec3f ambColor = scene->getAmbientLight();
  GLfloat ambArr[] = { ambColor.x(), ambColor.y(), ambColor.z(), 1.0 };
  glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambArr);

  // Initialize callback functions
  glutMouseFunc(mouse);
  glutMotionFunc(motion);  
  glutDisplayFunc(display);
  glutReshapeFunc(reshape);
  glutKeyboardFunc(keyboard);

  // Enter the main rendering loop
  glutMainLoop();
}

实际调用发生在 glCanvas::display 上,这个函数完成的内容实际上是对某个像素进行光线追踪

void GLCanvas::keyboard(unsigned char key, int i, int j) {
  switch (key) {
  case 'r':  case 'R':
    printf("Rendering scene... "); 
    fflush(stdout);
    if (renderFunction) renderFunction();
    printf("done.\n");
    break;
  case 't':  case 'T': {
    // visualize the ray tree for the pixel at the current mouse position
    int width = glutGet(GLUT_WINDOW_WIDTH);
    int height = glutGet(GLUT_WINDOW_HEIGHT);
    // flip up & down
    j = height-j; 
    int max = (width > height) ? width : height;
    // map the pixel coordinates: (0,0) -> (width-1,height-1);
    //      to screenspace: (0.0,0.0) -> (1.0,1.0);
    float x = ((i + 0.5) -  width/2.0) / float(max) + 0.5;
    float y = ((j + 0.5) - height/2.0) / float(max) + 0.5;
    RayTree::Activate();

    //完成的工作实际上是对某个像素进行光线追踪
    if (traceRayFunction) traceRayFunction(x,y);
    RayTree::Deactivate();
    // redraw
    display();
    break; }
  case 'q':  case 'Q':
    exit(0);
    break;
  default:
    printf("UNKNOWN KEYBOARD INPUT  '%c'\n", key);
  }
}

于是相应的函数就可被写为

void TraceRay(float x,float y){
    auto camera = parser->getCamera();
    auto ray = camera->generateRay(Vec2f(x, y));
    Hit hit;
    //仅仅只需要产生光线即可,并不需要返回值
    tracer->traceRay(ray, camera->getTMin(), 0, 1.0, 1.0, hit);
}

Summary

这次作业基本完成了光线追踪的大部分内容,简化的部分为

(1)将半透明的物体的阴影设置的跟不透明的一样,这部分是本次作业的credit

(2)折射部分的简化

注意的细节,改变起始点的时候不能太小了,我一开始设置为EPSILON,导致仍然有很多的噪点,改为0.001时问题就解决了。

对于命令行控制的变量,可以放到 global 文件中作为一个全局变量。

习惯:函数参数的命名以_ 开头,其他变量的命名采用驼峰命名法

评论区

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