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

Reference

https://github.com/ssloy/tinyrenderer/wiki