在 integrator_intersect_closest对光线和场景进行求交后,如果击中了物体并符合条件,就启动integrator_shade_surface进行物体的着色计算,在这一步才会开始填充render_buffer。
shade_surface是一个模板函数,第一个模板参数node_feature_mask为KERNEL_FEATURE_NODE_MASK_SURFACE & ~KERNEL_FEATURE_NODE_RAYTRACE,
它的定义为:
也就是说它的值为
这表示可以进行表面着色的kernel feature?
第二个模板参数为DEVICE_KERNEL_INTEGRATOR_SHADE_SURFACE,标识出当前的kernel。
下面详细拆分每一个步骤:
integrate_surface
这是一个模板函数,模板参数是node_feature_mask,即integrator_intersect_closest的第一个模板参数,它返回continue_path_label,标识此光线是否应该结束。
下面将integrate_surface拆解分析,现在暂时忽略掉一些feature,如,次表面散射,体渲染,path_guiding等。
读取ShaderData
ShaderData
ShaderData的定义如下:
其中包含了着色计算所需的信息。这些信息贯穿渲染中的整个着色流程,作为context一样的存在。
integrate_surface_shader_setup:
在这里,将场景相交信息和光线信息读取到了isect和ray中,然后继续调用shader_setup_from_ray初始化ShaderData,
shader_setup_from_ray函数对ShaderData的初始化分为以下步骤:
读取相交信息
这里将isect中的数据放到了sd中,都是一些普通数据。
读取光线信息
time用于动画效果,wi是入射光线方向的逆方向(注意这里的光线是指从相机发射的光线,也就是视线方向,相当于渲染方程中的wo),用来进行着色计算。
根据相交的三角形图元进行计算
首先是使用triangle_normal计算三角形的法线,计算方式如下:
这里先通过prim取到三角形的v_index,再通过v_index取出3个顶点,三角形的法线等于三个点构成的两个向量的叉乘。object_negative_scale_applied判断三角形是否应用了负向的缩放,比如把一个物体沿x轴放大-2倍,这时三角形的法线就要反过来。
如果没有使用SHADER_SMOOTH_NORMAL特性,那么ShaderData的N(着色计算的法线)和Ng(三角形几何的法线)都是刚刚计算出的法线,如果使用了SHADER_SMOOTH_NORMAL,那么N还需要对计算出的法线进行平滑处理,
triangle_smooth_normal源码:
考虑到整个三角形只有一个面法线,为了保证着色平滑,最好对相交点的法线进行更精确的计算,具体的方法就是使用重心坐标uv来对三角形三个顶点的点法线进行插值,得到最终的法线。
计算完法线之后,再通过prim信息(相交的图元索引)取出此物体的shader,它记录物体的使用的shader的第一个节点的位置。后续还会取出shade_flag,sd→flag = kernel_data_fetch(shaders, (sd→shader & SHADER_MASK)).flags,flag记录了shade的特性。
再下一步是通过相交的uv信息(三角形的重心坐标)计算三角形内的精确相交点位,triangle_point_from_uv源码如下:
同样取出三角形的3个顶点,相交点 P = a + u * vec(b - a) + v * vec(c - a),注意相交点的计算是在物体局部空间中进行的,还需要把它转换到世界空间中。
DPDU暂时忽略。
背面测试
通过法线和入射光线夹角的cos值来计算相交点是否为光照背面,注意wi是入射方向的逆方向,所以夹角cos值小于0表示该点位于物体背面,物体背面的点需要将它的法线反过来,同时将SD_BACKFACING记录到shade_flag中。
Surface Shader 计算
读取完ShaderData,下一步对shader进行计算,对应的代码为:
shader计算的函数为surface_shader_eval,它的源码为:
接下来详细拆解分析此函数。
计算max_closures
这一步计算ShaderGraph中有多少了closure,在上面的代码注释中有说明,当光线路径有PATH_RAY_TERMINATE | PATH_RAY_SHADOW | PATH_RAY_EMISSION 3种标志时,max_closures为0,因为此时它们不需要存储closures。PATH_RAY_TERMINATE意味着此光线路径需要结束,自然不需要储存closures;PATH_RAY_SHADOW表示此光线路径为shadow ray,不进行着色计算,也不需要储存closures;PATH_RAY_EMISSION表示这是一条自发光物体发射的光线(比如光源),它也不需要存储closures(可能是自发光着色计算是不会出现多个closure)。
如果光线路径不在上诉三种情况,max_closures由use_caustics_storage标记决定,这是焦散功能的标志,这里将其看作false,所以max_closures = kernel_data.max_closures。
计算后的max_closures需要储存到ShaderData中,
这里可以看出num_closure和num_closure_left两个属性的含义,num_closure表示此ShaderData中已实际保存的closure数量,num_closure_left表示未保存的closure数量。目前已保存的closure为空,后续会生成这些closure并填充到ShaderData。
计算Shader
Shader中包含了一个ShaderGraph,ShaderGraph中又包含了很多Shader Node。Shader决定了物体的材质,进而决定物体渲染出的外观。
这一步将解析并计算物体的Shader,从上面的代码可以看出,Shader的计算有OSL、SVM和默认3种分支,大多数情况下渲染使用的是SVM,关于SVM,在SVM文章中有详细介绍。
使用SVM计算Shader:
svm_eval_nodes
这个函数就是计算shader的核心代码,它的源码片段如下:
这是整个函数的结构,它的主体是一个死循环,不断地遍历Shader中的每一个节点,并对它们进行计算。
在SVM中,每一个Shader节点都会被编译成一个或多个SVM Node,SVM Node计算产生相关的数据会被存到一个stack中,供后续的节点使用,所以这里也定义了一个stack,用来保存这些数据。
closure_weight用于保存closure的计算结果Doubtful ,它的类型为Spectrum,意为光谱,实际是一个float3类型,
offset取出此shader的偏移地址,sd→shader在根据相交的三角形图元进行计算中初始化,它指示了shader第一个节点的位置。
read_node从场景的所有节点中根据offset读取节点,它的源码:
在这里offset会自动增加。
SVM Node的类型一定是放在int4的第一个数据的,所以取出node后根据node.x来判断它的类型。
了解了svm_eval_nodes的运作机制之后,下面是一些计算这些node的例子
SVM Node示例
NODE_VALUE_F
这个节点可以让用户直接使用,假设在某些节点的输入中直接输入了float值(比如BSDF节点的roughness),SVM也会自动生成这个节点。很明显这是一个输出节点,它没有任何输入,所有Shader的第一个节点必然是这种输出节点,即使用户没有使用SVM也会自动生成。
处理它的代码片段:
svm_node_value_f源码:
它的“计算”非常简单,在SVM编译时,将他的具体的输出值保存在node.y处,该值在stack的位置保存在node.z处,这里将值保存至stack的对应的位置,等后续的节点使用即可。这里将ivalue从uint转为了float,他们都是4个字节,转换是安全的。
NODE_CLOSURE_BSDF (CLOSURE_BSDF_DIFFUSE_ID)
既然是Shader计算,最重要的自然是BSDF的节点计算,BSDF节点也是所有节点中最复杂的,这里以一个BSDF节点中最简单的Diffuse BSDF节点为例,主要了解Shader计算的流程。
编译Diffuse BSDF
Diffuse BSDF节点的编译函数如下:
Diffuse BSDF主要有4个参数:Roughness,Color,Normal和Tangent。
首先处理颜色参数——生成一个NODE_CLOSURE_WEIGHT或NODE_CLOSURE_SET_WEIGHT节点,并把颜色数据打包进去。
然后处理param1和param2参数,这里param1为Roughness,param2为空,生成一个NODE_CLOSURE_BSDF节点,并将该节点的type(Diffuse BSDF节点的type为CLOSURE_BSDF_DIFFUSE_ID)、param1与param2的位置和值、此Shader的mix_weight_offset(这个几乎不会使用)打包进去。
最后处理Normal、Tangent、param3和param4参数,这里不生成新的节点,仅打包他们的位置数据(说明这四个数据不允许直接填入值,只能连接其他节点的输出,比如Normal必然是不能直接填值的)。
这是生成的3个int4内存示意图,为了方便,假设Color属性是直接输入的颜色值,不与其他节点的输出相连,所以第一个节点是NODE_CLOSURE_SET_WEIGHT。
int4(1) | int4(2) | int4(3) |
---|
NODE_CLOSURE_SET_WEIGHT | NODE_CLOSURE_BSDF | Normal.offset |
Color.r | (CLOSURE_BSDF_DIFFUSE_ID, Roughness.offset, null, mix_weight_offset) | Tangent.offset |
Color.g | Roughness | null |
Color.b | null | null |
SVM节点处理
知道了Diffuse BSDF生成的节点布局后,再看svm_eval_nodes是如何处理的。
svm_eval_nodes源码片段:
首先处理NODE_CLOSURE_SET_WEIGHT,它使用的函数svm_node_closure_set_weight源码为:
它将颜色储存到了closure_weight中,closure_weight是NODE_CLOSURE_BSDF节点的处理函数svm_node_closure_bsdf的一个入参,所以这里相当于将颜色从NODE_CLOSURE_SET_WEIGHT传递到NODE_CLOSURE_BSDF了。
svm_node_closure_bsdf函数比较特殊,
- 它是一个模板函数,有两个模板参数node_feature_mask和type,node_feature_mask记录了shader的一些featue,type记录该Shader的类型,比如SHADER_TYPE_SURFACE。
- 它会返回一个offset,而不借助read_node函数中的offset自增,因为NODE_CLOSURE_BSDF一般不止占用一个int4(比如现在就占用了两个),需要自行计算后续节点的偏移。
读取节点数据1
svm_node_closure_bsdf源码片段:
第一步是读取NODE_CLOSURE_BSDF节点关联的数据,node.y中保存的数据为(CLOSURE_BSDF_DIFFUSE_ID, Roughness.offset, null, mix_weight_offset)。mix_weight_offset用于读取mix_weight,它决定了当前BSDF Shader在最终着色计算中的比例(cycles中允许一个材质使用多种BSDF 节点),这里假设只用了Diffuse BSDF,所以mix_weight_offset为空,mix_weight = 1。
read_node读取NODE_CLOSURE_BSDF节点的下一个节点(offset已经自动加1了)。这里保存的是Normal.offset和Tangent.offset。
过滤无需计算的情况
svm_node_closure_bsdf源码片段:
第二步是过滤掉不需要进行计算的场景,其中使用到了宏IF_KERNEL_NODES_FEATURE,它的定义为:
所以IF_KERNEL_NODES_FEATURE(BSDF)等价于:
通过模板参数node_feature_mask判断当前节点是否为BSDF节点,同理,IF_KERNEL_NODES_FEATURE(EMISSION)通过模板参数node_feature_mask判断当前节点是否为EMISSION节点。从上面的条件和注释中可以知道svm_node_closure_bsdf只有在两种情况下才会需要计算,
- 该节点为SHADER_TYPE_SURFACE类型的BSDF节点,且mix_weight不为0。
- 该节点为EMISSION类型的节点,且它是一个PRINCIPLED BSDF。
如果确定该节点不需要计算,将调用并返回svm_node_closure_bsdf_skip,它的源码为:
用来获取准确的offset。这里就可以看出CLOSURE_BSDF_PRINCIPLED_ID是最特殊的,它的数据比其他的BSDF多占了4个节点。
读取节点数据2
svm_node_closure_bsdf源码片段:
继续读取节点的数据,法线N的地址保存在data_node.x,如果该地址为空,就使用三角形图元的法线。后面的param1取出的是Roughness的值,param2未使用,可以忽略。
Setup BSDF
svm_node_closure_bsdf源码片段:
接下来就是根据不同地BSDF类型进行不同的处理,Diffuse BSDF对应的类型为CLOSURE_BSDF_DIFFUSE_ID。
首先计算weight,weight = closure_weight * mix_weight,其中closure_weight就是basecolor,它是从之前的NODE_CLOSURE_SET_WEIGHT传过来的,mix_weight为BSDF的“权重”。
接下来分配了一个OrenNayarBsdf,先来看下它的定义:
包含了bsdf计算所需的信息。
OrenNayar指的是Oren-Nayar,一个用于表面反射模型的算法,通过考虑表面微观粗糙度对光的散射效果来更准确地模拟材质的外观。这种模型由迈克尔·奥伦(Michael Oren)和谢弗·乃亚(Shree K. Nayar)在1994年提出。
再看bsdf_alloc函数,它的源码为:
这里的计算了sample_weight,它的值为weight三个分量的平均值,从上面的流程可以知道,这里的weight是此BSDF的基础颜色(Base Color),以基础颜色作为BSDF的采样权重,这是在BSDF计算中一种常用的方法,因为基础颜色可以一定程度上反应材质反射或散射光线的能力,这里sample_weight等于基础颜色3个分量的平均值也许并不是十分准确,但依然可以做为采样权重的判断标准,对于其他类型的BSDF,这个sample_weight会有一些调整。
只有sample_weight大于CLOSURE_WEIGHT_CUTOFF(1e-5f),才会进行新的closure分配。需要注意的是,这里调用closure_alloc传入的ClosureType参数是CLOSURE_NONE_ID。
closure_alloc源码:
所谓的”分配“,其实是在ShaderData的closure数组中取了一个元素出来,并初始化它的type和weight,在这里type其实并未真正初始化,它的值是CLOSURE_NONE_ID。
注意到这里要求分配的size小于等于ShaderClosure的大小,ShaderClosure定义如下:
可以看出,除了SHADER_CLOSURE_BASE之外,它至少可以另外包含2个float3和4个float数据,对于OrenNayarBsdf绰绰有余。
分配完成之后,需要记录ShaderData中num_closure和num_closure_left的值,它们在之前计算max_closures阶段被初始化。
BSDF分配完毕之后,再回到svm_node_closure_bsdf函数,
svm_node_closure_bsdf源码片段:
bsdf分配成功后,初始化它的法线数据,并根据roughness是否为0决定使用DiffuseBsdf还是OrenNayarBsdf,在通常的BSDF模型中diffuse是不考虑roughness的,只有specular才会考虑,OrenNayarBsdf将roughness也考虑进去了。这里假设roughness不为0,需要使用OrenNayarBsdf。
在代码中调用bsdf_diffuse_setup将OrenNayarBsdf强制转为成了DiffuseBsdf,因为DiffuseBsdf中的数据是OrenNayarBsdf的子集,所以这个转换是安全的。
bsdf_oren_nayar_setup源码:
针对BSDF的各个属性进行了一些预计算,具体的计算方式为:
σ=roughnessa=π+((3∗π−4)/6)∗σ1b=π+((3∗π−4)/6)∗σσ
并且将bsdf→type赋值为CLOSURE_BSDF_OREN_NAYAR_ID。这个函数最后返回的是SD_BSDF和 SD_BSDF_HAS_EVAL两个标志为,它们会被记录到ShaderData的flag中。这就相当于告诉渲染器,这个ShaderData中有BSDF节点,并且已经进行了预计算。
总结
Surface Shader阶段用于对物体的材质进行处理,物体的材质是通过一个Shader来定义的,它由多个Shader节点组成,所以在进行光照计算前必须对各个节点进行预计算,并确定它最终使用的BSDF模型和对应的参数。
过滤closure
经过上一步的Surface Shader计算之后,ShaderData中已经保存了需要的ShaderClosure,但并不是所有的ShaderClosure都需要计算,可以根据用户的设置进行一些调整,将不需要的closure从着色计算中剔除掉。
integrate_surface源码片段:
过滤closure主要使用surface_shader_prepare_closures函数,下面分析它是如何过滤调不需要的closure的。
surface_shader_prepare_closures源码片段:
首先这个功能需要在kernel_data的integrator中保存需要过滤的filter_closures,它是一个bitmap。如果选用过滤CLOSURE_EMISSION,ShaderData中的closure_emission_background会被置0。
下面的过滤只针对PATH_RAY_CAMERA,也就是从相机发出的光线。
如果需要过滤直接光照,ShaderData的flag中的SD_BSDF_HAS_EVAL会被清除,这表示后续不再进行BSDF的计算。
遍历每一个colsure,检查filter_closures中是否过滤DIFFUSE,GLOSSY,TRANSMISSION这些类型的closure,比如上面的OrenNayarBsdf就属于DIFFUSE,如果当前colsure是需要过滤的类型,就将它的type置为CLOSURE_NONE_ID,并将sample_weight置为0。
如果需要过滤的closure类型为TRANSPARENT,就将它的type置为CLOSURE_HOLDOUT_ID,并将sample_weight置为0,然后在ShaderData的flag中保存一个标志位SD_HOLDOUT。
surface_shader_prepare_closures源码片段:
下面的这个过滤流程主要针对的是焦散的feature,暂时不对它做深入研究。
有没有可能在Shader计算之前做closure的过滤,将filter_closures传递到svm_eval_nodes函数中,在生成bsdf的时候就阻止它。
计算Holdout节点
integrate_surface源码片段:
Holdout节点是一个Shader节点,它可以让物体变得”透明“,这里的透明指的是渲染出的图像在该物体是透明的,用于实现一些特殊的美术效果,关于它的详细介绍:Blender Holdout节点
由于这个节点比较特殊,所以它不在Surface Shader阶段处理。
Holdout节点实现原理TODO
写入自发光film
integrate_surface源码片段:
这一步并不是渲染所必须的,它可以将场景中的自发光物体写入一个”纹理“之中,在后处理阶段就可以使用它来实现一些美术效果。
#TODO
确认光线是否结束
integrate_surface源码片段:
从代码的注释上来看,大部分光线已经在intersect_closest阶段终止了,这个步骤是为了Emission和让throughput除以probability。并且保证在计算sss时光线进入和脱离物体不会触发两次这个步骤。
integrate_surface_terminate源码:
continuation_probability保存在path中,如果光线的path_flag不包含PATH_RAY_TERMINATE_ON_NEXT_SURFACE,就将其取出,否则continuation_probability视为0。
如果continuation_probability为0,表示此光线需要终止,否则更新path中的throughput,将它除以continuation_probability。由于最终throughput的值是要乘到光照计算结果的,所以这里就相当于将最终结果除以了continuation_probability,对应俄罗斯轮盘的校正这个步骤。
PATH_RAY_TERMINATE_ON_NEXT_SURFACE这个标志位是在intersect_closest阶段置入的,它的置入条件是此光线已经在俄罗斯轮盘赌中失败并且此光线击中的物体是自发光物体。
那么这个步骤的意义就很清楚了:
- 此光线本来已经需要停止,但是它击中了一个自发光物体,自发光可能是需要写入film里面的,所以在上一步写入自发光film完成后,在这里终止它。
- 这条光线是正常的光线,在这里将他的throughput除以将它除以continuation_probability。
计算直接光照
光照计算是着色最重要的一步了,在上面的步骤中,光照计算所需的数据已经准备完毕,接下来就可以使用蒙特卡洛方法来计算着色了。
integrate_surface源码片段:
path_state_rng_load读取采样的随机数信息,它将会用于蒙特卡洛积分的随机数采样。
PROFILING_EVENT只在CPU渲染时使用,这里忽略它。
计算直接光照的函数为integrate_surface_direct_light。
在光源上采样
计算直接光照时在光源上采样一个点,将他的辐射率作为渲染方程的输入项,用来计算出出射方向(相机方向)的辐射率。
integrate_surface_direct_light源码片段:
首先可以看到,只有场景中应用了直接光源效果,并且ShaderData中标记了有BSDF需要计算时(SD_BSDF_HAS_EVAL),光照计算的流程才会继续。
后面对场景中的光源进行采样,这里的采样并不是针对所有光源,而是根据光源的大小、亮度以及它们对当前像素影响的重要性来决定每个光源被采样的概率,不同的光线可能会采样到不同的光源,也会得到不同的计算结果。
这里并不详细研究光源的采样算法,只关心光源采样得到的结果。
#TODO
光源采样的结果保存在LightSample结构体中,它的定义为:
其中的每一个成员都有注释。
计算光照强度
通过采样得到了光源的信息之后,就可以以此来计算它在着色点提供的光照强度,也就是入射光的辐射率。
integrate_surface_direct_light源码片段:
这里先定义了一个emission_sd_storage,然后将其转换为ShaderData。ShaderDataCausticsStorage的定义为:
它是一个“空”的结构体,仅用于分配内存空间,它的大小为ShaderData减去MAX_CLOSURE - CAUSTICS_MAX_CLOSURE = 64 - 4 = 60个ShaderClosure的大小,也就是说它是一个特殊的ShaderData,一般的ShaderData可以保存64个ShaderClosure,它只可以保存4个ShaderClosure。
AS_SHADER_DATA的定义为:
用来将输入类型的指针转换为ShaderData类型的指针。
所以上面的步骤就是为了定义一个ShaderData类型的指针emission_sd,其中没有任何数据,用来保存计算过程中产生的数据。
后面定义的ray,bsdf_eval也是同样用法。
最关键的计算光照的函数为light_sample_shader_eval,他返回一个Spectrum类型的变量light_eval,这就是光源光照强度的计算结果。
接下来详细分析light_sample_shader_eval的计算流程。
计算constant发光体
light_sample_shader_eval源码片段:
light_sample_shader_eval的第一步就是通过surface_shader_constant_emission来判断当前光源是不是constant发光体。
surface_shader_constant_emission源码:
从这里可以看出,是否为constant发光体取决于shader_flag中是否包含SD_HAS_CONSTANT_EMISSION,目前想要有这个标志,需要当前shader中自发光节点的颜色和强度输入都是常数,也就是这两个输入不能是其他节点的输出连接过来的。这样就可以直接从传入的shader中的constant_emission直接读取数据,不需要再经过复杂的节点计算了。
读取出的Spectrum信息保存至eval变量中,并且如果该光源是几何体的话,还要将其法线调整保证ls→Ng与ls→D夹角大于90度(注意法线反转的条件是ls→Ng与ls→D点乘大于0,也就是在它们的夹角小于90度的时候才反转),这是因为ls→D是着色点到光源的向量,对于光源来说,它是一个入向量,它与法线的夹角大于90度时它们位于光源同一侧。
计算背景光
light_sample_shader_eval源码片段:
如果此次采样的光源是背景光的话,调用shader_setup_from_background计算emission_sd。
shader_setup_from_background源码:
这里将光源的数据填入了ShaderData中,其中,ray_P是在背景计算时才有的,它的值ls→P是光源的位置或方向(对于没有位置的光源而言);sd→P表示shader点的位置,在背景计算中它被初始化成ls→D;其余的N, Ng, wi都初始化成-ls→D。sd→shader直接取了场景中的背景的shader。
再下一步,调用surface_shader_eval计算emission_sd中的shader,surface_shader_eval在Surface Shader 计算中已经介绍过,使用SVM来处理所有shader节点。在处理shader的background时,sd→closure_emission_background将被初始化为背景颜色,并且为sd→flag置上SD_EMISSION标志位。
最后一步,调用surface_shader_background来计算光照强度,surface_shader_background源码:
直接返回sd→closure_emission_background,即背景颜色。
所以背景光强度的计算结果就是设置的背景颜色。
计算其他类型的光源
除了上面两种情况,场景中的光源就是使用了节点的自发光物体或者光源,这类光源可以实现更好的光照效果,计算起来也更加复杂。
light_sample_shader_eval源码片段:
第一步调用shader_setup_from_sample计算emission_sd,shader_setup_from_sample源码:
它的功能时将ls中的数据经过必要的处理后填充到emission_sd中。其中sd→wi表示入射光的方向的逆方向,这里初始化为-ls→D。其余数据都是一些常规的初始化,这里忽略DPDU。
第二步与背景光计算相同,调用surface_shader_eval计算shader,不再赘述。它也和背景光一样,将sd→closure_emission_background将被初始化为自发光颜色。
最后调用surface_shader_emission计算光照强度,它的计算过程如下:
首先通过sd→Ng和sd→wi(法线和入射光逆方向)计算自发光的pdf,它的pdf非常简单,如果Ng和wi夹角小于90度,pdf为1,否则pdf为0,也就是只考虑光源面向物体的一侧的光照,然后再将pdf与自发光颜色相乘。
计算最终的光照强度
经过上面3种不同光源的计算,光照数据已经保存在eval中了,但是最后的光照还需要一点处理,
light_sample_shader_eval源码片段:
首先,计算出的光照强度需要乘以ls→eval_fac,它在光源采样的时候得到。其次,如果此光源在场景中是一个真正的光源的话(不是自发光的物体),最终的结果还要乘以光源的强度,因为除了自发光节点外,光源本身也是有颜色和亮度设置的。
计算物体的BSDF
在光照强度计算完毕后,下一步就是计算物体的BSDF。
integrate_surface_direct_light源码片段:
BSDF的计算产生两个结果,一个是bsdf_eval,一个是bsdf_pdf,bsdf_pdf是当前光线采用的BSDF的pdf,因为一个物体的Shader可能是多个BSDF的结合。bsdf_eval的定义为:
它包含了3项,漫反射,高光和二者相加的结果。
计算BSDF调用surface_shader_bsdf_eval函数,它的源码片段:
bsdf_eval_init先将bsdf_eval初始化为0,
然后继续调用_surface_shader_bsdf_eval_mis函数进行计算,
_surface_shader_bsdf_eval_mis源码:
这里遍历了shader中所有的closure,然后分别对它们进行计算,只有BSDF和BSSRDF的closure才会被计算,其中,又只有未被排除的BSDF才会进入bsdf_eval计算流程,否则只是将该closure的sample_weight加入到sum_sample_weight中。(有的BSDF是会被排除在计算之外的,该信息保存在光源采样时获得的LightSample.shader中)。
通过上面对sample_weight可知,每一个Closure的sample_weight等于它的Base Color乘以它的混合权重(如果有多个BSDF的话),所以他们的和sum_sample_weight会接近于物体的最终颜色。
bsdf_eval
可以看出这是核心计算bsdf的函数,它返回计算结果eval,并且将pdf填充到bsdf_pdf。
bsdf_eval源码片段:
这里就是根据closure具体的类型来计算结果,依然以之前的CLOSURE_BSDF_OREN_NAYAR_ID为例,它的计算函数是bsdf_oren_nayar_eval。
bsdf_oren_nayar_eval源码:
Oren Nayar光照模型如下:
其中的常量A和B已经在Setup BSDF阶段准备数据时已经计算好了,这里暂时没有考虑反照率ρ(也就是baseColor)。