Assignment 4¶
这次作业实现了光线追踪基本的内容,考虑了折射与反射的内容,并且加入了阴影的判断。
Tasks¶
(1)给 PhongMaterial
类中加入折射与反射相关的参数 relfectiveColor
transparentColor
以及 indexOfRefraction
并添加相应的访问函数。
relfectiveColor
transparentColor
的解释
可被认为是光线在 RGB 三个维度上的反射和折射后的结果,比如 reflectiveColor
为0.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)为了方便反射和折射产生新光线,我们还需要写两个函数,分别是
用于生成反射光线的方向,其中 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 文件中作为一个全局变量。
习惯:函数参数的命名以_
开头,其他变量的命名采用驼峰命名法