games101 HomeWork7

Games101 HomeWork7

导航

导航

多线程

这次的作业,我想先教大家实现多线程,再进行操作。多线程多我的吸引力实在是太大了。废话不多说,我们直接开始:

多线程主要是在Render中进行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void Renderer::Render(const Scene& scene)
{
std::vector<Vector3f> framebuffer(scene.width * scene.height);
bool IsUseMultiThread=true;
int spp = 1;
if(!IsUseMultiThread)
{
float scale = tan(deg2rad(scene.fov * 0.5));
float imageAspectRatio = scene.width / (float)scene.height;
Vector3f eye_pos(278, 273, -800);
int m = 0;
// change the spp value to change sample ammount
std::cout << "SPP: " << spp << "\n";
for (uint32_t j = 0; j < scene.height; ++j) {
for (uint32_t i = 0; i < scene.width; ++i) {
// generate primary ray direction
float x = (2 * (i + 0.5) / (float)scene.width - 1) *
imageAspectRatio * scale;
float y = (1 - 2 * (j + 0.5) / (float)scene.height) * scale;

Vector3f dir = normalize(Vector3f(-x, y, 1));
for (int k = 0; k < spp; k++){
framebuffer[m] += scene.castRay(Ray(eye_pos, dir), 0) / spp;
}
m++;
}
UpdateProgress(j / (float)scene.height);
}
UpdateProgress(1.f);
// save framebuffer to file
string filename ="binaryWithoutMultiThread";
filename +=std::to_string(std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now()-start).count())+".ppm";
FILE* fp = fopen(filename.c_str() , "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] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].x), 0.6f));
color[1] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].y), 0.6f));
color[2] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].z), 0.6f));
fwrite(color, 1, 3, fp);
}
fclose(fp);
}

这是Render的原代码,我需要加速的地方是对每一个像素着色的地方,也就是这个for循环。首先我们把for循环改成单层的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (uint32_t p = 0; p < hxw; ++p)
{
int i = p % scene.height;
int j = p / scene.height;
// generate primary ray direction
float x = (2 * (i + 0.5) / (float)scene.width - 1) *
imageAspectRatio * scale;
float y = (1 - 2 * (j + 0.5) / (float)scene.height) * scale;

Vector3f dir = normalize(Vector3f(-x, y, 1));
for (int k = 0; k < spp; k++)
{
framebuffer[p] += scene.castRay(Ray(eye_pos, dir), 0) / spp;
}
pocess++;
UpdateProgress(pocess / (float)(scene.height * scene.width));
}

然后加入这几个头文件:

1
2
3
4
5
#include <thread>
#include <pthread.h>
#include <omp.h>
//linux library for getting cpu num
#include "unistd.h"

并在CMakeLists.txt中加上这句话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//这是修改后的CmakeList.txt
cmake_minimum_required(VERSION 3.10)
project(RayTracing)

// DIY
FIND_PACKAGE( OpenMP REQUIRED)
if(OPENMP_FOUND)
message("OPENMP FOUND")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}")
endif()
//DIY
set(CMAKE_CXX_STANDARD 17)

add_executable(${PROJECT_NAME} main.cpp Object.hpp Vector.cpp Vector.hpp Sphere.hpp global.hpp Triangle.hpp Scene.cpp
Scene.hpp Light.hpp AreaLight.hpp BVH.cpp BVH.hpp Bounds3.hpp Ray.hpp Material.hpp Intersection.hpp
Renderer.cpp Renderer.hpp)


然后在Render中使用int cpuNum= sysconf(_SC_NPROCESSORS_CONF);获取你电脑的CPU数量,并保存起来,然后使用omp_set_num_threads(cpuNum);设置你的线程数量为你的CPU数量。
然后便是最精彩的部分了,首先我们直接使用

1
#pragma omp parallel for

来多线程启动这句命令下的一个for循环。像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma omp parallel for shared(pocess)
for (uint32_t p = 0; p < hxw; ++p)
{
int i = p % scene.height;
int j = p / scene.height;
// generate primary ray direction
float x = (2 * (i + 0.5) / (float)scene.width - 1) *
imageAspectRatio * scale;
float y = (1 - 2 * (j + 0.5) / (float)scene.height) * scale;

Vector3f dir = normalize(Vector3f(-x, y, 1));
for (int k = 0; k < spp; k++)
{
framebuffer[p] += scene.castRay(Ray(eye_pos, dir), 0) / spp;
}
pocess++;
UpdateProgress(pocess / (float)(scene.height * scene.width));
}

但是我们得到的进度条非常的抽象,接下来我们使用进程锁🔓来防止这种情况。
image
critical指令用于保证其相关联的代码只在一个线程中执行。我们在UpdateProgress之前加上这一句话: #pragma omp critical这样就得到了正确的进度条。
image
这一步很重要,请确保你完成了这一步,再进行下面的操作。不然你没次的运行时间可能达到3-5mins。这里是修改后的Render函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
void Renderer::Render(const Scene &scene)
{
std::vector<Vector3f> framebuffer(scene.width * scene.height);
float scale = tan(deg2rad(scene.fov * 0.5));
float imageAspectRatio = scene.width / (float)scene.height;
Vector3f eye_pos(278, 273, -800);
bool IsUseMultiThread = true;
string filename = "binary";
int spp = 1;//把spp改为1,不然会跑的很慢!!!!!!!!!!
if (!IsUseMultiThread)
{
int m = 0;
// change the spp value to change sample ammount
std::cout << "SPP: " << spp << "\n";
for (uint32_t j = 0; j < scene.height; ++j)
{
for (uint32_t i = 0; i < scene.width; ++i)
{
// generate primary ray direction
float x = (2 * (i + 0.5) / (float)scene.width - 1) *
imageAspectRatio * scale;
float y = (1 - 2 * (j + 0.5) / (float)scene.height) * scale;

Vector3f dir = normalize(Vector3f(-x, y, 1));
for (int k = 0; k < spp; k++)
{
framebuffer[m] += scene.castRay(Ray(eye_pos, dir), 0) / spp;
}
m++;
}
UpdateProgress(j / (float)scene.height);
}
UpdateProgress(1.f);
filename += "_WithOut_" + std::to_string(std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - start).count());
}
else
{
int m = 0;
// change the spp value to change sample ammount
std::cout << "SPP: " << spp << "\n";
int cpuNum = sysconf(_SC_NPROCESSORS_CONF);
std::cout << "Cpu Num :" << cpuNum << std::endl;
omp_set_num_threads(cpuNum);
// int handle[cpuNum];
float minY = 0.0f, maxY = 0.0f;
int hxw = scene.width * scene.height;
long long pocess =0;
#pragma omp parallel for shared(pocess)
for (uint32_t p = 0; p < hxw; ++p)
{
int i = p % scene.height;
int j = p / scene.height;
// generate primary ray direction
float x = (2 * (i + 0.5) / (float)scene.width - 1) *
imageAspectRatio * scale;
float y = (1 - 2 * (j + 0.5) / (float)scene.height) * scale;

Vector3f dir = normalize(Vector3f(-x, y, 1));
for (int k = 0; k < spp; k++)
{
framebuffer[p] += scene.castRay(Ray(eye_pos, dir), 0) / spp;
}
pocess++;
#pragma omp critical
UpdateProgress(pocess / (float)(scene.height * scene.width));
}
minY = maxY + 1.0f;
UpdateProgress(1.f);
filename += "_With_" + std::to_string(std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - start).count());
}
filename += ".ppm";
// save framebuffer to file
FILE *fp = fopen(filename.c_str(), "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] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].x), 0.6f));
color[1] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].y), 0.6f));
color[2] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].z), 0.6f));
fwrite(color, 1, 3, fp);
}
fclose(fp);
}

注意:请不要直接复制我的代码,写入文件的部分我还做了一些修改

直接复制是跑不起来的,你应该自己写一遍多线程的流程。并将文件写入改回你自己想要的版本。
当然,由于你还没有写CastRay,和很多函数,所以你只能得到这样的图片:
image

前置任务

  • Triangle::getIntersection in Triangle.hpp: 将你的光线-三角形相交函数粘贴到此处,请直接将上次实验中实现的内容粘贴在此。
  • IntersectP(const Ray& ray, const Vector3f& invDir,const std::array<int, 3>& dirIsNeg) in the Bounds3.hpp: 这个函数的作用是判断包围盒BoundingBox 与光线是否相交,请直接将上次实验中实现的内容粘贴在此处,并且注意检查 t_enter = t_exit 的时候的判断是否正确。
  • getIntersection ( BVHBuildNode node, const Ray ray)in BVH.cpp: BVH查找过程,请直接将上次实验中实现的内容粘贴在此处
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//Triangle.hpp
inline Intersection Triangle::getIntersection(Ray ray)
{
Intersection inter;

if (dotProduct(ray.direction, normal) > 0)
return inter;
double u, v, t_tmp = 0;
Vector3f pvec = crossProduct(ray.direction, e2);
double det = dotProduct(e1, pvec);
if (fabs(det) < EPSILON)
return inter;

double det_inv = 1. / det;
Vector3f tvec = ray.origin - v0;
u = dotProduct(tvec, pvec) * det_inv;
if (u < 0 || u > 1)
return inter;
Vector3f qvec = crossProduct(tvec, e1);
v = dotProduct(ray.direction, qvec) * det_inv;
if (v < 0 || u + v > 1)
return inter;
t_tmp = dotProduct(e2, qvec) * det_inv;

// TODO find ray triangle intersection
if(t_tmp<0)
return inter;
inter.distance=t_tmp;
inter.happened=true;
inter.normal= this->normal;
inter.coords=ray(t_tmp);
inter.obj=this;
inter.m=this->m;
return inter;
}
//Bounds3.hpp
inline bool Bounds3::IntersectP(const Ray &ray, const Vector3f &invDir,
const std::array<int, 3> &dirIsNeg) const
{
// invDir: ray direction(x,y,z), invDir=(1.0/x,1.0/y,1.0/z), use this because Multiply is faster that Division
// dirIsNeg: ray direction(x,y,z), dirIsNeg=[int(x>0),int(y>0),int(z>0)], use this to simplify your logic
// TODO test if ray bound intersects
Vector3f t_minTemp = (pMin - ray.origin) * invDir;
Vector3f t_maxTemp = (pMax - ray.origin) * invDir;
Vector3f t_min = Vector3f::Min(t_minTemp, t_maxTemp);
Vector3f t_max = Vector3f::Max(t_minTemp, t_maxTemp);

float t_min_time = std::max(t_min.x, std::max(t_min.y, t_min.z));
float t_max_time = std::min(t_max.x, std::min(t_max.y, t_max.z));
return t_max_time >= -std::numeric_limits<float>::epsilon() && t_min_time <= t_max_time;
}
//BVH.cpp
Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
{
// TODO Traverse the BVH to find intersection
Intersection intersect;
Vector3f invdir(1./ray.direction.x,1./ray.direction.y,1./ray.direction.z);
std::array<int, 3> dirIsNeg;
dirIsNeg[0] = ray.direction.x>0;
dirIsNeg[1] = ray.direction.y>0;
dirIsNeg[2] = ray.direction.z>0;
if(!node || !node->bounds.IntersectP(ray,ray.direction_inv,dirIsNeg))
{
return intersect;
}

if(!node->right && !node->left)
return node->object->getIntersection(ray);

Intersection isect2= getIntersection(node->left,ray);
Intersection isect1= getIntersection(node->right,ray);
return isect1.distance < isect2.distance ? isect1:isect2;
}

