5398 字
27 分钟
【CG】Games101-010学习笔记 & 作业解析

纹理的应用#

Environment Map#

p环境贴图 Environment Map 从名字我们就可以猜到它是将环境光记录到一张纹理上,并且我们认为环境光来自无限远处,让后我们可以将这个纹理作为 Bilnn-Phong 模型的环境光系数上这样我们就能得到相对准确的环境光了。

Spherical Environment Map#

那么我们记录环境光能?我们可以采用这么一种方式:在环境中放置一个非常光滑的镜子球,那么环境光照射到这个球上,如果我们能记录这个球上的光照信息,那么我们不就能记录到环境光了,这个球反射的光就是环境光的信息。而使用球体来记录环境光信息的方式,我们就叫它Spherical Environment Ma。 但是我们的图片信息都是二维的所以我们需要对这个球壳进行展开,就像我们的世界地图一样,我们需要将地球表面上的信息展开为一个二维的图片,但是根据高斯绝妙定理来说:一个曲面的**高斯曲率(Gaussian Curvature)**是其“内蕴”属性。这意味着,只要你不拉伸、不撕裂、不折皱这个表面,它的曲率就不会改变。所以我们是没方法直接将球体“完美”地展开成平面。为此我们需要使用一种投影技术。在图形学中通常我们使用一种名为 Spherical Map 的方式将球体展开成平面。 可以看到展开后维度高的地方都被扭曲了,这是投影所导致问题的。那怎么办呢?

Cube Map#

我们换一种投影方式,我们使用一个外切正方体将球保住,根据不同的经纬度从球心出发射出一条射线,这条射线会分别交于球体和立方体,所以我们可以通过这种方式将球上的信息映射到立方体上,而这种方式我们称为 Cube Map。 这么做了之后我们会得到6张图,因为立方体有6个表面 但是这么做后会有一个问题,之前我们通过球体可以很轻松的通过两个角度确定一个光照信息,而使用cube map之后我们需要进行多一步的转换,但是通常非常快。

Bump / Normal Mapping#

既然我们可以使用贴图替换Bilnn-Phong模型中的环境光系数,以及漫反射系数(纹理贴图), 那么我们发散一下想,是不是还能替换其他的项,是的我们还能定义在不同的位置的点的相对高度是多少(凹凸贴图),还能定义不同的点的法线是多少(法线贴图) 这样做我们可以使用一个光滑的球体模型表示一些表面高度不同的模型,通过着色欺骗眼睛。在不把几何形体变复杂的情况下,可以获得更精细的效果。那么它是怎么做的呢?我们通过凹凸贴图告诉原来的模型不同的点应该怎么变化,这个点该+多少的高度,这个点该-多少高度,而通过凹凸贴图我们可以知道变化之后的点的法线应该是多少从而得到法线贴图 那么我们怎么通过凹凸贴图上变换后的点得到对应的新的法线呢? 我们可以先对这个点所在的函数求个导得到这个点所在的切线方向的向量,然后取这个切线的法向量即可得到。 而实际上我们的图是二维的,所以我们使用类似的方式,求梯度向量,然后求它的法向量

而更现代的做法是使用Displacement Mapping,他会对顶点做真正的移动

3D Procedural Noise#

纹理不仅仅是二维的,当然还可以是三维的对于一个模型他所有的内部的点也都有一个纹理进行映射,并且我们可以使用程序自动生成带有随机效果的噪声纹理,比如大理石的纹理,通过噪声算法我们可以得到纹理的随机效果

Ambient Occlusion#

纹理还可以应用到环境光遮蔽上,对于一个模型如果它存在一个凹陷区域,例如人的耳朵内部,我们使用Bilnn-Phong着色的话耳朵内部其实也会被环境光系数的存在照亮,但现实中不是这样的,所以我们需要使用一种环境光遮蔽的方法解决这个问题,而最简单的做法就是使用环境光贴图AO Map来解决了

3D Texture#

我们在常用的渲染模型通常是针对物体表面的,而在医疗等领域需要渲染更为详细的内部数据,在医院中通常我们通过扫描病人不同深度的信息得到一组图片,然后我们可以将这些图片组合成一张3d纹理

Geometry#

