栅格化渲染源码解析-neural_renderer源码(三)栅格化

本文着重探讨栅格化最重要的栅格化部分。这部分涉及到了栅格化的核心代码,此核心代码是比较抽象的,因此我们重点探讨其中的物理意义,理解了每个步骤的物理意义,理解起栅格化就会容易很多。

栅格化的定义:将三维场景中的物体进行着色投影到成像平面,形成二维图形的过程。其详细定义和流程可以参考其他文章,本文暂不重复描述。

基于代码选取,栅格化渲染的代码较多,例如基于openGL的渲染管线,本文选取的是neural_renderer,此代码主要的创新点是如何在渲染中回传梯度,但是其栅格化部分的代码写的具有不错的可读性。我们暂不讨论代码中关于梯度的问题,只是详细解析一下neural_renderer是如何进行栅格化渲染的流程的。

其他部分可详细参考另外两篇文章。

栅格化渲染源码解析-neural_renderer源码(一)概览

栅格化渲染源码解析-neural_renderer源码(二)光照部分

目录

一、栅格化整体流程

1.1 遍历步骤

1.2 基于chainer.cuda的遍历方法

二、计算面片的inv

2.1 变量及作用

2.2 反向直接跳过

2.3 将面片归一化到图像尺寸上

2.4 运算与存储

三、遍历像素运算depth和到面片三个点的相对距离

3.1 遍历像素及运算

3.2 确认该像素落入面片之中

3.3 计算该像素点到面片的相对距离

3.4 深度计算和更新z-buffer

3.5 此步骤的重要意义

四、成像采样

4.1 成像采样

4.2 计算该点所属的位置

4.3 加权采样

五、个人点评


一、栅格化整体流程

1.1 遍历步骤

代码的栅格化部分分了三个步骤

1.遍历所有面片for each faces,算出face_inv,face_inv此变量可以看作面片face的线性代数逆,算出此变量的目的在于快速的运算成像点到该面片三个顶点的距离

2.遍历所有for each pixel,这个pixel是栅格化成像后所有像素点的pixel,在每个pixel上遍历所有面片,如果该面片落在这个pixel上,则运算该面片上对应的点的depth,更新z-buffer,找到每个pixel对应的面片索引。

3.采样成像,针对每个像素点的面片索引,找到对应的texture上的点,然后成像,形成图像。

1.2 基于chainer.cuda的遍历方法

查阅chainer文档的话,发现此写法是cupy的复用。

chainer官方文档

​​​​​​Define your own function — Chainer 7.8.1 documentation

例如需要遍历不同的faces

# for each face
faces_inv = self.xp.zeros_like(self.faces)
loop = self.xp.arange(self.batch_size * self.num_faces).astype('int32')

loop即是需要遍历的数量。

            chainer.cuda.elementwise(
                'int32 _, raw float32 faces, raw float32 faces_inv',
                '',
                string.Template('''
                    /* face[v012][RGB] */
                    const int is = ${image_size};
                    const float* face = (float*)&faces[i * 9];
                    float* face_inv_g = (float*)&faces_inv[i * 9];

                    /* return if backside */
                    if ((face[7] - face[1]) * (face[3] - face[0]) < (face[4] - face[1]) * (face[6] - face[0]))
                        continue;
# .....
''').substitute(
                    image_size=self.image_size,
                ),
                'function',
            )(loop, self.faces, faces_inv)

具体语法可参阅相应的文档。

在string之中的即是cuda每个element之中的代码,这种代码可以自动实现cuda的并行,并且可读性和理解性要好很多。

二、计算面片的inv

2.1 变量及作用

计算面片inv只有两个变量,一个是面片,另一个是面片的inv。

面片inv的作用,类似于面片的逆,可以利用此矩阵快速的计算出一个点到该面片顶点的距离。

具体的计算方式如下,如果该点落在面片内,则与x,y,z与face_inv相乘可以得出该店到这个面片三个顶点的相对距离。具体计算方法如下所示,这种计算方法在下文中会出现,这里提前放出来,方便理解face_inv的作用:

w[0] = face_inv[3 * 0 + 0] * xi + face_inv[3 * 0 + 1] * yi + face_inv[3 * 0 + 2];
w[1] = face_inv[3 * 1 + 0] * xi + face_inv[3 * 1 + 1] * yi + face_inv[3 * 1 + 2];
w[2] = face_inv[3 * 2 + 0] * xi + face_inv[3 * 2 + 1] * yi + face_inv[3 * 2 + 2];

xi和yi就是该像素点的坐标,w0表示像素点到面片其中一个顶点的相对距离。

2.2 反向直接跳过

面片分为正反,如果是反向的面片,则不需要计算该面片的inv。对应贴图在对面片生成贴图的时候,反面不需要进行计算和贴图。

/ * return if backside * /
if ((face[7] - face[1]) * (face[3] - face[0]) < (face[4] - face[1]) * (face[6] - face[0]))
    continue;

2.3 将面片归一化到图像尺寸上