再次运行,你应该能得到黑色的图了:
image

任务

在本次实验中,你只需要修改这一个函数:

  • castRay(const Ray ray, int depth)in Scene.cpp: 在其中实现Path Trac-ing 算法

CastRay()

我不会按着原理来讲这个东西,闫令琪大神已经讲的够好了,我按照算法的顺序来重新讲一下这个事情:
image
那么我们直奔主题,首先我们回忆一下我们的Path Tracing 在做的是一件什么样的事情:
我们要渲染一个点的颜色,要知道哪些光源的光线会反射到他身上,也要知道那些哪体的光线会反射到他身上。
首先判断光线(像素发出)是否会与物体相交:

1
2
3
Intersection intersToScene = Scene::intersect(ray);//这是我们获取的碰撞体
if (!intersToScene.happened)//如果返回的碰撞体没有发生碰撞,返回0
return Vector3f();

这句之后的说明已经发生了碰撞
我们利用RR(俄罗斯轮盘赌)来保证期望真实的情况下,设立光线反射出口:
对应到代码是这样的:

1
2
3
//RR
if(get_random_float()>RussianRoulette)
return Vector3f(0);

接着我们需要对光源进行采样,计算之后递归需要的一系列数值:

1
2
3
4
5
6
7
8
9
10
11
Vector3f L_dir = {0, 0, 0}, L_indir = {0, 0, 0};
// Calculate the Intersection from point to light in order to calculate direct Color
Intersection LightPos;
float lightpdf = 0.0f;
sampleLight(LightPos, lightpdf);//引用传参,得到LightPos和lightpdf。得到光源采样结果
Vector3f LightDir = LightPos.coords - intersToScene.coords;
float dis = dotProduct(LightDir, LightDir);
Vector3f LightDirNormal = LightDir.normalized();
Ray rayToLight(intersToScene.coords, LightDirNormal);//用点的位置和方向创建对象
Intersection interToLight = Scene::intersect(rayToLight);//计算得到的光线会和什么相交
auto f_r = intersToScene.m->eval(ray.direction, LightDirNormal, intersToScene.normal);//计算BRDF

然后判断着色点到光源是否有阻碍

1
2
3
4
if (interToLight.distance - LightDir.norm() > -0.005)//如果反射光线到第一个物体的距离比两点连线大(或者相等,处理浮点相等困难问题),说明中间没有阻碍
{
L_dir = LightPos.emit * f_r * dotProduct(LightDirNormal, intersToScene.normal) * dotProduct(-LightDirNormal, LightPos.normal) / dis / lightpdf / RussianRoulette;//照着公式抄
}

接着计算其他物体的辐射,对着色点进行立体角采样,得到出射方向,并计算出射光线与其他物体的求交。

1
2
3
4
Vector3f wi = intersToScene.m->sample(ray.direction, intersToScene.normal).normalized();
// Ray indirRay = Ray(intersToScene.coords, wi);
Ray indirRay(intersToScene.coords, wi);
Intersection intersToPoint = Scene::intersect(indirRay);

如果相交了,而且这个点不是光源,计算后续能量。

1
2
3
4
5
if (intersToPoint.happened && !intersToPoint.m->hasEmission())
{
float pdf = intersToScene.m->pdf(ray.direction, wi, intersToScene.normal);
L_indir = castRay(indirRay, depth + 1) * intersToScene.m->eval(ray.direction, wi, intersToScene.normal) * dotProduct(wi, intersToScene.normal) / RussianRoulette / pdf;
}

最后返回光源和物体反射颜色的和:

1
return L_dir + L_indir;

编译运行,你应当得到这样的图:
image
没有灯,这是为什么呢?当我们看向光源的时候,应该加上光源的颜色,稍微修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Intersection intersToScene = Scene::intersect(ray);
if (!intersToScene.happened)
return Vector3f();
Vector3f L_dir = {0, 0, 0}, L_indir = {0, 0, 0};
if (intersToScene.m->hasEmission())
L_dir+=intersToScene.m->getEmission();
Intersection LightPos;
float lightpdf = 0.0f;
sampleLight(LightPos, lightpdf);
Vector3f LightDir = LightPos.coords - intersToScene.coords;
float dis = dotProduct(LightDir, LightDir);
Vector3f LightDirNormal = LightDir.normalized();
Ray rayToLight(intersToScene.coords, LightDirNormal);
Intersection interToLight = Scene::intersect(rayToLight);
auto f_r = intersToScene.m->eval(ray.direction, LightDirNormal, intersToScene.normal);
if (interToLight.distance - LightDir.norm() > -0.005)
{
L_dir += LightPos.emit * f_r * dotProduct(LightDirNormal, intersToScene.normal) * dotProduct(-LightDirNormal, LightPos.normal) / dis / lightpdf;
}
//后面代码不动

再次编译运行,得到结果:
image
对比一下吧~
image
你会发现我们的噪点非常的多,修改SPP值,再运行几次吧~下面是我的电脑的运行时间和结果,你可以作为参考:

SPP(无特殊标记为784$\times$784)分辨率 time(min/ s)(分钟或者秒) image
1 (0, 15) image
4 (0, 56) image
8 (1, 114) image
16 (3, 227) image
32 (6, 409) image
128 (26, 1604) image
128(1568$\times$1568) image
注:上面的图片都可以在代码合集里看到。

Microfacet材质

参考知乎文章基于物理着色(二)- Microfacet材质和多层材质,来自文刀秋二大神。
与普通的着色模型的区别在于,普通的着色模型假设着色的区域是一个平滑的表面,表面的方向可以用一个单一的法线向量来定义来定义。而Microfacet模型则认为 1)着色的区域是一个有无数比入射光线覆盖范围更小的微小表面组成的粗糙区域。2)所有这些微小表面都是光滑镜面反射的表面。因为这种模型认为着色区域的表面是由无数方向不同的小表面组成的,所以在Microfacet模型中,着色区域并不能用一个法线向量来表示表面的方向,只能用一个概率分布函数\(D\)来计算任意方向的微小表面在着色区域中存在的概率。
image
近年来有许多新的分布函数被发明出来例如Beckmann,GGX等,他们的能量分布都更接近用光学仪器测量的反射数据。Disney BRDF也使用了GGX分布。本文后面的所有渲染结果也使用了GGX。
提高题目鸽了,我跑出来效果不是很明显,先放着了。代码是写好了的,可以自己阅读代码学习。
下面这张图片就是spp=128,1568$\times$1568下花了三个多小时跑出来的结果。其中球和右边绿色的墙面使用了Microfacet材质。
image
image
可能我实现的一般,尽管我用了128的spp还是会有那么多的黑色点出现,或者这就是Microfacet的效果?我不明白,但是我的Hw7无论如何都结束了,我花了太多的时间了。下一个作业,我们再见~

代码汇总

zhywyt-github

下/上一篇

下一篇:
上一篇:光线追踪加速

多线程之OMP

记录在学习games101的时候碰到的多线程知识

以下所有结果均在Ubuntu 22.04.2 LTS操作系统下使用g++ 11.3.0运行
image
所有的问题来自下面这段代码,这是games101 的第七次作业的一部分,需要使用多线程加速Path Tracing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
int use_critical =0;
float pocess=0;
float scale = tan(deg2rad(scene.fov * 0.5));
float imageAspectRatio = scene.width / (float)scene.height;
Vector3f eye_pos(278, 273, -800);
std::cout << "SPP: " << spp << "\n";
int cpuNum= sysconf(_SC_NPROCESSORS_CONF);
std::cout<<"Cpu Num :" <<cpuNum<<std::endl;
omp_set_num_threads(cpuNum);
//int handle[cpuNum];
float minY=0.0f,maxY=0.0f;
int m = 0;
int hxw=scene.width*scene.height;
#pragma omp parallel for shared(pocess)
for (uint32_t p = 0; p < hxw; ++p)
{
int i = p % scene.height;
int j = p / scene.height;
// generate primary ray direction
float x = (2 * (i + 0.5) / (float)scene.width - 1) *
imageAspectRatio * scale;
float y = (1 - 2 * (j + 0.5) / (float)scene.height) * scale;

Vector3f dir = normalize(Vector3f(-x, y, 1));
for (int k = 0; k < spp; k++)
{
framebuffer[p] += scene.castRay(Ray(eye_pos, dir), 0) / spp;
}
pocess++;
#pragma omp critical
UpdateProgress(pocess / (float)(scene.height * scene.width));
}
//Threadlist[i]=std::thread(RenderWithMultiThread,minY,maxY);
minY=maxY+1.0f;
UpdateProgress(1.f);
// save framebuffer to file
string filename ="binaryWithMultiThread";
filename +=std::to_string(std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now()-start).count())+".ppm";
FILE* fp = fopen(filename.c_str(), "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] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].x), 0.6f));
color[1] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].y), 0.6f));
color[2] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].z), 0.6f));
fwrite(color, 1, 3, fp);
}
fclose(fp);