在图形学中我们通常对几何做出这样的分类

  • 隐式几何
  • 代数表面 algebraic surface
  • 水平集 level sets
  • 距离函数 distance functions
  • …
  • 显示几何
  • 点阵云 point cloud
  • 多边形面 polygon mesh
  • 细分 subdivision
  • …

隐式表示#

隐式的表示方法就是不会告诉你这些点具体在哪,而是会告诉你这些点满足一个什么样的关系,这通常是我们数学中常用的,例如我们使用代数方程x2+y2+z2=1x^2+y^2+z^2 = 1表示一个球等等。我们通常可以使用一个方程f(x,y,z)=0f(x,y,z) = 0表示一个几何形体。
使用隐式表示有好处也有坏处。
我们先来看这么一个隐式几何

f(x,y,z)=(2−x2+y2)2+z2−1=0f(x,y,z) = (2-\sqrt{x^2+y^2})^2+z^2 -1 = 0

为了得到具体的几何表面,我们需要求解这个方程,但是这是一个相对困难的事情,但使其它是圆环,我们通过这个式子很难看出来,这是它的坏处 但是隐式表示还有一个优点,我们可以很轻松的判断一个点是不是在这个几何表面的内部还是外部 只要将这个点代入方程所对应的函数中,

  • 如果这个函数值大于0,则这个点在外部
  • 如果这个函数值小于0,则这个点在内部

显示表示#

显示表示就是直接用点将这个几何表面上的点表示出来,我们之前使用的一直都是显示表示。
除此之外我们还有一种映射的表示方法,我们先定义好这些点在二维上的位置,然后通过一个函数将这个点映射到三维空间中去。即f:R2→R3;(u,v)↦(x,y,z)f:\mathbb{R}^2\to \mathbb{R}^3;(u,v)\mapsto(x,y,z) 对于显示的表示,我们不好判断一个点是否在几何表面的内外

Constructive Solid Gemoetry#

隐式的表示需要使用非常复杂的数学公式来描述非常的不直观,对于简单的几何体我们还能找到一个数学公式进行描述而对于复杂的几何体怎么描述呢?一个牛怎么表示呢?一个房子呢?

表示复杂的几何非常困难,但是我们可以通过搭积木的方式将不同的简单的几何体组合起来表示复杂的几何。有一种组合方式就是通过几何运算,我们可以把几何看成一系列点的集合,然后我们可以对这些点集进行集合运算 这种操作在CAD等建模软件中使用的非常广泛

Distance Functions#

Distance Function 这个概念非常简单,他描述了空间中任何一个点p\mathbf{p}到某个物体表面SS的最短距离。也就是我们把空间中所有的点都定义一个距离出来,这就构成了距离场 Distance field 其具体的数学表示是: 对于一个封闭曲面SS,其有向距离函数f(P)f(\mathbf{P})定义为:

f(p)=sgn(p)⋅min⁡q∈S∥p−q∥f(\mathbf{p}) = \text{sgn}(\mathbf{p}) \cdot \min_{\mathbf{q} \in S} \|\mathbf{p} - \mathbf{q}\|

其中:

  • ∥p−q∥\|\mathbf{p} - \mathbf{q}\|:点 p\mathbf{p} 到物体表面上最近点 q\mathbf{q} 的欧几里得距离。
  • sgn(p)\text{sgn}(\mathbf{p}):符号函数,用于区分内外:
  • f(p)>0f(\mathbf{p}) > 0:点在物体外部
  • f(p)<0f(\mathbf{p}) < 0:点在物体内部
  • f(p)=0f(\mathbf{p}) = 0:点正好在物体表面 例如
    球体的距离函数(点到原点的距离减去半径)
f(p)=∥p∥−rf(\mathbf{p}) = \|\mathbf{p}\| - r

平面的距离函数(点在法线方向上的投影)

f(p)=n⋅p+df(\mathbf{p}) = \mathbf{n} \cdot \mathbf{p} + d

通过混合两个物体的 SDF 我们可以得到他们的中间的边界 下面的过程就是将两个物体的SDF进行混合再回复成几何的过程, 那么我们怎么得到SDF混合后的几何形体呢?我们只需要找到SDF中值等于0的点就行了,