面片需要被归一化到图像尺寸之上。

/* p[num][xy]: x, y is normalized from [-1, 1] to [0, is - 1]. */
float p[3][2];
for (int num = 0; num < 3; num++) for (int dim = 0; dim < 2; dim++)
    p[num][dim] = 0.5 * (face[3 * num + dim] * is + is - 1);

之前的面片顶点,涉及到投影,是[-1, 1]的区间之上,现在我们需要将其进行归一化,归一化到图像image_size之上,相当于投影的坐标转换为相机坐标系,方便计算。代码中is表示image_size

2.4 运算与存储

然后就是运算与存储,具体的运算公式可以参考几何代数,博主前面已经把该face_inv的物理意义说明白了,就是快速运算像素点到该面片三个顶点的方法,这里就不重新把公式推一遍了,知道其物理意义即可。

/* compute face_inv */
float face_inv[9] = {
    p[1][1] - p[2][1], p[2][0] - p[1][0], p[1][0] * p[2][1] - p[2][0] * p[1][1],
    p[2][1] - p[0][1], p[0][0] - p[2][0], p[2][0] * p[0][1] - p[0][0] * p[2][1],
    p[0][1] - p[1][1], p[1][0] - p[0][0], p[0][0] * p[1][1] - p[1][0] * p[0][1]};
float face_inv_denominator = (
    p[2][0] * (p[0][1] - p[1][1]) +
    p[0][0] * (p[1][1] - p[2][1]) +
    p[1][0] * (p[2][1] - p[0][1]));
for (int k = 0; k < 9; k++) face_inv[k] /= face_inv_denominator;

/* set to global memory */
for (int k = 0; k < 9; k++) face_inv_g[k] = face_inv[k];

在计算了每个面片的逆之后,将该面片的逆存储全局变量之中。

三、遍历像素运算depth和到面片三个点的相对距离

3.1 遍历像素及运算

因为是基于cuda的并行运算,因此需要确定该并行单元执行的是哪个像素的计算。因此需要把当前像素点x和y坐标计算出。

const int is = ${image_size};
const int nf = ${num_faces};
const int bn = i / (is * is);
const int pn = i % (is * is);
const int yi = pn / is;
const int xi = pn % is;

i就是loop传入的变量,表示并行单元的index,每个并行单元可以根据自己得到的i从而获取自己所在的位置xi和y1

3.2 确认该像素落入面片之中

栅格化是面片向着成像平面投影,但是对应成像平面上的点来说,该点是向着物体的面片去投影,这也是我们之前计算face_inv的意义。

但是在此之前,我们需要确定,哪些面片投影之后,会与该像素点有重叠,哪些面片没有重叠。这就是下面计算的意义:

/* return if backside */
if ((face[7] - face[1]) * (face[3] - face[0]) < (face[4] - face[1]) * (face[6] - face[0]))
    continue;

/* check [py, px] is inside the face */
if (((yp - face[1]) * (face[3] - face[0]) < (xp - face[0]) * (face[4] - face[1])) ||
    ((yp - face[4]) * (face[6] - face[3]) < (xp - face[3]) * (face[7] - face[4])) ||
    ((yp - face[7]) * (face[0] - face[6]) < (xp - face[6]) * (face[1] - face[7]))) continue;

反面不渲染,这个我们刚刚已经在计算face_inv的时候跳过了,这里也需要跳过。下面部分,即判断该像素点能否落入面片内的代码,如果落到面片之外,就直接跳过即可。具体公式推导,依然可以参考几何代数,这里我们也不详细推导了。物理意义就是,像素点向着面片投影,没在面片内,那该面片就跳过。

注意这里涉及到一个坐标系的转换,就是yp和xp,因为face变量是投影到变量,它的空间是[-1, 1],而不是[0, img_size-1],因此像素点需要进行一下坐标转换,转到[-1, 1]的范围之上。

  • 即计算face_inv和像素投影的时候,坐标系是[0, img_size-1]
  • 计算像素点落入面片内时,坐标系[-1, 1]

3.3 计算该像素点到面片的相对距离

此时就用到了我们之前说的面片的逆,即像素点直接乘以面片的逆,得到该点投影到面片时到面片三个顶点的相对距离。

/* compute w = face_inv * p */
float w[3];
for (int k = 0; k < 3; k++)
w[0] = face_inv[3 * 0 + 0] * xi + face_inv[3 * 0 + 1] * yi + face_inv[3 * 0 + 2];
w[1] = face_inv[3 * 1 + 0] * xi + face_inv[3 * 1 + 1] * yi + face_inv[3 * 1 + 2];
w[2] = face_inv[3 * 2 + 0] * xi + face_inv[3 * 2 + 1] * yi + face_inv[3 * 2 + 2];

此相对距离需要进行归一化,从而算出到三个顶点距离的权重

/* sum(w) -> 1, 0 < w < 1 */
float w_sum = 0;
for (int k = 0; k < 3; k++) {
    w[k] = min(max(w[k], 0.), 1.);
    w_sum += w[k];
}
for (int k = 0; k < 3; k++) w[k] /= w_sum;

