NormalMap 法线纹理:比较经常使用算法
HeightMap 高度纹理(视差映射):手机平台不经常使用,使用法线纹理替代。函数
Occlusion Map:细节纹理优化
Secondarspa
y Maps (Detail Maps) & Detail Mask:细节纹理code
Bump Map Type | Describe |
NormalMap | 法线图,映射公式:normal=pixel*2-1,反映射:pixel=(normal+1)/2. 法线存储既能够在模型空间,也能够在切线空间。//unity顶点输入结构带切线变量,通常存在切线空间更佳。 |
HeightMap | 灰度图(黑白纹理-强度值),颜色越浅该表面越向外凸起,颜色深越凹。视差映射技术,与Occlusion Map搭配使用体验更佳,计算昂贵。 |
Occlusion Map | 灰度图,表面细节更丰富。颜色值白色表示应该接收彻底间接照明的区域,黑色表示没有间接照明。如裂缝或褶皱,实际上不会接收到太多的间接光,可与高度图一块儿使用。 |
Detail Maps | 参考StandardShader:第二细节纹理,应用第二反照率图和第二法线图,在近距离观察时有清晰的细节,好比毛孔、细小的裂缝等。计算昂贵。 |
高度图为了模拟平面的凹凸程度,将高度(黑白色)数据存储在纹理中,因为纹理数据是二维的,即u轴和v轴,那为了获得这些数据为每一个片断生成法向量,可分别在u轴和v轴上采样。先从U轴计算:f(u)=h ,若是知道了斜率就能求得u轴上全部点的法向量,但斜率由h的变化程度高低决定。为了近似获得从一个点到下一个点的高度差:orm
图八:斜率采样示意图,从f(0)到f(1)对象
这是对切向量的一个粗略的估算,它把整张纹理做为线性的斜率。那为了不这种粗略计算,能够采样两个靠的更近点的。例如,从0到1/2,那这两点的斜率=f(1/2)-f(0),同时f因子被缩小,须要乘以相应的倍数。2(f(1/2)−f(0))。扩展开来,能够获得以下的函数:δ值越小越精确,必须大于0小于1。blog
图九:有限差分get
那么切向量就是[1, f’(u), 0]T,从切向量计算法向量[f’(u),1,0]T:
sampler2D _HeightMap; float4 _HeightMap_TexelSize;//xy是纹素坐标(uv),zw是整张纹理宽高 float2 delta = float2(_HeightMap_TexelSize.x, 0);//u轴 float h1 = tex2D(_HeightMap, i.uv);//模型uv在高度图采样 float h2 = tex2D(_HeightMap, i.uv + delta);//二次采样 //第一步套用公式 //i.normal = float3(1, (h2 - h1) / delta.x, 0); //第二步优化,缩放向量并不改变方向,消除了除法操做 //i.normal = float3( delta.x, (h2 - h1), 0); //第三步改变垂直方向,须要获得法向量正向垂直于表面,那么逆时针旋转90度以翻转x份量符号.//Y是扰动法向量的高低变化因子 i.normal = float3( h1 - h2, 1 , 0); i.normal = normalize(i.normal);
有限差分只在一个方向近似求值,为了更好近似能够在两个方向线性逼近:
图十:中心差分
float2 delta = float2(_HeightMap_TexelSize.x * 0.5, 0); float h1 = tex2D(_HeightMap, i.uv - delta); float h2 = tex2D(_HeightMap, i.uv + delta); i.normal = float3(h1 - h2, 1, 0);
那么f’(u,v)计算f’(v)同理,切向量[0,f’(v),1]T,法向量是[0,1,-f’(v)]:
float2 du = float2(_HeightMap_TexelSize.x * 0.5, 0); float u1 = tex2D(_HeightMap, i.uv - du); float u2 = tex2D(_HeightMap, i.uv + du); //float3 tu = float3(1, u2 - u1, 0); float2 dv = float2(0, _HeightMap_TexelSize.y * 0.5); float v1 = tex2D(_HeightMap, i.uv - dv); float v2 = tex2D(_HeightMap, i.uv + dv); //float3 tv = float3(0, v2 - v1, 1); //i.normal = cross(tv, tu);//直接使用叉积求出垂直于u和v轴的法向量=>(0*(v2-v1)-(u2-u1)*1, 1*1-0*0, (u2-u1)*0-1*(v2-v1))=(u1-u2, 1, v1-v2) i.normal = float3(u1 - u2, 1, v1 - v2);
i.normal = normalize(i.normal);
高度图是每帧采样实时计算法线,为了不计算,采用预制法线纹理代替。
图十一:Unity中使用高度图
导入高度图做为法线贴图预先计算法线纹理必须勾选Create from Grayscale,白色表示相对更高,黑色表示相对更低。
像素份量范围是[0,1],而法线份量范围[-1,1]。相互映射转换公式为:
pixel = (normal+1)/2;
normal = pixel · 2 – 1;
法线纹理呈现淡蓝色,这是由于法向映射最多见的约定是将向上的方向存储在Z份量中(垂直于表面外侧),又因为DXT5nm纹理压缩格式的缘由,只存储了X与Y份量舍弃了Z份量(Y份量存储在G通道,X份量存储在A通道,RB通道被舍弃)。经过推导法向量的单位向量可得Z份量:
|N| = |N|2 = Nx2 + Ny2 + Nz2 = 1;
Nz = 根号(1 -Nx2 - Ny2);
//第一种方法
// Unpack normal as DXT5nm (1, y, 1, x) or BC5 (x, y, 0, 1) //dxt5压缩对应的位置取wy i.normal.xy = tex2D(_NormalMap, i.uv).wy * 2 - 1; i.normal.xy *= _BumpScale;//计算Z以前缩放才有效,平坦凹凸程度 i.normal.z = sqrt(1 - saturate(dot(i.normal.xy, i.normal.xy)));//dot模拟平方计算-((x,y)*(x,y))=-x方-y方 i.normal = i.normal.xzy; i.normal = normalize(i.normal);
//UnityStandardUtils.cginc包含了解码法线函数,替代上面的方法 i.normal = UnpackScaleNormal(tex2D(_NormalMap, i.uv), _BumpScale); i.normal = i.normal.xzy; i.normal = normalize(i.normal);
第二细节纹理与MainTexture合并,简要代码以下:
//顶点uv坐标映射到纹理uv i.uv.xy = TRANSFORM_TEX(v.uv, _MainTex); i.uv.zw = TRANSFORM_TEX(v.uv, _DetailTex); //计算第二纹理的影响 float3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Tint.rgb; albedo *= tex2D(_DetailTex, i.uv.zw) * unity_ColorSpaceDouble;//颜色空间转换
第二细节纹理的法线映射
i.normal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale); i.normal = UnpackScaleNormal(tex2D(_DetailNormalMap, i.uv.zw), _DetailBumpScale); i.normal = i.normal.xzy; i.normal = normalize(i.normal);
方式一:(main.normal + details.normal) * 0.5; 简单容易,但结果不是很好。主纹理和细节纹理都变得平坦。理想状况下,当其中一个是平的,指望它不会影响到另外一个。
float3 mainNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale); float3 detailNormal = UnpackScaleNormal(tex2D(_DetailNormalMap, i.uv.zw), _DetailBumpScale); i.normal = (mainNormal + detailNormal) * 0.5; i.normal = i.normal.xzy; i.normal = normalize(i.normal);
方式二:用z份量作缩放因子求偏导函数,而后相加。[Mx, My, Mz]T = [Mx/Mz, My/Mz, 1]T 同理求得detail偏导函数,而后相加:[Mx/Mz + Dx/Dz, My/Mz + Dy/Dz, 1]T .效果很好,可是在合并陡峭时仍将失去细节。
float3 mainNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale); float3 detailNormal = UnpackScaleNormal(tex2D(_DetailNormalMap, i.uv.zw), _DetailBumpScale); i.normal = float3(mainNormal.xy / mainNormal.z + detailNormal.xy / detailNormal.z, 1); i.normal = i.normal.xzy; i.normal = normalize(i.normal);
方式三:白色调和,对上一步合并法线分别乘以MzDz,而后再去掉x和y的缩放因子夸大缩放,使陡峭更加明显,同时平坦的法线,它不会影响到另外一个了。
float3 mainNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale); float3 detailNormal = UnpackScaleNormal(tex2D(_DetailNormalMap, i.uv.zw), _DetailBumpScale); i.normal = float3(mainNormal.xy + detailNormal.xy, mainNormal.z * detailNormal.z); //UnityStandardUtils包含了混合函数 //i.normal = BlendNormals(mainNormal, detailNormal); i.normal = i.normal.xzy; i.normal = normalize(i.normal);View Code
切线空间的法线纹理:顶点为原点,z轴为法线方向,x轴为切线方向,y轴为垂直于xz的副切线方向。Unity导入模型计算切线默认使用了mikktspace(在顶点着色器计算),也能够在片元着色器计算cross获得副切线向量。
顶点下计算:
struct VertexData { float4 tangent : TANGENT; }; struct Interpolators { float4 tangent : TEXCOORD2; };
使用UnityCG中的UnityObjectToWorldDir在顶点程序中将切线转换为世界空间。 固然,这仅适用于切线的XYZ部分。 它的W份量须要不加修改地传递。
Interpolators MyVertexProgram (VertexData v) { Interpolators i; i.position = mul(UNITY_MATRIX_MVP, v.position); i.worldPos = mul(unity_ObjectToWorld, v.position); i.normal = UnityObjectToWorldNormal(v.normal); i.tangent = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w); i.uv.xy = TRANSFORM_TEX(v.uv, _MainTex); i.uv.zw = TRANSFORM_TEX(v.uv, _DetailTex); ComputeVertexLightColor(i); return i; }
如今咱们能够将法线从切线空间转换为世界空间。
float3 binormal = cross(i.normal, i.tangent.xyz) * i.tangent.w;
i.normal = normalize(
tangentSpaceNormal.x * i.tangent +
tangentSpaceNormal.y * i.normal +
tangentSpaceNormal.z * binormal
);
去掉显式YZ交换,将其与空间转换结合在一块儿。
//tangentSpaceNormal = tangentSpaceNormal.xzy;float3 binormal = cross(i.normal, i.tangent.xyz) * i.tangent.w; i.normal = normalize( tangentSpaceNormal.x * i.tangent + tangentSpaceNormal.y * binormal + tangentSpaceNormal.z * i.normal );
在构造副法线时,还有一个额外的细节。假设一个对象的scale设置为(- 1,1,1),这意味着它是镜像的。在这种状况下,咱们必须翻转副法线,来正确地镜像切线空间。事实上,当奇数维数为负时,咱们必须这样作。UnityShaderVariables经过定义float4 unity_WorldTransformParams变量来帮助咱们完成这个任务。当须要翻转副法线时,它的第四个份量为- 1,不然为1。
float3 binormal = cross(i.normal, i.tangent.xyz) *(i.tangent.w * unity_WorldTransformParams.w);
转换空间:
在世界空间下计算
fixed4 MyFrag(v2f v) : SV_TARGET{ //... float3 tangentSpaceNormal= UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale); #if defined(BINORMAL_PER_FRAGMENT) float3 binormal = cross(v.normal, v.tangent.xyz) * v.tangent.w; #else float3 binormal = v.binormal; #endif //把切线空间转到世界空间 //tangentSpaceNormal * [v.tangent,binromal, v.normal]T v.normal = normalize( tangentSpaceNormal.x * v.tangent + tangentSpaceNormal.y * binormal + tangentSpaceNormal.z * v.normal ); //... }
在切线空间计算
//计算副切线 float3 binormal = cross(normalize(i.normal), normalize(i.tangent.xyz)) * i.tangent.w; //切线空间矩阵//行优先的填充 float3x3 t_matrix = float3x3(i.tangent.xyz, binormal, i.normal); //把各类信息转到切线空间下参与计算
在顶点计算没必要计算叉乘函数,经过宏定义开启。
struct Interpolators { float4 position : SV_POSITION; float4 uv : TEXCOORD0; float3 normal : TEXCOORD1; #if defined(BINORMAL_PER_FRAGMENT) float4 tangent : TEXCOORD2; #else float3 tangent : TEXCOORD2; float3 binormal : TEXCOORD3; #endif float3 worldPos : TEXCOORD4; #if defined(VERTEXLIGHT_ON) float3 vertexLightColor : TEXCOORD5; #endif };
若是不肯定在哪里计算比较好,能够同时支持这两种方法。假设定义了BINORMAL_PER_FRAGMENT,咱们逐像素计算每一个片断的副法线。不然,逐顶点计算。在前一种状况下,咱们保持咱们的float4 tangent变量 。在后者中,咱们须要两个float3变量。
float3 CreateBinormal (float3 normal, float3 tangent, float binormalSign) { return cross(normal, tangent.xyz) * (binormalSign * unity_WorldTransformParams.w); } Interpolators MyVertexProgram (VertexData v) { Interpolators i; i.position = mul(UNITY_MATRIX_MVP, v.position); i.worldPos = mul(unity_ObjectToWorld, v.position); i.normal = UnityObjectToWorldNormal(v.normal); #if defined(BINORMAL_PER_FRAGMENT) i.tangent = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w); #else i.tangent = UnityObjectToWorldDir(v.tangent.xyz); i.binormal = CreateBinormal(i.normal, i.tangent, v.tangent.w); #endif i.uv.xy = TRANSFORM_TEX(v.uv, _MainTex); i.uv.zw = TRANSFORM_TEX(v.uv, _DetailTex); ComputeVertexLightColor(i); return i; } … void InitializeFragmentNormal(inout Interpolators i) { float3 mainNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale); float3 detailNormal = UnpackScaleNormal(tex2D(_DetailNormalMap, i.uv.zw), _DetailBumpScale); float3 tangentSpaceNormal = BlendNormals(mainNormal, detailNormal); #if defined(BINORMAL_PER_FRAGMENT) float3 binormal = CreateBinormal(i.normal, i.tangent.xyz, i.tangent.w); #else float3 binormal = i.binormal; #endif i.normal = normalize( tangentSpaceNormal.x * i.tangent + tangentSpaceNormal.y * binormal + tangentSpaceNormal.z * i.normal ); }
要对全部Pass块生效,须要使用CGINCLUDE … ENDCG包含
支持原做者!