而有一种叫做Level Set的方法将SDF的左右值存储在内存中,只需要找到所有等于0的点就好了

作业3解析#

环境和之前的一样,需要自行修改cmake,方式和原来一样不过多赘述
实现详情看代码和注释,我认为如果你跟着视频和笔记来,应该能很轻松看懂代码在干嘛

  1. 修改函数 rasterize_triangle(const Triangle& t) in rasterizer.cpp: 在此 处实现与作业 2 类似的插值算法,实现法向量、颜色、纹理颜色的插值。
void rst::rasterizer::rasterize_triangle(
const Triangle &t, const std::array<Eigen::Vector3f, 3> &view_pos) {
// 获取三角形顶点的齐次坐标
auto v = t.toVector4();
// 找到三角形的屏幕包围盒
float min_x = std::min(v[0].x(), std::min(v[1].x(), v[2].x()));
float max_x = std::max(v[0].x(), std::max(v[1].x(), v[2].x()));
float min_y = std::min(v[0].y(), std::min(v[1].y(), v[2].y()));
float max_y = std::max(v[0].y(), std::max(v[1].y(), v[2].y()));
// 向下/向上取整,确定像素遍历范围
int x_min = std::floor(min_x);
int x_max = std::ceil(max_x);
int y_min = std::floor(min_y);
int y_max = std::ceil(max_y);
// 遍历包围盒内的每一个像素
for (int x = x_min; x <= x_max; ++x) {
for (int y = y_min; y <= y_max; ++y) {
// 取像素中心点
float cx = x + 0.5f;
float cy = y + 0.5f;
// 判断像素中心是否在三角形内
if (insideTriangle(cx, cy, t.v)) {
// 计算重心坐标 (alpha, beta, gamma)
auto [alpha, beta, gamma] = computeBarycentric2D(cx, cy, t.v);
// 计算透视矫正后的深度值 Z 和 zp
float Z = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float zp = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() +
gamma * v[2].z() / v[2].w();
zp *= Z;
// 深度测试
int buf_index = get_index(x, y);
if (zp < depth_buf[buf_index]) {
// 更新深度缓冲
depth_buf[buf_index] = zp;
// 对颜色插值
auto interpolated_color =
alpha * t.color[0] + beta * t.color[1] + gamma * t.color[2];
// 对法线插值
auto interpolated_normal =
alpha * t.normal[0] + beta * t.normal[1] + gamma * t.normal[2];
// 对纹理坐标插值
auto interpolated_texcoords = alpha * t.tex_coords[0] +
beta * t.tex_coords[1] +
gamma * t.tex_coords[2];
// 对观察空间位置插值,由于我们进行着色的时候不仅仅是对观察空间中的三角形顶点着色,我们要做的是逐像素着色
// 所以我们需要对观察空间中的三角形的顶点view_pos进行插值得到三角形内部点在观察空间中的坐标,最后在着色中使用
auto interpolated_shadingcoords =
alpha * view_pos[0] + beta * view_pos[1] + gamma * view_pos[2];
// 将着色中所有需要用到的数据放在一个payload中,传入fragment shader
fragment_shader_payload payload(
interpolated_color, interpolated_normal.normalized(),
interpolated_texcoords, texture ? &*texture : nullptr);
payload.view_pos = interpolated_shadingcoords;
// 执行片fragment shader,获取该像素的最终颜色
auto pixel_color = fragment_shader(payload);
set_pixel(Eigen::Vector2i(x, y), pixel_color);
}
}
}
}
}
  1. 修改函数 get_projection_matrix() in main.cpp: 将你自己在之前的实验中 实现的投影矩阵填到此处,此时你可以运行 ./Rasterizer output.png normal 来观察法向量实现结果。

按照要求去做,我们将之前的实现复制过来

Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
{
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
float n = -zNear;
float f = -zFar;
float angle = eye_fov * MY_PI / 180.0;
float t = tan(angle / 2) * fabs(zNear);
float r = aspect_ratio * t;
float l = -r;
float b = -t;
Eigen::Matrix4f persp_to_ortho;
persp_to_ortho << n, 0, 0, 0, 0, n, 0, 0, 0, 0, n + f, -n * f, 0, 0, 1, 0;
Eigen::Matrix4f ortho;
ortho << 2 / (r - l), 0, 0, -(r + l) / (r - l), 0, 2 / (t - b), 0, -(t + b) / (t - b), 0, 0,
2 / (n - f), -(n + f) / (n - f), 0, 0, 0, 1;
projection = ortho * persp_to_ortho;
return projection;
}

