纹理最初的目的就是使用一张图片控制模型的外观。使用纹理映射技术,我们可以将一张图片黏在模型表面,逐纹素地控制模型的颜色。
在美术人员建模时,通常会在建模软件中利用纹理展开技术把纹理映射坐标存储在每个顶点上。纹理映射坐标定义了该顶点在纹理中对应的2D坐标。通常,这些坐标使用一个二维向量(u,v)表示,其中u是横向坐标,v是纵向坐标。因此纹理映射坐标也被称为uv坐标。
一、单张纹理
我们还是在之前的Shader基础上进行修改。首先复制一份之前的逐像素高光反射Shader
Shader "Unlit/TextureSampling"
{
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1)
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(1,256)) = 20
}
SubShader
{
Tags {
"LightMode"="ForwardBase" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct v2f
{
float4 vertex: SV_POSITION;
fixed3 worldNormal: TEXCOORD0;
fixed3 worldVert: TEXCOORD1;
};
v2f vert(appdata_base v)
{
v2f o;
// 将顶点坐标从物体空间转换到裁剪空间
o.vertex = UnityObjectToClipPos(v.vertex);
// 将法线从物体空间转换到世界空间
const fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldNormal = worldNormal;
// 缓存顶点的世界坐标
o.worldVert = mul(unity_ObjectToWorld, v.vertex);
return o;
}
fixed4 frag(v2f i):SV_Target
{
// 获取归一化的光源方向
const fixed3 worldLight = normalize(_WorldSpaceLightPos0);
// 环境光
const fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 计算漫反射
const fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(i.worldNormal,worldLight));
// 反射光向量
const fixed3 reflectDir = normalize(reflect(-worldLight, i.worldNormal));
// 视角方向向量 摄像机位置-顶点位置
const fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldVert);
// 计算高光反射
const fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
fixed3 color = ambient + diffuse+specular;
return fixed4(color,1);
}
ENDCG
}
}
}
在属性中定义一个纹理属性。其中“white”指的是内置纹理的名字,即一个全白纹理
_MainTex("MainTex",2D) = "white" {
}
然后在CG代码中声明对应的变量。这里要注意,除了需要声明_MainTex变量外,还需要声明一个_MainTex_ST变量用来得到缩放和平移值,也就是面板中的「Tiling」和「Offset」。这个名字并不是随便取的,而是Unity规定的使用纹理名_ST的方式声明某个纹理的属性。
sampler2D _MainTex;
float4 _MainTex_ST;
接下来我们需要在输出结构体中声明一个新的变量uv用来存储纹理坐标
float2 uv:TEXCOORD2;
然后在顶点着色器中计算纹理坐标。在appdata_base中就可以直接获取到,但为了让我们定义的属性可以对其进行控制,我们需要乘上缩放并加上偏移
// 计算uv坐标
o.uv = v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
最后在片元着色器中对其进行纹理采样,可以直接使用tex2D函数,它的第一个参数是需要被采样的纹理,第二个参数是一个float类型的纹理坐标。它将返回计算得到的纹素值。我们直接将返回值乘到漫反射计算公式中
const fixed3 albedo = tex2D(_MainTex,i.uv).rgb;
const fixed3 diffuse = _LightColor0.rgb * albedo * _Diffuse.rgb * saturate(dot(i.worldNormal, worldLight));
最后在面板中选择一张图片,效果如下

二、凹凸映射
所谓的凹凸映射就是使用一张纹理来修改模型表面的法线(法线贴图),来让模型看起来像是凹凸不平的效果。在之前计算漫反射时,光的入射方向是不变的,唯一能确定计算结果的是法线方向。对于一个凹凸不平的物体表面,其法线方向也是不同的。那么要想让一个光滑的物体表面看起来凹凸不平,也只需要改变其法线方向即可

实现凹凸映射主要有两种方式,一种是使用高度纹理来模拟表面位移,然后得到一个修改后的法线值,这种方法也叫高度映射。另一种方法是使用一张法线纹理来直接存储表面法线,这种方法也叫法线映射。
2.1 高度纹理
高度图中存储的是强度值,用来表示模型表面局部的海拔高度,图中颜色越浅表明该位置的表面越凸出,反之越内凹。我们可以通过PS中的「滤镜->3D->生成凹凸图」来生成一张图片的高度图。

高度图的优点是非常直观,我们可以很明确的知道一个模型表面的凹凸情况。但缺点是在实时计算时不能直接得到表面法线,而是需要根据像素的灰度值计算而来,因此更耗费性能。
2.2 法线纹理
法线纹理中存储的是表面的法线方向。由于法线方向的各个分量范围在[−1,1][-1,1][−1,1]之间,而像素的分量范围在[0,1][0,1][0,1]之间,所以需要进行映射
pixel=normal∗0.5+0.5 pixel = normal*0.5 + 0.5 pixel=normal∗0.5+0.5
也就是说,我们在Shader中对法线纹理进行纹理采样后,还要对结果进行一次逆映射,即
normal=pixel∗2−1 normal = pixel*2 - 1 normal=pixel∗2−1
根据法线纹理中存储的法线所在坐标空间的不同,法线纹理还分为模型空间法线纹理和切线空间法线纹理。下图中左侧的是模型空间法线纹理,右侧的是切线空间法线纹理。可以发现,前者是五颜六色的,而后者确是比较单一的蓝紫色。这是因为前者所有法线所在的坐标空间是同一个空间,即模型空间。而每个点存储的法线方向是各异的,比如法线(0,1,0)经过映射后存储到纹理中就对应了RGB(0.5,1,0.5),也就是浅绿色;(0,-1,0)经过映射后就变成了(0.5,0,0.5)也就是紫色。后者每个法线方向所在的坐标空间都是不一样的,即每个点各自的切线空间。也就是说,如果一个点的法线方向与模型本身的法线方向一样,则在切线空间中,新的法线方向就是z轴方向,也就是(0,0,1),映射后为RGB(0.5,0.5,1)浅蓝色。显示为紫色则代表这个顶点的法线与模型的法线偏移较大。

同样的,我们也可以直接使用PS获取到图片的法线纹理,操作是「滤镜->3D->生成法线图」

使用模型空间存储法线的优点如下:
- 实现简单,看起来直观。
- 纹理坐标缝合处和尖锐的边角部分,可见的突变较少。
使用切线空间存储法线的有点更多: - 自由度高,可以应用到不同的模型。
- 可以实现UV动画。
- 可以重用法线纹理。
- 可压缩。
因而在很多情况下,人们都选择使用切线空间存储法线。
2.3 在Unity中实现凹凸映射
这里我们采用切线空法线纹理进行计算。有如下两种方案:
- 在顶点着色器中,将视角、光照方向等变换到切线空间,与法线进行计算,然后在片元着色器中进行采样、光照模型计算。
- 直接在片元着色器中,将采样得到的法线方向变换到世界空间下,再进行其他计算。
在性能上来说第一种方案由于第二种。但第二种方法更具通用性,因为有时我们需要在世界空间下进行一些计算。
切线空间纹理映射
首先我们来实现第一种方案。
依然是复制一份之前的Shader,并在其基础上进行修改。
先定义两个属性,一个用于添加法线纹理,另一个用于控制凹凸程度
_BumpMap("Normal Map",2D) = "bump" {
}
_BumpScale("Bump Scale",

最低0.47元/天 解锁文章
807

被折叠的 条评论
为什么被折叠?



