games101-homework-notes
Games101 作业笔记
Created: 2023-06-19T12:00+08:00
Published: 2023-08-17T16:23+08:00
Categories: ComputerGraphics
pa0
使用宏节约 angle / 180.0 * acos(-1)
:
#define SINDEG(a) sin(a / 180.0 * acos(-1))
#define COSDEG(a) cos(a / 180.0 * acos(-1))
hw1
Projection Matrix
绕 z 轴旋转只需要记住三个基绕 z 轴旋转后结果即可
perspective projection matrix 如果推导过一遍后可以直接照抄,我的写法是从 fovY 等参数中还原出 lrbtnf 等参数再带入推导出的 \(M_{persp}\)。
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
float zNear, float zFar)
{
// TODO: Implement this function
// Create the projection matrix for the given parameters.
// Then return it.
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
float n = -abs(zNear);
float f = -abs(zFar);
float angle = eye_fov / 180 * MY_PI;
float t = tan(angle / 2) * (-n); // lecture assume n < 0
float b = -t;
float r = t * aspect_ratio;
float l = -r;
projection << (2 * n) / (r - l), 0, -(r + l) / (r - l), 0,
0, 2 * n / (t - b), -(t + b) / (t - b), 0,
0, 0, (f + n) / (n - f), -2 * n * f / (n - f),
0, 0, 1, 0;
return projection;
}
Triangle
每个顶点都有如下属性,三角形只不过是顶点的数组而已
- 坐标,齐次坐标
- 法向量
- 颜色
- 纹理坐标
- 纹理指针
Rasterizer
我认为重点有两个
- 如何管理内部的三角形
- 如何将坐标映射到 frame buffer 中
第一个是通过 pos_buf 和 ind_buf 管理
第二个是在画三角形的时候,对三角形的顶点乘上 MVP 矩阵,调用画线算法,set pixel 到屏幕对应的 indices 中。
成员:
model 是移动物体到原点
view 是转动相机
project 是透视投影矩阵
pos_buf
: 存储一堆点(positions),id to positions,int to vector3f[],给一个 position 的 id 可以返回一堆点的 vector,相关函数:load_positions()
ind_buf
: 存储 id 到三角形片元列表,每个三角形列表有一个 id,然后每个三角形用三个 position 表示,所以是 int to vector3i[],相关函数:load_indices()
以上还是抽象的表示,和屏幕无关
一帧画面(frame)就是一堆像素
猜测 frame_buf 就是像素的 array,每个元素 vector3f
表示对应 pixel 的 RGB
depth_buf 就是像素深度的 array
屏幕的像素编号是从左上角开始,从左往右,从上到下,类似于:
0 1 2 3 4
5 6 7 8 9
视口变换后得到 image 坐标系,是 \(\mathbb{R}^2\),由此计算每个屏幕上的 pixel 对应的颜色。
OpenCV 里,pixel 的增长是从上到下,从左到右,类似于草稿纸上写字的顺序
下面的代码可以看到调用 cv::imshow
逐个像素绘制的顺序
#include <iostream>
#include <opencv2/opencv.hpp>
#include <Eigen/Eigen>
int main(int argc, const char** argv)
{
std::vector<Eigen::Vector3f> frame_buffer;
int height = 300, width = 2 * height;
frame_buffer.resize(height * width);
int key = 0;
int i = 0;
std::fill(frame_buffer.begin(), frame_buffer.end(), Eigen::Vector3f{0, 0, 0});
while(key != 27)
{
frame_buffer[i] << 255,0,0;
cv::Mat image(height, width, CV_32FC3, frame_buffer.data());
image.convertTo(image, CV_8UC3, 1.0f);
cv::cvtColor(image, image, cv::COLOR_RGB2BGR);
cv::imshow("image", image);
i++;
i%= (height * width);
std::cout << "index: " << i << std::endl;
key = cv::waitKey(10);
}
return 0;
}
// clang-format on
process
- 压点到 buffer 里
- 遍历每一个三角形,对其做 MVP 和 视口变换,得到 image 坐标
- 对三角形的 image 坐标,转成 int(use floor truncate),调用画线算法,set pixel
hw2
static bool insideTriangle(): 测试点是否在三角形内。你可以修改此函数的定义,这意味着,你可以按照自己的方式更新返回类型或函数参数。
修改 insideTriangle 定义,接受 float
inside triangle
如果逆时针沿着三角形走,那么三角形内部的点在左侧
note. ssloy 介绍了一种直接计算重心坐标系的方法。
rasterize_triangle
视口变换得到的屏幕坐标系,我们姑且称之为 image 好了,内部点取值范围为实数,\(\mathbb{R}^2\) 就是视口变换后得到的坐标,该坐标要对应到一个像素上,像素的索引是 \((i_{row},j_{col})\),i 和 j 只能是整数。
根据左闭右开的原则,视口变换后得到的坐标在 \(x \in [0, width), y \in [0, height)\) 才是有效的,对于刚好在「外沿」上的点,没法为其分配一个像素,对于在 pixel 内部的点,为了方便获取其 pixel 对应的 index,对其坐标做一个 ground,如 3.7 变到 3,转换坐标系的整数坐标上。
如上图,虚线部分无法为其分配像素
下面是关于如何将整数坐标对应到 pixel index 上。
y | row index of pixel index |
---|---|
h-1 | 0 |
h-2 | 1 |
h-3 | 2 |
y | h-1-y |
// given image (x, y) return buffer index
int rst::rasterizer::get_index(int x, int y)
{
return (height-y-1)*width + x;
}
// x, y of point should be integer
void rst::rasterizer::set_pixel(const Eigen::Vector3f& point, const Eigen::Vector3f& color)
{
//old index: auto ind = point.y() + point.x() * width;
auto ind = (height-point.y() - 1)*width + point.x();
frame_buf[ind] = color;
}
所以获得 bounding box 后,我们用 \((x + 0.5, y + 0.5)\) 去测试 pixel 中心在不在三角形内部,但使用 \((x, y)\) 去 get indx and set index of pixel
my hw2 bugs
使用 \(y + 0.5\) set index 导致三角形分裂屏幕两侧
set_pixel()
中如果使用 \(y+0.5\),将导致多 0.5 width
遮挡的 z
我还是弄不明白这个 z,完全没有必要拉到 \(f_1 + f_2\)
注意初始化的时候,用了 infity
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::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
}
}
所以所有的 z 都要是正的
hw2 summary
弄清楚什么时候用 x,什么时候用 x+0.5
hw3
Texture
使用 cv2 读入一张图片,提供 getColor 操作从图片中提取颜色
为从 T 空间获得函数的值提供了 code 基础
Shader
各种 shader in main.cpp
要知道 shader 作用是:根据光照和人眼的观察角度、顶点本身的属性,来确定顶点的颜色
note. 这就是所谓的写 shader 吧
输入 viewspace 中的一个顶点,包括
- 顶点的坐标
- 顶点的法向量
- 顶点的颜色
- 光照:强度、光的位置
- 人眼的观察位置
输出顶点的颜色
displacement shader
阅读 FCG5,可知 bump 存储的是相对高度,对其求导就是真实的 normal(法线),参考 FCG
但是这个 normal 是定义在 surface 局部坐标下的,当 surface 在 viewspace 中翻转时候,normal 在 surface 坐标系下坐标不变,但是在 viewspace 中坐标改变,这个坐标系就是切线空间,基用 TBN 表示。我认为作业里只是随便给了一个 t。参考 FCG5 的 7.2.2 节
displacement 就是说原来的点位置需要沿着 normal 移动一个距离,这个距离助教通过 RGB 的 norm 定义。
- 求导
- 切线空间到 viewspace 的变换
- 沿着切线移动
Shader(Payload)
Shader 下面有两种 shader,一个是 vertex,另一个是 fragment
虽然叫做 Shader,但是其实是 Shader 的 payload,也就是 shader 的输入
vertex_shader_payload
顶点着色载量
/**
* vertex_shader_payload 类
* @prop {Vector3f} position - 坐标
* 内部只有一个 position,难道是要 shade 的点的坐标?
/
fragment_shader_payload
表示一个 shader 的输入,shader 就是光、人眼和顶点的属性来推断顶点的颜色,这里表示顶点的属性
/**
* fragment_shader_payload 类
* @prop {Vector3f} view_pos - viewspace position,相机视角下的坐标
* @prop {Vector3f} color - 颜色
* @prop {Vector3f} normal - 可能是片元的法向量
* @prop {Vector2f} tex_coords - 对应纹理上的 (u, v) 坐标
* @prop {Texture*} texture - 纹理指针
/
rasterizer
jump
Process
通过 obj 初始化 triangle list,初始化的有每个三角形的:
- 顶点坐标
- 顶点的法向量
- 顶点的 \((u,v)\) 坐标
然后就是渲染一堆三角形
通过命令行调整使用的纹理和 shader,纹理有:
- hmap,就是表示物体表面法向量的图片
- spot-texture 就是奶牛的纹理图
设置好着色器,调用 draw(triangleList)
方法
draw 内,遍历每个三角形
- 计算三角形每个顶点 model 和 view 变换后的坐标,也就是在 view space 中的坐标,代码叫做 viewspace
- 计算每个顶点的 normal 变换后在 viewsapce 中坐标,涉及法线转换
- 调用 rasterize triangle
rasterize 过程中,遍历像素中心坐标时,需要还原像素中心对应点在 viewspace 中的坐标,所以参数有 view_pos
可能涉及到透视矫正,参考我的 lecture notes,以及 https://zhuanlan.zhihu.com/p/509902950,插值过程很简单的
my bugs in hw3
rasterize_triangle 内部直接 return
为了防止 code nesting,所以把 continue 写成 return 了
每个光源都会导致环境光
Blinn-Phong 模型中,for loop 内也要计算环境光
做了透视矫正后没法画凹凸贴图和位移贴图
不知道 bug 在哪儿,也不知道咋 debug(太菜了
hw4
jump
hw5
global 提供解二次函数、clamp(夹)、更新进度条、定义材质等
light:只有位置和强度
object:和 FCG 中基本一致,
- 定义了 Blinn-Feng 模型中的那些 kd,ks 之类,还有 ior 就是反射率,反射需要的那个指数
- 是否有交点函数,传入引用是为了直接在内部修改引用的对象,来获得交点,定义了一堆参数,难以看出每个参数具体的作用,需要结合具体实现类,如 Mesh 和 Sphere 的接口来看这些参数
- 获取表面材质
sphere
这部分我学习到的是,定义一个物体(Object),不一定需要通过 triangle mesh 的方式,Geometry Lecture 就是在描述各种定义物体的方法。对于 ray tracing,一个物体只需要实现求交点、获取表面性质的接口就行。
Triangle mesh
定义方法和 FCG5 中一致
每一个顶点有空间中的坐标和 stCoordinates,后者应该是纹理坐标。
rayTriangleIntersect
注意相交的条件是:\(t, \alpha, \beta, \gamma > 0\) 四个都大于 0 才行
FCG5 介绍了一种早停的方式
Renderer
trace
给定一束光,object ordering 判断 ray 是否和 scene 内部的 object 相交。
std::optional<hit_payload> trace(
const Vector3f &orig, const Vector3f &dir,
const std::vector<std::unique_ptr<Object>> &objects)
这里涉及到了 optional
这个 template,这相当于一种多返回值的实现方式,若相交,则不仅要知道 true
,还要知道相交点的性质,也就是 hit_payload。可以参考 C++17 新特性之 std::optional(上)
castRay
计算某个方向 ray 看到的颜色,会递归调用。
- 调用 trace,判断是否相交,否则直接返回背景色
- 判断 hit_payload 的材质。在 FCG5 中,这个叫做 hit_record
- 通过 switch case 分别处理不同材质的反射
施加微扰
case REFLECTION_AND_REFRACTION:
{
Vector3f reflectionDirection = normalize(reflect(dir, N));
Vector3f refractionDirection = normalize(refract(dir, N, payload->hit_obj->ior));
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);
float kr = fresnel(dir, N, payload->hit_obj->ior);
hitColor = reflectionColor * kr + refractionColor * (1 - kr);
break;
}
若有交点,那么交点的坐标需要沿着法向量 N 平移一点,我的猜测是在 FCG5 中 4.5.3 也有提到,如果不平移,那么可能导致 object ordering 时候再次相交
Render
没有必要从各种 MVP 变换的角度去思考,MVP 矩阵是从光栅化的角度推导出来的,ray tracing 相当于光线打到了 near plane 上,每一个 pixel 就坐落于 near plane 上,这也是 FCG5 4.3.2 中提到的。
还是注意像素的编号是从左上角开始:
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;
float z_near = -1;
float top = fabs(z_near) * scale;
float right = imageAspectRatio * top;
float pixel_width = 2.0 * right / scene.width;
float pixel_height = 2.0 * top / scene.height;
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 = -right + (i + 0.5) * pixel_width;
y = top - (j + 0.5) * pixel_height;
Vector3f dir = normalize(Vector3f(x, y, z_near)); // 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);
}
bug
三角形求交,只判断了 \(t, \alpha, \beta\) 三个变量大于 0,漏了 \(\gamma\)
hw6
Light 有 position 和 intensity
Area Light 有 normal 表示光源朝向,实现采样接口
Sphere
求交函数 intersect 告诉我们,ray 的 t interval 没有用
Triangle
求交参考 Sphere
Bounds
AABB 的抽象表示,由 pmin 和 pmax 表示
pMin.x
和 pMax.x
构成 x 轴的对面,其他以此类推。
IntersectP
开始我不知道负数怎么简化了逻辑,参考 https://zhuanlan.zhihu.com/p/434911674
BVH
包括包围盒 BVHBuildNode 和加速结构 BVHAccel
recursiveBuild 二叉树构建方法
输入:object list
输出:node 组成的二叉树
首先构建一个空 node
如果 list 大小为 1,填充 Node 并返回。
如果 list 大小为 2,对 list[0] 和 list[1] 调用得到两个 leaf 节点,和本节点连接并返回
如果 list 大小大于 2:
通过 objects 逐个的 bounds 的中心 union 起来,得到一个大的 bounds,然后找到最大的轴
将 objects 依据此轴的坐标排序,排序后均分为两堆,递归调用本方法得到两个 node,与本节点连接。
getIntersection
若 node 内只有一个 object,先看是否会和 bounds 交,会的话,直接返回 和 object intersect 的结果
根据构建算法可知,若 node 内部没有 object 指向,则必有 left 和 right 两个节点
分别得到 left 和 right 的 intersection,若都不交,那也不交,若有交,用近的那个
SAH
如果 objects 在轴上的排列不均匀,均分为两份效果就不好。
如果 有 n 个物体,就有 n -1 种划分方法,如果可以为每一种划分方式计算一个代价,选择最小的代价的划分方式就好。
实际的做法是分桶
参考 https://zhuanlan.zhihu.com/p/50720158
在实现的时候,相比于计算可能划分的代价然后寻找代价最小的划分,一种更好的办法是将节点 C 所包围的空间沿着跨度最长的那个坐标轴的方向将空间均等的划分为若干个桶(Buckets),划分只会出现在桶与桶之间的位置上。如图所示,若桶的个数为 n 则只会有 n−1 种划分的可能。
本菜鸟也没实现。。。下次一定
hw7
Material
Material(type, emission)
不同于 hw6,不再区分 light 和 object,而是 object 的 material 可能会发光。注意 emmission 不为 0 就表示材质会发光。
eval
eval 的定义,wo 的方向都要和 N 同一侧,才可以有 diffuse,这一点和课程的约定是一样的
Sphere
Sample
阅读可知 sample 得到的法方向是从球心向外的,就像 sphere 是一个光源一样
Triangle
Sample
涉及三角形内随机采样算法
定义均匀:三角形内随便画一个闭合区域,落于此区域的概率是区域的面积除以三角形面积,等价于落于长方形 x,y 内的概率是 2xy。
均匀分布线性变换后还是均匀的
https://kingins.cn/2022/03/08/三角形随机均匀点采样算法/
Scene
SampleLight
遍历所有发光的物体,随机选择一个,采样
shade
我自己写了一个 shade 函数
需要注意光法线的朝向,除此以外没有要注意的点
Vector3f Scene::shade(const Intersection &intersection, const Vector3f &wo) const
{
Vector3f L_emit, L_dir, L_indir;
Material *m = intersection.m;
Object *hitObject = intersection.obj;
Vector3f hitPoint = intersection.coords;
Vector3f N = intersection.normal; // normal
if (hitObject->hasEmit())
{
L_emit = m->getEmission();
}
switch (m->getType())
{
case DIFFUSE: // diffuse
{
// direct
Vector3f shadowPointOrig = (dotProduct(wo, N) < 0) ? hitPoint + N * EPSILON : hitPoint - N * EPSILON;
Intersection inter;
float pdf_light;
sampleLight(inter, pdf_light);
auto ws = normalize(inter.coords - shadowPointOrig); // hit to light sample point
auto NN = inter.normal;
Vector3f lightDir = ws;
float lightDistance = (inter.coords - shadowPointOrig).norm();
Intersection blockInter = Scene::intersect(Ray(shadowPointOrig, lightDir));
bool isBlocked = blockInter.happened && (lightDistance - blockInter.distance) > EPSILON;
if (!isBlocked && dotProduct(-ws, NN) > 0)
{
L_dir = inter.emit * m->eval(-wo, ws, N) * dotProduct(ws, N) * dotProduct(-ws, NN) / (lightDistance * lightDistance) / pdf_light;
}
// indirect
if (get_random_float() > RussianRoulette)
{ // not pass RR
// L_indir = 0;
}
else
{
Vector3f wi;
wi = intersection.m->sample(wo, N);
Intersection indirInter = intersect(Ray(shadowPointOrig, wi));
if (indirInter.happened && !indirInter.obj->hasEmit())
{
L_indir = shade(indirInter, wi) * m->eval(-wo, wi, N) * dotProduct(wi, N) / m->pdf(wo, wi, N) / RussianRoulette;
}
}
break;
}
}
return L_emit + L_dir + L_indir;
}
// Implementation of Path Tracing
Vector3f Scene::castRay(const Ray &ray, int depth) const
{
// TO DO Implement Path Tracing Algorithm here
Intersection intersection = Scene::intersect(ray);
Vector3f hitColor = this->backgroundColor;
if (!intersection.happened)
{
return hitColor;
}
else
{
return Scene::shade(intersection, ray.direction);
}
}
hw8
不会搭环境,用 xmake 了,https://github.com/star-hengxing/GAMES101-xmake
话说「可以」,到底需不需要我们实现呢,实现的话有没有相应的提示呢,我好疑惑。
除此之外,我们可以仿真弹簧系数无限大的弹簧。不用再考虑弹簧力,而是用解约束的方法来更新质点位置:只要简单的移动每个质点的位置使得弹簧的长度保持原长。修正向量应该和两个质点之间的位移成比例,方向为一个质点指向另一质点。每个质点应该移动位移的一半。