纹理的另一种常见的应用就是凹凸映射。凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多细节。这种方法不会真的改变模型的顶点位置,只是让模型看起来是“凹凸不平”的,但可以从模型的轮廓处看出“破绽”。
有两种主要的方法可以迎来进行凹凸映射;一种方法是使用一张高度纹理来模拟表面位移,然后得到一个修改后的法线值,这种方法也被称为高度映射;另一种方法则是使用一张法线纹理来直接存储表面法线,这种方法又被称为法线映射。
我们首先来看第一种技术,既使用一张高度图来实现凹凸映射。高度图中储存的是强度值,它用于表示模型表面局部的海拔高度。因此,颜色越浅表明该位置的表面越向外凸起,而颜色越深表明该位置的表面越向里凹。这种方法的好处是非常直观,但缺点是计算更加复杂,在实时计算是不能直接得到表面法线,而是需要由像素的灰度值计算而得,因此需要消耗更多的性能。
高度图通常回合法线映射一起使用,用于给出表面的凹凸的额外信息。也就是说,我们通常会使用法线映射来修改光照。
法线纹理中存储的就是表面的法线方向。由于法线方向的分量范围在[-1,1],而像素的分量范围[0,1],因此我们需要做一个映射,通常使用的映射就是:
这就要求,我们在Shader中对返校纹理进行纹理采样后,还需要对结果进行一次反映射的过程,以得到原先的法线方向。反映射的过程实际就是使用上面映射函数的逆函数:
然而,由于方向是相对于坐标空间来说的,那么法线纹理中储存的法线方向在哪个坐标空间中呢?
对于模型顶点自带的法线,它们是定义在模型空间中的,因此一种直接的想法就是将修改后的模型空间中的表面法线储存在一张纹理中,这种纹理被称为模型空间的法线纹理。然而,在实际制作中,我们往往会采用另一种坐标空间,即模型顶点的切线空间来存储法线。对于模型的每个顶点,它都有一个属于自己的切线空间,这个切线空间的原点就是该顶点本身,而z轴是顶点的法线方向(n),x轴是顶点的切线方向(t),而y轴可由法线和切线叉积而得,也被称为是副切线,或者副法线。
这种纹理被称为是切线空间的法线纹理
由于法线纹理中存储的法线是切线空间下的方向,因此我们通常有两种选择:一种选择是在切线空间下进行光照计算,此时我们需要把光照方向、视角方向变换到切线空间下;另一种选择是在世界空间下进行光照计算,此时我们需要把采样得到的法线方向变换到世界空间下,再和世界空间下的光照方向和视角方向进行计算。从效率上来说,第一种方法往往由于第二种方法,因为我们可以在顶点着色器中就完成对光照
1、在切线空间下的计算在切线空间下计算光照模型。基本思路是:在片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角方向、光照方向等进行计算,最终得到光照结果。
我们先在新建的场景里去掉天空盒,去掉天空盒的方法在这:去掉天空盒的方法在这在此文章最底下
再新建以个材质,最后再新建一个Shader,把这个Shader赋给刚刚建好的材质。
打开Shader文件,我们开始编写代码
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter7-NormalMapTangentSpace"
{
Properties
{
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex("Main Tex", 2D) = "White" {}
_BumpMap("Normal Map", 2D) = "bump" {}
_BumpScale("Bump Scale", Float) = 1.0
_Specular("Specular", Color) = (1, 1, 1, 1)
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
pass
{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
TANGENT_SPACE_ROTATION;
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = tex2D(_BumpMap, i.uv.zw);
fixed4 packeNormal = tex2D(_BumpMap, i.uv.zw);
fixed3 tangentNormal;
tangentNormal = UnpackNormal(packeNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack"Specular"
}
返回Unity中查看,可以通过调整材质面板中的Bump Scale属性来改变模型的凹凸程度。
2、在世界空间下计算和上面一样,我们先新建材质和Shader
打开建好的Shader,开始编写代码
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Custom/Chapter_NormalMapWorldSpace"
{
Properties
{
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex("Main Tex", 2D) = "White" {}
_BumpMap("Normal Map", 2D) = "bump" {}
_BumpScale("Bump Scale", Float) = 1.0
_Specular("Specular", Color) = (1, 1, 1, 1)
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
pass
{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack"Specular"
}
从视觉表现上,在切线空间下和在世界空间下计算光照几乎没有任何差别。在Unity 4.x版本中,在不需要使用Cubemap进行环境映射的情况下,内置的Unity Shader使用的是切线空间来进行发现映射和光照计算。儿子Unity 5.x中,所有内置的Unity Shader都使用了世界空间进行光自己素昂。这也是为什么Unity5.x中表面着色器更容易报错,因为它们使用了更多差值寄存器来村春变换矩阵。
Unity中的法线纹理当我们吧法线纹理的纹理类型标识省Normal map时,可以使用Unity的内置函数UnpackNormal来得到正确的法线方向。
当我们需要使用那些包含了法线映射的内置Untiy Shader时,必须吧使用的法线纹理按上面的方式标识成Normal map才能得到正确结果。
当我们把纹理类型设置成Normal map后,还有一个复选框是Create from Grayscale,当我们把一张高度图导入Unity后,除了需要把它的纹理类型设置成Normal map外,还需要勾选Create from Grayscale,这样就可以得到类似图中的结果,然后,我们就可以把它和切线空间下的法线纹理同等对待了。
当勾选了Create from Grayscale后,还多出了两个选项——Bumpiness和Filtering。其中Bumpiness用于控制凹凸程度,而Filtering决定我们使用哪种方式来计算凹凸程度。Smooth,这使得生成后的法线纹理会比较平滑;另一种是Sharp,它会使用Sobel滤波来生成法线。Sobel滤波实现非常简单,我们只需要在一个3*3的滤波器中计算x和y的方向上的导数,然后从中得到法线即可。具体方法是:对于高度图中的每个像素,我们考虑它与水平方向和竖直方向上的像素差,把它们的差当成该点对于的法线和在x和y方向上的位移,然后使用之前提到的映射函数存储到法线纹理的r和g分量即可。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)