然后运行对应的命令,你会得到下面这张图 看起来是正确的,但是方向错了,这是games101中的经典问题,由于我们之前的投影矩阵尝试修复这个问题,我们尝试改回去会怎样

float n = zNear;
float f = zFar;

我们得到了下面的图,可以看到已经是正面了,但是上下左右颠倒了。我们修改一下ortho,翻转它的x和y轴,最终我们得到的矩阵是

Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
float zNear, float zFar) {
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
float n = zNear;
float f = zFar;
float angle = eye_fov * MY_PI / 180.0;
float t = tan(angle / 2) * fabs(zNear);
float r = aspect_ratio * t;
float l = -r;
float b = -t;
Eigen::Matrix4f persp_to_ortho;
persp_to_ortho << n, 0, 0, 0, 0, n, 0, 0, 0, 0, n + f, -n * f, 0, 0, 1, 0;
Eigen::Matrix4f ortho;
ortho << -2 / (r - l), 0, 0, (r + l) / (r - l), 0, -2 / (t - b), 0,
(t + b) / (t - b), 0, 0, 2 / (n - f), -(n + f) / (n - f), 0, 0, 0, 1;
projection = ortho * persp_to_ortho;
return projection;
}

得到的结果是

  1. 修改函数 phong_fragment_shader() in main.cpp: 实现 Blinn-Phong 模型计 算 Fragment Color.

在实现phong_fragment_shader()之前我们看看fragment_shader()的逻辑看看它是怎么实现可视化法线的,当我们输入./Rasterizer output.png normal指令的时候走的是这个分支

else if (argc == 3 && std::string(argv[2]) == "normal") {
std::cout << "Rasterizing using the normal shader\n";
active_shader = normal_fragment_shader;
}

而active_shader是Shader中的一个回调函数,这让你可以自定义Shader 而normal_fragment_shader就是直接将

Eigen::Vector3f normal_fragment_shader(const fragment_shader_payload &payload) {
Eigen::Vector3f return_color = (payload.normal.head<3>().normalized() +
Eigen::Vector3f(1.0f, 1.0f, 1.0f)) /
2.f;
Eigen::Vector3f result;
result << return_color.x() * 255, return_color.y() * 255,
return_color.z() * 255;
return result;
}

其中下面代码的作用是将法线从区间 [−1,1][-1, 1] 映射到 [0,1][0, 1]

Eigen::Vector3f return_color = (payload.normal.head<3>().normalized() +
Eigen::Vector3f(1.0f, 1.0f, 1.0f)) /
2.f;

最后将[0,1][0, 1]映射到颜色进行输出 phong_fragment_shader()的具体实现如下,解析看代码和注释

Eigen::Vector3f phong_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};
// 高光项的p
float p = 150;
Eigen::Vector3f color = payload.color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;
Eigen::Vector3f result_color = {0, 0, 0};
// Ambient通常只计算一次,不放在光源循环里也可以
// 我们先计算基础环境光,并且累加到结果颜色中
Eigen::Vector3f ambient = ka.cwiseProduct(amb_light_intensity);
result_color += ambient;
for (auto &light : lights) {
// 计算光照向量 从点指向光源
Eigen::Vector3f l = (light.position - point).normalized();
// 计算观察向量 从点指向相机
Eigen::Vector3f v = (eye_pos - point).normalized();
// 计算半程向量
Eigen::Vector3f h = (l + v).normalized();
// 距离的平方,用于能量衰减
float r2 = (light.position - point).squaredNorm();
// 漫反射项 Kd * (I/r^2) * max(0, n·l)
float diff_dot = std::max(0.0f, normal.dot(l));
Eigen::Vector3f diffuse = kd.cwiseProduct(light.intensity / r2) * diff_dot;
// 高光项 Ks * (I/r^2) * pow(max(0, n·h), p)
float spec_dot = std::max(0.0f, normal.dot(h));
Eigen::Vector3f specular =
ks.cwiseProduct(light.intensity / r2) * std::pow(spec_dot, p);
// 累加结果
result_color += (diffuse + specular);
}
return result_color * 255.f;
}

