总览
在这部分的课程中,我们将专注于使用光线追踪来渲染图像。在光线追踪中最重要的操作之一就是找到光线与物体的交点。一旦找到光线与物体的交点,就可以执行着色并返回像素颜色。在这次作业中,我们需要实现两个部分:光线的生成和光线与三角的相交。本次代码框架的工作流程为:
-
从
main
函数开始。我们定义场景的参数,添加物体(球体或三角形)到场景中,并设置其材质,然后将光源添加到场景中。 -
调用
Render(scene)
函数。在遍历所有像素的循环里,生成对应的光线并将返回的颜色保存在帧缓冲区(framebuffer)中。在渲染过程结束后,帧缓冲区中的信息将被保存为图像。 -
在生成像素对应的光线后,我们调用
CastRay
函数,该函数调用trace
来查询光线与场景中最近的对象的交点。 -
然后,我们在此交点执行着色。我们设置了三种不同的着色情况,并且已经为你提供了代码。
你需要修改的函数是:
• Renderer.cpp
中的 Render()
:这里你需要为每个像素生成一条对应的光线,然后调用函数 castRay() 来得到颜色,最后将颜色存储在帧缓冲区的相应像素中。
• Triangle.hpp
中的 rayTriangleIntersect()
: v0, v1, v2 是三角形的三个顶点,orig 是光线的起点,dir 是光线单位化的方向向量。tnear, u, v 是你需要使用我们课上推导的 Moller-Trumbore 算法来更新的参数。
实现
Renderer.cpp->Render()的实现
建议先把这个文章看一下:
Ray-Tracing: Generating Camera Rays
在作业3里我们的坐标变换是正向的,从世界坐标到相机坐标到NDC坐标到渲染的像素坐标,而在光线追踪里我们要把光线从相机原点发射光线到屏幕上的每一个像素(知道的是像素坐标),我们需要做逆向的操作从渲染的像素坐标(注意左上角为原点)到NDC坐标到世界坐标,因为我们计算交点比较深度都是在世界坐标系里计算的。
- 渲染像素坐标->NDC坐标([0,1]的正方形)
- NDC坐标->屏幕像素坐标([-1,1]的正方形【原点在左下角】)
因为原点在左上角,所以第二个式子需要乘一个负号:
我们的渲染图像的纵横比不一定是1,考虑到这一点我们需要对公式进行校正(这样使得每个分隔的方格都是正方形)
- 屏幕像素坐标->相机坐标
这里第二行和第三行右边应该是 P i x e l N D C x PixelNDC_x PixelNDCx和 P i x e l N D C y PixelNDC_y PixelNDCy
我们把相机放在原点,相机朝z轴负向看物体,然后渲染图像的平面(z=n)离原点的距离是1:
由几何关系
所以相机空间的位置为:
这里相机坐标系是和世界坐标系是重合的,如果要再进一步到世界坐标系我们还需要进行一个矩阵的映射
在本作业中,我们使用的公式整合一下即:
P i x e l C a m e r a x = ( 2 ∗ P i x e l x + 0.5 I m a g e W i d t h − 1 ) ∗ I m a g e A s p e c t R a t i o ∗ t a n ( α / 2 ) P I x e l C a m e r a y = ( 1 − 2 ∗ P i x e l y + 0.5 I m a g e H e i g h t ) ∗ t a n ( α / 2 ) PixelCamera_x = \left(2*\frac{Pixel_x + 0.5}{ImageWidth}-1\right)*ImageAspectRatio*tan(\alpha/2)\\ PIxelCamera_y = \left(1 - 2 * \frac{Pixel_y + 0.5}{ImageHeight}\right) * tan(\alpha/2) PixelCamerax=(2∗ImageWidthPixelx+0.5−1)∗ImageAspectRatio∗tan(α/2)PIxelCameray=(1−2∗ImageHeightPixely+0.5)∗tan(α/2)
这样我们就可以写出我们的代码了,一个是修改x和y,一个是对dir使用normalize归一化:
void Renderer::Render(const Scene& scene)
{
std::vector<Vector3f> framebuffer(scene.width * scene.height);
float scale = std::tan(deg2rad(scene.fov * 0.5f));
float imageAspectRatio = scene.width / (float)scene.height;
// Use this variable as the eye position to start your rays.
Vector3f eye_pos(0);
int m = 0;
for (int j = 0; j < scene.height; ++j)
{
for (int i = 0; i < scene.width; ++i)
{
// generate primary ray direction
float x = (2 * ((float)i + 0.5) / scene.width - 1) * scale * imageAspectRatio;
float y = (1 - 2 * ((float)j + 0.5) / scene.height) * scale;
// TODO: Find the x and y positions of the current pixel to get the direction
// vector that passes through it.
// Also, don't forget to multiply both of them with the variable *scale*, and
// x (horizontal) variable with the *imageAspectRatio*
Vector3f dir = normalize(Vector3f(x, y, -1)); // Don't forget to normalize this direction!
framebuffer[m++] = castRay(eye_pos, dir, scene, 0);
}
UpdateProgress(j / (float)scene.height);
}
// save framebuffer to file
FILE* fp = fopen("binary.ppm", "wb");
(void)fprintf(fp, "P6\n%d %d\n255\n", scene.width, scene.height);
for (auto i = 0; i < scene.height * scene.width; ++i) {
static unsigned char color[3];
color[0] = (char)(255 * clamp(0, 1, framebuffer[i].x));
color[1] = (char)(255 * clamp(0, 1, framebuffer[i].y));
color[2] = (char)(255 * clamp(0, 1, framebuffer[i].z));
fwrite(color, 1, 3, fp);
}
fclose(fp);
}
Triangle.cpp->rayTriangleIntersect()的实现
详细的推导可以看:Möller-Trumbore algorithm
按照公式写代码即可,这里使用了Vector.hpp的dotProduct
和crossProduct
函数,判断相交的条件是射线的t大于等于0且和三角形平面的三个重心插值坐标也都大于等于0:
bool rayTriangleIntersect(const Vector3f& v0, const Vector3f& v1, const Vector3f& v2, const Vector3f& orig,
const Vector3f& dir, float& tnear, float& u, float& v)
{
// TODO: Implement this function that tests whether the triangle
// that's specified bt v0, v1 and v2 intersects with the ray (whose
// origin is *orig* and direction is *dir*)
// Also don't forget to update tnear, u and v.
Vector3f E1 = v1 - v0;
Vector3f E2 = v2 - v0;
Vector3f S = orig - v0;
Vector3f S1 = crossProduct(dir, E2);
Vector3f S2 = crossProduct(S, E1);
if (dotProduct(S1, E1) == 0)
return false;
tnear = dotProduct(S2, E2) / dotProduct(S1, E1);
u = dotProduct(S1, S) / dotProduct(S1, E1);
v = dotProduct(S2, dir) / dotProduct(S1, E1);
if (tnear >= 0 && u >= 0 && v >= 0 && (1 - u - v) >= 0)
return true;
return false;
}
效果
其他代码批注
Renderer.cpp->castRay()
Vector3f castRay(
const Vector3f &orig, const Vector3f &dir, const Scene& scene,
int depth)
{
if (depth > scene.maxDepth) {
return Vector3f(0.0,0.0,0.0);
}//深度大于最大深度,这里的maxDepth是5,返回黑色
Vector3f hitColor = scene.backgroundColor;//设置hitColor是背景色,这里是Vector3f(0.235294, 0.67451, 0.843137)
if (auto payload = trace(orig, dir, scene.get_objects()); payload)//找到了和物体的交点
{
Vector3f hitPoint = orig + dir * payload->tNear;//交点的位置
Vector3f N; // normal
Vector2f st; // st coordinates
payload->hit_obj->getSurfaceProperties(hitPoint, dir, payload->index, payload->uv, N, st);//得到表面的信息
switch (payload->hit_obj->materialType) {
case REFLECTION_AND_REFRACTION//考虑反射和折射
{
Vector3f reflectionDirection = normalize(reflect(dir, N));//反射方向
Vector3f refractionDirection = normalize(refract(dir, N, payload->hit_obj->ior));//折射方向
//为避免浮点误差反射和折射的作用点是hitPoint加减一个小的epsilon,这里是0.00001
//反射光在物体内反射点移动到物体内,反之移动到物体外
Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
hitPoint - N * scene.epsilon :
hitPoint + N * scene.epsilon;
//折射光在物体内折射点移动到物体内,反之移动到物体外
Vector3f refractionRayOrig = (dotProduct(refractionDirection, N) < 0) ?
hitPoint - N * scene.epsilon :
hitPoint + N * scene.epsilon;
//递归找到反射的颜色
Vector3f reflectionColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1);
//递归找到折射的颜色
Vector3f refractionColor = castRay(refractionRayOrig, refractionDirection, scene, depth + 1);
//使用fresnel公式计算反射率
float kr = fresnel(dir, N, payload->hit_obj->ior);
//颜色是反射和折射的颜色的加权和
hitColor = reflectionColor * kr + refractionColor * (1 - kr);
break;
}
case REFLECTION://考虑反射,没有折射
{
float kr = fresnel(dir, N, payload->hit_obj->ior);
Vector3f reflectionDirection = reflect(dir, N);
Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
hitPoint + N * scene.epsilon :
hitPoint - N * scene.epsilon;
hitColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1) * kr;
break;
}
default:
{
// [comment]
// We use the Phong illumation model int the default case. The phong model
// is composed of a diffuse and a specular reflection component.
// [/comment]
// 设置环境光和镜面反射光
Vector3f lightAmt = 0, specularColor = 0;
//相机发出的dir作用线在物体外部,阴影的作用点移动到物体交点的外部,反之移动到内部
Vector3f shadowPointOrig = (dotProduct(dir, N) < 0) ?
hitPoint + N * scene.epsilon :
hitPoint - N * scene.epsilon;
// [comment]
// Loop over all lights in the scene and sum their contribution up
// We also apply the lambert cosine law
// [/comment]
for (auto& light : scene.get_lights()) {
//从光源和物体的交点指向光源的矢量
Vector3f lightDir = light->position - hitPoint;
// square of the distance between hitPoint and the light:光源和物体的交点指向光源的矢量的长度
float lightDistance2 = dotProduct(lightDir, lightDir);
//归一化
lightDir = normalize(lightDir);
float LdotN = std::max(0.f, dotProduct(lightDir, N));
// is the point in shadow, and is the nearest occluding object closer to the object than the light itself?
//判断光源和物体是否相交(是否有遮挡)
auto shadow_res = trace(shadowPointOrig, lightDir, scene.get_objects());
//有遮挡且【阴影点和遮挡物交点的距离(变化了一个小epsilon)】小于【光源和物体交点的距离(没有变化一个xnepsilon)】
bool inShadow = shadow_res && (shadow_res->tNear * shadow_res->tNear < lightDistance2);
// 如果没有阴影说明光打得到这个物体,加上散射光项(Phong模型),但是没有除距离的平方
lightAmt += inShadow ? 0 : light->intensity * LdotN;
//计算反射方向
Vector3f reflectionDirection = reflect(-lightDir, N);
//计算镜面反射
specularColor += powf(std::max(0.f, -dotProduct(reflectionDirection, dir)),
payload->hit_obj->specularExponent) * light->intensity;
}
//最终的颜色好药乘系数,环境光要乘payload->hit_obj->evalDiffuseColor(st)为0.2
hitColor = lightAmt * payload->hit_obj->evalDiffuseColor(st) * payload->hit_obj->Kd + specularColor * payload->hit_obj->Ks;
break;
}
}
}
return hitColor;
}
Renderer.cpp->reflect(), Renderer.cpp->rrefract(), Renderer.cpp->fresnel()
这几个函数涉及光的折射和反射,推导见:
计算机图形学十二:Whitted-Style光线追踪原理详解及实现细节-4 反射与折射
相关代码为:
// Compute reflection direction
Vector3f reflect(const Vector3f &I, const Vector3f &N)
{
return I - 2 * dotProduct(I, N) * N;
}
// [comment]
// Compute refraction direction using Snell's law
//
// We need to handle with care the two possible situations:
//
// - When the ray is inside the object
//
// - When the ray is outside.
//
// If the ray is outside, you need to make cosi positive cosi = -N.I
//
// If the ray is inside, you need to invert the refractive indices and negate the normal N
// [/comment]
Vector3f refract(const Vector3f &I, const Vector3f &N, const float &ior)
{
float cosi = clamp(-1, 1, dotProduct(I, N));
float etai = 1, etat = ior;
Vector3f n = N;
if (cosi < 0) { cosi = -cosi; } else { std::swap(etai, etat); n= -N; }
float eta = etai / etat;
float k = 1 - eta * eta * (1 - cosi * cosi);
return k < 0 ? 0 : eta * I + (eta * cosi - sqrtf(k)) * n;
}
fresnel公式计算反射率,
// [comment]
// Compute Fresnel equation
//
// \param I is the incident view direction
//
// \param N is the normal at the intersection point
//
// \param ior is the material refractive index
// [/comment]
float fresnel(const Vector3f &I, const Vector3f &N, const float &ior)
{
float cosi = clamp(-1, 1, dotProduct(I, N));
float etai = 1, etat = ior;
if (cosi > 0) { std::swap(etai, etat); }
// Compute sini using Snell's law
float sint = etai / etat * sqrtf(std::max(0.f, 1 - cosi * cosi));
// Total internal reflection,全反射
if (sint >= 1) {
return 1;
}
else {
float cost = sqrtf(std::max(0.f, 1 - sint * sint));
cosi = fabsf(cosi);
float Rs = ((etat * cosi) - (etai * cost)) / ((etat * cosi) + (etai * cost));
float Rp = ((etai * cosi) - (etat * cost)) / ((etai * cosi) + (etat * cost));
return (Rs * Rs + Rp * Rp) / 2;
}
// As a consequence of the conservation of energy, transmittance is given by:
// kt = 1 - kr;
}
Renderer.cpp->trace()
std::optional返回一个hit_payload类型,这里的初始化是空,如果找到最近的相交的物体则返回一个有效的hit_payload的值,关于std::optional详情见:C++17之std::optional全方位详解
// [comment]
// Returns true if the ray intersects an object, false otherwise.
//
// \param orig is the ray origin
// \param dir is the ray direction
// \param objects is the list of objects the scene contains
// \param[out] tNear contains the distance to the cloesest intersected object.
// \param[out] index stores the index of the intersect triangle if the interesected object is a mesh.
// \param[out] uv stores the u and v barycentric coordinates of the intersected point
// \param[out] *hitObject stores the pointer to the intersected object (used to retrieve material information, etc.)
// \param isShadowRay is it a shadow ray. We can return from the function sooner as soon as we have found a hit.
// [/comment]
std::optional<hit_payload> trace(
const Vector3f &orig, const Vector3f &dir,
const std::vector<std::unique_ptr<Object> > &objects)
{
float tNear = kInfinity;
std::optional<hit_payload> payload;
for (const auto & object : objects)
{
float tNearK = kInfinity;
uint32_t indexK;
Vector2f uvK;
if (object->intersect(orig, dir, tNearK, indexK, uvK) && tNearK < tNear)//寻找最近的物体然后让返回值有效
{
payload.emplace();
payload->hit_obj = object.get();
payload->tNear = tNearK;
payload->index = indexK;
payload->uv = uvK;
tNear = tNearK;
}
}
return payload;
}
Sphere.hpp->intersect()
判断是否和所有球相交
bool intersect(const Vector3f& orig, const Vector3f& dir, float& tnear, uint32_t&, Vector2f&) const override
{
// analytic solution
Vector3f L = orig - center;
float a = dotProduct(dir, dir);
float b = 2 * dotProduct(dir, L);
float c = dotProduct(L, L) - radius2;
float t0, t1;
if (!solveQuadratic(a, b, c, t0, t1))//解一元二次方程
return false;
if (t0 < 0)
t0 = t1;
if (t0 < 0)
return false;
tnear = t0;
return true;
}
Triangle.hpp->intersect()
判断是否和所有三角形相交
bool intersect(const Vector3f& orig, const Vector3f& dir, float& tnear, uint32_t& index,
Vector2f& uv) const override
{
bool intersect = false;
for (uint32_t k = 0; k < numTriangles; ++k)
{
const Vector3f& v0 = vertices[vertexIndex[k * 3]];
const Vector3f& v1 = vertices[vertexIndex[k * 3 + 1]];
const Vector3f& v2 = vertices[vertexIndex[k * 3 + 2]];
float t, u, v;
if (rayTriangleIntersect(v0, v1, v2, orig, dir, t, u, v) && t < tnear)
{
tnear = t;
uv.x = u;
uv.y = v;
index = k;
intersect |= true;
}
}
return intersect;
}
SPhere.hpp->getSurfaceProperties()
void getSurfaceProperties(const Vector3f& P, const Vector3f&, const uint32_t&, const Vector2f&,
Vector3f& N, Vector2f&) const override
{
N = normalize(P - center);//获得法线
}
Triangle.hpp->getSurfaceProperties()
void getSurfaceProperties(const Vector3f&, const Vector3f&, const uint32_t& index, const Vector2f& uv, Vector3f& N,
Vector2f& st) const override
{
//获得三角形的顶点
const Vector3f& v0 = vertices[vertexIndex[index * 3]];
const Vector3f& v1 = vertices[vertexIndex[index * 3 + 1]];
const Vector3f& v2 = vertices[vertexIndex[index * 3 + 2]];
//三角形两条边的矢量
Vector3f e0 = normalize(v1 - v0);
Vector3f e1 = normalize(v2 - v1);
//三角形平面的法矢量
N = normalize(crossProduct(e0, e1));
//st坐标的插值
const Vector2f& st0 = stCoordinates[vertexIndex[index * 3]];
const Vector2f& st1 = stCoordinates[vertexIndex[index * 3 + 1]];
const Vector2f& st2 = stCoordinates[vertexIndex[index * 3 + 2]];
st = st0 * (1 - uv.x - uv.y) + st1 * uv.x + st2 * uv.y;
}