次表面散射的通俗理解,即对于某些半透明物体,如蜡烛,皮肤,牛奶等。光线进入物体,在物体内部发生散射现象,最终从入射点不同的位置射出。
这使得对于物体表面的每个像素着色来讲,不止受到该点入射光的影响,还会收到表面其余散射出来的光线的影响。这让次表面散射与漫反射区别开来。
1.简单的散射近似
简单的散射近似使用了环绕光照(wrap lighting)
Lambert(理想的漫反射模型)在光线与表面法线方向垂直时,值为0
环绕光照修改Lambert,使得Lambert的值域在缩小,a=环绕光照值
效果:使得物体原本应该黑暗的地方拥有一定的光照,即相比Lambert模型,阴影部分后退。
环绕光照函数
y=(x+wrap)/(1+wrap)=1-(1-x)/(1+wrap),x<=1.0
常规漫反射计算:
float diffuse=max(0,dot(L,N));
环绕光照漫反射计算:
float wrap_diffuse=max(0,(dot(L,N)+wrap)/(1+wrap));
L 光照方向
N 表面法线
颜色漂移
根据环绕光照公式,生成皮肤着色查找表,然后根据皮肤查找表进行着色。
思路:提高暗部亮度,阴影过渡处颜色替换。
使用查找表是为了加速计算。
// Generate 2D lookup table for skin shading
//input:p(x,y)
//output:color(r,g,b,a)
float4 GenerateSkinLUT(float2 P : POSITION) : COLOR
{
//wrap环绕系数
float wrap = 0.2;
//散射宽度
float scatterWidth = 0.3;
//散射颜色
float4 scatterColor = float4(0.15, 0.0, 0.0, 1.0);
//光泽度
float shininess = 40.0;
//NdotL:表面法线Normal与光照方向LightDir的点积
//NdotH:表面法线Normal与半角向量HalfVec的点积
float NdotL = P.x * 2 - 1; // remap from [0, 1] to [-1, 1]
float NdotH = P.y * 2 - 1;
//进行环绕光照方程计算后的值
float NdotL_wrap = (NdotL + wrap) / (1 + wrap); // wrap lighting
//使用环绕光照的漫反射
float diffuse = max(NdotL_wrap, 0.0);
// add color tint at transition from light to dark
//smoothstep(start,end,t);
//tend,return 1
float scatter = smoothstep(0.0, scatterWidth, NdotL_wrap) *
smoothstep(scatterWidth * 2.0, scatterWidth,
NdotL_wrap);
//计算高光Blinn-Phong模型
float specular = pow(NdotH, shininess);
if (NdotL_wrap <= 0) specular = 0;
float4 C;
C.rgb = diffuse + scatter * scatterColor;
C.a = specular;
return C;
}
// Shade skin using lookup table
half3 ShadeSkin(sampler2D skinLUT,
half3 N,
half3 L,
half3 H,
half3 diffuseColor,
half3 specularColor) : COLOR
{
half2 s;
s.x = dot(N, L);
s.y = dot(N, H);
//s*0.5+0.5,将点积的值域从[-1,1]重新映射到[0,1]
half4 light = tex2D(skinLUT, s * 0.5 + 0.5);
return diffuseColor * light.rgb + specularColor * light.a;
}
方法解析:
第一个函数是生成2D查找表的方程,根据第二个函数的输入,最终应该是生成一张texture2D的贴图,贴图的每个像素点记录了我们需要的颜色信息。
第二个函数即根据点积结果查找表中的颜色值,和漫反射颜色进行计算叠加‘。
实际操作上应该是提前烘焙好皮肤2D查找表贴图,然后在shader里直接使用该贴图读取颜色值和diffuse做运算。
理论很简单,甚至没有什么技术含量,效果根据上图来讲也比较一般。
’
2.用深度映射模拟吸收
比起上一个的改进是,这个技术模拟光在物体内传播的吸收过程,这使得我们需要计算光在物体中前进的距离,因为距离越长,被物体吸收的光线也就越多。表现就是,对于一个半透明的物体,光源位于物体背后时,我们在物体前方可以看到物体部分区域被照亮,照亮程度与厚度(光在物体内部传播的距离)有关。这个技术不考虑光进入物体内部产生的折射现象,同时只适用于凸面物体
距离的计算思路是和阴影映射的方法相似。关于阴影映射参考:
http://blog.csdn.net/xiaoge132/article/details/51458489
以光源为视点进行烘焙(将摄像机的位置移动到光源位置进行烘焙),获得表面上的点与光源之间的距离,存储到一张texture中。 using standard projective texture mapping将图像投射到场景中。然后在渲染pass中,给需要渲染的点,从texture中我们获得它到光源的距离,同时获得他背后的点,即光的入射点的距离,两者距离相减即得到光在物体的传播距离。然后根据这个距离,按照自己预先设定的衰减查找表,进行着色。
The Vertex Program for the Depth Pass
//深度Pass程序
//顶点着色器的输入结构体
struct a2v {
float4 pos : POSITION;
float3 normal : NORMAL;
};
//顶点着色器的输出,即片段着色器的输入
struct v2f {
float4 hpos : POSITION;
float dist : TEXCOORD0; // distance from light
};
//顶点着色器程序
//输入:a2v结构体,模型视图投影矩阵,模型视图矩阵,grow值
v2f main(a2v IN,
uniform float4x4 modelViewProj,
uniform float4x4 modelView,
uniform float grow)
{
v2f OUT;
float4 P = IN.pos;
//沿法线方向缩放顶点
P.xyz += IN.normal * grow; // scale vertex along normal
//矩阵变换,返回一个顶点
OUT.hpos = mul(modelViewProj, P);
//距离计算,返回一个距离的浮点值
OUT.dist = length(mul(modelView, IN.pos));
return OUT;
}
The Fragment Program for the Depth Pass
float4 main(float dist : TEX0) : COLOR
{
return dist; // return distance
}
The Fragment Program Function for Calculating Penetration Depth Using Depth Map
// Given a point in object space, lookup into depth textures
// returns depth
float trace(float3 P,
uniform float4x4 lightTexMatrix, // to light texture space
uniform float4x4 lightMatrix, // to light space
uniform sampler2D lightDepthTex,
)
{
// transform point into light texture space
float4 texCoord = mul(lightTexMatrix, float4(P, 1.0));
// get distance from light at entry point
float d_i = tex2Dproj(lightDepthTex, texCoord.xyw);
// transform position to light space
float4 Plight = mul(lightMatrix, float4(P, 1.0));
// distance of this pixel from light (exit)
float d_o = length(Plight);
// calculate depth
float s = d_o - d_i;
return s;
}
技术缺陷是没有考虑到光的漫反射,光源在物体背后时会显示背面细节