开启辅助访问
 找回密码
 立即注册

<渲染基础>-法线贴图

快活 回答数0 浏览数475
法线贴图

1. 切线空间

切线空间,是基于物体表面的空间,是在3D渲染领域众多坐标系之一。其一台重要的用途是法线映射(Normal Mapping)。其与视察映射(Parallax Mapping)、位移贴图(Displacement Mapping)等属于Bump Mapping范畴。相对于后两种,后两种方法可以提供更加真实逼真的表面凹凸感。
对与3D渲染来说,有模型空间、世界空间、相机空间、家用投影空间、标准设备空间、屏幕空间。对于每一种空间,3D渲染都会有对应的转换矩阵,将一台空间坐标系转换到另外一台空间坐标系。往微观上面来说,有切空间,微元空间,切空间是基于模型的表面来建立的一种坐标系,对于微元空间,是建立在原子级别的坐标系,但是对于原子级别的坐标系,我们并没有对应的矩阵来转换因为太小了,计算量巨大,即使有也得不偿失。所以针对于微元空间,3D渲染借助于一些数学以及物理学公式来建立模型算子来估算一些我们想要得到的计算结果。
但是对于表面空间,其计算量与最终得到的收益来衡量,其最终得到的收益是大于计算量的,所以3D渲染建立了一台坐标系,并切构建了对应的坐标系系统。
切空间中,3D渲染规定,切空间由tangent(切线T)轴,bitangent(副法线B)轴,法线轴(N)组成的切空间坐标系。其中规定切线轴(T)与纹理坐标U增长方向一致,副法线轴与纹理坐标V增长方向一致,法线轴由法线贴图中读取得到。这就是切空间坐标系TBN,基于物体表面空间建立的一种坐标系。
2. 切线副法线的计算

2.1 在CPU侧计算



该图表示的是一台切空间坐标系,已知三角形三个顶点 V_0 、V_1 、 V_2 ,以及纹理坐标 T_0(u_0,v_0) 、 T_1(u_1,v_1) 、 T_2(u_2,v_2) 。
定义三角形的两条边为:
E_0= V_1-V_0\\ E_1=V_2-V_0  
那么对应使用纹理坐标表示这个两条边差值:
(t_1,b_1)=(u_1-u_0,v_1-v_0)\\ (t_2,b_2)=(u_2-u_0,v_2-v_0)
那么我们如下关系式:
E_0 =t_1T+b_1B\\ E_1 =t_2T+b_2B
其中T、B表示切线与副法线,因为我们知道T与纹理坐标U增长方向一致,B与纹理坐标V增长方向一致。
我们将上纹理坐标与位置坐标的关系公式用矩阵表示:
\left[ \begin{matrix} E_0\\ E_1 \end{matrix} \right]= \left[ \begin{matrix} t_1&amp;b_1\\ t_2&amp;b_2 \end{matrix} \right] \left[ \begin{matrix} T\\ B \end{matrix} \right]   
将 E_0 、 E_1 ,T、B拆分为分量模式:
\left[ \begin{matrix} E0x&amp;E0_y&amp;E0_z\\ E1x&amp;E1_y&amp;E1_z\\ \end{matrix} \right]= \left[ \begin{matrix} t_1&amp;b_1\\ t_2&amp;b_2\\ \end{matrix} \right] \left[ \begin{matrix} T_x&amp;T_y&amp;T_z\\ B_x&amp;B_y&amp;B_z \end{matrix} \right]
对等式进行交换:
\left[ \begin{matrix} T_x&amp;T_y&amp;T_z\\ B_x&amp;B_y&amp;B_z \end{matrix} \right]= \left[ \begin{matrix} t_1&amp;b_1\\ t_2&amp;b_2\\ \end{matrix} \right]^{-1} \left[ \begin{matrix} E0x&amp;E0_y&amp;E0_z\\ E1x&amp;E1_y&amp;E1_z\\ \end{matrix} \right]
根据矩阵相关知识,我们知道A=\left[\begin{matrix}a&amp;b\\ c&amp;b \end{matrix} \right] 的逆矩阵为 A^{-1} :
A^{-1}=\frac{1}{ad-bc} \left[ \begin{matrix} d&amp;-b\\ -c&amp;a \end{matrix} \right]
所以以上公式可以进一步表示为:
\left[ \begin{matrix} T_x&amp;T_y&amp;T_z\\ B_x&amp;B_y&amp;B_z\\ \end{matrix} \right]= \frac{1}{t_1b_2-b_1t_2} \left[ \begin{matrix} b_2&amp; -b_1\\ -t_2&amp;t_1 \end{matrix} \right] \left[ \begin{matrix} E0x&amp;E0_y&amp;E0_z\\ E1x&amp;E1_y&amp;E1_z \end{matrix} \right]
该公式是最终的计算公式,我们知道,其中, t_1 、 t_2 、 b_1 、 b_2 与 E_0 、 E_1 都是已知变量,我们可以很轻松的计算出来TB的值。需要注意是的计算出来的向量的长度小于1,需要规格化一下才使用。
而我们计算出来的TB是基于单个三角形的,但是一般法线贴图是基于每个顶点进行。对于每个顶点所在的所有的三角形的对应的切空间值进行求平均,就可以得到该顶点的切空间的值。
对于切空间来说,我们知道TBN是相互垂直的,只要我们知道其中两个变量轴,就可以叉乘得到第三个轴,组装成TBN矩阵,进行后续的计算。
2.2 在着色器中计算


  • 原理
    在OpenGLES3.x的着色器中,增加了偏导数函数,偏导数函数(HLSL中的ddx和ddy,GLSL中的dFdx和dFdy)是片元着色器中的一台用于计算任何变量基于屏幕空间坐标的变化率的指令
    其实在三角形栅格化期间,GPU并行执行时求像素块中的变量的差值计算的。数学计算模型为:
    dFdx(p(x,y))=p(x+1,y) -p(x,y)\\ dFdy(p(x,y))=p(x,y+1) -p(x,y)
    我们将之对应到CPU中的计算上来看,是否就是我们定义两条边的差值公式。那么按照上面的计算公式,我们就可以计算出TB向量了。
  • 着色器中实现
