零. 前言
在學(xué)習(xí)可微渲染前,需要掌握圖形學(xué)入門知識,可參考Metal與圖形渲染入門篇:繪制圖片。

一. 可微渲染與Nvdiffrast
1. 可微渲染的作用
在人工智能領(lǐng)域中,深度學(xué)習(xí)發(fā)揮著非常重要的作用,我們知道,在深度學(xué)習(xí)領(lǐng)域有兩個非常重要的概念:前向傳播和反向傳播,反向傳播要求有對每個輸入值期望得到的已知輸出,來計算損失函數(shù)的梯度,反向傳播的前提之一,就是可微。
傳統(tǒng)的光柵化渲染管線不可微,其原因是:在傳統(tǒng)渲染中,光柵化、深度混合階段都是離散的,這并不符合神經(jīng)網(wǎng)絡(luò)訓(xùn)練的要求。

而可微渲染,讓傳統(tǒng)的渲染賦予了深度學(xué)習(xí)的可能性,我們可以基于可微渲染做非常多的事情,如最近大火的AIGC,將人工智能和計算機圖形學(xué)融合起來,是一件非常讓人振奮的目標,能夠創(chuàng)造無限的可能性。
2. Nvdiffrast的概述
Nvdiffrast支持開發(fā)者根據(jù)Rasterization,Interpolation,Texture filtering,Antialiasing四個接口進行自定義操作,實現(xiàn)自己想要的可微渲染的效果??梢曰贑uda和OpenGL進行開發(fā),對于開發(fā)人員來說非常友好。

二. Nvdiffrast的配置和跑通
這次復(fù)現(xiàn)基本沒踩什么坑,根據(jù)官網(wǎng)拉取倉庫,并在根目錄下執(zhí)行
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
執(zhí)行代碼輸出三角形圖片:
python samples/torch/triangle.py --cuda
三. 代碼實戰(zhàn)(triangle.py)
本文首先分析最簡單的源碼triangle.py,這個文件主要定義了一個三角形的頂點,并使用光柵化和插值操作,輸出了一個三角形。
1 Rasterize(光柵化)
1.1 函數(shù)調(diào)用
rasterize函數(shù)參數(shù)如下:

- 輸入:
glctx:上下文
pos:頂點坐標,格式為(x, y, z, w)
tri:頂點的標號
resolution:生成的像素點的數(shù)量(height * width,必須是8的倍數(shù))
- 輸出:
第一個元素rast:輸出batch_size個、resolution的維度(height * width)個像素點,每個像素點的內(nèi)容為(u, v, z/w, triangle_id),其中:(u, v)對應(yīng)的是坐標,z/w代表深度,triangle_id對應(yīng)該像素點所在的三角形id,在這個例子下,由于只有一個三角形,triangle_id恒為1);如果像素點不在三角形內(nèi),則輸出(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)
# 三角形的三個頂點對應(yīng)的標號
tri = tensor([[0, 1, 2]], dtype=torch.int32)
# 產(chǎn)生的像素點的數(shù)組,這里生成了8 * 8個像素點,每個像素點的內(nèi)容為(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')
根據(jù)輸出可以發(fā)現(xiàn),共有64個像素點,從上到下,每一行在三角形內(nèi)的像素點的數(shù)量分別為:0、5、4、3、2、1、0、0,下圖是該三角形的示意圖(但這一步還不會帶顏色)。

2 插值(Interpolation)
2.1 函數(shù)調(diào)用
interpolate的調(diào)用參數(shù)如下:

- 輸入:
attr:感覺說attributes這個說得不是很清楚,可能是每個頂點對應(yīng)的RGB值,插值時,會根據(jù)和三個頂點的距離及這三個頂點對應(yīng)的RGB,去計算當前像素的RGB值。
rast:上面光柵化的輸出作為這里的輸入
tri:和光柵化的一樣,頂點的標號,可能對應(yīng)的就是attr。
- 輸出:
第一個元素out:輸出batch_size個、(height * width)個像素點,每個像素點的內(nèi)容為歸一化后的(r, g, b);如果像素點不在三角形內(nèi),則輸出(0, 0, 0, 0)。
第二個元素貌似是用于反向傳播,本例子未使用,先不管,輸出
2.2 舉例分析
繼續(xù)以上面的例子,其代碼如下:
# 三角形的三個頂點對應(yīng)的標號
tri = tensor([[0, 1, 2]], dtype=torch.int32)
# 光柵化的輸出
rast, _ = dr.rasterize(glctx, pos, tri, resolution=[8, 8])
# 插值時三個頂點對應(yīng)的RGB權(quán)重
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')

這一步相對于上一步會有個顏色的插值操作,左上角對應(yīng)的[R, G, B]接近[1, 0, 0]。
(PS:為什么不是嚴格的[1, 0, 0],個人見解是:將畫布分割成了一個8 * 8的像素格子,但三角形左上角真正的頂點坐標位于[-1, 1]區(qū)間的(-0.8, -0.8)中,假設(shè)畫布大小為8 * 8,那左上角的像素點距離上方和左方1個格子,而真正的頂點距離上方和左方8 * 0.2 / 2 = 0.8個格子,因此,左上角的像素點的R值約為1 - 0.2 = 0.8)
大概是下面的圖的意思,白色的代表真正的頂點。

3. 坐標和色值轉(zhuǎn)換、輸出圖像
根據(jù)上面插值步驟得到的64個像素點的RGB值,需要對其進行坐標和色值轉(zhuǎn)換,并輸出圖像,這一步比較好理解,不贅述了,看看代碼和輸出:
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]]]
值得注意的是,坐標轉(zhuǎn)換過來后,每一行在三角形內(nèi)的像素點的數(shù)量分別為:0、0、1、2、3、4、5、0了,即對應(yīng)之前那個圖像,就對了