OMP如何跑起来?

在linux操作系统下,使用g++ test.cpp \-fopenmp \-o test编译。先测试一下这段代码,以确保你会使用g++进行编译:

1
2
3
4
5
6
7
8
9
#include <iostream>
using std::cout;
using std::endl;
int main()
{
#pragma omp parallel
cout<<"hello,openmp!\n";
cout.flush();
}

编译运行:

1
2
g++ test.cpp -fopenmp -o test
./test

你可能得到的是不同的结果,但应该也只是数量上的不同,这取决于你电脑的核心数目。
image
我们正式开始

#pragma omp parallel

这个预处理用于开启多线程,上面已经实验过了,这里不进行过多的解释。接下来让我们控制线程的数量:

线程数量

#pragma omp parallel num_threads(2)

1
2
3
4
5
6
7
8
9
#include <iostream>
using std::cout;
using std::endl;
int main()
{
#pragma omp parallel num_threads(2)
cout<<"hello,openmp!\n";
cout.flush();
}

运行这段代码,你可以看到hello 只剩下了两个。

APIomp_set_num_threads()

首先添加头文件#include"omp.h",然后使用APIomp_set_num_threads()

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include"omp.h"
using std::cout;
using std::endl;
int main()
{
omp_set_num_threads(2);
#pragma omp parallel
cout<<"hello,openmp!\n";
cout.flush();
}

可以得到和上面一样的结果。

环境变量OMP_NUM_THREADS

在编译之前,加上这句修改环境变量的指令,就可以实现运行前,编译后修改线程数量了。

1
export OMP_NUM_THREADS=2

在此之前,把你的测试代码修改为这样:并重新编译,然后你便可以控制你的线程数量了

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include"omp.h"
using std::cout;
using std::endl;
int main()
{
#pragma omp parallel
cout<<"hello,openmp!\n";
cout.flush();
}

image

加速for循环

#omp pragma parallel for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <chrono>
#include "omp.h"
using std::cout;
using std::endl;
int main()
{
long long sum = 0;
auto start = std::chrono::system_clock::now();
#pragma omp parallel reduction(+:sum)
#pragma omp for
for (int i = 3; i < 500000; i++)
{
bool flag = true;
for (int j = 2; j < i; j++)
{
if (i % j == 0)
{
flag = false;
break;
}
}
if (flag)
sum += i;
}
auto end = std::chrono::system_clock::now();
cout<<std::chrono::duration_cast<std::chrono::microseconds>(end-start).count()<<endl;
cout << "sum: " << sum << endl;
cout.flush();
}

我随便写了一段,来对比多线程与单线程的差距:图中第一个是16线程跑出来的\(2530ms\),也就是2.5s,第二个是单线程,\(14147ms\),也就是14.1s接近7倍的差距,没有16倍的速度是很正常的,我这个测试程序写的非常随意。只是单纯的说明加速情况而已。当然,答案是一样的。

image

上面代码中还出现了reduction(+:sum)这样的指令,这是我们接下来要讲的东西。

规约操作

使用reduction()来制定规约操作

1
reduction(<operator>: <variable list>)

这个指令可以让所有线程的结果通过规约合并在一起,比如上面程序的+:sum,就是每个线程分别计算sum,再累加在一起。

本文参考

大佬的博客

Linux批量修改文件名字

在做这样一件事情的时候我遇到了困难:我有十几个文件的日期都是以点作为分割符的,但是我需要提交的文件名中不能有.,那我需要把这些文件名改成-为分割符。

mv

我只知道mv可以修改文件的名字,但是也只能修改一个:
mv 7.20.png 7-20.png
于是我望着我剩下的文件发呆
image
百度准备解决一次,用一辈子

rename

经过一番百度,我才发现mv只能进行单个文件的命名修改,使用rename才能进行批量修改:
大佬的rename详解
我使用的rename 是Perl版本的,那么我可以参照这个表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Perl版本
-v, --verbose 详细:成功重命名的文件的打印名称。
-0, --null 从STDIN读取时,请使用\0作为记录分隔符
-n, --nono 不执行任何操作:打印要重命名的文件名,但不重命名。
-f, --force 覆盖:允许覆盖现有文件
--path, --fullpath 重命名完整路径:包括任何目录组件。默认
-d, --filename, --nopath, --nofullpath 不重命名目录:仅重命名路径的文件名部分
-h, --help 帮助:打印提要和选项。
-m, --man 手册: 打印手册页.
-V, --version 版本: 显示版本号.
-e 表达: 作用于文件名的代码. 可以重复来构建代码(比如“perl-e”)。如果没有-e,则第一个参数用作代码。
-E 语句:对文件名执行操作的代码,如-e,但终止于 ';'.
# C语言版本
-v, --verbose 提供视觉反馈,其中重命名了哪些文件(如果有的话)
-V, --version 显示版本信息并退出。
-s, --symlink 在符号链接目标上执行重命名
-h, --help 显示帮助文本并退出

rename \-v "s/7-/7./g" *
image
rename \-v "s/7./7-/" *
image

image

games101 HomeWork6

Games101 HomeWork6

导航

导航

作业要求

  • IntersectP(const Ray& ray, const Vector3f& invDir,const std::array<int, 3>& dirIsNeg) in the Bounds3.hpp: 这个函数的作用是判断包围盒BoundingBox 与光线是否相交,你需要按照课程介绍的算法实现求交过程。
  • getIntersection(BVHBuildNode* node, const Ray ray)in BVH.cpp: 建立BVH 之后,我们可以用它加速求交过程。该过程递归进行,你将在其中调用你实现的Bounds3::IntersectP

前置要求

  • Render() in Renderer.cpp: 将你的光线生成过程粘贴到此处,并且按照新框架更新相应调用的格式。
  • Triangle::getIntersection in Triangle.hpp: 将你的光线-三角形相交函数
    粘贴到此处,并且按照新框架更新相应相交信息的格式

Render()

新框架里把光线写成了一个类,castRay的参数列表也改成了这样:

1
Vector3f castRay(const Ray &ray, int depth) const;

Ray类中的部分代码如下,我只保留了这些有用的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Ray{
//Destination = origin + t*direction
Vector3f origin;
Vector3f direction, direction_inv;
double t;//transportation time,
double t_min, t_max;
Ray(const Vector3f& ori, const Vector3f& dir, const double _t = 0.0): origin(ori), direction(dir),t(_t) {
direction_inv = Vector3f(1./direction.x, 1./direction.y, 1./direction.z);
t_min = 0.0;
t_max = std::numeric_limits<double>::max();
}
Vector3f operator()(double t) const{return origin+direction*t;}
};

初始化一个Ray类对象需要一个起点点和一个向量,我们也可以使用ray(t)来直接访问某点的坐标。知道这些之后,我们就可以完成Render的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Render
for (uint32_t j = 0; j < scene.height; ++j) {
for (uint32_t i = 0; i < scene.width; ++i) {
// generate primary ray direction
float x = (2 * (i + 0.5) / (float)scene.width - 1) *
imageAspectRatio * scale;
float y = (1 - 2 * (j + 0.5) / (float)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));
framebuffer[m++]=scene.castRay(Ray(eye_pos,dir), 0);
}
UpdateProgress(j / (float)scene.height);
}

Triangle::getIntersection()

这个函数进行的操作是,在一个三角形对象中判断一根光线是否与自己相交。下图是Triangle类的数据成员声明,它继承了抽象类Object类,但是Object中没有任何数据成员,这个不用关心。可以看到三角形保存了点、边、和法线等等

1
2
3
4
5
6
7
8
9
10
//#Triangle.hpp
class Triangle : public Object
{
public:
Vector3f v0, v1, v2; // vertices A, B ,C , counter-clockwise order
Vector3f e1, e2; // 2 edges v1-v0, v2-v0;
Vector3f t0, t1, t2; // texture coords
Vector3f normal;
Material* m;
};

在getIntersection中,我们需要返回的是一个intersection也就是碰撞体,看一下intersection类的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//#Intersection.hpp
struct Intersection
{
Intersection(){
happened=false;
coords=Vector3f();
normal=Vector3f();
distance= std::numeric_limits<double>::max();
obj =nullptr;
m=nullptr;
}
bool happened;
Vector3f coords;
Vector3f normal;
double distance;
Object* obj;
Material* m;
};

可以看到,如果没有发生碰撞,我们不需要对inter做任何操作,发生了碰撞,我们需要设置它的纹理坐标、法线、光路长度、模型指针、物体指针。这些在三角形对象中都有,检测之后一一设置就好了。

1
2
3
4
5
6
7
8
9
//#getIntersection()
if(t_tmp<0)return inter;
inter.distance = t_tmp;
inter.happened=true;
inter.normal=normal;
inter.coords=ray(t_tmp);
inter.obj=this;
inter.m=m;
return inter;

普通要求

IntersectP()

这个函数需要我们完成光线与包围盒的求交判断。

1
2
3
4
5
6
7
Vector3f t_minTemp=(pMin-ray.origin)*invDir;
Vector3f t_maxTemp=(pMax-ray.origin)*invDir;
Vector3f t_min=Vector3f::Min(t_minTemp,t_maxTemp);
Vector3f t_max=Vector3f::Max(t_minTemp,t_maxTemp);

float t_enter=std::max(t_min.x,std::max(t_min.y,t_min.z));
float t_out=std::min(t_max.x,std::min(t_max.y,t_max.z));

先计算进入盒子的时间和出盒子的时间,然后返回判断条件的真假就好了

1
2
3
if(t_out>=0.0f&&t_enter<=t_out)
return true;
return false;

getIntersection()