最后我们得到的效果是

  1. 修改函数 texture_fragment_shader() in main.cpp: 在实现 Blinn-Phong 的基础上,将纹理颜色视为公式中的 kd,实现 Texture Shading Fragment Shader.

这个非常简单和上一题类似,看代码和注释吧

Eigen::Vector3f
texture_fragment_shader(const fragment_shader_payload &payload) {
Eigen::Vector3f return_color = {0, 0, 0};
if (payload.texture) {
float u = payload.tex_coords.x();
float v = payload.tex_coords.y();
// 将 uv 限制在 [0, 1] 范围内
u = std::clamp(u, 0.0f, 1.0f);
v = std::clamp(v, 0.0f, 1.0f);
// 使用处理后的坐标进行采样
return_color = payload.texture->getColor(u, v);
}
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);
// 将纹理颜色归一化到 [0, 1] 范围作为漫反射系数 kd,
// 其他的和 bilnn-phong shader 类似
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 point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;
Eigen::Vector3f result_color = {0, 0, 0};
Eigen::Vector3f ambient = ka.cwiseProduct(amb_light_intensity);
result_color += ambient;
for (auto &light : lights) {
Eigen::Vector3f l = (light.position - point).normalized();
Eigen::Vector3f v = (eye_pos - point).normalized();
Eigen::Vector3f h = (l + v).normalized();
float r2 = (light.position - point).squaredNorm();
float diff_dot = std::max(0.0f, normal.dot(l));
Eigen::Vector3f diffuse = kd.cwiseProduct(light.intensity / r2) * diff_dot;
float spec_dot = std::max(0.0f, normal.dot(h));
Eigen::Vector3f specular =
ks.cwiseProduct(light.intensity / r2) * std::pow(spec_dot, p);
result_color += (diffuse + specular);
}
return result_color * 255.f;
}

得到下面的效果

  1. 修改函数 bump_fragment_shader() in main.cpp: 在实现 Blinn-Phong 的 基础上,仔细阅读该函数中的注释,实现 Bump mapping.

这个题根据TODO的要求去做就行了

Eigen::Vector3f bump_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 n = payload.normal;
float kh = 0.2, kn = 0.1;
// 我们根据TODO去做就好了
// // 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)
// Let n = normal = (x, y, z)
float x = n.x();
float y = n.y();
float z = n.z();
// 计算切线 t
// Vector t = (x*y/sqrt(x*x+z*z), sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
Eigen::Vector3f t;
t << x * y / std::sqrt(x * x + z * z), std::sqrt(x * x + z * z),
z * y / std::sqrt(x * x + z * z);
// 计算Bitangent b
// Vector b = n cross product t
Eigen::Vector3f b = n.cross(t);
// 构建 TBN 矩阵
// Matrix TBN = [t b n]
Eigen::Matrix3f TBN;
TBN << t.x(), b.x(), n.x(), t.y(), b.y(), n.y(), t.z(), b.z(), n.z();
// 获取纹理信息
float u = payload.tex_coords.x();
float v = payload.tex_coords.y();
float w = payload.texture->width;
float h = payload.texture->height;
// 为了防止 OpenCV 越界,我们必须对坐标做 Clamp 处理
auto get_h = [&](float target_u, float target_v) {
// 限制在 [0, 1] 范围内
target_u = std::clamp(target_u, 0.0f, 1.0f);
target_v = std::clamp(target_v, 0.0f, 1.0f);
return payload.texture->getColor(target_u, target_v).norm();
};
float h_uv = get_h(u, v);
// dU = kh * kn * (h(u+1/w,v)-h(u,v))
// dV = kh * kn * (h(u,v+1/h)-h(u,v))
float dU = kh * kn * (get_h(u + 1.0f / w, v) - h_uv);
float dV = kh * kn * (get_h(u, v + 1.0f / h) - h_uv);
// 计算扰动后的法线 ln Vector ln = (-dU, -dV, 1)
Eigen::Vector3f ln = {-dU, -dV, 1.0f};
// 转换到观察空间并归一化 Normal n = normalize(TBN * ln)
n = (TBN * ln).normalized();
Eigen::Vector3f result_color = n;
return result_color * 255.f;
}

