最近研究头发实时渲染,发现一篇比较好的文章,因此翻译出来,一方面增强自己对原论文的理解,另一方面希望能对需要的人有所帮助。原论文传送门。我翻译水平一般,尽可能认真严肃的翻译,如有错误,还望提醒。
PartI
我(原作者)决定分三部分尽可能详细讲解Marschner论文《Light Scattering from Human Hair Fibers》中研究的内容。
首先,我得提醒你为了理解论文,你必须具备一定的物理学和数学背景而不是shader语法本身。比如原论文经常提到 Snell’s law (我理解是菲涅尔定律)或者probability density functions(概率分布函数)。
Marschner提出的模型主要优点是基于光线通过头发纤维时发生的真实的物理现象。所以我们先学习一下头发丝在电镜下的样子。
将每一根头发丝当成半透明的圆柱,得到以下模型:
模型显示头发上独特且非常明显的反射是R,TT和TRT。
R——光线从头发丝表面反弹给观察者
TT——光线折射进头发然后折射给观察者
TRT——光线折射进头发丝,再内部反射后最终折射给观察者
在整个论文中使用的符号是基于切线空间的(我以前就在坐标系上面吃过亏)。
这里是Marschner头发渲染模型所需的所有变量:
u—头发的切线方向,方向是从头发根部到顶部。
w—头发的法线方向,面向观察者。
v—头发的次法线方向,基于v和w计算出来的右手坐标系方向(这点我弄错了)。v-w是法平面。
wi—光照方向。
wr—摄像机(观察者)方向(有待验证,我是按照Worf96的建议将其处理成光线散出的方向)。
θi,θr—与法平面的倾角(0°垂直于头发,u为Pi,-u为-Pi(这里的Pi应该指90°))。
φi,φr—围绕头发的方位角(measured so that v is 0 and w is +PI)。
还有几个被用到的参数:
θd = (θr-θi)/2 —差分方位角
φd = (φr-φi)/2—相对方位
θh = (θi+θr)/2—半角
φh = (φi+φr)/2—半角
当然,还有一些头发丝的常量参数,这些参数可以在原论文的Table1(第8页)找到。
心中有了这些后,我们能近似得到以下头发光照模型:
S = SR + STT + STRT,
Sp = Mp (q i , q r ) x Np (q d , f d ) for P = R, TT, TRT
接下来我们只要找到谁是M和N。
PartII
在上节中我提到在Marschner的论文中被提到的两个函数
S = SR + STT + STRT,
Sp = Mp (q i , q r ) x Np (q d , f d ) for P = R, TT, TRT
M部分
这实际上只是概率分布函数,而且最佳的选择是高斯分布函数(或者叫Normal Distribution)。
M部分的运算如下:
MR (θh) = g( Beta R , θh – Alpha R).
MTT (θh) = g( Beta TT , θh – Alpha TT).
MTRT (θh ) = g( Beta TRT , θh – Alpha TRT).
N部分
N部分的运算实际上有点诡异。这里是其主要步骤:
转化为Miller-Bravais index。
这是为了改变2D物理的折射指数,目的是简化三维圆柱的光线在二维横截面上面的处理。在研究了Snell`s Law后我们确定了折射系数如下:
解决cubic equation(立方体函数)
记住如下图片:
我们需要寻找出谁是入射角,然后我们能得出如下的近似模拟方程:
解决菲涅尔方程
菲涅尔方程被用来模拟无衰减情况下的反射模型。(一直不理解的菲尼尔方程难道是这样?有待验证)
吸收因子
这其实非常简洁,仅仅是:
衰减因子
这是获得融合了反射和吸收因子的,因此Marschner的论文称其为“吸收和反射衰减”。
第一个导数是:
N部分(最终版)
N函数为:
NR (q d , f d )
= NP (0, q d , f d ).
NTT (q d , f d )
= NP (1, q d , f d ).
NTRT (q d , f d ) = NP (2, q d , f d ).
在最后部分,Marschner为了避免奇异情况,建议了一个更复杂的计算模型。但我实现下来看,我不能说这有多大的帮助。所以我坚持原始的NTRT算法。
整个模型
用一个方程来总结整个Marschner头发模型:
PartIII
这是Marschner Shader的最后一部分。我将介绍如何有效的处理这个光照模型,怎么增加环境光和漫反射光,并且在最后一部分我也将提供生成Marschner查找贴图的代码资源和展示一个我在CS中的效果。
查找贴图
由于在shader中要进行大量的运算,最佳的运算就是使用查找贴图,进而尽可能的减少刷新。
我们很容易注意到Marschner的论文中,除了定义的常量,M函数值取决于θi和θr,N函数取决于θd和φd。尽管这或许在开始是一个好的优化,考虑到所有角度必须是从反三角函数中运算得到,比如acos和asin,这些都不是一开始就能得到的,因此直接通过cos和sin建立索引贴图听起来是个不错的想法。
所有角度上的正弦值和余弦值获取方法都能在GPU Gems2上被找到,第23章:
- sin q i = (light · Tangent),
- sin q o = (eye · Tangent).
- lightPerp = light – (light · tangent) x tangent,
- eyePerp = eye – (eye · tangent) x tangent.
- cos f d = (eyePerp · lightPerp) x ((eyePerp · eyePerp) x (lightPerp · lightPerp))-0.5
至于θd,如果我们注意到θd取决于θi和θr,在弄清楚之后我们能从查找贴图上使用一个通道来索引这两个值。
创建这两张图的最简单方法是生成一张M函数贴图和N函数贴图(包含Mr,Mtt,Mtrt和θd)。然而在原始论文中Ntt和Ntrt都只有三个通道,但我们考虑到吸收率也有一个通道,我们就能简化到只有一个通道( but
they can be reduced to only one channel if we consider the absorption to have one channel as well.)。
环境光和漫反射光
Marschner光照模型只是指定了灯光的反射部分,所以为了获得好的视觉效果,我们为这模型增加环境光和漫反射光。
我使用的灯光来自于Nalu Demo,更详细的介绍:传送门
[plain] view plain copy
- /* Compute diffuse lighting with phi-dependent component */
- float diffuse = sqrt(max(0.0001, 1 - uv1.x * uv1.x));
- /* Pass colors */
- float4 diffuseColor;
- diffuseColor.rgb = diffuse * objColor.rgb * DiffuseCol;
- diffuseColor.a = objColor.a;
- float3 ambientColor;
- ambientColor = objColor.rgb * AmbientCol;
- float3 lighting = (( M.r * N.r + M.g * N.g + M.b * N.b ) / (cos_qd * cos_qd));
- lighting += diffuseColor.rgb;
- OUT.xyz = lighting + diffuseColor.rgb * 0.2 + IN.AmbientColor;
到此翻译结束。