in vec2 vTexCoords;
in vec3 vNormals;
in vec3 vPositions;
uniform sampler2D normalMap;
out vec4 fragColor;
void main()
{
vec3 tangentNormal = textuure(normalMap,vTexCoords).xyz * 2.0 - 1.0;
vec3 E1 = dFdx(vPositions);
vec3 E2 = dFdy(vPositions);
vec3 t1b1 = dFdx(vTexCoords);
vec3 t2b2 = dFdy(vTexCoords);
vec3 T = normalize(E1 * t2b2.t, E2 * t1b1.t);
  vec3 B = -normalize(cross(N,T));
vec3 N = normalize(vNormals);
mat3 TBN = mat3(T,B,N);
fragColor = vec4(1.0,0.0,0.0,1.0);
}

  • 从切空间渲染法线贴图,或是在世界空间渲染法线贴图,看自个的选择。
3. 法线贴图的生成


  • 生成法线贴步骤

    • 转换为灰度图像
      初始输入图像记做O,通过转换,将输入图像转换为灰度图像,因为对于生成法线贴图来说,色彩在生成的过程中时无用的。把图像作为一台顶点网格抽象出来,每个顶点代表图像上的一台像素点,每个顶点的z置等于灰度图像的值,而不是它的RGB值。
    • 求图像的偏导数
      图像从数学层面解释,其是一台2D函数,我们记做F(x,y)表示,它将空间中的坐标与像素值建立关联,但是这个函数不是一台连续函数,因为它仅仅在空间中某些离散坐标点有定义。如果描述这个抽象出来的顶点网格都有法线并且是连续的。那么就需要对这个2D函数进行偏导数,以求取各个抽象出来的顶点值的关联或者是差值来作为描述顶点网格之间凹凸连续变化。其计算的数学公式为:
      \vec{N_x}=\frac{-\frac{∂F}{∂_x}}{\sqrt{\frac{∂F}{∂_x}^{2}+\frac{∂F}{∂_y}^{2}+1}}\\ \vec{N_y}=\frac{-\frac{∂F}{∂_y}}{\sqrt{\frac{∂F}{∂_x}^{2}+\frac{∂F}{∂_y}^{2}+1}}\\ \vec{N_z}=\frac{1}{\sqrt{\frac{∂F}{∂_x}^{2}+\frac{∂F}{∂_y}^{2}+1}}
      其中F函数,是我们抽象记做的F(x,y)函数, \frac{∂F}{∂_x} 代表的是偏导数,也就是说,对图像灰度值进行偏导数。换句话说,就是求像素灰度值之间的变化率,因为函数F只能表示离散的像素之间的关系,并不能表示两个像素之间变化的值大小,偏导数却可以做到
    • 归一化
      图像存储只能存储到0到1之间的值,而我们计算的却是矢量,它的范围是-1到1之间,所以需要转换到0到1之间,转换公式为:
      R=\frac{(\vec{N_x} + 1)}{2}\\ G=\frac{(\vec{N_y} + 1)}{2}
      RG是红绿通道,而没有转换Z值,因为法线量总是面向z轴的正方向,所以不需要映射

  • 着色器代码生成如下:
#version 300 es
uniform sampler2D colorMap;
uniform vec2 imageSize;
in vec2 vTexCoord;
out vec4 fragColor;
float clamp1(float pX, float pMax)
{
    if (pX &gt; pMax)
    return pMax;
    else if (pX &lt; 0.0)
    return 0.0;
    else
    return pX;
}
float intensity(vec4 col)
{
    return 0.3 * col.x + 0.59 * col.y + 0.11 * col.z;
}
void main()
{
    float ws = 1.0/imageSize.x;
    float hs = 1.0/imageSize.y;
    float c[10];
    vec2 sTexCoord = vec2(vTexCoord.x,1.0 - vTexCoord.y);
    vec4 col = texture(colorMap,sTexCoord);
    c[3] = intensity(texture(colorMap, vec2(clamp1(sTexCoord.x+ws,imageSize.x), clamp1(sTexCoord.y+hs,imageSize.y))));
    c[6] = intensity(texture(colorMap, vec2(clamp1(sTexCoord.x+ws,imageSize.x), clamp1(sTexCoord.y,imageSize.y))));
    c[9] = intensity(texture(colorMap, vec2(clamp1(sTexCoord.x+ws,imageSize.x), clamp1(sTexCoord.y-hs,imageSize.y))));
    c[2] = intensity(texture(colorMap, vec2(clamp1(sTexCoord.x,imageSize.x), clamp1(sTexCoord.y+hs,imageSize.y))));
    c[8] = intensity(texture(colorMap, vec2(clamp1(sTexCoord.x,imageSize.x), clamp1(sTexCoord.y-hs,imageSize.y))));
    c[1] = intensity(texture(colorMap, vec2(clamp1(sTexCoord.x-ws,imageSize.x), clamp1(sTexCoord.y+hs,imageSize.y))));
    c[4] = intensity(texture(colorMap, vec2(clamp1(sTexCoord.x-ws,imageSize.x), clamp1(sTexCoord.y,imageSize.y))));
    c[7] = intensity(texture(colorMap, vec2(clamp1(sTexCoord.x-ws,imageSize.x), clamp1(sTexCoord.y-hs,imageSize.y))));
    float dx = -(c[3]+2.0*c[6]+c[9])+(c[1]+2.0*c[4]+c[7]);
    float dy = c[7]+2.0*c[8]+c[9]-(c[1]+2.0*c[2]+c[3]);
    float tempvalue = sqrt(dx * dx + dy * dy + 1.0);
    float dz = 1.0/tempvalue;
    float zx = (-dx/tempvalue + 1.0)/2.0;
    float zy = (-dy/tempvalue + 1.0)/2.0;
    fragColor = vec4(zx,zy,dz,col.a);
}
4. 结束语

如果对您有些许帮助,请推荐给自个的朋友,感谢关注、点赞和收藏。
如果文章有错误之处,请留言,笔者感激不尽。
使用道具 举报
| 来自云南
快速回复
您需要登录后才可以回帖 登录 | 立即注册

当贝投影