通过此归一化操作,即算出了到三个面片的距离的等权重。

 例如图中,三角形中间的点就是该三角形投影后,在成像平面上的像素点,w1,w2, w3是该点到三角面片的三个顶点的权重。此步骤就是为了计算出此权重。

3.4 深度计算和更新z-buffer

按照前文所述,每个面片的顶点,已经投影到相机坐标系,即z坐标即是该面片的深度。因此该像素点在面片上的投影就是到三个面片顶点的加权平均,很容易就算出其深度zp的值

/* compute 1 / zp = sum(w / z) */
const float zp = 1. / (w[0] / face[2] + w[1] / face[5] + w[2] / face[8]);
if (zp <= ${near} || ${far} <= zp) continue;

算出之后即可用zp和z-buffer进行比较,从而更新z-buffer之中的值

/* check z-buffer */
if (zp < depth_min) {
    depth_min = zp;
    face_index_min = fn;
    for (int k = 0; k < 3; k++) weight_min[k] = w[k];
    if (${return_depth}) for (int k = 0; k < 9; k++) face_inv_min[k] = face_inv[k];
}

注意z-buffer更新时候既要更新该点的像素深度,同时需要更新对应的面片索引,以便后文从面片上进行像素采样。到三个顶点的权重也需要被存储下来,方便后文进行运算。

/* set to global memory */
if (0 <= face_index_min) {
    depth_map[i] = depth_min;
    face_index_map[i] = face_index_min;
    for (int k = 0; k < 3; k++) weight_map[3 * i + k] = weight_min[k];
    if (${return_depth}) for (int k = 0; k < 9; k++) face_inv_map[9 * i + k] = face_inv_min[k];

3.5 此步骤的重要意义

此步骤的重要意义,在于运算出,

1.每个像素点上的点,对应于原图哪个三角面片

2.算出该面片上的点到三个顶点的相对权重是多少

运用以上两个信息,即可对栅格化图像进行采样

四、成像采样

4.1 成像采样

成像采样是根据前面算算出的信息,在texture上进行采样。已知了成像点到每个面片的相对位置,已知了每个面片的深度,这个步骤就简单了非常多。

texture在图像上的存储是按照 [batch_size, num_faces, texture_size, texture_size, texture_size, RGB] 这种尺寸存储的。因此对应于每个三角面片的texture是 [texture_size, texture_size, texture_size, RGB] 这种形式存在的。

成像即是在这个texture之中取样出对应的像素点。

4.2 计算该点所属的位置

之前计算的到三个点的weight被归一化到0和1之间,现在要把它转换成texture空间上的坐标,即将0-1之间的量转换成 0, texture_size -1 之间的量。

/* get texture index (float) */
float texture_index_float[3];
for (int k = 0; k < 3; k++) {
    float tif = weight[k] * (ts - 1) * (depth / (face[3 * k + 2]));
    tif = max(tif, 0.);
    tif = min(tif, ts - 1 - ${eps});
    texture_index_float[k] = tif;
}

4.3 加权采样

对于texture而言,它的值是离散的,即texture之中,只有0,1,2,... texture_size - 1个离散的点,而不是连续的值,但是我们之前算出来的值归一化之后的 texture_index_float是离散的,因此需要对其进行离散化操作。具体如何离散化?我们可以用一维来举例,对于一维而言,很容易理解。

 例如该位置是11.45,那它的值就是11和12两个对应的像素值的加权平均。

将一维扩充到三维,就是对应的2^3=8个点上的加权平均。通过此加权平均进行采样。

/* blend */
float new_pixel[3] = {0, 0, 0};
for (int pn = 0; pn < 8; pn++) {
    float w = 1;                         // weight
    int texture_index_int[3];            // index in source (int)
    for (int k = 0; k < 3; k++) {
        if ((pn >> k) % 2 == 0) {
            w *= 1 - (texture_index_float[k] - (int)texture_index_float[k]);
            texture_index_int[k] = (int)texture_index_float[k];
        } else {
            w *= texture_index_float[k] - (int)texture_index_float[k];
            texture_index_int[k] = (int)texture_index_float[k] + 1;
        }
    }

    int isc = texture_index_int[0] * ts * ts + texture_index_int[1] * ts + texture_index_int[2];
    for (int k = 0; k < 3; k++) new_pixel[k] += w * texture[isc * 3 + k];
    sampling_indices[pn] = isc;
    sampling_weights[pn] = w;
}
for (int k = 0; k < 3; k++) pixel[k] = new_pixel[k];

至此,我们梳理完了整个栅格化流程

五、个人点评

此栅格化是以面片为单位的栅格化,与openGL这种,以顶点为单位的栅格化还是不太一样的,但是此方法栅格化之后的效果不错,可视化可控,可以作为栅格化原理的方案之一。

并且此方法没有像openGL一样被封装到代码之中,此方法可以看到整个栅格化的源码,具有不错的研究价值。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

祥瑞Coding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值