零. 前言
在学习可微渲染前,需要掌握图形学入门知识,可参考Metal与图形渲染入门篇:绘制图片。
一. 可微渲染与Nvdiffrast
1. 可微渲染的作用
在人工智能领域中,深度学习发挥着非常重要的作用,我们知道,在深度学习领域有两个非常重要的概念:前向传播和反向传播,反向传播要求有对每个输入值期望得到的已知输出,来计算损失函数的梯度,反向传播的前提之一,就是可微。
传统的光栅化渲染管线不可微,其原因是:在传统渲染中,光栅化、深度混合阶段都是离散的,这并不符合神经网络训练的要求。
而可微渲染,让传统的渲染赋予了深度学习的可能性,我们可以基于可微渲染做非常多的事情,如最近大火的AIGC,将人工智能和计算机图形学融合起来,是一件非常让人振奋的目标,能够创造无限的可能性。
2. Nvdiffrast的概述
Nvdiffrast支持开发者根据Rasterization,Interpolation,Texture filtering,Antialiasing四个接口进行自定义操作,实现自己想要的可微渲染的效果。可以基于Cuda和OpenGL进行开发,对于开发人员来说非常友好。
二. Nvdiffrast的配置和跑通
这次复现基本没踩什么坑,根据官网拉取仓库,并在根目录下执行
pip install .
此外还需要一些pip包的配置,在这里,我的requirement.txt是:
certifi==2022.12.7
charset-normalizer==2.1.1
filelock==3.9.0
fsspec==2023.4.0
glfw==2.6.2
idna==3.4
imageio==2.31.5
imageio-ffmpeg==0.4.9
Jinja2==3.1.2
MarkupSafe==2.1.2
mpmath==1.3.0
networkx==3.0
ninja==1.11.1.1
numpy==1.24.4
Pillow==9.3.0
psutil==5.9.6
PyOpenGL==3.1.7
requests==2.28.1
sympy==1.12
torch==2.1.0+cu118
torchaudio==2.1.0+cu118
torchvision==0.16.0+cu118
triton==2.1.0
typing_extensions==4.4.0
urllib3==1.26.13
执行代码输出三角形图片:
python samples/torch/triangle.py --cuda
三. 代码实战(triangle.py)
本文首先分析最简单的源码triangle.py
,这个文件主要定义了一个三角形的顶点,并使用光栅化和插值操作,输出了一个三角形。
1 Rasterize(光栅化)
1.1 函数调用
rasterize函数参数如下:
- 输入:
glctx:上下文
pos:顶点坐标,格式为(x, y, z, w)
tri:顶点的标号
resolution:生成的像素点的数量(height * width,必须是8的倍数)
- 输出:
第一个元素rast:输出batch_size个、resolution的维度(height * width)个像素点,每个像素点的内容为(u, v, z/w, triangle_id),其中:(u, v)对应的是坐标,z/w代表深度,triangle_id对应该像素点所在的三角形id,在这个例子下,由于只有一个三角形,triangle_id恒为1);如果像素点不在三角形内,则输出(0, 0, 0, 0)。
第二个元素貌似是用于反向传播,本例子未使用,先不管
1.2 举例分析
以生成一个8 * 8个像素的三角形为例,其代码如下:
# 三角形的三个顶点:(x, y, z, w)
pos = tensor([[[-0.8, -0.8, 0, 1], [0.8, -0.8, 0, 1], [-0.8, 0.8, 0, 1]]], dtype=torch.float32)
# 三角形的三个顶点对应的标号
tri = tensor([[0, 1, 2]], dtype=torch.int32)
# 产生的像素点的数组,这里生成了8 * 8个像素点,每个像素点的内容为(u, v, z/w, triangle_id)
rast, _ = dr.rasterize(glctx, pos, tri, resolution=[8, 8])
print(rast, end='\n')
输出如下:
tensor([[[[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000, 0.0000],
[0.7812, 0.1094, 0.0000, 1.0000],
[0.6250, 0.2656, 0.0000, 1.0000],
[0.4688, 0.4219, 0.0000, 1.0000],
[0.3125, 0.5781, 0.0000, 1.0000],
[0.1563, 0.7344, 0.0000, 1.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000, 0.0000],
[0.6250, 0.1094, 0.0000, 1.0000],
[0.4687, 0.2656, 0.0000, 1.0000],
[0.3125, 0.4219, 0.0000, 1.0000],
[0.1562, 0.5781, 0.0000, 1.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000, 0.0000],
[0.4687, 0.1094, 0.0000, 1.0000],
[0.3125, 0.2656, 0.0000, 1.0000],
[0.1562, 0.4219, 0.0000, 1.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000, 0.0000],
[0.3125, 0.1094, 0.0000, 1.0000],
[0.1562, 0.2656, 0.0000, 1.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000, 0.0000],
[0.1562, 0.1094, 0.0000, 1.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000]]]], device='cuda:0')
根据输出可以发现,共有64个像素点,从上到下,每一行在三角形内的像素点的数量分别为:0、5、4、3、2、1、0、0,下图是该三角形的示意图(但这一步还不会带颜色)。
2 插值(Interpolation)
2.1 函数调用
interpolate
的调用参数如下:
- 输入:
attr:感觉说attributes这个说得不是很清楚,可能是每个顶点对应的RGB值,插值时,会根据和三个顶点的距离及这三个顶点对应的RGB,去计算当前像素的RGB值。
rast:上面光栅化的输出作为这里的输入
tri:和光栅化的一样,顶点的标号,可能对应的就是attr。
- 输出:
第一个元素out:输出batch_size个、(height * width)个像素点,每个像素点的内容为归一化后的(r, g, b);如果像素点不在三角形内,则输出(0, 0, 0, 0)。
第二个元素貌似是用于反向传播,本例子未使用,先不管,输出
2.2 举例分析
继续以上面的例子,其代码如下:
# 三角形的三个顶点对应的标号
tri = tensor([[0, 1, 2]], dtype=torch.int32)
# 光栅化的输出
rast, _ = dr.rasterize(glctx, pos, tri, resolution=[8, 8])
# 插值时三个顶点对应的RGB权重
col = tensor([[[1, 0, 0], [0, 1, 0], [0, 0, 1]]], dtype=torch.float32)
out, _ = dr.interpolate(col, rast, tri)
Tensor([[[[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000],
[0.7812, 0.1094, 0.1094],
[0.6250, 0.2656, 0.1094],
[0.4688, 0.4219, 0.1094],
[0.3125, 0.5781, 0.1094],
[0.1563, 0.7344, 0.1094],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000],
[0.6250, 0.1094, 0.2656],
[0.4687, 0.2656, 0.2656],
[0.3125, 0.4219, 0.2656],
[0.1562, 0.5781, 0.2656],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000],
[0.4687, 0.1094, 0.4219],
[0.3125, 0.2656, 0.4219],
[0.1562, 0.4219, 0.4219],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000],
[0.3125, 0.1094, 0.5781],
[0.1562, 0.2656, 0.5781],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000],
[0.1562, 0.1094, 0.7344],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000]],
[[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000]]]], device='cuda:0')
这一步相对于上一步会有个颜色的插值操作,左上角对应的[R, G, B]接近[1, 0, 0]。
(PS:为什么不是严格的[1, 0, 0],个人见解是:将画布分割成了一个8 * 8的像素格子,但三角形左上角真正的顶点坐标位于[-1, 1]区间的(-0.8, -0.8)中,假设画布大小为8 * 8,那左上角的像素点距离上方和左方1个格子,而真正的顶点距离上方和左方8 * 0.2 / 2 = 0.8个格子,因此,左上角的像素点的R值约为1 - 0.2 = 0.8)
大概是下面的图的意思,白色的代表真正的顶点。
3. 坐标和色值转换、输出图像
根据上面插值步骤得到的64个像素点的RGB值,需要对其进行坐标和色值转换,并输出图像,这一步比较好理解,不赘述了,看看代码和输出:
img = out.cpu().numpy()[0, ::-1, :, :] # Flip vertically.
img = np.clip(np.rint(img * 255), 0, 255).astype(np.uint8) # Quantize to np.uint8
print("Saving to 'tri.png'.")
imageio.imsave('tri.png', img)
[[[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]]
[[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]]
[[ 0 0 0]
[ 40 28 187]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]]
[[ 0 0 0]
[ 80 28 147]
[ 40 68 147]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]]
[[ 0 0 0]
[120 28 108]
[ 80 68 108]
[ 40 108 108]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]]
[[ 0 0 0]
[159 28 68]
[120 68 68]
[ 80 108 68]
[ 40 147 68]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]]
[[ 0 0 0]
[199 28 28]
[159 68 28]
[120 108 28]
[ 80 147 28]
[ 40 187 28]
[ 0 0 0]
[ 0 0 0]]
[[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]
[ 0 0 0]]]
值得注意的是,坐标转换过来后,每一行在三角形内的像素点的数量分别为:0、0、1、2、3、4、5、0了,即对应之前那个图像,就对了