PyTinyRenderer软渲染器-05
到目前为止,我们已经成功地实现了一些基本的三维渲染功能。我们从最基础的模型加载开始,接着实现了线段绘制,然后进行了三角形光栅化,涉及了平面着色、光照模型与背面剔除。还使用了Z-Buffer算法处理了隐藏面消除问题,以及成功地将纹理贴在了模型上。使用了三种着色方法——Flat Shading、Gouraud Shading、Phong Shading进行了片元着色。已经对渲染流程有了一个大致的理解。但是,随着我们不断地添加新的功能,我们的代码变得越来越复杂。为了使其更加模块化,更易于理解和扩展。我们将会把相关的功能进行归类,将复杂的功能分解为更小的、更易于管理的部分,为我们接下来的章节打好基础。
关于Z-Buffer的小问题
在这之前我们注意一个小问题,深度值z是在三维空间中计算的,它的范围可能远大于255,也可能有负值。这可能导致深度图在颜色上没有明显的变化。
所以正确的做法是先找到z值的最大和最小值,然后将这个范围映射到0到255的范围。这样,深度图就会有更明显的深度变化。于是我们可以在渲染结束后,遍历一遍Z-buffer,找到最大和最小的深度值,然后用这两个值来做归一化。另外需要移除 -sys.maxsize - 1
的值。这是因为这个值是初始化Z-buffer的时候设定的,它代表的是无穷远的深度。在计算最大和最小深度值时,不应该考虑这个值。
因为相机通常位于原点,而场景则位于Z轴的负方向。因此,一个物体离相机越远,它的Z值就越小(也就是说,更负)。在进行归一化和映射到0-255的范围时,需要确保正确处理这些负数,于是可以先将Z值偏移,使其变为正数,然后再进行归一化和映射。
1z_image = MyImage((width, height))
2
3# -sys.maxsize - 1 最小值
4z_buffer = [-sys.maxsize - 1] * width * height
5
6# 在所有三角形都被渲染后,遍历Z-buffer以找到最大和最小的深度值
7z_min = min(z for z in z_buffer if z != -sys.maxsize - 1)
8z_max = max(z for z in z_buffer if z != -sys.maxsize - 1)
9
10# 然后,遍历Z-buffer,对每个深度值进行归一化,然后将其映射到0到255的范围
11for i in range(len(z_buffer)):
12 if z_buffer[i] != -sys.maxsize - 1:
13 # 首先将Z值偏移,使其变为正数
14 z_positive = z_buffer[i] - z_min
15 # 然后归一化深度值
16 z_normalized = z_positive / (z_max - z_min)
17 # 映射到0到255的范围
18 depth_color = int(z_normalized * 255)
19 # 在深度图中设置像素颜色
20 x = i % width
21 y = i // width
22 z_img.putpixel((x, y), (depth_color, depth_color, depth_color))
23
24z_image.save('z_out.bmp')
项目代码重构
在TinyRenderer项目中,作者首先提醒我们不要直接使用他的代码,而应该自己动手编写。实现顶点和片元着色器是本节的重点,我们也就完全基于之前的代码修改而来,核心目的是抽取出顶点着色器和片元着色器的逻辑,所以我们抽取出一个GL的代码,里面的内容固定不变,把顶点着色器和片元着色器的逻辑暴露出来即可。
在此之前先看看其他基础类的变化:
Vec2、Vec3,支持了使用数组构造,支持单个参数构造,支持下标访问:
1import math
2
3
4class Vec2:
5 def __init__(self, x, y=None):
6 if isinstance(x, (list, tuple)) and len(x) == 2 and y is None:
7 self.x, self.y = x[0], x[1]
8 else:
9 self.x, self.y = x, y
10
11 def __add__(self, other):
12 return Vec2(self.x + other.x, self.y + other.y)
13
14 def __sub__(self, other):
15 return Vec2(self.x - other.x, self.y - other.y)
16
17 def __mul__(self, other):
18 if isinstance(other, (int, float)): # 向量和标量的乘法
19 return Vec2(self.x * other, self.y * other)
20 elif isinstance(other, Vec2): # 两个向量的点积
21 return self.x * other.x + self.y * other.y
22
23 def __rmul__(self, scalar):
24 return self.__mul__(scalar)
25
26 def norm(self):
27 return math.sqrt(self.x * self.x + self.y * self.y)
28
29 def normalize(self, l=1):
30 norm = self.norm()
31 self.x *= l / norm
32 self.y *= l / norm
33 return self
34
35 def __getitem__(self, index):
36 if index == 0:
37 return self.x
38 elif index == 1:
39 return self.y
40 else:
41 raise IndexError("Index out of range")
42
43 def __setitem__(self, index, value):
44 if index == 0:
45 self.x = value
46 elif index == 1:
47 self.y = value
48 else:
49 raise IndexError("Index out of range")
50
51 def __str__(self):
52 return f'({self.x}, {self.y})'
53
54
55class Vec3:
56 def __init__(self, x, y=None, z=None):
57 if isinstance(x, (list, tuple)) and len(x) == 3 and y is None and z is None:
58 self.x, self.y, self.z = x[0], x[1], x[2]
59 else:
60 self.x, self.y, self.z = x, y, z
61
62
63 def __add__(self, other):
64 return Vec3(self.x + other.x, self.y + other.y, self.z + other.z)
65
66 def __sub__(self, other):
67 return Vec3(self.x - other.x, self.y - other.y, self.z - other.z)
68
69 def __mul__(self, other):
70 if isinstance(other, (int, float)): # 向量和标量的乘法
71 return Vec3(self.x * other, self.y * other, self.z * other)
72 elif isinstance(other, Vec3): # 两个向量的点积
73 return self.x * other.x + self.y * other.y + self.z * other.z
74
75 def __rmul__(self, scalar):
76 return self.__mul__(scalar)
77
78 def cross(self, other):
79 return Vec3(self.y * other.z - self.z * other.y,
80 self.z * other.x - self.x * other.z,
81 self.x * other.y - self.y * other.x)
82
83 def norm(self):
84 return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
85
86 def normalize(self, l=1):
87 norm = self.norm()
88 self.x *= l / norm
89 self.y *= l / norm
90 self.z *= l / norm
91 return self
92
93 def __getitem__(self, index):
94 if index == 0:
95 return self.x
96 elif index == 1:
97 return self.y
98 elif index == 2:
99 return self.z
100 else:
101 raise IndexError("Index out of range")
102
103 def __setitem__(self, index, value):
104 if index == 0:
105 self.x = value
106 elif index == 1:
107 self.y = value
108 elif index == 2:
109 self.z = value
110 else:
111 raise IndexError("Index out of range")
112
113 def __str__(self):
114 return f'({self.x}, {self.y}, {self.z})'
OBJ读取文件的更改,支持了顶点、纹理、法向量的读取:
1from PIL import Image
2
3from vector import Vec3, Vec2
4
5
6class OBJFile:
7 def __init__(self, filename):
8 self.model_file = filename
9 self.vertex: list[Vec3] = [] # 顶点坐标数组
10 self.tex_coord: list[Vec2] = [] # 纹理坐标数组
11 self.norms: list[Vec3] = [] # 法向量数组
12
13 self.facet_vrt: list[int] = [] # 面片顶点索引
14 self.facet_tex: list[int] = [] # 面片纹理索引
15 self.facet_nrm: list[int] = [] # 面片法向量索引
16
17 self.diffuse_map: Image = None # 漫反射贴图
18 self.normal_map: Image = None # 法线贴图
19 self.specular_map: Image = None # 镜面反射贴图
20
21 self.parse()
22
23 def parse(self):
24 with open(self.model_file, 'r') as file:
25 for line in file:
26 components = line.strip().split()
27 if not components:
28 continue
29 if components[0] == 'v':
30 self.vertex.append(Vec3(*map(float, components[1:4])))
31 elif components[0] == 'vn':
32 self.norms.append(Vec3(*map(float, components[1:4])))
33 elif components[0] == 'vt':
34 self.tex_coord.append(Vec2(float(components[1]), float(components[2])))
35 # self.tex_coord.append(float(coord) for coord in components[1:])
36 elif components[0] == 'f':
37 for sp in components[1:]:
38 p = sp.split('/')
39 self.facet_vrt.append(int(p[0])-1)
40 self.facet_tex.append(int(p[1])-1)
41 self.facet_nrm.append(int(p[2])-1)
42 # 打印顶点、面片、纹理坐标和法向量的数量
43 print(f"# v# {self.n_vertex()} f# {self.n_face()} vt# {len(self.tex_coord)} vn# {len(self.norms)}")
44
45 # 加载纹理
46 self.load_texture("_diffuse.tga", self.diffuse_map)
47 # self.load_texture("_nm_tangent.tga", self.normal_map)
48 # self.load_texture("_spec.tga", self.specular_map)
49
50 def n_vertex(self) -> int:
51 # 返回顶点数量
52 return len(self.vertex)
53
54 def n_face(self) -> int:
55 # 返回面片数量
56 return int(len(self.facet_vrt) / 3)
57
58 def vert(self, i: int, n: int = None) -> Vec3:
59 # 返回顶点坐标
60 if n is None:
61 return self.vertex[i]
62 else:
63 return self.vertex[self.facet_vrt[i * 3 + n]]
64
65 def load_texture(self, suffix: str, img: Image):
66 # 加载纹理
67 dot = self.model_file.rfind('.')
68 if dot == -1:
69 return
70 tex_file = self.model_file[:dot] + suffix
71 self.diffuse_map = Image.open(tex_file)
72 print(f'Loading texture: {tex_file}--->{img}')
73 # print(f"texture file {tex_file} loading {'ok' if img.load(tex_file) else 'failed'}")
74
75 def normal(self, uvf: Vec2 = None, i: int = None, n: int = None) -> Vec3:
76 """
77 返回法向量
78 """
79 if uvf is not None:
80 # 从法线贴图获取法线
81 c = self.normal_map.getpixel((uvf.x * self.normal_map.width, uvf.y * self.normal_map.height))
82 return Vec3(c[2], c[1], c[0]) * (2. / 255.) - Vec3(1, 1, 1)
83 # 从法向量数组获取法线
84 return self.norms[self.facet_nrm[i * 3 + n]]
85
86 def uv(self, iface: int, n: int) -> Vec2:
87 """
88 返回纹理坐标
89 """
90 return self.tex_coord[self.facet_tex[iface * 3 + n]]
对于颜色,也抽取一个Color类:
1class Color:
2 def __init__(self, r, g=None, b=None):
3 if isinstance(r, (list, tuple)) and len(r) == 3 and g is None and b is None:
4 self.r, self.g, self.b = r[0], r[1], r[2]
5 else:
6 self.r, self.g, self.b = r, g, b
7
8 def __getitem__(self, index):
9 if index == 0:
10 return self.r
11 elif index == 1:
12 return self.g
13 elif index == 2:
14 return self.b
15 else:
16 raise IndexError("Index out of range")
17
18 def __setitem__(self, index, value):
19 if index == 0:
20 self.r = value
21 elif index == 1:
22 self.g = value
23 elif index == 2:
24 self.b = value
25 else:
26 raise IndexError("Index out of range")
27
28 def get_color(self):
29 return (self.r, self.g, self.b)
现在就来到了最重要的部分,拆解出 gl.py
, triangle这部分是固定代码也放在 gl.py 中:
1import numpy as np
2
3from camera import Camera
4from image import MyImage
5from matrix import Matrix
6from shader import IShader
7from vector import Vec3, Vec2
8
9
10def local_2_homo(v: Vec3):
11 """
12 局部坐标变换成齐次坐标
13 """
14 m = Matrix(4, 1)
15 m[0][0] = v.x
16 m[1][0] = v.y
17 m[2][0] = v.z
18 m[3][0] = 1.0
19 return m
20
21
22# 模型变换矩阵
23def model_matrix():
24 return Matrix.identity(4)
25
26
27def view_matrix(camera: Camera):
28 r_inverse = np.identity(4)
29 t_inverse = np.identity(4)
30 for i in range(3):
31 r_inverse[0][i] = camera.right[i]
32 r_inverse[1][i] = camera.up[i]
33 r_inverse[2][i] = -camera.front[i]
34
35 t_inverse[i][3] = -camera.position[i]
36 view = np.dot(r_inverse, t_inverse)
37 return Matrix.from_np(view)
38
39
40# 透视投影变换矩阵(调整FOV)
41def projection_matrix(eye):
42 projection = Matrix.identity(4)
43 projection[3][2] = -1.0 / eye.z * 0.01
44 return projection
45
46
47# 此时我们的所有的顶点已经经过了透视投影变换,接下来需要进行透视除法
48def projection_division(m: Matrix):
49 m[0][0] = m[0][0] / m[3][0]
50 m[1][0] = m[1][0] / m[3][0]
51 m[2][0] = m[2][0] / m[3][0]
52 m[3][0] = 1.0
53 return m
54
55
56def viewport_matrix(x, y, w, h, depth):
57 """
58 视口变换将NDC坐标转换为屏幕坐标
59 """
60 m = Matrix.identity(4)
61 m[0][3] = x + w / 2.
62 m[1][3] = y + h / 2.
63 m[2][3] = depth / 2.
64
65 m[0][0] = w / 2.
66 m[1][1] = h / 2.
67 m[2][2] = depth / 2.
68 return m
69
70
71def homo_2_vertices(m: Matrix):
72 """
73 去掉第四个分量,将其恢复到三维坐标
74 """
75 return Vec3([int(m[0][0]), int(m[1][0]), int(m[2][0])])
76
77
78def get_mvp(camera, eye, width, height, depth):
79 model_ = model_matrix()
80 view_ = view_matrix(camera)
81 projection_ = projection_matrix(eye)
82 viewport_ = viewport_matrix(width / 8, height / 8, width * 3 / 4, height * 3 / 4, depth)
83 return model_, view_, projection_, viewport_
84
85
86def triangle_area_2d(a: Vec2, b: Vec2, c: Vec2) -> float:
87 """
88 计算三角形面积
89 """
90 return .5 * ((b.y - a.y) * (b.x + a.x) + (c.y - b.y) * (c.x + b.x) + (a.y - c.y) * (a.x + c.x))
91
92
93def barycentric(A, B, C, P):
94 """
95 计算重心坐标 u, v, w
96 """
97 total_area = triangle_area_2d(A, B, C)
98 if total_area == 0:
99 return None # 或者抛出一个异常,或者返回一个特殊的值
100 u = triangle_area_2d(P, B, C) / total_area
101 v = triangle_area_2d(P, C, A) / total_area
102 w = triangle_area_2d(P, A, B) / total_area
103 return Vec3(u, v, w)
104
105
106def triangle(screen_coords: list[Vec3], shader: IShader, img: MyImage, z_buffer):
107 p0, p1, p2 = screen_coords
108 min_x = max(0, min(p0.x, p1.x, p2.x))
109 max_x = min(img.width - 1, max(p0.x, p1.x, p2.x))
110 min_y = max(0, min(p0.y, p1.y, p2.y))
111 max_y = min(img.height - 1, max(p0.y, p1.y, p2.y))
112 P = Vec2((0, 0))
113
114 # 遍历包围盒内的每个像素
115 for P.y in range(min_y, max_y + 1):
116 for P.x in range(min_x, max_x + 1):
117 # 计算当前像素的重心坐标
118 bc_screen = barycentric(p0, p1, p2, P)
119 if bc_screen is None:
120 continue
121 # 如果像素的重心坐标的任何一个分量小于0,那么这个像素就在三角形的外部,我们就跳过它
122 if bc_screen.x < 0 or bc_screen.y < 0 or bc_screen.z < 0:
123 continue
124 skip, color = shader.fragment(bc_screen)
125 if skip:
126 continue
127 # 计算当前像素的深度
128 z = p0.z * bc_screen.x + p1.z * bc_screen.y + p2.z * bc_screen.z
129 # 检查Z缓冲区,如果当前像素的深度比Z缓冲区中的值更近,那么就更新Z缓冲区的值,并绘制像素
130 idx = P.x + P.y * img.width
131 if z_buffer[idx] < z:
132 z_buffer[idx] = z
133 img.putpixel((P.x, P.y), color)
IShader.py抽取Shader的结构:
1from abc import ABC, abstractmethod
2
3from PIL import Image
4
5from color import Color
6from vector import Vec2, Vec3
7
8
9class IShader(ABC):
10 @staticmethod
11 def sample_2d(img: Image, uvf: Vec2):
12 pixel = img.getpixel((uvf.x * img.width, uvf.y * img.height))
13 return pixel
14
15 @abstractmethod
16 def vertex(self, iface: int, n: int):
17 pass
18
19 @abstractmethod
20 def fragment(self, bar: Vec3) -> (bool, Color):
21 pass
main.py 中实现这个Shader:
1import sys
2
3from camera import Camera
4from color import Color
5from gl import triangle, get_mvp, projection_division, homo_2_vertices, local_2_homo
6from image import MyImage
7from obj import OBJFile
8from shader import IShader
9from vector import Vec3, Vec2
10
11width = 900
12height = 900
13depth = 255
14
15# 光照方向
16light_dir = Vec3(0, 0, 1)
17
18# 摄像机摆放的位置
19eye = Vec3(1, 1, 2)
20center = Vec3(0, 0, 0)
21up = Vec3(0, 1, 0)
22
23camera = Camera(eye, up, center - eye)
24
25
26model_, view_, projection_, viewport_ = get_mvp(camera, eye, width, height, depth)
27
28
29class MyShader(IShader):
30 def __init__(self):
31 self.varying_intensity: Vec3 = Vec3(0, 0, 0)
32 self.uv_coords: list[Vec2] = [Vec2.zero()] * 3
33
34 def vertex(self, iface: int, n: int) -> Vec3:
35 """
36 顶点着色器
37 :param iface: 面索引
38 :param n: 顶点索引
39 :return:
40 """
41 self.varying_intensity[n] = max(0, obj.normal(None, iface, n) * light_dir)
42 self.uv_coords[j] = obj.uv(i, n)
43 v: Vec3 = obj.vert(i, n)
44 return homo_2_vertices(viewport_ * projection_division(projection_ * view_ * model_ * local_2_homo(v)))
45
46 def fragment(self, bar: Vec3):
47 """
48 片元着色器
49 :param bar: 重心坐标
50 :return:
51 """
52 intensity = self.varying_intensity * bar
53 if intensity < 0:
54 return True, None
55
56 uv0, uv1, uv2 = self.uv_coords
57 uv = uv0 * bar.x + uv1 * bar.y + uv2 * bar.z
58 tga = obj.diffuse_map
59 color = tga.getpixel((int(uv.x * tga.width), tga.height - 1 - int(uv.y * tga.height)))
60 color = Color(int(color[0] * intensity), int(color[1] * intensity), int(color[2] * intensity))
61 return False, color
62
63
64if __name__ == '__main__':
65 shader: MyShader = MyShader()
66 image = MyImage((width, height))
67 z_image = MyImage((width, height))
68
69 # -sys.maxsize - 1 最小值
70 z_buffer = [-sys.maxsize - 1] * width * height
71
72 obj = OBJFile('african_head.obj')
73
74 for i in range(obj.n_face()):
75 screen_coords = [None, None, None] # 第i个面片三个顶点的屏幕坐标
76 world_coords = [None, None, None] # 第i个面片三个顶点的世界坐标
77 uv_coords = [None, None, None]
78 for j in range(3):
79 screen_coords[j] = shader.vertex(i, j)
80 triangle(screen_coords, shader, image, z_buffer)
81 image.save('out.bmp')
82
83 z_image = MyImage((width, height))
84 # 在所有三角形都被渲染后,遍历Z-buffer以找到最大和最小的深度值
85 z_min = min(z for z in z_buffer if z != -sys.maxsize - 1)
86 z_max = max(z for z in z_buffer if z != -sys.maxsize - 1)
87
88 # 然后,遍历Z-buffer,对每个深度值进行归一化,然后将其映射到0到255的范围
89 for i in range(len(z_buffer)):
90 if z_buffer[i] != -sys.maxsize - 1:
91 # 首先将Z值偏移,使其变为正数
92 z_positive = z_buffer[i] - z_min
93 # 然后归一化深度值
94 z_normalized = z_positive / (z_max - z_min)
95 # 映射到0到255的范围
96 depth_color = int(z_normalized * 255)
97 # 在深度图中设置像素颜色
98 x = i % width
99 y = i // width
100 z_image.putpixel((x, y), (depth_color, depth_color, depth_color))
101
102 z_image.save('z_out.bmp')
好了,现在我们就只需要关心 shader中vertex与fragment的实现即可。
现在只需要稍微改下fragment就可以实现TinyRenderer的效果:
1def fragment(self, bar: Vec3):
2 intensity = self.varying_intensity * bar
3 if intensity < 0:
4 return True, None
5
6 if intensity > .85:
7 intensity = 1
8 elif intensity > .60:
9 intensity = .80
10 elif intensity > .45:
11 intensity = .60
12 elif intensity > .30:
13 intensity = .45
14 elif intensity > .15:
15 intensity = .30
16 else:
17 intensity = 0
18 color = Color(int(255 * intensity), int(155 * intensity), 0)
19 return False, color
应用法线贴图
在此需要更改obj文件的一个方法,如果 UV 坐标系的 y 方向和图像的 y 方向是相反的,那么就需要反转 y 坐标:
1def normal(self, uvf: Vec2 = None, i: int = None, n: int = None) -> Vec3:
2 """
3 返回法向量
4 """
5 if uvf is not None:
6 # 从法线贴图获取法线
7 c = self.normal_map.getpixel((uvf.x * self.normal_map.width, self.normal_map.height - 1 - uvf.y * self.normal_map.height))
8 return Vec3(c[2], c[1], c[0]) * (2. / 255.) - Vec3(1, 1, 1)
9 # 从法向量数组获取法线
10 return self.norms[self.facet_nrm[i * 3 + n]]
vector也新增了to_matrix的方法:
1def to_matrix(self):
2 m = Matrix(3, 1).m
3 m[0, 0] = self.x
4 m[1, 0] = self.y
5 m[2, 0] = self.z
6 return m
7
8def to_matrix(self):
9 m = Matrix(2, 1).m
10 m[0, 0] = self.x
11 m[1, 0] = self.y
12 return m
接下来应用法线贴图:
1import sys
2
3from camera import Camera
4from color import Color
5from gl import triangle, get_mvp, projection_division, homo_2_vertices, local_2_homo
6from image import MyImage
7from matrix import Matrix
8from obj import OBJFile
9from shader import IShader
10from vector import Vec3, Vec2
11
12width = 900
13height = 900
14depth = 255
15
16# 光照方向
17light_dir = Vec3(0, 0, 1)
18
19# 摄像机摆放的位置
20eye = Vec3(1, 1, 2)
21center = Vec3(0, 0, 0)
22up = Vec3(0, 1, 0)
23
24camera = Camera(eye, up, center - eye)
25
26
27model_, view_, projection_, viewport_ = get_mvp(camera, eye, width, height, depth)
28
29
30class MyShader(IShader):
31 def __init__(self):
32 self.varying_uv: Matrix = Matrix(2, 3)
33 self.uniform_M: Matrix = Matrix.identity(4)
34 self.uniform_MIT: Matrix = Matrix.identity(4)
35
36 def vertex(self, iface: int, n: int) -> Vec3:
37 """
38 顶点着色器
39 :param iface: 面索引
40 :param n: 顶点索引
41 :return:
42 """
43 self.varying_uv.set_col(n, obj.uv(iface, n))
44 v: Vec3 = obj.vert(iface, n)
45 return homo_2_vertices(viewport_ * projection_division(projection_ * view_ * model_ * local_2_homo(v)))
46
47
48 def fragment(self, bar: Vec3):
49 """
50 片元着色器
51 :param bar: 重心坐标
52 :return:
53 """
54 uv: Vec2 = Vec2((self.varying_uv.m @ bar.to_matrix())[0][0], (self.varying_uv.m @ bar.to_matrix())[1][0])
55
56 n = (self.uniform_MIT * local_2_homo(obj.normal(uv))).m
57 n: Vec3 = Vec3(n[0][0], n[1][0], n[2][0]).normalize()
58 l = (self.uniform_M * local_2_homo(light_dir)).m
59 l: Vec3 = Vec3(l[0][0], l[1][0], l[2][0]).normalize()
60
61 intensity: float = max(0.0, n * l)
62 if intensity < 0:
63 return True, None
64 tga = obj.diffuse_map
65 color = tga.getpixel((int(uv.x * tga.width), tga.height - 1 - int(uv.y * tga.height)))
66 # color = [255, 255, 255]
67 color = Color(int(color[0] * intensity), int(color[1] * intensity), int(color[2] * intensity))
68 return False, color
69
70
71if __name__ == '__main__':
72 shader: MyShader = MyShader()
73 shader.uniform_M = projection_ * model_ * view_
74 shader.uniform_MIT = (projection_ * model_ * view_).transpose()
75
76 image = MyImage((width, height))
77 z_image = MyImage((width, height))
78
79 # -sys.maxsize - 1 最小值
80 z_buffer = [-sys.maxsize - 1] * width * height
81
82 obj = OBJFile('african_head.obj')
83
84 for i in range(obj.n_face()):
85 screen_coords = [None, None, None] # 第i个面片三个顶点的屏幕坐标
86 world_coords = [None, None, None] # 第i个面片三个顶点的世界坐标
87 uv_coords = [None, None, None]
88 for j in range(3):
89 screen_coords[j] = shader.vertex(i, j)
90 triangle(screen_coords, shader, image, z_buffer)
91 image.save('out.bmp')
法线贴图通常看起来是蓝色的,这是因为它们是用来存储表面法线方向信息。在法线贴图中,RGB 颜色的每一个通道(红、绿、蓝)都对应了三维空间中的一个轴(X、Y、Z)。这种方式可以将一个三维向量编码为一个颜色。
在一个标准的法线贴图中:
- 红色通道对应 X 方向,从左(0)到右(255);
- 绿色通道对应 Y 方向,从下(0)到上(255);
- 蓝色通道对应 Z 方向,从里(0)到外(255)。
当一个表面的法线直接指向观察者时,对应的法线颜色是 (128, 128, 255)。这是因为 X 和 Y 方向(红色和绿色通道)没有偏移(128 是中间值,表示没有偏移),而 Z 方向(蓝色通道)是最大值 255,表示法线指向观察者。这就是为什么法线贴图通常看起来是蓝色的原因。
应用镜面反射贴图
1def specular(self, uvf: Vec2 = None):
2 c = self.specular_map.getpixel((uvf.x * self.specular_map.width, self.specular_map.height - 1 - uvf.y * self.specular_map.height))
3 return c / 128
对于镜面反射可以先计算反射光线向量r,然后计算镜面反射specular。最后,它将颜色的每个分量乘以 (intensity + 0.6 * specular)
,并确保结果不超过255。这将模拟镜面反射对颜色的影响。
1def fragment(self, bar: Vec3):
2 """
3 片元着色器
4 :param bar: 重心坐标
5 :return:
6 """
7 uv: Vec2 = Vec2((self.varying_uv.m @ bar.to_matrix())[0][0], (self.varying_uv.m @ bar.to_matrix())[1][0])
8
9 n = (self.uniform_MIT * local_2_homo(obj.normal(uv))).m
10 n: Vec3 = Vec3(n[0][0], n[1][0], n[2][0]).normalize()
11 l = (self.uniform_M * local_2_homo(light_dir)).m
12 l: Vec3 = Vec3(l[0][0], l[1][0], l[2][0]).normalize()
13
14 r = (n*(n*l*2.) - l).normalize()
15 specular = pow(max(r.z, 0.0), obj.specular(uv))
16
17 intensity: float = max(0.0, n * l)
18 if intensity < 0:
19 return True, None
20 tga = obj.diffuse_map
21 color = tga.getpixel((int(uv.x * tga.width), tga.height - 1 - int(uv.y * tga.height)))
22 a = int(color[0] * (intensity + 0.8 * specular))
23 b = int(color[1] * (intensity + 0.8 * specular))
24 c = int(color[2] * (intensity + 0.8 * specular))
25 color = (min(a, 255), min(b, 255), min(c, 255))
26 return False, color
其实重点看一下fragment中的实现即可,下面是使用了镜面反射的贴图的效果:
关于TinyRenderer的lesson06中提到的光照模型,其实在之前的笔记中有写过, 《Blinn-Phong光照模型与着色方法》 ,理论相关的内容可以参考,其中介绍了 Blinn-Phong 光照模型,用于计算每个像素点的颜色。首先解释了为何能看到物体,并将光线分为镜面反射、漫反射和环境光三类。接着详述泛光模型(只考虑环境光)、Lambert 漫反射模型(增加漫反射项)以及 Phong 反射模型(在 Lambert 基础上加入镜面反射)。最后引入 Blinn-Phong 改进版,通过半程向量优化角度计算效率。还比较了 Flat Shading 等不同着色方法对渲染效果的影响。笔记中这些都是Bilibili的闫令琪老师的《计算机图形学入门》中的内容,先搞清楚理论就容易理解代码了。
本节就先这样,代码我放在这里了: https://github.com/zouchanglin/PyTinyRenderer