目录
1.什么是UV
对于三维模型,有两个最重要的坐标系统,一是顶点的位置(X,Y,Z)坐标,另一个就是UV坐标。什么是UV?简单的说,就是贴图影射到模型表面的依据。 完整的说,其实应该是UVW(因为XYZ已经用过了,所以另选三个字母表示)。U和V分别是图片在显示器水平、垂直方向上的坐标,取值一般都是0~1,也 就是(水平方向的第U个像素/图片宽度,垂直方向的第V个像素/图片高度)。那W呢?贴图是二维的,何来三个坐标?嗯嗯,W的方向垂直于显示器表面,一般 用于程序贴图或者某些3D贴图技术(记住,确实有三维贴图这种概念!),对于游戏而言不常用到,所以一般我们就简称UV了。
定义纹理:
Properties
{
// 主纹理
_MaxTex ("_MaxTex",2d) = "white" {}
}
sampler2D _MaxTex;
float4 _MaxTex_ST;
_MainTex_ST:表示该纹理的偏移缩放属性,在属性面板上表现出tilling 和 offset ,float4 类型xy 存的是缩放 zw 存的是偏移
通过 _MainTex_ST 重新计算uv texcord *_MainTex_ST.xy+_MainTex_ST.zw
或者使用内置方法 TRANSFORM_TEX(uv,_MainTex)
tex2D(_MainTex,uv) 对_MainTex采样,得到该uv 坐标下的颜色
上完整代码:这里加了漫反射和高光的代码有不懂的可以看:漫反射,高光反射
Shader "Unlit/005"
{
Properties
{
// 漫反射颜色
_Diffuse ("Diffuse",Color) = (1,1,1,1)
// 高光反射颜色值
_Specular ("Specular",Color) = (1,1,1,1)
// 高光反射值
_Gloss ("_Gloss",range(1,100)) = 5
// 主纹理
_MaxTex ("_MaxTex",2d) = "white" {}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MaxTex;
float4 _MaxTex_ST;
float4 _Specular;
float4 _Diffuse;
float _Gloss;
struct v2f
{
float4 vertex : POSITION;
float2 uv : TEXCOORD1;
float3 worldNormal : TEXCOORD0;
float3 viewDir : TEXCOORD2;
};
v2f vert (appdata_base v)
{
v2f o;
// 将对象空间中的点变换到齐次坐标中的摄像机裁剪空间
o.vertex = UnityObjectToClipPos(v.vertex);
// 计算uv坐标
o.uv = TRANSFORM_TEX(v.texcoord,_MaxTex);
// o.uv = v.texcoord.xy * _MaxTex_ST.xy + _MaxTex_ST.zw;
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.viewDir = normalize(WorldSpaceViewDir(o.vertex));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 纹理采样
float3 texColor = tex2D(_MaxTex,i.uv);
// 计算漫反射
float3 diffuse = texColor * _LightColor0.rgb * _Diffuse.rgb * (dot(_WorldSpaceLightPos0.xyz,i.worldNormal) * 0.5 + 0.5);
// 计算高光
float3 halfVector = normalize(normalize(_WorldSpaceLightPos0.xyz) + i.viewDir);
// 计算高光
float3 specular = texColor * _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(halfVector,i.worldNormal)),_Gloss);
return float4(diffuse + specular,1);
}
ENDCG
}
}
}
2.凹凸纹理
凹凸纹理的本质是通过一张存储了顶点法线坐标的图片,营造出一种凹凸的效果,模型本身并没有改变
为什么改变了法线就能营造出凹凸的结果,因为法线的作用就是就算光照,现实生活中,你看到的物体觉得很立体,就是因为光照到物体上产生了阴影,有明暗区分,所以觉得很立体,凹凸贴图通过改变法线,间接改变了光照的方向,和阴影产生的结果,让你看起来有种立体的感觉。
凹凸纹理 分为两种:
-
高度贴图:高度贴图,存储的并不是顶点的法线,而是强度值,表示该像素的海拔高度,颜色越深,表示越往里凹,因为越凹越看不见,如上高度映射图,越白的地方高度越高,越黑的地方高度越低,需要根据灰度值,转到表面法线计算比较复杂,也不利于美术制作。
-
法线贴图: 存储的就是顶点的法线坐标.
法线纹理中存储的是表面的法线方向。由于法线方向的各个分量范围在[ − 1 , 1 ] 之间,而像素的分量范围在[ 0 , 1 ] 之间,所以需要进行映射
pixel=normal∗0.5+0.5
所以,我们在Shader中对法线纹理进行纹理采样后,还要对结果进行一次逆映射,即
normal=pixel∗2−1
为什么法线贴图大部分都是浅蓝色呢?
浅蓝色表示改法线贴图存储的是切线空间下的法线,顶点的原始法线是(0,0,1),映射到像素上RGB(0.5,0.5,1),浅蓝色.
法线贴图有两种形式:
-
模型空间下的法线坐标
-
切线空间下的法线坐标
模型空间法线:
优点: 直接就能获取到模型的法线,不需要空间转换,因为所有的法线都是在同一坐标空间下,所以边角处通过线性插值得到的效果更加平滑.
缺点:因为 模型空间存储的是绝对法线,只适用于创建该法线的的模型,如果换个模型就得不到正确的结果,且无法进行uv偏移,
切线空间下法线
- 自由度更高。
- 切线空间下的法线纹理是相对的法线纹理,即使在不同网格上也可以得到合理的结果。
- 可以进行uv 动画,可以通过移动一个纹理的uv坐标来实现凹凸移动的效果。
- 可以重用法线纹理。
- 可以压缩,切线空间下,z轴总是正方向的,可以同过xy推导出 z坐标 = sqrt(1-max(0,dot(xy,xy)))。
如何实现?
要计算光照,就要统一坐标空间,要么在切线空间,要么在世界空间。
切线空间: 在顶点中将 光源方向 和 视角方向 转换到切线空间下,然后在片元中计算光照模型
世界空间:把法线转换到世界空间下,因为有时候我们需要用到世界空间下的法线坐标和光照方向比如对cubemap 进行环境采样,因为纹理坐标是逐像素的,所以要在片元着色器中进行采样然后进行一个矩阵变换,比上面多了一次矩阵运算。
我们先来实现以下 切线空间的纹理映射:
接下来需要将光源方向和视角方向变换到切线空间,要完成这步操作需要求出变换所需的旋转矩阵。我们已知的是变换后的三个向量:
- b = (0,1,0)
- t = (1,0,0)
- n = (0,0,1)
假设变换前的三个向量为:
- b′ = ( xb,yb,zb)
- t′ = (xt,yt, zt)
- n′ = ( xn,yn,zn)
变换矩阵为 :
可以求得 c1 = t′ , c2 = b′ , c3 = n′ 。
shader 代码:
// 求副切线向量 float3 biNormal = cross(normalize(v.normal),normalize(v.tangent.xyz))*v.tangent.w; // 求出旋转矩阵 float3x3 rotation = float3x3(v.tangent.xyz,biNormal,v.normal);
可以使用unity内置的自定义写法
TANGENT_SPACE_ROTATION
在 UnityCG.cginc中 实际上就是帮我们写了上面求出的切线空间旋转变换矩阵
Shader "Unlit/006"
{
Properties
{
// 漫反射颜色
_Diffuse ("Diffuse",Color) = (1,1,1,1)
// 高光反射颜色值
_Specular ("Specular",Color) = (1,1,1,1)
// 高光反射值
_Gloss ("_Gloss",range(1,100)) = 5
// 主纹理
_MaxTex ("MaxTex",2d) = "white" {}
// 法线纹理
_BumpMap ("Bump Map",2d) = "white" {}
// 控制凹凸程度
_BumpScale ("Bump Scale",float) = 1
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MaxTex;
float4 _MaxTex_ST;
float4 _Specular;
float4 _Diffuse;
float _Gloss;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
struct v2f
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float2 normalUv : TEXCOORD1;
float3 lightDir : TEXCOORD2;
float3 viewDir : TEXCOORD3;
};
v2f vert (appdata_tan v)
{
v2f o;
// 将对象空间中的点变换到齐次坐标中的摄像机裁剪空间
o.vertex = UnityObjectToClipPos(v.vertex);
// 计算uv坐标
o.uv = TRANSFORM_TEX(v.texcoord,_MaxTex);
o.normalUv = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
// TANGENT_SPACE_ROTATION
// 求副切线向量
float3 biNormal = cross(normalize(v.normal),normalize(v.tangent.xyz))*v.tangent.w;
// 求出旋转矩阵
float3x3 rotation = float3x3(v.tangent.xyz,biNormal,v.normal);
// 求切线空间的光源方向
o.lightDir = normalize(mul(rotation,UnityWorldToObjectDir(_WorldSpaceLightPos0.xyz)));
// 切线空间视角方向
o.viewDir = normalize(mul(rotation,ObjSpaceLightDir(v.vertex)));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 主纹理采样
fixed4 texColor = tex2D(_MaxTex,i.uv);
// 法线贴图采样
fixed4 packedNormal = tex2D(_BumpMap,i.normalUv);
fixed3 tangentNormal;
tangentNormal.xy = (packedNormal.xy * 2 - 1 ) * _BumpScale;
tangentNormal.z = sqrt(1 - max(0,dot(tangentNormal.xy,tangentNormal.xy)));
// 用内置方法 UnpackNormal 图片类型为NormalMap
//fixed3 tangentNormal = UnpackNormal(packedNormal);
// tangentNormal.xy *= _BumpScale;
// 计算漫反射
float3 diffuse = texColor.rgb * _LightColor0.rgb * _Diffuse.rgb * (dot(i.lightDir,tangentNormal) * 0.5 + 0.5);
// 计算高光
float3 halfVector = normalize(i.lightDir + i.viewDir);
// 计算高光
float3 specular = texColor.rgb * _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(halfVector,tangentNormal)),_Gloss);
return float4(diffuse + specular,1);
}
ENDCG
}
}
}
给上主纹理贴图,和法线贴图,法线贴图可以用主纹理在PhotoShop中,滤镜->3D->生成法线贴图就制作好了,不过下项目中通常是美术给的
shader效果如下:左边是没用法线的,右边是加了法线纹理的可以看出加了法线纹理更有凹凸感
我们在用 世界空间纹理映射实现以下:
我们需要在顶点着色器中计算出从切线空间转换到世界空间的变换矩阵,但一个插值寄存器最多只能存储float4大小的变量,所以对于矩阵这种变量,我们需要定义三个float3类型的变量拆开存储。但为了充分利用插值寄存器的空间,我们将其定义为float4类型,多出来的一个维度可以用来存储世界空间下的顶点位置。uv变量也可以定义成float4类型,其中xy分量用来存储贴图纹理坐标,zw分量用来存储法线纹理坐标。
直接上代码了,都在代码里详细备注:
Shader "Unlit/007"
{
Properties
{
// 漫反射颜色
_Diffuse ("Diffuse",Color) = (1,1,1,1)
// 高光反射颜色值
_Specular ("Specular",Color) = (1,1,1,1)
// 高光反射值
_Gloss ("_Gloss",range(1,100)) = 5
// 主纹理
_MaxTex ("MaxTex",2d) = "white" {}
// 法线纹理
_BumpMap ("Bump Map",2d) = "white" {}
// 控制凹凸程度
_BumpScale ("Bump Scale",float) = 1
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MaxTex;
float4 _MaxTex_ST;
float4 _Specular;
float4 _Diffuse;
float _Gloss;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
struct v2f
{
float4 vertex : POSITION;
float4 uv : TEXCOORD0;
float4 T2w0 : TEXCOORD1;
float4 T2w1 : TEXCOORD2;
float4 T2w2 : TEXCOORD3;
};
v2f vert (appdata_tan v)
{
v2f o;
// 将对象空间中的点变换到齐次坐标中的摄像机裁剪空间
o.vertex = UnityObjectToClipPos(v.vertex);
// 计算uv坐标
o.uv.xy = TRANSFORM_TEX(v.texcoord,_MaxTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord,_BumpMap);
float2 normalUv = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldBiTangent = cross(worldTangent,worldNormal) * v.tangent.w;
fixed3 worldPos = UnityObjectToWorldDir(v.vertex.xyz);
o.T2w0 = float4(worldTangent.x,worldBiTangent.x,worldNormal.x,worldPos.x);
o.T2w1 = float4(worldTangent.y,worldBiTangent.y,worldNormal.y,worldPos.y);
o.T2w2 = float4(worldTangent.z,worldBiTangent.z,worldNormal.z,worldPos.z);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 主纹理采样
fixed4 texColor = tex2D(_MaxTex,i.uv.xy);
// 法线贴图采样
fixed4 packedNormal = tex2D(_BumpMap,i.uv.zw);
//fixed3 tangentNormal;
//tangentNormal.xy = (packedNormal.xy * 2 - 1 ) * _BumpScale;
//tangentNormal.z = sqrt(1 - max(0,dot(tangentNormal.xy,tangentNormal.xy)));
// 用内置方法 UnpackNormal 图片类型为NormalMap
fixed3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
// 将切线空间法线变换到世界空间
fixed3 worldNormal = normalize(fixed3(dot(i.T2w0.xyz,tangentNormal),dot(i.T2w1.xyz,tangentNormal),dot(i.T2w2.xyz,tangentNormal)));
// 计算漫反射
float3 diffuse = texColor.rgb * _LightColor0.rgb * _Diffuse.rgb * (dot(_WorldSpaceLightPos0.xyz,worldNormal) * 0.5 + 0.5);
// 计算高光
float3 viewDir = _WorldSpaceCameraPos.xyz - float3(i.T2w0.z,i.T2w1.z,i.T2w2.z);
float3 halfVector = normalize(_WorldSpaceLightPos0.xyz + viewDir);
// 计算高光
float3 specular = texColor.rgb * _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(halfVector,worldNormal)),_Gloss);
return float4(diffuse + specular,1);
}
ENDCG
}
}
}
效果如下:分别为 无法线贴图,切线空间纹理映射,世界空间纹理映射
3.渐变纹理映射
纹理不止能用来定义一个物体的颜色,也可以用来存储任何表面的属性。一种常见的用法就是使用渐变纹理来控制漫反射光照的结果。使用一张从冷到暖的渐变图片用于纹理采样,并将采样结果用于计算漫反射模型。就可以得到一种插画风格的渲染效果,物体的轮廓线相比于传统的漫反射更加明显。很多卡通风格的渲染中都使用了这种技术。
Shader "Unlit/008"
{
Properties
{
// 漫反射颜色
_Diffuse ("Diffuse",Color) = (1,1,1,1)
// 高光反射颜色值
_Specular ("Specular",Color) = (1,1,1,1)
// 高光反射值
_Gloss ("_Gloss",range(1,100)) = 5
// 主纹理
_MaxTex ("_MaxTex",2d) = "white" {}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MaxTex;
float4 _MaxTex_ST;
float4 _Specular;
float4 _Diffuse;
float _Gloss;
struct v2f
{
float4 vertex : POSITION;
float3 worldNormal : TEXCOORD0;
float3 viewDir : TEXCOORD2;
};
v2f vert (appdata_base v)
{
v2f o;
// 将对象空间中的点变换到齐次坐标中的摄像机裁剪空间
o.vertex = UnityObjectToClipPos(v.vertex);
// 计算uv坐标
//o.uv = TRANSFORM_TEX(v.texcoord,_MaxTex);
// o.uv = v.texcoord.xy * _MaxTex_ST.xy + _MaxTex_ST.zw;
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.viewDir = normalize(WorldSpaceViewDir(o.vertex));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 半兰伯特漫反射
float halfLambert = (dot(_WorldSpaceLightPos0.xyz,i.worldNormal) * 0.5 + 0.5);
// 采样渐变纹理 使用半兰伯特做uv坐标
float3 texColor = tex2D(_MaxTex,float2(halfLambert,halfLambert));
float3 diffuse = texColor * _LightColor0.rgb * _Diffuse.rgb * texColor;
// 计算高光
float3 halfVector = normalize(normalize(_WorldSpaceLightPos0.xyz) + i.viewDir);
// 计算高光
float3 specular = texColor * _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(halfVector,i.worldNormal)),_Gloss);
return float4(diffuse + specular,1);
}
ENDCG
}
}
}
效果:
4.遮罩纹理
在游戏中地形场景中,假设有草地和沙漠的接壤处,为了保护各自区域的反射值,我们就会用到了遮罩纹理,假设草地需要高光,沙漠不需要高光。
实现非常简单,就是增加一张遮罩纹理,需要高光的为白色,不需要的地方为黑色,因为白色的rgb值为 255/255 = 1。同常只需要一个通道就可以实现了,然后在计算高光的地方 乘上遮罩的采样值。
Shader "Unlit/009"
{
Properties
{
// 漫反射颜色
_Diffuse ("Diffuse",Color) = (1,1,1,1)
// 高光反射颜色值
_Specular ("Specular",Color) = (1,1,1,1)
// 高光反射值
_Gloss ("_Gloss",range(1,100)) = 5
// 主纹理
_MaxTex ("_MaxTex",2d) = "white" {}
// 高光遮罩纹理
_SpecularMask ("Specular Mask",2d) = "white" {}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MaxTex;
float4 _MaxTex_ST;
sampler2D _SpecularMask;
float4 _SpecularMask_ST;
float4 _Specular;
float4 _Diffuse;
float _Gloss;
struct v2f
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float2 maskUv : TEXCOORD1;
float3 worldNormal : TEXCOORD2;
float3 viewDir : TEXCOORD3;
};
v2f vert (appdata_base v)
{
v2f o;
// 将对象空间中的点变换到齐次坐标中的摄像机裁剪空间
o.vertex = UnityObjectToClipPos(v.vertex);
// 计算uv坐标
o.uv = TRANSFORM_TEX(v.texcoord,_MaxTex);
o.maskUv = TRANSFORM_TEX(v.texcoord,_SpecularMask);
// o.uv = v.texcoord.xy * _MaxTex_ST.xy + _MaxTex_ST.zw;
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.viewDir = normalize(WorldSpaceViewDir(o.vertex));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 texColor = tex2D(_MaxTex,i.uv);
// 半兰伯特漫反射
float halfLambert = (dot(_WorldSpaceLightPos0.xyz,i.worldNormal) * 0.5 + 0.5);
// 采样渐变纹理 使用半兰伯特做uv坐标
float3 diffuse = texColor * _LightColor0.rgb * _Diffuse.rgb * halfLambert;
// 计算高光
float3 halfVector = normalize(normalize(_WorldSpaceLightPos0.xyz) + i.viewDir);
// 采样遮罩纹理
float3 maskColor = tex2D(_SpecularMask,i.maskUv);
// 计算高光
float3 specular = texColor * _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(halfVector,i.worldNormal)),_Gloss) * maskColor;
return float4(diffuse + specular,1);
}
ENDCG
}
}
}
地形图: 遮罩图:
效果如下:左边是不加遮罩纹理,右边是加了遮罩纹理。
可以看出,不加遮罩纹理两边都有高光。