得到的下面的效果

  1. 修改函数 displacement_fragment_shader() in main.cpp: 在实现 Bump mapping 的基础上,实现 displacement mapping.

还是着色TODO去做

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;
float kh = 0.2f, kn = 0.1f;
// 取出原始数据
Eigen::Vector3f original_n = payload.normal;
Eigen::Vector3f point = payload.view_pos;
// 构造 TBN(和 bump_fragment_shader 完全一致)
float x = original_n.x();
float y = original_n.y();
float z = original_n.z();
Eigen::Vector3f t;
float denom = std::sqrt(x * x + z * z);
if (denom < 1e-6f) { // 防止法线几乎沿 Y 轴导致除零
t << 0.0f, 1.0f, 0.0f;
} else {
t << x * y / denom, denom, z * y / denom;
}
Eigen::Vector3f b = original_n.cross(t);
Eigen::Matrix3f TBN;
TBN << t.x(), b.x(), original_n.x(), t.y(), b.y(), original_n.y(), t.z(),
b.z(), original_n.z();
// 采样高度图
float u = payload.tex_coords.x();
float v = payload.tex_coords.y();
float w = payload.texture->width;
float h = payload.texture->height;
auto get_h = [&](float tu, float tv) -> float {
tu = std::clamp(tu, 0.0f, 1.0f);
tv = std::clamp(tv, 0.0f, 1.0f);
return payload.texture->getColor(tu, tv).norm();
};
float h_uv = get_h(u, v);
// 位移顶点位置(这是 displacement 和 bump 的核心区别)
// Position p = p + kn *n * h(u,v)
point = payload.view_pos + kn * original_n * h_uv;
// 计算扰动法线(和 bump 完全一致)
float dU = kh * kn * (get_h(u + 1.0f / w, v) - h_uv);
float dV = kh * kn * (get_h(u, v + 1.0f / h) - h_uv);
Eigen::Vector3f ln = {-dU, -dV, 1.0f};
Eigen::Vector3f n = (TBN * ln).normalized(); // 最终使用的法线
// 使用位移后的 point + 扰动后的 n 进行完整 Blinn-Phong 光照
Eigen::Vector3f result_color = {0, 0, 0};
// ambient
Eigen::Vector3f ambient = ka.cwiseProduct(amb_light_intensity);
result_color += ambient;
for (auto &light : lights) {
Eigen::Vector3f l = (light.position - point).normalized();
Eigen::Vector3f view = (eye_pos - point).normalized();
Eigen::Vector3f h_vec = (l + view).normalized();
float r2 = (light.position - point).squaredNorm();
// diffuse
float diff_dot = std::max(0.0f, n.dot(l));
Eigen::Vector3f diffuse = kd.cwiseProduct(light.intensity / r2) * diff_dot;
// specular
float spec_dot = std::max(0.0f, n.dot(h_vec));
Eigen::Vector3f specular =
ks.cwiseProduct(light.intensity / r2) * std::pow(spec_dot, p);
result_color += diffuse + specular;
}
return result_color * 255.f;
}

最后我们得到

[Bonus 3 分] 尝试更多模型: 找到其他可用的.obj 文件,提交渲染结果并 把模型保存在 /models 目录下。这些模型也应该包含 Vertex Normal 信息。

我们调整一下摄像机位置和物体的模型变换就能得到下面这幅图,由于不好调整就这样吧

[Bonus 5 分] 双线性纹理插值: 使用双线性插值进行纹理采样, 在 Texture 类中实现一个新方法 Vector3f getColorBilinear(float u, float v) 并 通过 fragment shader 调用它。为了使双线性插值的效果更加明显,你应该 考虑选择更小的纹理图。请同时提交纹理插值与双线性纹理插值的结果,并 进行比较。

这5分有点难拿,后面补吧,实现不好

Games101

已完结

第 10 篇,共 14 篇本文属于系列

【CG】Games101-010学习笔记 & 作业解析
https://blog.cglab.top/posts/games_series/games101_10/
作者
Artichoke
发布于
2026-04-02
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 205 天

部分内容可能已过时