由于工作需求,我们的采集流程中使用的是基于Mitsuba的可微渲染,而采集得到的高光贴图需要在Blender中进行渲染成图,两边的BSDF渲染效果不统一问题困扰了我们许久。因此本文的核心需求是,阅读Blender和Mitsuba的源码,找出其差异,并在源码级别实现在Blender中复现Mitsuba的高光渲染效果。最后本文对Blender源码做了修改和重编译,用新的Blender编译版本实现了该渲染效果对齐的需求。
Blender和Mitsuba渲染场景及参数的对齐
为了方便对齐两边的渲染效果,我需要首先把两边的渲染场景对齐,其中Mitsuba使用xml文件储存场景信息,而Blender则是工程文件,我具体进行的对齐有:
- 两边都取消所有点光源、面光源等,都只使用同一张exr格式的环境贴图,对贴图亮度的scale都设置为0.05,使用的环境贴图是Equirectangular Mapping格式。
- 两边camera的参数对齐,包括外参:相机的姿态矩阵和内参:相机的fov、cx、cy等,都使用perspective透视模式。( 这部分其实包含很多具体的学习内容和计算,可以参考我的博客文章:https://yaelcassini.github.io/2023/11/07/Camera-Realted-Knowledge/ )
- 两边渲染分辨率的对齐,都使用6960*4640。
- 两边spp(sample per pixel)的对齐,实验过程中都使用1024。
- 两边渲染结果都使用exr格式输出。( 这里一开始由于Blender的颜色管理出问题导致误以为两边光照不相同,浪费了很多时间,具体可以参考我的博客文章:https://yaelcassini.github.io/2023/04/24/Color-Management/ )
- 两边的最大bounce次数对齐,其中,Blender中Max Bounces都设置为0时,才是只有直接光照的结果,而不是设置为1时。
- 两边渲染使用的Mesh完全相同,(载入Blender时要注意Blender的坐标系是z轴朝上的,import obj会默认做一个绕x轴的90度旋转。)
- 模型贴图都使用exr格式输入,不做颜色空间转换避免出现问题。
Mistuba源码阅读及分析
Mitsuba场景文件中的bsdf信息如下:
1 | <bsdf id="obj_bsdf" type="roughconductor"> |
可以看到使用的是roughconductor模型,主要代码在mitsuba2\src\bsdfs\roughconductor.cpp
文件中,bsdf的相关计算在RoughConductor类的eval函数中,我们项目中使用的是学长修改过的mitsuba,因此可以支持fresnel_schlick近似的计算,学习源码后转化的公式为:
$$ result = \frac{F * D * G}{4 * cos \theta_i} $$
$$ D = D_{NDF_Beckmann} (n,h,α)=\frac{1}{(\pi α^2 (n·h)^4 )} e^{(\frac{(n·h)^2-1}{α^2 (n·h)^2 })} $$
$$ G = G_2(l,v,h) = G_1(l) * G_1(v) $$
$$ G_1(v) = G_{GGX_Beckmann}(v) = \left {
\begin{array}{l l}
\frac{ 3.535 c + 2.181 c^2 }{ 1 + 2.276 c + 2.577 c^2 } & \quad \text{if $c < 1.6$}\
1 & \quad \text{if $c \geq 1.6$}
\end{array} \right. $$
$$ 其中:c = \frac{n·v}{ \alpha \sqrt{1 - (n·v)^2} } $$
$$ F = F_{Schlick}(v,h) = f0 + (1 - f0) * (1-v·h)^5 $$
Blender源码阅读及分析(主要针对Glossy BSDF着色节点的跟踪)
首先,放一下根据阅读源码和调试整理出的关于Blender一次渲染从顶端到底端调用的调用堆栈:
Blender中“Principled BSDF”节点是没有Beckmann分布的,只提供GGX和Multiscatter GGX两个微表面模型,因此我们流程中使用的是提供Beckmann分布的“Glossy BSDF”节点。因此需求转化为分析“Glossy BSDF”节点的具体实现公式,因此在大致学习了Blender源码的编写结构之后就从“Glossy BSDF”结点入手进行具体分析。
首先,Blender支持OSL(Open Shader Language),源码中有内置shader nodes的osl实现版本。因此在一开始通过关键词检索源码的阶段我自然而然的找到了osl实现的部分,虽然后来发现只关注osl是显然不行的,但此处还是顺着我的探索历程来整理。
(PS: 这里说一嘴,虽然osl模式有很多限制,但是胜在相比于普通渲染模式的代码阅读起来更类似GLSL等Shader语言,非常流畅,而且Blender应该是做了两种模式下渲染效果的对齐的,因此OSL可以作为辅助功能,比如想查看某个BSDF的具体计算过程,就可以先看OSL代码。)
- “Glossy BSDF”节点的底层实现使用OSL编写,在blender源码的
intern\cycles\kernel\osl\shaders\node_glossy_bsdf.osl
文件中,其中使用到了microfacet
函数。 - 接下来自然而然地去找
microfacet
函数的实现,找到intern\cycles\kernel\osl\closures_template.h
文件中使用OSL_CLOSURE_STRUCT_
系列关键字做了Microfacet、microfacet和用到的参数及其类型的声明。 - 由于工程里面没有找到其他的Microfacet关键字,因此下一步我选择搜索哪里用到了
closures_template.h
,找到了以下几个文件:
- closures_setup.h
- closures.cpp
- osl.h
- types.h
Blender源码使用的方式是以上的每个文件里使用#define重新定义OSL_CLOSURE_STRUCT_
系列关键字的含义,以减轻代码的重复性和冗余性。
closures_setup.h
中引入template用于创建Closure结构体。
closures.cpp
中引入了两次template,一次用于创建返回值为ClosureParam的函数,猜测是用于返回参数列表,另一次是用于注册闭包。
osl.h
中引入template用于flatten_closure_tree
函数中,根据函数名及函数体内容猜测,该函数用于展开OSL闭包树,将其转换为单一BSDF表示。
types.h
中引入template用于在枚举类型OSLClosure
中声明不同shading类型的名字和值。
根据关键词
osl_closure_microfacet
搜索只能找到closures_setup.h
中的osl_closure_microfacet_setup
函数。该函数体内,在判断distribution为beckmann时,分三种情况讨论,折射,glass和else,我们需要的显然是最后一种情况。- 用正则表达式搜索
osl_closure_microfacet_setup
函数,发现在osl.h
中使用了switch判断,case为OSL_CLOSURE_##Upper##ID时,调用了osl_closure##lower##setup函数,猜测调用的方式时渲染时,在flatten_closure_tree函数中判断closure->id(应该是一个枚举类型OSLClosureType的变量),发现节点的枚举值是OSL_CLOSURE##Upper##ID(types.h),调用osl_closure##lower##setup(osl.h)。而closures.cpp中的register_closures函数则把小写的shading node名字#lower,大写的OSL_CLOSURE##Upper##ID,返回参数列表的osl_closure##lower##_params()函数绑定了。
- 用正则表达式搜索
最后一种情况调用
bsdf_microfacet_beckmann_setup
函数,并把bsdf类的fresnel_type变量赋值为MicrofacetFresnel::NONE,变量赋值为CLOSURE_BSDF_MICROFACET_BECKMANN_ID,从这里开始不再出现CLOSURE_BSDF_MICROFACET_ID,而需要顺着CLOSURE_BSDF_MICROFACET_BECKMANN_ID找。搜索CLOSURE_BSDF_MICROFACET_BECKMANN_ID找到的bsdf_sample函数,位于
intern\cycles\kernel\closure\bsdf.h
文件中,在判断ShaderClosure->type后调用bsdf_microfacet_beckmann_sample
函数。bsdf_eval函数中调用的bsdf_microfacet_beckmann_eval是计算bsdf的核心,位于
intern\cycles\kernel\closure\bsdf_microfacet.h
文件中。位于
intern\cycles\scene
的shader_nodes.cpp中的NODE_DEFINE(GlossyBsdfNode)中定义了glossy_bsdf节点,并在其中进行了分布枚举类型的名字和ID注册对应,比如:distribution_enum.insert("beckmann", CLOSURE_BSDF_MICROFACET_BECKMANN_ID);
由于“Principled BSDF”节点是有Fresnel项的,因此看一下Pricipled BSDF的代码做一个对比,发现该节点的osl实现里面用到了
generalized_schlick_bsdf
函数,我发现该函数是可以输出beckmann并且有fresnel项的,因此我使用Blender的OSL模式,调用该函数进行渲染,在使用如下设置和参数时,实现了同光照同场景下和Mitsuba渲染效果的对齐:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17shader simple_material(
string distribution = "beckmann",
color f0 = color(0.04, 0.04, 0.04),
float roughness = 0.1,
normal Normal = N,
output closure color BSDF = 0)
{
color reflection_tint = color(1, 1, 1);
color transmission_tint = color(0, 0, 0);
float roughness_x = roughness;
float roughness_y = roughness;
vector tangent = vector(0, 0, 1);
color f90 = color(1, 1, 1);
float exponent = 5;
BSDF = generalized_schlick_bsdf(Normal, tangent, reflection_tint, transmission_tint, roughness_x, roughness_y, f0, f90, exponent, distribution);
}在OSL模式下对齐渲染效果是一个巨大的进步,之后我就只需要对照osl的实现修改普通渲染模式下的实现。在微表面模型设置为
beckmann
,fresnel项使用schlick近似,使用generalized_schlick_bsdf
渲染时,Blender中的bsdf计算可以转化为以下公式:
$$ result = \frac{F * D}{4 * cos\theta_i * (1 + G_{l} + G(v))} $$
$$ D = D_{NDF_Beckmann} (n,h,α)=\frac{1}{(\pi α^2 (n·h)^4 )} e^{(\frac{(n·h)^2-1}{α^2 (n·h)^2 })} $$
$$ G(v) = G_{GSF_Beckmann} = \frac{1.0 - 1.259c + 0.369c^2}{3.535c + 2.181c^2} $$
$$ 其中:c = \frac{n·v}{ \alpha \sqrt{1 - (n·v)^2} } $$
$$ F = F_{Schlick}(v,h) = f0 + (1 - f0) * (1-v·h)^5 $$
- 结论:目前通过对比Blender和Mitsuba源码得到的结论是:在我们的流程中,Blender使用的Glossy BSDF里面不包含Fresnel项的赋值和计算,因此Fresnel值在进行beckmann_setup的时候被设置为
MicrofacetFresnel::NONE
(BSDF计算对比如下图)。在修改时要注意两边的Roughness有没有进行平方映射要对齐,通过对源码的阅读,Mistuba的Roughness是不进行平方映射的,因此Blender中也要去掉平方映射。其中,G项的不同我本来以为是Blender写错了,后来才发现还是自己太无知了,Blender使用的是高度相关遮蔽阴影型,而Mitsuba使用的是分离遮蔽阴影型。虽然具体计算不同但原理相同,计算结果差异也可忽略。具体可以参考毛星云大佬的文章:https://zhuanlan.zhihu.com/p/81708753
Blender 调试流程
- 下载Blender源码
最好在Blender官方Git项目上选择一个稳定的branch,比如我选择的是blender-v4.0-release和blender-v3.6-release。可以用命令行git clone也可以直接download zip。笔者由于当时git网络不好使都是直接下载代码压缩包解压的,主要用到的是blender-v4.0-release这个分支。
- Blender仓库地址:https://github.com/blender/blender
- Git clone指定分支指令:
git clone -b blender-v4.0-release https://github.com/blender/blender
- 下载SVN,并使用SVN下载Blender编译需要的库,SVN可以理解为另一种形式的git,具体的区别笔者也米有深入了解,主打一个能用就行。笔者用的是TortoiseSVN,下载的是LTS 64位版本。下载后就可以像git一样在文件资源管理器右键寻找svn使用。
- TortoiseSVN下载地址:https://tortoisesvn.net/downloads.html
- 在Blender源码的同级路径新建lib文件夹,再在其中新建win64_vc15文件夹,之后文件资源管理器右键选择
SVN Checkout
,在URL of repository一栏输入Blender官方提供lib下载地址,在这里要注意,SVN也有版本管理,默认是HEAD revision,不同的Blender版本最好选择不同的revision,可以在SVN的GUI页面选择Show log,在其中查看自己下载的Blender版本对应的revision代号。比如,笔者下载的blender-v4.0-release分支,打开revision log之后,选择的是Message为“Windows: 4.0 Liabrary Update”的revision,代号为63491,所以直接把63491填到revision后面的框内点击ok就可以下载对应的lib。
- Blender官方提供的lib下载地址:https://svn.blender.org/svnroot/bf-blender/trunk/lib/win64_vc15
完成源码和library下载后的文件夹结构:
├────blender/
│ ├────.git/
│ ├────build_files/
│ ├────extern/
│ ├────intern/
│ ├────locale/
│ ├────release/
│ ├────scripts/
│ ├────tests/
│ ├────tools/
│ ├────make.bat
│ ├────CMakeLists.txt
│ ├────pyproject.toml
│ ├────README.md
│ └────···
├────lib/
│ ├────win64_vc15/
│ │ ├────alembic
│ │ ├────blosc
│ │ ├────boost
│ │ ├────brotli
└────└────└────···编译构建,在blender源码目录下,使用cmd进行命令行操作。
4.1.用命令行直接编译,生成文件在 build_windows_Full_x64_vc16_Release\bin\Release
- 命令:
make
4.2. 生成IDE工程,在 build_windows_Full_x64_vc16_Release 文件夹中生成VS工程。打开 Blender.sln,
- 命令:
make full nobuild
4.2.1. CMakePredefinedTargets/INSTALL 工程上右键,执行 Build,这步会将一些文件放到输出目录。这个只需执行一次,不过切换Debug/Release,也要执行一次。
4.2.2. blender工程是默认工程,直接点击绿色三角。
Blender 源码修改步骤
把输入的Color值作为f0传递给bsdf,Blender中用了一些很神奇的操作传值,主要的读值操作在
intern\cycles\kernel\svm\closure.h
的svm_node_closure_bsdf
函数中,集中在函数体的开头,当时也读了一段时间才读懂,由于篇幅限制此处不做详细解释,后续可能会单独写一篇。而传值操作在intern\cycles\scene\shader_nodes.cpp
中的void GlossyBsdfNode::compile
函数中,对该函数的修改:1
2
3
4
5
6
7
8
9
10
11closure = distribution;
/* TODO: Just use weight for legacy MultiGGX? Would also simplify OSL. */
if (closure == CLOSURE_BSDF_MICROFACET_MULTI_GGX_ID) {
BsdfNode::compile(
compiler, input("Roughness"), input("Anisotropy"), input("Rotation"), input("Color"));
}
else {
- BsdfNode::compile(compiler, input("Roughness"), input("Anisotropy"), input("Rotation"));
+ BsdfNode::compile(compiler, input("Roughness"), input("Anisotropy"), input("Rotation"), input("Color"));
}在
intern\cycles\kernel\svm\closure.h
的svm_node_closure_bsdf
函数中添加Fresnel项的内存分配以及材质信息赋值,这个函数相当于是设置材质固有信息,比如roughness和Normal等,没有拿到入射光线出射光线等信息,并不直接进行bsdf计算。(这里由于我是和指定的mistuba模型对齐,使用schlick近似,因此直接把exponent设置为5,如果有别的需求也可以添加node输入节点后传值设置。)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78case CLOSURE_BSDF_MICROFACET_GGX_ID:
case CLOSURE_BSDF_MICROFACET_BECKMANN_ID:
case CLOSURE_BSDF_ASHIKHMIN_SHIRLEY_ID:
case CLOSURE_BSDF_MICROFACET_MULTI_GGX_ID: {
#ifdef __CAUSTICS_TRICKS__
if (!kernel_data.integrator.caustics_reflective && (path_flag & PATH_RAY_DIFFUSE))
break;
#endif
Spectrum weight = closure_weight * mix_weight;
ccl_private MicrofacetBsdf *bsdf = (ccl_private MicrofacetBsdf *)bsdf_alloc(
sd, sizeof(MicrofacetBsdf), weight);
+ ccl_private FresnelGeneralizedSchlick *fresnel =
+ (bsdf != NULL) ? (ccl_private FresnelGeneralizedSchlick *)closure_alloc_extra(
+ sd, sizeof(FresnelGeneralizedSchlick)) : NULL;
- if (!bsdf) {
+ if (!bsdf || !fresnel) {
break;
}
- float roughness = sqr(param1);
+ float roughness = param1;
- bsdf->ior = 1.0f;
+ bsdf->ior = 0.0f;
+ kernel_assert(stack_valid(data_node.w));
+ fresnel->f0 = rgb_to_spectrum(stack_load_float3(stack, data_node.w));
+ fresnel->f90 = one_spectrum();
+ fresnel->exponent = 5;
+ fresnel->reflection_tint = one_spectrum();
+ fresnel->transmission_tint = zero_spectrum();
+ bsdf->ior = ior_from_F0(fresnel->f0.x);
/* compute roughness */
float anisotropy = clamp(param2, -0.99f, 0.99f);
if (data_node.y == SVM_STACK_INVALID || fabsf(anisotropy) <= 1e-4f) {
/* Isotropic case. */
bsdf->T = zero_float3();
bsdf->alpha_x = roughness;
bsdf->alpha_y = roughness;
}
else {
bsdf->T = stack_load_float3(stack, data_node.y);
/* rotate tangent */
float rotation = stack_load_float(stack, data_node.z);
if (rotation != 0.0f) {
bsdf->T = rotate_around_axis(bsdf->T, bsdf->N, rotation * M_2PI_F);
}
if (anisotropy < 0.0f) {
bsdf->alpha_x = roughness / (1.0f + anisotropy);
bsdf->alpha_y = roughness * (1.0f + anisotropy);
}
else {
bsdf->alpha_x = roughness * (1.0f - anisotropy);
bsdf->alpha_y = roughness / (1.0f - anisotropy);
}
}
/* setup bsdf */
if (type == CLOSURE_BSDF_MICROFACET_BECKMANN_ID) {
sd->flag |= bsdf_microfacet_beckmann_setup(bsdf);
+ bsdf_microfacet_setup_fresnel_generalized_schlick(kg, bsdf, sd, fresnel, false);
}
else if (type == CLOSURE_BSDF_ASHIKHMIN_SHIRLEY_ID) {
sd->flag |= bsdf_ashikhmin_shirley_setup(bsdf);
}
else {
sd->flag |= bsdf_microfacet_ggx_setup(bsdf);
if (type == CLOSURE_BSDF_MICROFACET_MULTI_GGX_ID) {
kernel_assert(stack_valid(data_node.w));
const Spectrum color = rgb_to_spectrum(stack_load_float3(stack, data_node.w));
bsdf_microfacet_setup_fresnel_constant(kg, bsdf, sd, color);
}
}
break;
}加了上面的代码之后发现Blender无法正常渲染,整个人脸部分都是全黑色的,printf输出debug之后发现是上述增加的
closure_alloc_extra
无法为Fresnel项分配内存,到函数内部debug发现是sd->num_closure变量不够用,因此找到了设置该变量的位置:intern\cycles\scene\shader_graph.cpp
中的int ShaderGraph::get_num_closures()
,修改如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38int num_closures = 0;
foreach (ShaderNode *node, nodes) {
ClosureType closure_type = node->get_closure_type();
if (closure_type == CLOSURE_NONE_ID) {
continue;
}
else if (CLOSURE_IS_BSSRDF(closure_type)) {
num_closures += 3;
}
else if (CLOSURE_IS_BSDF_MULTISCATTER(closure_type)) {
num_closures += 2;
}
else if (CLOSURE_IS_PRINCIPLED(closure_type)) {
num_closures += 12;
}
else if (CLOSURE_IS_VOLUME(closure_type)) {
/* TODO(sergey): Verify this is still needed, since we have special minimized volume storage
* for the volume steps. */
num_closures += MAX_VOLUME_STACK_SIZE;
}
else if (closure_type == CLOSURE_BSDF_MICROFACET_BECKMANN_GLASS_ID ||
closure_type == CLOSURE_BSDF_MICROFACET_GGX_GLASS_ID ||
closure_type == CLOSURE_BSDF_HAIR_CHIANG_ID ||
closure_type == CLOSURE_BSDF_HAIR_HUANG_ID)
{
num_closures += 2;
}
+ else if (closure_type == CLOSURE_BSDF_MICROFACET_BECKMANN_ID)
+ {
+ num_closures += 2;
+ }
else {
// printf("this is else, num_closures+1.\n");
++num_closures;
}
}
return num_closures;修改之后,Blender可以正常渲染了,但是整体的高光效果非常暗,整体亮度偏低,这里我又做了很多对照实现,分层渲染分析高光的Color、直接光、间接光,最后发现高光部分的直接光和Ground Truth对照组(OSL渲染的generalized_bsdf)有一个大致的线性倍数关系,实验后发现倍数刚好是f0的值,查看glossy bsdf node的OSL代码后发现,Blender的逻辑似乎是在BSDF计算后再乘上Color值,而我把f0值直接输入Color就会导致错误产生,为了避免这种干扰,我选择修改Glossy BSDF Node的输入节点设置,增加一个f0输入接口,而Color值设为1.0,修改位置如下:
intern\cycles\scene\shader_nodes.cpp
中的NODE(GlossyBsdfNode)
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24NodeType *type = NodeType::add("glossy_bsdf", create, NodeType::SHADER);
SOCKET_IN_COLOR(color, "Color", make_float3(0.8f, 0.8f, 0.8f));
SOCKET_IN_NORMAL(normal, "Normal", zero_float3(), SocketType::LINK_NORMAL);
SOCKET_IN_FLOAT(surface_mix_weight, "SurfaceMixWeight", 0.0f, SocketType::SVM_INTERNAL);
static NodeEnum distribution_enum;
distribution_enum.insert("beckmann", CLOSURE_BSDF_MICROFACET_BECKMANN_ID);
distribution_enum.insert("ggx", CLOSURE_BSDF_MICROFACET_GGX_ID);
distribution_enum.insert("ashikhmin_shirley", CLOSURE_BSDF_ASHIKHMIN_SHIRLEY_ID);
distribution_enum.insert("multi_ggx", CLOSURE_BSDF_MICROFACET_MULTI_GGX_ID);
SOCKET_ENUM(distribution, "Distribution", distribution_enum, CLOSURE_BSDF_MICROFACET_GGX_ID);
SOCKET_IN_VECTOR(tangent, "Tangent", zero_float3(), SocketType::LINK_TANGENT);
SOCKET_IN_FLOAT(roughness, "Roughness", 0.5f);
SOCKET_IN_FLOAT(anisotropy, "Anisotropy", 0.0f);
SOCKET_IN_FLOAT(rotation, "Rotation", 0.0f);
+ SOCKET_IN_COLOR(f0, "F0", make_float3(0.5f, 0.5f, 0.5f));
SOCKET_OUT_CLOSURE(BSDF, "BSDF");
return type;intern\cycles\scene\shader_nodes.cpp
中的void GlossyBsdfNode::compile
,把原来的input("Color")
改为input("F0")
:1
2
3
4
5
6
7
8
9
10
11closure = distribution;
/* TODO: Just use weight for legacy MultiGGX? Would also simplify OSL. */
if (closure == CLOSURE_BSDF_MICROFACET_MULTI_GGX_ID) {
BsdfNode::compile(
compiler, input("Roughness"), input("Anisotropy"), input("Rotation"), input("Color"));
}
else {
- BsdfNode::compile(compiler, input("Roughness"), input("Anisotropy"), input("Rotation"));
+ BsdfNode::compile(compiler, input("Roughness"), input("Anisotropy"), input("Rotation"), input("F0"));
}intern\cycles\scene\shader_nodes.h
中的GlossyBsdfNode定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class GlossyBsdfNode : public BsdfNode {
public:
SHADER_NODE_CLASS(GlossyBsdfNode)
void simplify_settings(Scene *scene);
ClosureType get_closure_type()
{
return distribution;
}
NODE_SOCKET_API(float3, tangent)
NODE_SOCKET_API(float, roughness)
NODE_SOCKET_API(float, anisotropy)
NODE_SOCKET_API(float, rotation)
+ NODE_SOCKET_API(float3, f0)
NODE_SOCKET_API(ClosureType, distribution)
void attributes(Shader *shader, AttributeRequestSet *attributes);
bool has_attribute_dependency()
{
return true;
}
bool is_isotropic();
};source\blender\nodes\shader\nodes\node_shader_bsdf_glossy.cc
中的static void node_declare
函数中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17b.add_input<decl::Color>("Color").default_value({0.8f, 0.8f, 0.8f, 1.0f});
b.add_input<decl::Float>("Roughness")
.default_value(0.5f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR);
b.add_input<decl::Float>("Anisotropy").default_value(0.0f).min(-1.0f).max(1.0f);
b.add_input<decl::Float>("Rotation")
.default_value(0.0f)
.min(0.0f)
.max(1.0f)
.subtype(PROP_FACTOR);
b.add_input<decl::Vector>("Normal").hide_value();
b.add_input<decl::Vector>("Tangent").hide_value();
b.add_input<decl::Float>("Weight").unavailable();
+ b.add_input<decl::Color>("F0").default_value({0.5f, 0.5f, 0.5f, 1.0f});
b.add_output<decl::Shader>("BSDF");
增加输入节点后的Glossy BSDF:
为了保持OSL模式下渲染效果也一致,修改了
intern\cycles\kernel\osl\shaders\node_glossy_bsdf.osl
,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51shader node_glossy_bsdf(color Color = 0.8,
string distribution = "ggx",
float Roughness = 0.2,
float Anisotropy = 0.0,
float Rotation = 0.0,
normal Normal = N,
normal Tangent = 0.0,
+ color F0 = 0.0,
output closure color BSDF = 0)
{
/* compute roughness */
float roughness = Roughness * Roughness;
float roughness_u, roughness_v;
float aniso = clamp(Anisotropy, -0.99, 0.99);
/* rotate tangent around normal */
vector T = Tangent;
if (abs(aniso) <= 1e-4) {
roughness_u = roughness;
roughness_v = roughness;
}
else {
if (Rotation != 0.0)
T = rotate(T, Rotation * M_2PI, point(0.0, 0.0, 0.0), Normal);
if (aniso < 0.0) {
roughness_u = roughness / (1.0 + aniso);
roughness_v = roughness * (1.0 + aniso);
}
else {
roughness_u = roughness * (1.0 - aniso);
roughness_v = roughness / (1.0 - aniso);
}
}
if (distribution == "Multiscatter GGX")
BSDF = Color * microfacet_multi_ggx_aniso(Normal, T, roughness_u, roughness_v, Color);
+ else if (distribution == "Beckmann" || distribution == "beckmann")
+ {
+ color reflection_tint = color(1, 1, 1);
+ color transmission_tint = color(0, 0, 0);
+ float roughness_x = Roughness;
+ float roughness_y = Roughness;
+ color F90 = color(1, 1, 1);
+ BSDF = generalized_schlick_bsdf(Normal, T, reflection_tint, transmission_tint, roughness_x, roughness_y, F0, F90, 5, distribution);
+ }
else
BSDF = Color * microfacet(distribution, Normal, T, roughness_u, roughness_v, 0.0, 0);
}Blender中创建ShaderData变量的位置在:
intern\cycles\kernel\integrator\shader_surface.h
的ccl_device int integrate_surface
函数中source\blender\nodes\shader\nodes\node_shader_bsdf_glossy.cc
文件中有一部分也用了glossy bsdf node的输入,不过这部分被包裹在#ifdef WITH_MATERIALX
定义中,因此应该只有materialX时才会用到。
另一种可能的Blender源码修改方法
在后面做其他工作的时候又想到因为Principled BSDF用的是generalized bsdf,是否可以直接在里面加一个beckmann分布,有了之前的经验,这次很快就把该分布加上了,但是渲染效果偏暗并不能对齐,这里由于牵扯的变量太多了,时间原因暂且不深究,只在下面记录在Principled BSDF加入beckmann分布选项的方法:
source\blender\makesrna\intern\rna_nodetree.cc
中:1
2
3
4
5
6
7
8
9
10
11static const EnumPropertyItem node_principled_distribution_items[] = {
+ {SHD_GLOSSY_BECKMANN, "BECKMANN", 0, "Beckmann", ""},
{SHD_GLOSSY_GGX, "GGX", 0, "GGX", ""},
{SHD_GLOSSY_MULTI_GGX,
"MULTI_GGX",
0,
"Multiscatter GGX",
"GGX with additional correction to account for multiple scattering, preserve energy and "
"prevent unexpected darkening at high roughness"},
{0, nullptr, 0, nullptr, nullptr},
};intern\cycles\blender\shader.cpp
中:1
2
3
4
5
6
7
8
9
10
11
12
13
14else if (b_node.is_a(&RNA_ShaderNodeBsdfPrincipled)) {
BL::ShaderNodeBsdfPrincipled b_principled_node(b_node);
PrincipledBsdfNode *principled = graph->create_node<PrincipledBsdfNode>();
switch (b_principled_node.distribution()) {
+ case BL::ShaderNodeBsdfPrincipled::distribution_BECKMANN:
+ principled->set_distribution(CLOSURE_BSDF_MICROFACET_BECKMANN_GLASS_ID);
+ break;
case BL::ShaderNodeBsdfPrincipled::distribution_GGX:
principled->set_distribution(CLOSURE_BSDF_MICROFACET_GGX_GLASS_ID);
break;
case BL::ShaderNodeBsdfPrincipled::distribution_MULTI_GGX:
principled->set_distribution(CLOSURE_BSDF_MICROFACET_MULTI_GGX_GLASS_ID);
break;
}
前期学习阶段中使用到的阅读资料和学到的新知识点
Cycles官方开发文档
Next Event Estimation(NEE)
- 在查看Blender源码的过程中发现了一个
DEVICE_KERNEL_INTEGRATOR_SHADE_SURFACE_MNEE
,查询后认为MNEE指的应该是Manifold Next Event Estimation,这是一种可以正确的渲染焦散(caustic)的路径追踪方式。
- 其中,NEE是Next Event Estimation的缩写,指的是次事件估计。次事件估计是一种多重重要性采样的方法,简而言之就是对于每次bounce,直接光照通过光源采样得到,间接光照通过BSDF采样得到,并且最后通过多重重要性采样结合起来。
HIP
ROCm平台是平行于CUDA的概念,是AMD的软件平台,用于A卡上面加速GPU计算,在源码级别支持CUDA程序。而HIP是在A卡上使用的编程模型,对标CUDA编程模型。HIP几乎是克隆CUDA,大多数情况下稍加修改就能转换,并且可以在N卡上运行。
clang-fromat
clang-fromat是一种代码风格统一管理工具,Blender用其进行代码的风格统一和规范,所谓代码风格指的是:修饰符偏移,括号对齐,宏定义对齐等格式上的规范。
Open Shading Language(OSL)
OSL是一种语法类似C语言的开放式着色语言,在blender中可以用OSL实现自己创建shading node。在渲染时.osl文件被编译为用于渲染的.oso文件,大多数支持OSL着色器的渲染引擎都附带OSL编译器。
参考资料:
- https://docs.blender.org/manual/en/latest/render/shader_nodes/osl.html
- https://github.com/AcademySoftwareFoundation/OpenShadingLanguage/blob/main/src/doc/osl-languagespec.pdf
- https://juejin.cn/post/7147477548220776484
OptiX
OpitX和Cuda都是NVidia的渲染框架,OptiX是专为光线追踪设计的,相比与Cuda在渲染更复杂的场景或者材质时可以提供更快的渲染时间。另外,目前在Blender中要使用OSL编程着色模型并且使用GPU渲染的话,似乎只支持OptiX不支持Cuda(截至2023.12.21,Blender4.0)。
Cycles Kernel Language
Wavefront Path Tracing:
Blender源码中,一种为了解决GPU编程时Kernel函数过大遇到的问题,把一个很大的Kernel拆分成很多小的Kernel的技术。
SIMD
SIMD(Single Instruction Multiple Data)即单指令流多数据流,是一种采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。简单来说就是一个指令能够同时处理多个数据。
Open Shading Language 教程
遇到的其他问题和尝试的其他方案
一开始不确定重编译Blender会不会遇到其他的问题,试图直接使用OSL模式渲染,就是在osl脚本里面调用generalized_schlick_bsdf,但是发现Blender对OSL的支持并不完备,使用GPU渲染只能选择OptiX框架,当时在OSL模式下无法渲染我们的材质球,或许是因为里面节点太多了,显示的报错是:
1
Error: Requested OSL group data size (6456) is greater than the maximum supported with OptiX (2048)
之后尝试的解决方案是把用shader graph做的节点分模块改写为OSL,再进行渲染,这样的好处是可以通过OptiX编译限制,并且Shader Graph更简洁,缺点是不利于美术人员后续对材质继续修改。但是试验后发现,虽然这样改写后可以渲染,但是Blender4.0使用OptiX渲染SSS的效果是错误的,不能直接使用。
后面又考虑了把Diffuse(包括SSS)和Specular分开渲染再合起来,后面通过实践和思考证明这样行不通,一起渲染时,Specular部分的间接光照会带上Diffuse的颜色并且较亮,而分开渲染时Specular的间接光照则是纯黑白的,且比较暗。分开渲染是行不通的,因此综上所述,只能走重编译的道路。
Blender3.6到4.0升级遇到的Principled Bsdf节点变化以及自动转换的评估:
3.6 Base Color 和Subsurface两个颜色输入,Subsurface是浮点数输入。
4.0 只有一个Base Color,自动添加了Mix Color节点,混合之前的两个输入作为Base Color的输入,该节点混合的Factor为原来的Subsurface输入(Clamp Factor)。
Roughness和Normal连接不变。
3.6的Subsurface除了作为mix factor还会连接在4.0的Subsurface模块的Scale输入上。
3.6的Specular输入在4.0连接在Specular模块的IOR Level上。
源码中IOR的描述:
1 | .description( |
自动转换后Specular的渲染会有一个很小的亮度差异,但我认为不影响最终效果。
但是在测试的过程中发现Blender4.0的SSS效果和之前有很大的差异,但是不能判断该差异是变好了还是变坏了,需要美术人员测试决定。