《GPU GEMS》16.次表面散射的实时近似

次表面散射的通俗理解,即对于某些半透明物体,如蜡烛,皮肤,牛奶等。光线进入物体,在物体内部发生散射现象,最终从入射点不同的位置射出。
这使得对于物体表面的每个像素着色来讲,不止受到该点入射光的影响,还会收到表面其余散射出来的光线的影响。这让次表面散射与漫反射区别开来。

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 表面法线

颜色漂移

《GPU GEMS》16.次表面散射的实时近似_第1张图片
Paste_Image.png

根据环绕光照公式,生成皮肤着色查找表,然后根据皮肤查找表进行着色。
思路:提高暗部亮度,阴影过渡处颜色替换。
使用查找表是为了加速计算。

// 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.用深度映射模拟吸收
比起上一个的改进是,这个技术模拟光在物体内传播的吸收过程,这使得我们需要计算光在物体中前进的距离,因为距离越长,被物体吸收的光线也就越多。表现就是,对于一个半透明的物体,光源位于物体背后时,我们在物体前方可以看到物体部分区域被照亮,照亮程度与厚度(光在物体内部传播的距离)有关。这个技术不考虑光进入物体内部产生的折射现象,同时只适用于凸面物体

《GPU GEMS》16.次表面散射的实时近似_第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;
}

技术缺陷是没有考虑到光的漫反射,光源在物体背后时会显示背面细节

你可能感兴趣的:(《GPU GEMS》16.次表面散射的实时近似)