要求建立BVH ,我们可以用它加速求交过程。
还记得么?包围盒是以二叉树的形式存储的,就像这样:
image
对一个包围盒,我们需要检查该包围盒是否与光线相交,如果有,那么判断这个包围盒是否存在子叶,如果有,检查递归检查子叶是否与光线有交点,直到这个包围盒不与光线相交;或者该包围盒不再存在子叶,则判断这个物体是否与光线相交。代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
{
// TODO Traverse the BVH to find intersection
// invDir: ray direction(x,y,z), invDir=(1.0/x,1.0/y,1.0/z), use this because Multiply is faster that Division
// dirIsNeg: ray direction(x,y,z), dirIsNeg=[int(x>0),int(y>0),int(z>0)], use this to simplify your logic
Intersection intersect;
Vector3f invdir(1./ray.direction.x,1./ray.direction.y,1./ray.direction.z);
std::array<int, 3> dirIsNeg;
dirIsNeg[0] = ray.direction.x>0;
dirIsNeg[1] = ray.direction.y>0;
dirIsNeg[2] = ray.direction.z>0;
if(!node || !node->bounds.IntersectP(ray,ray.direction_inv,dirIsNeg))
{
return intersect;
}

if(!node->right && !node->left)
return node->object->getIntersection(ray);

Intersection isect2= getIntersection(node->left,ray);
Intersection isect1= getIntersection(node->right,ray);
return isect1.distance < isect2.distance ? isect1:isect2;
}

运行./RayTracing

注意!运行前,请确保你的/build目录下有imag_BVHimag_SAH目录,我把生成的图片放在里面了!!!

像这样
image

image
很棒的光影!但是渲染这张图片需要花费我的电脑6-8秒钟,BVH还是太慢了,接着,我们来学习新的加速模式SAH(Surface Area Heuristic)。

提高题(SAH加速)

首先,做BVH的时候,我们划分空间的方式是从中间进行的,不考虑空间中物体的密度大小。这样可能导致划分非常的不合理。而简单来说SAH就是指在划分空间的时候,先判断两块空间所需的时间(进行估计),估计的方法有很多。
image
image

我们的优化目标就是这个总消耗\(C\)

  • 0.125为无关消耗,可以理解为计算这个消耗的消耗 \(C_{trav}\)
  • 第二个和第三个分别是左节点和右节点的消耗
  • 最后的maxAreaDiv就是总当前节点包围盒的总面积分之一 \(\frac{1}{S_N}\)
1
float cost = 0.125f + (leftCount * leftBounds3.SurfaceArea() + rightCount * rightBounds3.SurfaceArea()) * maxAreaDiv;

枚举一定数量的划分方案,然后估算这些方案的消耗值。通过求最大值得到划分方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Bounds3 centroidBounds;
for (int i = 0; i < objects.size(); ++i)
centroidBounds =
Union(centroidBounds, objects[i]->getBounds().Centroid());
int dim = centroidBounds.maxExtent();
float maxArea = centroidBounds.SurfaceArea();
float minCost = std::numeric_limits<float>::infinity();
int minClipNum = 0;
float BoundingClip = 10;

float BoundingClipDiv = 1.0f / BoundingClip;
float maxAreaDiv = 1.0f / maxArea;
for (int i = 0; i < BoundingClip; i++)
{
Bounds3 leftBounds3;
Bounds3 rightBounds3;
float leftCount = 0.0f;
float rightCount = 0.0f;
auto middlingTemp = objects.begin() + std::floor(objects.size() * (i * BoundingClipDiv));
for (auto j = objects.begin(); j < middlingTemp; j++)
{
leftBounds3 = Union(leftBounds3, (*j)->getBounds().Centroid());
leftCount = leftCount + 1.0f;
}
for (auto j = middlingTemp; j < objects.end(); j++)
{
rightBounds3 = Union(rightBounds3, (*j)->getBounds().Centroid());
rightCount = rightCount + 1.0f;
}
float cost = 0.125f + (leftCount * leftBounds3.SurfaceArea() + rightCount * rightBounds3.SurfaceArea()) * maxAreaDiv;
if (cost < minCost)
{
minClipNum = i;
minCost = cost;
}
}
middling = objects.begin() + std::floor(objects.size() * (minClipNum * BoundingClipDiv));

这里就不挂完整代码了,有兴趣的小伙伴可以去github上看。

结果

AMD® Ryzen 7 5800h with radeon graphics × 16

BVH ./RayTracing BVH

BVH用我的电脑跑出来最好的效果是最快7299ms

SAH ./RrayTracing SAH

SAH用我的电脑跑出来最好的效果是最快6732ms

DIY

有兴趣的小伙伴可以尝试修改float BoundingClip = 10;,它在BVHAccel::recursiveBuild中,代表SAH划分空间的时候枚举的空间数量。说不定会有更好的加速效果哦~

代码汇总

zhywyt-github

下/上一篇

下一篇:
上一篇:Ray Tracing 光线追踪

games101 HomeWork5

Games101 HomeWork5

导航

导航

任务

  • Renderer.cpp 中的 Render():这里你需要为每个像素生成一条对应的光线,然后调用函数 castRay() 来得到颜色,最后将颜色存储在帧缓冲区的相应像素中。
  • Triangle.hpp 中的 rayTriangleIntersect(): v0, v1, v2 是三角形的三个顶点,orig 是光线的起点,dir 是光线单位化的方向向量。tnear, u, v 是你需要使用我们课上推导的 Moller-Trumbore 算法来更新的参数。

rayTriangleIntersect()

我们首先来完成这个纯算法的东西:
image
忘记了也没关系,我正好想复习一下。首先我们可以用起点和光线的方向来表示光线上的每一个点,也就是\(\vec{O}+t\vec{D}\),接着,如果这个点经过三角形所在的平面,那么就可以由重心坐标所表示,也就是

\[\vec O+t\vec D =(1-b_1-b_2)\vec P_0+b_1\\vec P_1 +b_2\vec P_2 \]

使用克拉默法则可以解出\(t、b_1、b_2\)。再然后,如果重心坐标三个数值都大于零,说明交点在三角形内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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.
auto e1 = v1 - v0, e2 = v2 - v0, s = orig - v0;
auto s1 = crossProduct(dir, e2), s2 = crossProduct(s, e1);

float t = dotProduct(s2, e2) / dotProduct(s1, e1);
float b1 = dotProduct(s1, s) / dotProduct(s1, e1);
float b2 = dotProduct(s2, dir) / dotProduct(s1, e1);

if (t > 0.0 && b1 > 0.0 && b2 > 0.0 && (1 - b1 - b2) > 0.0)
{
tnear = t;
u = b1;
v = b2;
return true;
}
return false;
}

Render()

这里存在着一系列较为复杂的坐标变换,我们一个个来看。首先在给出的框架中,我们是对屏幕像素进行了遍历,得到了\((i , j)\),但是我们需要的是观察空间下的坐标。

1
2
3
4
5
6
7
8
9
10
11
for (int j = 0; j < scene.height; ++j)
{
for (int i = 0; i < scene.width; ++i)
{
float x;
float y;
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);
}

关于这一段的解释,我翻了很久的博客,没有找到自己满意的,所以仔细研究了一下,才发现他们的解释方向不那么优。不应该把imageAspectRatio加在前面的坐标来解释,实际上也是这样的,这也是缩放因子的一部分才对。请看下面解释:
image
image
到这里了,其实\(x\)轴已经很简单了:
image
代码的实现也就不难了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Render
for (int j = 0; j < scene.height; ++j)
{
for (int i = 0; i < scene.width; ++i)
{
// generate primary ray direction
float x;
float y;
// 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*
x=(((i+0.5f)/static_cast<float>(scene.width-1)*2.0f)-1.0f)*scale*imageAspectRatio;
y=(1.0f-(j+0.5f)/static_cast<float>(scene.height-1)*2.0f)*scale;
//其实就是把"zNear"设置为了 1
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);
}

其中width和height\(-1\)的原因我也不是很懂,只知道不减好像会出现两个蓝点。

推导正确性实验

这次作业没有提高题,那么我们来测试一下上面的推导的正确性:
我稍微修改了Render函数,让他可以接受一个zNear的参数,方便我来实验,然后对输出也做了一些修改,这是修改后的Render函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void Renderer::Render(const Scene& scene,const int zNear)
{
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)
{
float x;
float y;
x=(((i+0.5f)/static_cast<float>(scene.width-1)*2.0f)-1.0f)*scale*imageAspectRatio*zNear;
y=(1.0f-(j+0.5f)/static_cast<float>(scene.height-1)*2.0f)*scale*zNear;
Vector3f dir = normalize(Vector3f(x, y, -zNear));
framebuffer[m++] = castRay(eye_pos, dir, scene, 0);
}
UpdateProgress(j / (float)scene.height);
}
// save framebuffer to file
string filename="binary";
filename+="_"+std::to_string(zNear)+".ppm";
FILE* fp = fopen(filename.c_str(), "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);
}

输出

./RayTracing
image
./RayTracing 2
image
./RayTracing 10
image

可以看到,输出结果完全一样,实际上也只是对一个单位化之前的向量做了数乘而以,对结果当然没有影响,但是对我们理解这个东西应该会有些作用。

代码汇总

image

下/上一篇

下一篇:Ray Tracing光线追踪加速
上一篇:Bezier曲线的绘制

games101 HomeWork4

Games101 HomeWork4

  • bezier:该函数实现绘制 Bézier 曲线的功能。它使用一个控制点序列和一个OpenCV::Mat 对象作为输入,没有返回值。它会使 t 在 0 到 1 的范围内进行迭代,并在每次迭代中使 t 增加一个微小值。对于每个需要计算的 t,将调用另一个函数 recursive_bezier,然后该函数将返回在 Bézier 曲线上 t处的点。最后,将返回的点绘制在 OpenCV ::Mat 对象上。
  • recursive_bezier:该函数使用一个控制点序列和一个浮点数 t 作为输入,实现 de Casteljau 算法来返回 Bézier 曲线上对应点的坐标。

bezier & recursive_bezier

