包文鼎
###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
结果:
#####测试2:铁杉
指令:ifs -input fern.txt -points 50000 -iters 30 -size 400 -output fern.tga
结果:
###Assignment1
这个作业要求我们用OrthographicCamera的方式实现Ray Cast, 并且拥有颜色和灰度两种显示方式。这次作业相比上一次,我们需要更多的发挥自己的能力和ppt中的相关知识解决问题,也拥有更高的自由度。
####代码介绍
#####Sphere类
首先是构造函数,有radius
,center
和material
三个参数
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
这个输入只有一个物体
透视图:
距离可视化:
#####输入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不垂直,可以从图片看到我们的比例是正确的
透视图:
距离可视化:
#####输入三
指令:raytracer -input scene1_07.txt -size 200 200 -output output1_07.tga -depth -2 2 depth1_07.tga
这个输入有三个物体,且摄像机的中心在一个物体内,存在t < 0
的情况,这个条件下我们能够正确的处理
透视图:
距离可视化:
其余输出也都验证过了,但是特别的地方就不赘述了
###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类需要记录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类判断比较复杂,我是根据三条边a,b,c上的点满足
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;
}
####结果展示
这个样例是最简单的Diffuse Shading, 这两张分别是ambient light(0, 0, 0)和(0.5, 0.5, 0.5)
这个样例包括了shade_back选项,可以看到camera在球的里面却能看到效果 以下分别是output, depth 和 normal
这个样例包含最基础的Triangle, 右图是没有shade_back参数的
这个样例是通过多个Triangle组成立体图形,包括cube, rabbit
最后是一个测试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的形式实现,我认为这是挺有成就感的一件事。