bezier函数需要完成整个Bezier曲线的绘制,而recursive_bezier需要完成Bezier曲线中任意一点的坐标计算。

bezier

1
2
3
4
5
6
7
8
void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window){
// TODO: Iterate through all t = 0 to t = 1 with small steps, and call de Casteljau's
// recursive Bezier algorithm.
for(double i=0;i<1.;i+=0.0001){
auto point=recursive_bezier(control_points,i);
window.at<cv::Vec3b>(point.y,point.x)[1]=255;
}
}

这里有一点需要强调,window.at<>()的第一个元素[0]是B通道,而[2]才是R通道,所以我们需要把[1]也就是G通道设置为255,这样才满足作业要求中的设置为绿色。

recursive_bezier

给定所有的控制点,每对应一个t值需要给出对应的坐标。递归很容易实现:

1
2
3
4
5
6
7
8
9
10
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t){
// TODO: Implement de Casteljau's algorithm
std::vector<cv::Point2f> recursive_list;
for (int i = 0; i < control_points.size() - 1; i++){
recursive_list.push_back(((1 - t)) * (control_points[i]) + (t) * (control_points[i + 1]));
}
if(recursive_list.size()==1)
return recursive_list[0];
return recursive_bezier(recursive_list,t);
}

修改main函数的绘制语句

1
2
3
4
5
6
7
8
9
10
11
12
//main
` if (control_points.size() == 4)
{
naive_bezier(control_points, window);
bezier(control_points, window);
// bezier_with_antialiasing(control_points,window);
cv::imshow("Bezier Curve", window);
cv::imwrite("my_bezier_curve.png", window);
key = cv::waitKey(0);

return 0;
}

make&build

./BezierCurve
随便点击四个点,得到这样的图像:
image
如果不是黄色请检查你的bezier函数,在使用at<>()设置颜色的时候选择的下标是不是[1]。

提高题

对于一个曲线上的点,不只把它对应于一个像素,你需要根据到像素中心的距离来考虑与它相邻的像素的颜色。
要求利用距离实现反走样,我设计了这么一个选择方式:
image
核心选择代码如下

1
float color = std::max(static_cast<uchar>((255*(2-distance)/2)),window.at<cv::Vec3b>(miny+j,minx+k)[1]);

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
inline float get_distance(const cv::Point2f&p1,const cv::Point2f&p2){
return (p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y);
}
void bezier_with_antialiasing(const std::vector<cv::Point2f> &control_points, cv::Mat &window){
for (float i = 0; i < 1; i+=0.001f)
{
auto point = recursive_bezier(control_points,i);
float minx=std::floor(point.x),miny=std::floor(point.y);
//calculate the four points's color arount the input point
for(int k=0;k<=1;k++)
{
for(int j=0;j<=1;j++)
{
float distance=get_distance(cv::Point2f(miny+j,minx+k),point);
std::cout<<distance<<std::endl;
float color = std::max(static_cast<uchar>((255*(2-distance)/2)),window.at<cv::Vec3b>(miny+j,minx+k)[1]);
window.at<cv::Vec3b>(miny+j, minx+k)[1] = color;
}
}
}
}

运行./BezierCurve -a

image
放大观察,可以看到不错的反走样效果。
image

番外:不同数量的控制点

我实现了一些小把戏,可以在控制台中控制是否开启anti和控制控制点的数量,你可以键入
./BezierCurve \--help
来获取使用向导:
image

四个点的Bezier曲线绘制展示

前面的实现可以做到绘制任意数量控制点的Bezier曲线,只需要修改CallBack函数就好了,修改如下:

1
2
3
4
5
6
7
8
9
10
int point_num=4;//global variale
void mouse_handler(int event, int x, int y, int flags, void *userdata)
{
if (event == cv::EVENT_LBUTTONDOWN && control_points.size() < point_num)//这里修改为了point_num
{
std::cout << "Left button of the mouse is clicked - position (" << x << ", "
<< y << ")" << '\n';
control_points.emplace_back(x, y);
}
}

./BezierCurve \-a 5
image
甚至10个点,其实都是一样的操作
./BezierCurve \-a 10
image

main函数

这里给出main函数的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int main(int argc,const char*argv[]) 
{
if(argc==2&&std::string(argv[1])=="--help"){
std::cout<<"The first data is use antialiasing or not.\nInput '-a' to use it!\nInput '-' to defualt\n"
<<"The second data is number of contol point, maybe you should input more than 4"<<std::endl;
return 0;
}
cv::Mat window = cv::Mat(700, 700, CV_8UC3, cv::Scalar(0));
cv::cvtColor(window, window, cv::COLOR_BGR2RGB);
cv::namedWindow("Bezier Curve", cv::WINDOW_AUTOSIZE);
cv::setMouseCallback("Bezier Curve", mouse_handler, nullptr);
bool use_anti =false;
int key = -1;
if(argc>=2&&std::string(argv[1])=="-a")use_anti=true;
if(argc>=3&&std::stoi(argv[2])>4)point_num=std::stoi(argv[2]);
while (key != 27)
{
for (auto &point : control_points)
{
cv::circle(window, point, 3, {255, 255, 255}, 3);
}
std::cout<<"point_num"<<control_points.size()<<std::endl;

if (control_points.size() == point_num)
{
std::string filename="bezier_"+std::to_string(point_num)+"num_";
if(!use_anti){
filename+="_without_anti";
naive_bezier(control_points, window);
bezier(control_points, window);
}
else {
filename+="with_anti";
bezier_with_antialiasing(control_points,window);
}
filename+=".png";
cv::imshow("Bezier Curve", window);
cv::imwrite(filename, window);
key = cv::waitKey(0);
return 0;
}
cv::imshow("Bezier Curve", window);
key = cv::waitKey(20);
}
return 0;
}

代码汇总

zhywyt-github

导航

导航

下/上一篇

下一篇:Ray Tracing光线追踪
上一篇:obj导入与phong

games101 HomeWork3

Games101 HomeWork3

导航

导航

作业要求

第三次作业才是真正上强度的作业,作业要求和质量都特别高,先来看看所有的要求:

  • 1 . 修改函数rasterize_triangle(const Triangle& t) in rasterizer.cpp: 在此处实现与作业 2 类似的插值算法,实现法向量、颜色、纹理颜色的插值。
  • 2 . 修改函数 get_projection_matrix() in main.cpp: 将你自己在之前的实验中实现的投影矩阵填到此处,此时你可以运行./Rasterizer output.png normal来观察法向量实现结果。
  • 3 . 修改函数 phong_fragment_shader() in main.cpp: 实现 Blinn-Phong 模型计算 Fragment Color.
  • 4 . 修改函数 texture_fragment_shader() in main.cpp: 在实现 Blinn-Phong的基础上,将纹理颜色视为公式中的 kd,实现 Texture Shading FragmentShader.
  • 5 . 修改函数 bump_fragment_shader() in main.cpp: 在实现 Blinn-Phong 的基础上,仔细阅读该函数中的注释,实现 Bump mapping.
  • 6 . 修改函数 displacement_fragment_shader() in main.cpp: 在实现 Bumpmapping 的基础上,实现 displacement mapping.
    提高题
  • 7 .尝试更多模型
  • 8 .双线性纹理插值
    不要慌张,一个一个来。先看第一题

rasterize_triangle中的插值

我们在第二题2x2的超采样的基础上进行,先看看题目的提示:

rasterize_triangle 函数与你在作业2 中实现的内容相似。不同之处在于被
设定的数值将不再是常数,而是按照 Barycentric Coordinates 对法向量、颜
色、纹理颜色与底纹颜色(Shading Colors) 进行插值。回忆我们上次为了计算
z value 而提供的**[alpha, beta, gamma],这次你将需要将其应用在其他参
的插值上。你需要做的是计算插值后的颜色**,并将Fragment Shader 计算得
到的颜色写入 framebuffer,这要求你首先使用插值得到的结果设置 fragment
shader payload
,并调用 fragment shader 得到计算结果。
可以看见,这里基本给出了插值的操作步骤,这里对一个重心坐标做一个解释:

重心坐标(Barycentric Coordinates)

给定三角形的三点坐标A, B, C,该平面内一点(x,y)可以写成这三点坐标的线性组合形式,即 \((x,y)=\alpha A+\beta B+\gamma C\) 并且有\(\alpha + \beta +\gamma =1\)点\((\alpha ,\beta ,\gamma )\)就是这个点的重心坐标。关于重心坐标的求法,这里直接给出代码,不做推导:

1
2
3
4
5
6
7
//重心坐标的求解
static std::tuple<float, float, float> computeBarycentric2D(float x, float y, const Vector4f* v){
float c1 = (x*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*y + v[1].x()*v[2].y() - v[2].x()*v[1].y()) / (v[0].x()*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*v[0].y() + v[1].x()*v[2].y() - v[2].x()*v[1].y());
float c2 = (x*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*y + v[2].x()*v[0].y() - v[0].x()*v[2].y()) / (v[1].x()*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*v[1].y() + v[2].x()*v[0].y() - v[0].x()*v[2].y());
float c3 = (x*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*y + v[0].x()*v[1].y() - v[1].x()*v[0].y()) / (v[2].x()*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*v[2].y() + v[0].x()*v[1].y() - v[1].x()*v[0].y());
return {c1,c2,c3};
}

image

插值

有了重心公式,就可以对各个属性进行插值了,先写一个工具函数这里框架给出了,直接调用就好,来计算各种插值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//三维向量版本,颜色、法线、view_pos
static Eigen::Vector3f interpolate(float alpha, float beta, float gamma, const Eigen::Vector3f& vert1, const Eigen::Vector3f& vert2, const Eigen::Vector3f& vert3, float weight)
{
return (alpha * vert1 + beta * vert2 + gamma * vert3) / weight;
}
//二维向量版本,用于纹理坐标
static Eigen::Vector2f interpolate(float alpha, float beta, float gamma, const Eigen::Vector2f& vert1, const Eigen::Vector2f& vert2, const Eigen::Vector2f& vert3, float weight)
{
auto u = (alpha * vert1[0] + beta * vert2[0] + gamma * vert3[0]);
auto v = (alpha * vert1[1] + beta * vert2[1] + gamma * vert3[1]);
u /= weight;
v /= weight;
return Eigen::Vector2f(u, v);
}

然后就是插值的实现了:

1
2
3
4
5
6
auto[alpha, beta, gamma] = computeBarycentric2D(i+0.5f, j+0.5f, t.v);
auto interpolated_color=interpolate(alpha,beta,gamma,t.color[0],t.color[1],t.color[2],1);
auto interpolated_normal=interpolate(alpha,beta,gamma,t.normal[0],t.normal[1],t.normal[2],1).normalized();
auto interpolated_shadingcoords=interpolate(alpha,beta,gamma,view_pos[0],view_pos[1],view_pos[2],1);
//二维版本
auto interpolated_texcoords=interpolate(alpha,beta,gamma,t.tex_coords[0],t.tex_coords[1],t.tex_coords[2],1);

最后调用fragment_shader计算最后的颜色值

1
2
3
4
payload.view_pos = interpolated_shadingcoords;
auto pixel_color = fragment_shader(payload);
// check zbuff
set_pixel(Eigen::Vector2i(i,j),pixel_color*IsInTriangleCount/4.0f);

get_projection_matrix() 投影矩阵

前面提过了,这里只给出代码:

1
2
3
4
5
6
7
8
9
10
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
{
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
eye_fov=eye_fov/180*MY_PI;
projection<<1/(aspect_ratio*tan(eye_fov/2.0f)) ,0,0,0,
0,1/tan(eye_fov/2.0f),0,0,
0,0,-(zFar+zNear)/(zFar-zNear),2*zFar*zNear/(zNear-zFar),
0,0,-1,0;
return projection;
}

运行 ./Rasterizer normal.png normal

normal.png
image

phong_fragment_shader() 光照模型

接下来需要实现的是Bline-Pong光照系统,首先来回忆一下Bline-Pong的操作步骤
image

环境色Ambient

image

漫反射Diffuse

image

镜面反射Specular

image

代码实现(看注释)

解释一下一些操作

  • Eigen::Vector3f::cwiseProduct 返回两个矩阵(向量)同位置的元素分别相乘的新矩阵(向量)。
  • std::pow(x,n)返回 \(x^n\)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//phong_fragment_shader

Eigen::Vector3f LightDir=light.position-point;
Eigen::Vector3f ViewDir=eye_pos-point;
//r ^ 2
float d=LightDir.dot(LightDir);
Eigen::Vector3f H=(LightDir.normalized()+ViewDir.normalized()).normalized();
//Ambient
Eigen::Vector3f Ambient= ka.cwiseProduct(amb_light_intensity);
float LdotN=(normal.normalized()).dot(LightDir.normalized());
float NdotH=(H.normalized()).dot(normal.normalized());
//Diffuse
Eigen::Vector3f Diffuse= std::max( LdotN , 0.0f)*kd.cwiseProduct(light.intensity/d);
//Specular
Eigen::Vector3f Specular= std::pow(std::max( NdotH , 0.0f),150)*ks.cwiseProduct(light.intensity/d);
result_color+=Ambient+Diffuse+Specular;

运行 ./Rasterizer phong.png phong

看看,我们的小牛又光滑了许多
phong.png
image

texture_fragment_shader() 纹理贴图

在基础任务中,纹理映射只需要在phong的基础上,把颜色换成纹理坐标对应的颜色就好了,在texture类中,框架已经实现了getColor函数,我们只需要在有纹理的时候调用getColor方法获取对应的颜色就好。

1
2
3
4
5
6
7
8
9
10
11
12
13
//texture_fragment_shader函数
if (payload.texture)
{
return_color=payload.texture->getColor(payload.tex_coords.x(),payload.tex_coords.y());
}
//getColor函数(框架已实现)
Eigen::Vector3f getColor(float u, float v)
{
auto u_img = u * width;
auto v_img = (1 - v) * height;
auto color = image_data.at<cv::Vec3b>(v_img, u_img);
return Eigen::Vector3f(color[0], color[1], color[2]);
}

运行./Rasterizer texture.png texture

如果你做对了这一步,那么你make之后使用上面的指令应该能得到这样的结果:
texture.png
image

bump_fragment_shader() 凹凸(法线)贴图

这一步天坑,我大概卡在这里改了两个小时的代码,每次跑出来都是段错误!!结果却不是算法问题,我哭死。在我几乎崩溃的时候,还是发现了这个小问题,纹理坐标的在边界的时候可能导致超出1.0,这个时候应该对其进行边界处理,防止数组越界。
关于凹凸贴图的实现,照着提示做就好了,不会有什么问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//错误代码
float x=normal.x(),y=normal.y(),z=normal.z();
Vector3f t =Vector3f(x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z));
Vector3f b = normal.cross(t);
Eigen:Matrix3f TBN ;
TBN<<t.x(),b.x(),normal.x(),
t.y(),b.y(),normal.y(),
t.z(),b.z(),normal.z();
float u=payload.tex_coords.x();
float v=payload.tex_coords.y();
float w=payload.texture->width;
float h=payload.texture->height;
float dU = kh * kn * (payload.texture->getColor(u+1/w,v).norm()-payload.texture->getColor(u,v).norm());
float dV = kh * kn * (payload.texture->getColor(u,v+1/h).norm()-payload.texture->getColor(u,v).norm());
Vector3f ln = Vector3f(-dU, -dV, 1);
Eigen::Vector3f result_color = {0, 0, 0};
result_color = (TBN * ln).normalized();
return result_color * 255.f;
//正确代码
Eigen::Vector3f bump_fragment_shader(const fragment_shader_payload& payload)
{
static long int numb=1;
std::cout<<"bump_fragment_shader"<<numb++<<std::endl;
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = payload.color;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);
auto l1 = light{{20, 20, 20}, {500, 500, 500}};
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};
std::vector<light> lights = {l1, l2};
Eigen::Vector3f amb_light_intensity{10, 10, 10};
Eigen::Vector3f eye_pos{0, 0, 10};
float p = 150;
Eigen::Vector3f color = payload.color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;
float kh = 0.2, kn = 0.1;

// TODO: Implement bump mapping here
// Let n = normal = (x, y, z)
// Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
// Vector b = n cross product t
// Matrix TBN = [t b n]
// dU = kh * kn * (h(u+1/w,v)-h(u,v))
// dV = kh * kn * (h(u,v+1/h)-h(u,v))
// Vector ln = (-dU, -dV, 1)
// Normal n = normalize(TBN * ln)
float x=normal.x(),y=normal.y(),z=normal.z();
Vector3f t =Vector3f(x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z));
Vector3f b = normal.cross(t);
Eigen:Matrix3f TBN ;
TBN<<t.x(),b.x(),normal.x(),
t.y(),b.y(),normal.y(),
t.z(),b.z(),normal.z();
float u=payload.tex_coords.x();
float v=payload.tex_coords.y();
float w=payload.texture->width;
float h=payload.texture->height;

//这里多一步防止越界的操作使用std::clamp(x , l , r);来保证数值在l 到 r之间
float dU = kh * kn * (payload.texture->getColor(std::clamp(u+1/w, 0.0f, 1.0f),v).norm()-payload.texture->getColor(u,v).norm());
float dV = kh * kn * (payload.texture->getColor(u,std::clamp(v+1/h, 0.0f, 1.0f)).norm()-payload.texture->getColor(u,v).norm());
Vector3f ln = Vector3f(-dU, -dV, 1);
Eigen::Vector3f result_color = {0, 0, 0};
result_color = (TBN * ln).normalized();
return result_color * 255.f;
}

运行./rasterizer bump.png bump

bump.png
image

displacement_fragment_shader() 位移贴图

位移贴图与凹凸(法线)贴图有所不同,凹凸贴图不会影响网格的实际多边形性质,只会影响在平面上计算光照的方式。位移贴图(常听到的置换贴图有点勉强),可以让模型表面产生一定的位移变化。(注意跟Bump凹凸贴图的区别,它可以实质性地改变模型的面数,而Bump凹凸贴图并没有实质性地改变模型面数)\(^{[1]}\)
那么该怎么做位移贴图呢?最关键的一步就是位移了:

1
point += kn*normal*payload.texture->getColor(u,v).norm();

除了这步,就是一个缝合了法线贴图和phong模型的着色方法。说实话,这里还真不是很懂,但是照着注释做确实能的要想要的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
Eigen::Vector3f displacement_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = payload.color;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

auto l1 = light{{20, 20, 20}, {500, 500, 500}};
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};

std::vector<light> lights = {l1, l2};
Eigen::Vector3f amb_light_intensity{10, 10, 10};
Eigen::Vector3f eye_pos{0, 0, 10};

float p = 150;

Eigen::Vector3f color = payload.color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;

float kh = 0.2, kn = 0.1;

// TODO: Implement displacement mapping here
// Let n = normal = (x, y, z)
// Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
// Vector b = n cross product t
// Matrix TBN = [t b n]
// dU = kh * kn * (h(u+1/w,v)-h(u,v))
// dV = kh * kn * (h(u,v+1/h)-h(u,v))
// Vector ln = (-dU, -dV, 1)
// Position p = p + kn * n * h(u,v)
// Normal n = normalize(TBN * ln)
float x=normal.x(),y=normal.y(),z=normal.z();
Vector3f t =Vector3f(x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z));
Vector3f b = normal.cross(t);
Eigen:Matrix3f TBN ;
TBN<<t.x(),b.x(),normal.x(),
t.y(),b.y(),normal.y(),
t.z(),b.z(),normal.z();
float u=payload.tex_coords.x();
float v=payload.tex_coords.y();
float w=payload.texture->width;
float h=payload.texture->height;
float dU = kh * kn * (payload.texture->getColor(std::clamp(u+1/w,0.f,1.f),v).norm()-payload.texture->getColor(u,v).norm());
float dV = kh * kn * (payload.texture->getColor(u,std::clamp(v+1/h,0.f,1.f)).norm()-payload.texture->getColor(u,v).norm());
Vector3f ln = Vector3f(-dU, -dV, 1);
//位移
point += kn*normal*payload.texture->getColor(u,v).norm();
Eigen::Vector3f result_color = {0, 0, 0};
normal = (TBN * ln).normalized();


Eigen::Vector3f ViewDir=eye_pos-point;
for (auto& light : lights)
{
// TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular*
// components are. Then, accumulate that result on the *result_color* object.
Eigen::Vector3f LightDir=light.position-point;
float d=LightDir.dot(LightDir);
Eigen::Vector3f H=(LightDir.normalized()+ViewDir.normalized()).normalized();
Eigen::Vector3f Ambient= ka.cwiseProduct(amb_light_intensity);
float LdotN=(normal.normalized()).dot(LightDir.normalized());
float NdotH=(H.normalized()).dot(normal.normalized());
Eigen::Vector3f Diffuse= std::max( LdotN , 0.0f)*kd.cwiseProduct(light.intensity/d);
Eigen::Vector3f Specular= std::pow(std::max( NdotH , 0.0f),150)*ks.cwiseProduct(light.intensity/d);
result_color+=Ambient+Diffuse+Specular;
}
return result_color * 255.f;
}

运行./Rasterizer displacement.png displacement

displacement.png
image

双线性插值

这个虽然简单,但是我们也来讲一下具体实现:
函数原型如下

1
Eigen::Vector3f getColorBilinear(float u,float v);

根据下面这张原理图,我们来一步步实现
image
第一步,将uv限制在\(0-1\)

1
2
u = std::clamp(u, 0.0f, 1.0f);
v = std::clamp(v, 0.0f, 1.0f);

把比例转化为纹理坐标,并计算相邻的四个像素点

1
2
3
4
5
6
auto u_img = u * width;
auto v_img = (1 - v) * height;
float uMax=std::min((float)width,std::ceil(u_img));
float uMin=std::max(0.0f,std::floor(u_img));
float vMax=std::min((float)height,std::ceil(v_img));
float vMin=std::max(0.0f,std::floor(v_img));

其中

  • ceil()向上去整
  • floor()向下取整

记录四个点的颜色

1
2
3
4
5
6
7
8
//Up Left Point
auto colorUL = image_data.at<cv::Vec3b>(vMax,uMin );
//Up Right Point
auto colorUR = image_data.at<cv::Vec3b>(vMax, uMax);
//Down Left Point
auto colorDL = image_data.at<cv::Vec3b>(vMin, uMin);
//Down Right Point
auto colorDR = image_data.at<cv::Vec3b>(vMin, uMax);

单线性插值

1
2
3
4
5
6
float uLerpNum=(u_img-uMin)/(uMax-uMin);
float vLerpNum=(v_img-vMin)/(vMax-vMin);
//U Up Lerp
auto colorUp_U_Lerp=uLerpNum*colorUL+(1-uLerpNum)*colorUR;
//U Down Lerp
auto colorDown_U_Lerp= uLerpNum*colorDL+(1-uLerpNum)*colorDR;

再进行一次插值

1
2
//V Lerp
auto color = vLerpNum*colorDown_U_Lerp+(1-vLerpNum)*colorUp_U_Lerp;

并修改texture_fragment_shader中获取材质的语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Eigen::Vector3f texture_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f return_color = {0, 0, 0};
if (payload.texture)
{
// TODO: Get the texture value at the texture coordinates of the current fragment
//双线性插值
return_color=payload.texture->getColorBilinear(payload.tex_coords.x(),payload.tex_coords.y());
}
Eigen::Vector3f texture_color;
texture_color << return_color.x(), return_color.y(), return_color.z();

Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = texture_color / 255.f;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

auto l1 = light{{20, 20, 20}, {500, 500, 500}};
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};

std::vector<light> lights = {l1, l2};
Eigen::Vector3f amb_light_intensity{10, 10, 10};
Eigen::Vector3f eye_pos{0, 0, 10};

float p = 150;

Eigen::Vector3f color = texture_color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;

Eigen::Vector3f result_color = {0, 0, 0};

for (auto& light : lights)
{
// TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular*
// components are. Then, accumulate that result on the *result_color* object.
Eigen::Vector3f LightDir=light.position-point;
Eigen::Vector3f ViewDir=eye_pos-point;
float d=LightDir.dot(LightDir);
Eigen::Vector3f H=(LightDir.normalized()+ViewDir.normalized()).normalized();
Eigen::Vector3f Ambient= ka.cwiseProduct(amb_light_intensity);
float LdotN=(normal.normalized()).dot(LightDir.normalized());
float NdotH=(H.normalized()).dot(normal.normalized());
Eigen::Vector3f Diffuse= std::max( LdotN , 0.0f)*kd.cwiseProduct(light.intensity/d);
Eigen::Vector3f Specular= std::pow(std::max( NdotH , 0.0f),150)*ks.cwiseProduct(light.intensity/d);
result_color+=Ambient+Diffuse+Specular;
}

return result_color * 255.f;
}

使用其他模型

由于凹凸贴图和位移贴图是需要法线贴图的,所以只有spot模型(小牛)是可以进行的,其他的模型都进行不料,那么我们就不做了。但是我在做纹理映射的时候,大抵是发生了奇怪的事情,没有办法很好的作出纹理贴图。那我摆栏了,效果如下:挺差的,我也解决不了了
我自己做了一些指令,具体如下:
image

bunny

./Rasterizer \- normal bunny 2
image
./Rasterizer \- phong bunny 2
image

Crate

./Rasterizer \- normal crate
具体为什么画出来这个样子我也不清楚,搞了一下午,摆栏了
image
./Rasterizer \- phong crate
image
我本来以为是因为摄像机的位置,然后我调整了摄像机的位置,又画出了这样的图片:这下没办法了
./Rasterizer \- phong crate 20
image
./Rasterizer \- texture crate
贴图也不知道为什么非常奇怪,罢了罢了
image

cube

./Rasterizer \- normal cube
image
还可以移动摄像机到(0,0,-10)去看看 (其实这个成像是错误的,因为只是移动了eye_pos,深度信息的计算并没有重新计算,所以深度还是以(0,0,10)来计算的。看到图像就很奇怪)
./Rasterizer \- normal cube \-10
image
./Rasterizer \- phong cube
image
./Rasterizer \- texture cube
就乐呵乐呵吧
image

rock

./Rasterizer \- normal rock 20
image
rock这个模型,如果用10的话,会超出视口范围,但是getIndex函数并没有对其进行该有的操作,导致会出现段错误,这里调远摄像机的位置来防止出现段错误。

1
2
3
4
int rst::rasterizer::get_index(int x, int y)
{
return (height-y)*width + x;
}

./Rasterizer \- phong rock 20
image
./Rasterizer \- texture rock 20
image

main函数源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
int main(int argc, const char** argv)
{
vector<string> model_list,shader_list;
if(argc==2&&std::string(argv[1])=="--help"){
std::cout<<
"The first data is output file name or a '-' to defult it"<<std::endl<<
"The second data is shader mode such as : normal phong texture nump displacement"<<std::endl<<
"The Thired data is optional which you can choose whatever model you want :spot cate bunny cube rock "<<std::endl<<
"The fourth data is optional you can change your eye's axis z,in range(2,50)"
<<std::endl;
return 0;
}
std::vector<Triangle*> TriangleList;
float angle = 140.0;
bool command_line = false;
std::string model_name("spot"),shader_name;
std::string filename = "output.png";
objl::Loader Loader;
std::string obj_path = "../models";
rst::rasterizer r(700, 700);
std::string texture_path;
// Load .obj File
bool loadout =false;
texture_path = "/spot/hmap.jpg";
if (argc <= 3)
{
loadout = Loader.LoadFile("../models/spot/spot_triangulated_good.obj");
}
else if(argc>=4){
if(std::string(argv[3])=="rock"){
loadout = Loader.LoadFile("../models/rock/rock.obj");
texture_path = "/rock/rock.png";
}
else if(std::string(argv[3])=="cube"){
loadout = Loader.LoadFile("../models/cube/cube.obj");
texture_path = "/cube/wall.tif";
}
else if(std::string(argv[3])=="bunny"){
loadout = Loader.LoadFile("../models/bunny/bunny.obj");
texture_path.clear();
}
else if(std::string(argv[3])=="crate"){
loadout = Loader.LoadFile("../models/Crate/Crate1.obj");
texture_path = "/Crate/crate_1.jpg";
}
else if(std::string(argv[3])=="spot"){
loadout = Loader.LoadFile("../models/spot/spot_triangulated_good.obj");
std::cout<<"Model Load succes"<<std::endl;
texture_path = "/spot/hmap.jpg";
}
std::cout<<"Model is "<<argv[3]<<std::endl;
model_name=argv[3];
}
// if(!texture_path.empty())
// r.set_texture(Texture(obj_path + texture_path));
if (argc==3||argc>=4&&string(argv[3])=="spot")
r.set_texture(Texture(obj_path + texture_path));
for (auto mesh : Loader.LoadedMeshes)
{
for (int i = 0; i < mesh.Vertices.size(); i += 3)
{
Triangle *t = new Triangle();
for (int j = 0; j < 3; j++)
{
t->setVertex(j, Vector4f(mesh.Vertices[i + j].Position.X, mesh.Vertices[i + j].Position.Y, mesh.Vertices[i + j].Position.Z, 1.0));
t->setNormal(j, Vector3f(mesh.Vertices[i + j].Normal.X, mesh.Vertices[i + j].Normal.Y, mesh.Vertices[i + j].Normal.Z));
t->setTexCoord(j, Vector2f(mesh.Vertices[i + j].TextureCoordinate.X, mesh.Vertices[i + j].TextureCoordinate.Y));
}
TriangleList.push_back(t);
}
}
std::function<Eigen::Vector3f(fragment_shader_payload)> active_shader = phong_fragment_shader;
if (argc >= 2)
{
command_line = true;
filename = std::string(argv[1]);
if(argc>=3)shader_name=argv[2];
if (argc >= 3 && std::string(argv[2]) == "texture")
{
std::cout << "Rasterizing using the texture shader\n";
active_shader = texture_fragment_shader;
if(argc >=4 && argv[3]=="spot"||argc==3){
texture_path = "/spot/spot_texture.png";
r.set_texture(Texture(obj_path + texture_path));
}
}
else if (argc >= 3 && std::string(argv[2]) == "normal")
{
std::cout << "Rasterizing using the normal shader\n";
active_shader = normal_fragment_shader;
}
else if (argc >= 3 && std::string(argv[2]) == "phong")
{
std::cout << "Rasterizing using the phong shader\n";
active_shader = phong_fragment_shader;
}
else if (argc >= 3 && std::string(argv[2]) == "bump")
{
if(model_name!="spot"){
std::cout<<"There is no normal texture!"<<std::endl;
return 0;
}
else{
std::cout << "Rasterizing using the bump shader\n";
active_shader = bump_fragment_shader;
}
}
else if (argc >= 3 && std::string(argv[2]) == "displacement")
{
if(model_name!="spot"){
std::cout<<"There is no normal texture!"<<std::endl;
return 0;
}
else{
std::cout << "Rasterizing using the bump shader\n";
active_shader = displacement_fragment_shader;
}
}
}
float eye_z=10;
if(argc>=5)eye_z=std::stoi(argv[4]);
Eigen::Vector3f eye_pos = {0,0,eye_z};
r.set_vertex_shader(vertex_shader);
r.set_fragment_shader(active_shader);
int key = 0;
int frame_count = 0;
if (command_line)
{
if(std::string(argv[1])=="-"){
filename=model_name+"_"+shader_name+"_"+std::to_string(int(eye_z))+".png";
}
r.clear(rst::Buffers::Color | rst::Buffers::Depth);
r.set_model(get_model_matrix(angle));
r.set_view(get_view_matrix(eye_pos));
r.set_projection(get_projection_matrix(45.0, 1, 0.1, 50));
std::cout<<"Before Draw"<<std::endl;
r.draw(TriangleList);
std::cout<<"After Draw shader with "<<shader_name<<std::endl;
cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data());
image.convertTo(image, CV_8UC3, 1.0f);
cv::cvtColor(image, image, cv::COLOR_RGB2BGR);
cv::imwrite(filename, image);
return 0;
}
while(key != 27)
{
r.clear(rst::Buffers::Color | rst::Buffers::Depth);
r.set_model(get_model_matrix(angle));
r.set_view(get_view_matrix(eye_pos));
r.set_projection(get_projection_matrix(45.0, 1, 0.1, 50));
//r.draw(pos_id, ind_id, col_id, rst::Primitive::Triangle);
r.draw(TriangleList);
cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data());
image.convertTo(image, CV_8UC3, 1.0f);
cv::cvtColor(image, image, cv::COLOR_RGB2BGR);
cv::imshow("image", image);
cv::imwrite(filename, image);
key = cv::waitKey(10);
if (key == 'a' )
{
angle -= 0.1;
}
else if (key == 'd')
{
angle += 0.1;
}
}
return 0;
}

引用

\({[1]}\) 【Blender】 详解 凹凸贴图 Bump/Normal/Displacement 三者的区别

代码合集

代码有些多,我就直接放github了。
zhywyt-github

下/上一篇

下一篇:Bezier曲线的绘制
上一篇:图形填充


2023-10-18

鸽了这么久又回来写101的框架解读了

框架解读

新增文件一览

1
2
3
4
#include "global.hpp"
#include "Shader.hpp"
#include "Texture.hpp"
#include "OBJ_Loader.h"

Ubuntu解决高分屏下Matlab工具栏字体过小

能够看到工具栏,说明你已经能够打开matlab了,不管你是以何种方式打开的。
首先打开matlab,然后在命令行输入一下代码:

1
2
3
4
#在命令行内输入如下命令,其中2.0是放大的尺度,根据需要自行设置
s = settings;
s.matlab.desktop.DisplayScaleFactor;
s.matlab.desktop.DisplayScaleFactor.PersonalValue = 2.0;

会给出一个重启以适应缩放的提示,重启matlab就好了。

games101 HomeWork2

Games101 HomeWork2

导航

导航

作业要求

  • rasterize_triangle(): 执行三角形栅格化算法
  • static bool insideTriangle(): 测试点是否在三角形内。你可以修改此函数的定义,这意味着,你可以按照自己的方式更新返回类型或函数参数。

先从简单的函数开始

insideTriangle

insideTriangle只需要检查点是否在三角形内部,我并没有修改函数的定义,是否在三角形中,返回一个bool 类型的值似乎挺正常的。
判断一个点是否在三角形中其实是挺简单的,这里用了课程中用到的差乘法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static bool insideTriangle(float x, float y, const Vector3f* _v)
{
// TODO : Implement this function to check if the point (x, y) is inside the triangle represented by _v[0], _v[1], _v[2]
Vector3f P=Vector3f(x,y,_v[0].z());
//三边向量
Vector3f AC=_v[2]-_v[0];
Vector3f CB=_v[1]-_v[2];
Vector3f BA=_v[0]-_v[1];
//顶点与目标点向量
Vector3f AP=P-_v[0];
Vector3f BP=P-_v[1];
Vector3f CP=P-_v[2];
//if cross product in the same direction ,its inside the triangle
if(AP.cross(AC).dot(BP.cross(BA))>0.0f&&
BP.cross(BA).dot(CP.cross(CB))>0.0f&&
CP.cross(CB).dot(AP.cross(AC))>0.0f)
{
return true;
}
return false;
}

然后是光栅化的实现了

rasterize_triangle

首先我们要找到三角形的一个包围盒(Bunding Box)然后遍历包围盒中的点,如果点在三角形内部,使用重心公式插值得到z值(这部分框架中已经给出实现),并且深度小于z-Buffer中的缓存,那么我们给这个像素的颜色重置为三角形的颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
//包围盒的建立
std::vector<float> arr_x{t.v[0].x(),t.v[1].x(),t.v[2].x()};
std::vector<float> arr_y{t.v[0].y(),t.v[1].y(),t.v[2].y()};
std::sort(arr_x.begin(),arr_x.end());
std::sort(arr_y.begin(),arr_y.end());

for(int x=arr_x[0];x<=arr_x[2];x+=1){
for(int y=arr_y[0];y<arr_y[2];y+=1){
if(insideTriangle(x+0.5f,y+0.5f,t.v)){

//这里是框架给出的重心公式插值代码
auto[alpha, beta, gamma] = computeBarycentric2D(x+0.5f, y+0.5f, t.v);
float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
//\end 重心公式插值

//如果目标点的z值(绝对值)小于缓存深度
if (z_interpolated < depth_buf[get_index(x, y)]) {
Eigen::Vector3f point(x, y, 1.0f);
set_pixel(point, t.getColor());
//重置深度缓存颜色
depth_buf[get_index(x, y)] = z_interpolated;
}
}
}
}
}

那么就完成了z-Buffer的步骤,编译运行得到这样的结果注意遮挡顺序,如果你的遮挡顺序错了,请检查你的透视投影矩阵。
image

放大可以看到存在明显的锯齿,但是普通要求也到这结束了。
image

提高题

使用Surper-Sampling对一个像素进2x2超采样。
首先,我们创建两个二维向量来存储深度缓存和颜色缓存。

1
2
3
//rasterizer.hpp
std::vector<std::array<float,4>> surpersample_corlor_buf;
std::vector<std::array<float,4>> surpersample_depth_buf;

同时修改构造函数和重置函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void rst::rasterizer::clear(rst::Buffers buff)
{
if ((buff & rst::Buffers::Color) == rst::Buffers::Color)
{
std::fill(frame_buf.begin(), frame_buf.end(), Eigen::Vector3f{0, 0, 0});
}
if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
{
std::array<float,4> inf;
inf.fill(std::numeric_limits<float>::infinity());
//DIY
std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
std::fill(surpersample_depth_buf.begin(), surpersample_depth_buf.end(), inf);
}
}

rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
{
frame_buf.resize(w * h);
depth_buf.resize(w * h);
//DIY
surpersample_corlor_buf.resize(w * h);
surpersample_depth_buf.resize(w * h);
}

接着就是光栅化的实现了,使用附近四个点是否在三角形内作为参数,设置颜色的值。这里其实是一种不那么严谨的卷机(个人观点),可以让三角形和周围产生的锯齿消失。下面是卷机盒的设计:
image
代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
for(int i=minX;i<maxX;i++)
{
for(int j=minY;j<maxY;j++)
{
int Index=get_index(i,j);
int l=0;
int IsInTriangleCount=0;
int IsDontBeCover=0;
if(IsUseSurperSampling)
{
//SurperSampling
for(auto&k : SampleOffset)
{
float SampleX=i+k.x();
float SampleY=j+k.y();
if(insideTriangle(SampleX,SampleY,t.v))
{
auto[alpha, beta, gamma] = computeBarycentric2D(SampleX, SampleY, t.v);
float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
// check zbuff
if(z_interpolated < surpersample_depth_buf[Index][l])
{
surpersample_depth_buf[Index][l]=z_interpolated;
IsDontBeCover++;
}
IsInTriangleCount++;
}
l=l+1;
}
Vector3f color= t.getColor()*IsInTriangleCount/4.0f;
if(IsDontBeCover>0)
{
set_pixel(Vector3f(i,j,0),color);
}
}
else
{
if(insideTriangle(i+0.5f,j+0.5f,t.v))
{
auto[alpha, beta, gamma] = computeBarycentric2D(i+0.5f, j+0.5f, t.v);
float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
// check zbuff
if(z_interpolated<depth_buf[Index])
{
depth_buf[Index]=z_interpolated;
set_pixel(Vector3f(i,j,1),t.getColor());
}
}
}
}
}

最后的输出图片如下,比起超采样之前的三角形,锯齿明显少了。
image
放大后可以看到明显的柔化:
image

下/上一篇

下一篇:obj导入与phong
上一篇:透视投影矩阵

|