马不停蹄写第二篇,我觉得第二篇总体应该会比第一篇简单一丢丢,因为第一篇讲了太多的光照的规则了,这些规则上的小坑比较恼人,但是大坑比如光照纹理的采样计算衰减什么的Unity都通过宏定义自动完成了,但是纹理这一篇我感觉重点是几个大坑,比如说切线空间与矩阵空间的转换,还有镜面折射的一些规则和语法。
基础纹理
基础纹理算是比较简单,我们只需要注意以下几点:
1.从顶点着色器VertexData中定义的TEXCOORD0中存储着模型的第一组纹理坐标值,这与我们VertexToFragment结构体中定义的TEXCOORD有所区别,在VToF中,TEXCOORD仅仅是一种变量名,它的内容由我们自己来决定。
2.Shader除了定义纹理本身,还需要定义一个float4的纹理变换属性,需要用纹理名_ST来标注它,ST分别是缩放(Scale)和平移(Translation)的缩写。一般说来,这个float4的xy存储缩放,而zw存储偏移。
我们在顶点着色器中定义一个纹理的缩放和偏移时,将_MainTex_ST.xy用于给顶点的纹理坐标进行缩放,将_MainTex_ST.zw用于给顶点的纹理坐标进行平移,但Unity中也有内置的宏TRANSFORM_TEX来帮助我们完成这一点,二者是相等的:
VToF.uv=v.uv*_MainTex_ST.xy+_MainTex_ST.zw;
//VToF.uv=TRANSFORM_TEX(v.uv,_MainTex);
在片元着色器中我们只需要将纹理采样,再和光照结果相乘输出就可以了(光照在上一篇博客已经讲过了),下面是代码:
Shader "Custom/TextureCombine"
{
Properties
{
_MainTex("Maintex",2D)="white"{}
_MainTexColor("MainTexColor",Color)=(1,1,1,1)
_SecondTex("SecondTex",2D)="white"{}
_SecondTexColor("SecondTexColor",Color)=(1,1,1,1)
_Specular("SpecularColor",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8,256))=20
}
SubShader
{
pass
{
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex myVertex
#pragma fragment myFragment
#include "Lighting.cginc"
sampler2D _MainTex;
fixed4 _MainTex_ST;
fixed4 _MainTexColor;
sampler2D _SecondTex;
fixed4 _SecondTex_ST;
fixed4 _SecondTexColor;
fixed4 _Specular;
fixed _Gloss;
struct VertexData
{
fixed4 vertex:POSITION;
fixed4 normal:NORMAL;
fixed4 uv:TEXCOORD0;
};
struct VertexToFragment
{
fixed4 pos:SV_POSITION;
fixed3 worldNormal:TEXCOORD0;
fixed3 worldPos:TEXCOORD1;
fixed2 uv:TEXCOORD2;
fixed2 detailUV:TEXCOORD3;
};
VertexToFragment myVertex(VertexData v)
{
VertexToFragment VToF;
VToF.pos=UnityObjectToClipPos(v.vertex);
VToF.worldNormal=UnityObjectToWorldNormal(v.normal);
VToF.worldPos=mul(unity_ObjectToWorld,v.vertex);
VToF.uv=TRANSFORM_TEX(v.uv,_MainTex);
VToF.detailUV=TRANSFORM_TEX(v.uv,_SecondTex);
return VToF;
}
fixed4 myFragment(VertexToFragment VToF):SV_TARGET
{
fixed3 worldLight=normalize(UnityWorldSpaceLightDir(VToF.worldPos));
fixed3 worldNormal=normalize(VToF.worldNormal);
fixed3 worldView=normalize(_WorldSpaceCameraPos.xyz-VToF.worldPos.xyz);
fixed3 reflection=normalize(reflect(-worldLight,worldNormal));
fixed3 Diffuse=_LightColor0.xyz*max(0,dot(worldLight,worldNormal));
fixed3 Specular=_Specular.xyz*pow(max(0,dot(worldView,reflection)),_Gloss);
fixed3 albedo=tex2D(_MainTex,VToF.uv.xy).rgb*_MainTexColor.rgb*tex2D(_SecondTex,VToF.detailUV.xy).rgb*_SecondTexColor.rgb;
//fixed3 Ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
return fixed4(Specular+(Diffuse*albedo),1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
我们需要注意在最终输出的时候颜色之间的+号和*号的意义,两个颜色相加,代表两个颜色叠加。两个颜色相乘,代表两个颜色相交处产生混合。例如我们将纹理采样的结果Albedo加上漫反射的结果,那么整个球体都能看到贴图,只有将Albedo乘以漫反射才能只在光照处看到贴图,例如下图,第一张是相加的结果,第二张是相乘的结果:
并且,由于颜色本身来说多通道的特性,我们使用一张“溅射图”来完成在多通道上同时采样来获得一张混合的贴图,通过减去对应已经使用的通道,来让采样的图片填充下一个通道:
Shader "Custom/TextureSplating"
{
Properties
{
_MainTex("Splat Map",2D)="White" {}
[NoScaleOffset] _Texture1("Texture 1",2D)="white" {}
[NoScaleOffset] _Texture2("Texture 2",2D)="white" {}
[NoScaleOffset] _Texture3("Texture 3",2D)="white" {}
[NoScaleOffset] _Texture4("Texture 4",2D)="white" {}
[NoScaleOffset] _Texture5("Texture 5",2D)="white" {}
//保留主纹理的平铺和偏移输入。而两张幅图则没有纹理偏移平铺输入
}
SubShader
{
pass
{
CGPROGRAM
#pragma vertex MyVertex
#pragma fragment MyFragment
#include "UnityCG.cginc"
float4 _Tint;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _Texture1;
sampler2D _Texture2;
sampler2D _Texture3;
sampler2D _Texture4;
sampler2D _Texture5;
struct vertexStr
{
float4 POSITION:SV_POSITION;
float2 uv:TEXCOORD0;
float2 uvSplat:TEXCOORD1;
};
struct VertexData
{
float4 POSITION:POSITION;
float2 uv:TEXCOORD0;
};
vertexStr MyVertex(VertexData ver)
{
vertexStr str;
str.uv=TRANSFORM_TEX(ver.uv,_MainTex);
str.POSITION=UnityObjectToClipPos(ver.POSITION);
str.uvSplat=ver.uv;
return str;
}
float4 MyFragment(vertexStr str):SV_TARGET
{
float4 splat=tex2D(_MainTex,str.uvSplat);
return tex2D(_Texture1,str.uv)*splat.r+tex2D(_Texture2,str.uv)*splat.g
+tex2D(_Texture3,str.uv)*splat.b +tex2D(_Texture5,str.uv)*splat.a
+tex2D(_Texture4,str.uv)*(1-splat.r-splat.g-splat.b-splat.a);//我们可以看到效果图中
//第一张图采用了主图的纹理采样,那么主图内就会填充第一张图的纹理
//第二张图采用了主推的纹理采样减一,就会在主图的另一个颜色中填充第二张图的纹理
}
ENDCG
}
}
}
我们使用一张各个颜色通道分明的图片,就可以获得多个贴图混合的结果:
总的来说基础的纹理采样并不困难,比较方便,只需要我们注意一下纹理的缩放平移的语法就行。
法线纹理采样:通过纹理修改模型的法线
纹理除了上面的普通的,漫反射贴图的采样以外,还可以进行凹凸映射:让一个平面受到光照后看起来凹凸不平,一般来说凹凸映射分为两种:
- 高度纹理映射:使用一张高度纹理来模拟表面位移,然后得出修改后的法线值。
- 法线纹理映射:使用一张法线纹理来存储表面法线,进而使用该法线进行光照计算。
我们这篇博客基本上都是采用法线纹理的采样。
法线纹理:存储表面的法线方向在顶点的切线空间中所对应的颜色。所以粗浅的看起来,它的颜色特别怪异,例如我们这篇博客的例子里用的这张法线贴图,充满着许多的蓝色和紫色:
这些蓝色说明这个像素的法线方向没有被扰动,则默认都是垂直表明向上的,方向为本来与平面垂直的方向(0,0,1),映射到颜色中对应了RGB(0.5,0.5,1)的浅蓝色。
同时,法线贴图也可以在模型空间下表示,这时法线纹理采样得到的法线方向存储在模型空间中,由于法线都处于同一空间下, 所以方向各异,映射出来的颜色也是五颜六色的。
这是我百度出来的一张图片,可以看到,它清晰地根据面片的分布分成了很多块,每一块中都五颜六色的:
不管存储在哪种空间下,最终通过贴图获得法线方向才是重点,但是二者也存在差异。
模型空间中存储法线纹理的好处有:
- 简单直观,不需要原始模型的法线信息和切线信息,我们只需要对模型空间的贴图采样得出结果然后将它转入世界空间计算就好了。
- 由于模型空间法线纹理中法线方向都处在同一个坐标空间下,在纹理坐标的缝合处和尖锐的边角部分缝隙较少,可以提供平滑的边界,而切线空间中的法线方向由纹理坐标得来,所以可能在尖锐处或者缝隙看出比较明显的缝合迹象。
切线空间存储法线纹理的好处有:
- 模型空间的法线纹理存储绝对法线信息,一张模型空间的法线贴图只能用到特定的模型上,而切线空间的法线纹理存储相对信息,即使运用到不同的网格中也能得到正确的结果。
- 由于采样方式类似与普通的漫反射贴图相似,所以可以进行UV动画的制作,这也是Shader水面制作的一个依据。
- 切线空间法线纹理可以重用,模型空间下的法线纹理每个面都对应了一块,而切线空间法线纹理多个面可以重用一张法线纹理。
- 切线空间法线纹理可以压缩,由于切线空间法线纹理得到的法线方向的Z轴分量总是正向的,所以可以只存储XY方向进而推导出Z方向(这一点在下面的代码中就可以看到)。而模型空间法线纹理不能这么做。
在我们这篇博客中,都是切线空间法线纹理进行的计算。
由于法线纹理中存储表面的法线方向的像素,但是由于法线方向的分量范围在(-1,1),像素的分量范围为(0,1),所以在法线纹理的采样(tex2D)后需要进行映射,映射函数为:
normal = pixel * 2 - 1
这个取值范围的映射非常普遍,在NDC从[-1,1]映射到[0,1]时也用到了这个映射函数,可以说是一个妇孺皆知的函数了。
在Shader中Unity为我们准备了宏:UnpackNormal(pixel)来进行切线空间法线方向的xy方向计算。当然这个式子我们手动写也没什么难度,UnpackNormal根据了不同的平台选择的压缩方法,此时我们对法线贴图的图片格式应该设置为NormalMap。但如果图片格式默认为Default的话,我们应该写手动的映射函数,否则会出现采样的错误。
法线方向的解压:
同样的,由于法线纹理经过压缩,通过采样以后只能得到法线向量的xy值,我们需要手动求出z轴的分量。我们模拟一个以像素为原点的法线坐标空间, 并且由于求得的法线向量必须为单位向量,所以法线向量的模长恒为1。在Shader中,这变成已知xy轴的分量加上法线向量长度为1这两个条件求出z轴的分量的数学题:
如下图,橙色的向量就是x分量和y分量组成的向量xy;绿色的线就是我们需要求出的z轴分量;黑色的箭头代表实际的法线向量,并且它是一个单位向量。
所以,由于θ角的对边为z轴分量,且斜边长度为1,那么sinθ的值即为z轴的分量。
并且,由于xy向量已知,我们可以知道cosθ的值,根据定理可以将cosθ转化为sinθ。有如下的推导
已知:
可以在代码中得出:
Normal.z=sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
所以,法线方向的Z轴分量等于1减去xy向量的模的平方。Shader的UnpackedNormal自动为我们完成了这一步。这也在下文我们的代码中有所体现。
很明显根据上文对法线纹理的介绍,切线空间法线纹理得到切线空间下的法线方向,模型空间的法线纹理得到模型空间下的法线方向,所以切线空间法线纹理在计算时我们可能需要:
- 将光照变量(视角方向,光照方向)转入到得到法线的切线空间中与法线进行计算。
- 或者将切线空间中的法线转入到世界空间或者模型空间与其他光照变量进行计算。
无论哪种计算方式,我们都需要得到切线空间与其他空间的转换矩阵。
而这个转换矩阵以及它的转置矩阵都要求我们获得切线空间中三个轴向在世界空间中的定义,而每个顶点的切线空间由它的法线、切线、副切线三个轴来定义,注意:这里的切线空间为右手坐标系(同样的我们构建一个转换矩阵都应该是右手坐标系矩阵,例如观察空间三个基轴在世界空间中的矩阵也是右手坐标系的):
我们将一个矢量或者坐标从一个空间传入另一个空间,例如A空间到B空间时,需要知道以下的信息:
- A空间的三个基轴在B空间下的表示,我们这里使用Xa,Ya,Za来表示。
- A空间的坐标原点在B空间下的表示,我们这里使用Oa来表示。
- A空间中需要转换到B空间的矢量或者坐标,我们这里假设它的三个值为a,b,c。
那么A空间的一个坐标转换到B空间,我们可以想象它在B空间从原点Oa出发,沿着Xa方向移动a个单位,沿着Ya方向移动b个单位,沿着Za方向移动c个单位,最终停下来的坐标就是我们需要的结果,可以有如下式子:
A空间坐标在B空间中的位置 = Oa + a(Xa)+ b(Ya)+ c(Za)
而对于矢量来说,由于矢量不存在位置,只存在方向,所以将矢量从A空间转换到B空间可以不需要原点坐标Oa:
A空间矢量在B空间中的方向 = a(Xa)+ b(Ya)+ c(Za)
注意,此时的Xa、Ya、Za并不是某个值,而分别是三个向量,例如Xa在A空间表示为(1,0,0),我们假设Xa的分量分别为Xax,Xay,Xaz,那么一个Xa可以转化成一个列矩阵,Ya和Za同理。然后将这个式子扩展开来,有如下的转换方式:
其中,abc是我们需要转换的矢量,那么很容易就得出它左乘的那个矩阵就是我们需要的从A空间转换到B空间的矩阵,我们将它美化一下,就可以得出由A到B空间的矢量转换矩阵:
所以,A到B空间的转换矩阵为:由处于B空间的A空间基轴组成的列矩阵。
同理,B到A空间的转换矩阵可以认为是:由A到B空间的转换矩阵的逆矩阵。由于它是个正交矩阵,所以它的逆矩阵正好就是该矩阵的转置,我们可以得出,B到A空间的转化矩阵为:由处于B空间的A空间基轴组成的行矩阵。即:
这个概念实际上特别特别不好记,我想了个口诀,叫做出列回行。意思就是:
- 将本空间向量转出用本空间在外空间表示的基轴的列矩阵。
- 将外空间向量转回用本空间在外空间表示的基轴的行矩阵。
我们将这个转换矩阵套用到我们法线纹理的转换中,那么很清楚,我们切线空间的三个基轴分别是:
X轴:切线Tangent。Y轴:副切线BioNormal。Z轴:法线Normal。
例如我们如果要构建从切线空间转入世界空间的矩阵,我们已知世界空间中的三个轴的方向,那么遵循出列回行,我们这里从本空间转出,所以应该是一个列矩阵:
float3x3 TangentToWorld=float3x3(float3(worldTangent.x,worldBionormal.x,worldNormal.x),
float3(worldTangent.y,worldBionormal.y,worldNormal.y),
float3(worldTangent.z,worldBionormal.z,worldNormal.z));
再例如我们如果要构建世界空间转入切线空间的矩阵,世界空间中的三个轴的表示我们也知道,那么遵循出列回行,我们这里要转回本空间,所以应该是一个行矩阵:
float3x3 WorldToTangent=float3x3(worldTangent,worldBionormal,worldNormal);
绕了一大圈,终于回到我们这一段最开始的那个议题:法线纹理采样获得新的法线方向进而光照计算。我们此时有两种方法:
- 将光照变量(视角方向,光照方向)转入到得到法线的切线空间中与法线进行计算:那么我们需要在模型空间或者世界空间中构建转回切线空间的行矩阵,然后将光照和视角向量转入切线空间中。
- 或者将切线空间中的法线转入到世界空间或者模型空间与其他光照变量进行计算中计算:那么我们需要在世界空间中构建转出切线空间的列矩阵,然后将根据法线纹理算出的切线空间中的法线方向转入世界空间中。
我们写两个例子,分别对应在切线空间下计算的法线纹理和在世界空间下计算的法线纹理(我们两个例子都是手动从像素映射到方向,我们也可以使用UnpackNormal,但是此时我们需要将图片方向设置为):
世界空间中计算:
我们需要将三个基轴都转入到世界空间中,在片元着色器得到的法线方向,左乘转换矩阵将法线方向得到世界空间中,再与其他的世界空间其他变量进行计算:
Shader "Custom/WorldgetNormal"
{
Properties
{
_MainTex("MainTex", 2D) = "white" {}
_BumpTex("BumpTex",2D)="white"{}
_Gloss("Gloss",Range(8,256))=10
_BumpScale("BumpScale",float)=1.0
}
SubShader
{
Pass
{
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex myVertex
#pragma fragment myFragment
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
float4 _BumpTex_ST;
float _BumpScale;
float _Gloss;
struct VertexData
{
float4 vertex:POSITION;
float3 normal:NORMAL;
float3 tangent:TANGENT;
float4 UV:TEXCOORD0;
};
struct FragmentToVertex
{
float4 pos:SV_POSITION;
float3 Normal:TEXCOORD0;
float3 worldTangent:TEXCOORD1;
float2 MainUV:TEXCOORD2;
float2 BumpUV:TEXCOORD3;
float3 worldPos:TEXCOORD4;
};
FragmentToVertex myVertex(VertexData v)
{
FragmentToVertex VToF;
VToF.pos=UnityObjectToClipPos(v.vertex);
VToF.worldPos=mul(unity_ObjectToWorld,v.vertex);
VToF.Normal=UnityObjectToWorldNormal(v.normal);
VToF.worldTangent=UnityObjectToWorldDir(v.tangent);
VToF.MainUV=TRANSFORM_TEX(v.UV,_MainTex);
VToF.BumpUV=TRANSFORM_TEX(v.UV,_BumpTex);
return VToF;
}
fixed4 myFragment(FragmentToVertex VToF):SV_TARGET
{
fixed3 Normal=normalize(VToF.Normal);
fixed3 worldTangent=normalize(VToF.worldTangent);
fixed3 worldBionormal=cross(worldTangent,Normal);
float3x3 TangentToWorld=float3x3(float3(worldTangent.x,worldBionormal.x,Normal.x),
float3(worldTangent.y,worldBionormal.y,Normal.y),
float3(worldTangent.z,worldBionormal.z,Normal.z));
fixed3 albedo=tex2D(_MainTex,VToF.MainUV).rgb;
fixed3 TangentPixel=tex2D(_BumpTex,VToF.BumpUV).rgb;
fixed3 TangentNormal;
TangentNormal=(TangentPixel*2-1)*_BumpScale;
TangentNormal.z=sqrt(1-dot(TangentNormal.xy,TangentNormal));
fixed3 worldNormal=mul(TangentToWorld,TangentNormal);
fixed3 worldView=normalize(UnityWorldSpaceViewDir(VToF.worldPos));
fixed3 worldLight=normalize(UnityWorldSpaceLightDir(VToF.worldPos));
fixed3 Diffuse=_LightColor0.rgb*max(0,dot(worldNormal,worldLight));
fixed3 Specular=_LightColor0.rgb*pow(max(0,dot(worldNormal,normalize(worldLight+worldView))),_Gloss);
return fixed4(Specular+Diffuse*albedo,1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
切线空间中计算:
由于法线方向得到本来就是在切线空间中,所以我们需要反其道尔行之, 将其他的变量转入到切线空间中,由于不需要使用到世界空间,我们只需要获得模型空间的基轴和模型空间的变量:
Shader "Hidden/TangentgetNormal"
{
Properties
{
_MainTex("MainTex", 2D) = "white" {}
_BumpTex("BumpTex",2D)="white"{}
_Gloss("Gloss",Range(8,256))=10
_BumpScale("BumpScale",float)=1.0
}
SubShader
{
Pass
{
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex myVertex
#pragma fragment myFragment
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
float4 _BumpTex_ST;
float _BumpScale;
float _Gloss;
struct VertexData
{
float4 vertex:POSITION;
float3 normal:NORMAL;
float3 tangent:TANGENT;
float4 UV:TEXCOORD0;
};
struct FragmentToVertex
{
float4 pos:SV_POSITION;
float3 objectNormal:TEXCOORD0;
float3 objectTangent:TEXCOORD1;
float2 MainUV:TEXCOORD2;
float2 BumpUV:TEXCOORD3;
float4 objectPos:TEXCOORD4;
};
FragmentToVertex myVertex(VertexData v)
{
FragmentToVertex VToF;
VToF.pos=UnityObjectToClipPos(v.vertex);
VToF.objectPos=v.vertex;
VToF.objectNormal=v.normal;
VToF.objectTangent=v.tangent;
VToF.MainUV=TRANSFORM_TEX(v.UV,_MainTex);
VToF.BumpUV=TRANSFORM_TEX(v.UV,_BumpTex);
return VToF;
}
fixed4 myFragment(FragmentToVertex VToF):SV_Target
{
fixed3 objectNormal=normalize(VToF.objectNormal);
fixed3 objectTangent=normalize(VToF.objectTangent);
fixed3 objectBionormal=cross(objectTangent,objectNormal);
float3x3 ObjectToTangent=float3x3(objectTangent,objectBionormal,objectNormal);
fixed3 tangentPixel=tex2D(_BumpTex,VToF.BumpUV);
fixed3 tangentNormal;
tangentNormal.xy=(tangentPixel*2-1)*_BumpScale;
tangentNormal.z=sqrt(1-dot(tangentNormal.xy,tangentNormal.xy));
fixed3 tangentView=mul(ObjectToTangent,normalize(ObjSpaceViewDir(VToF.objectPos)));
fixed3 tangentLight=mul(ObjectToTangent,normalize(ObjSpaceLightDir(VToF.objectPos)));
fixed3 Diffuse=_LightColor0.rgb*max(0,dot(tangentNormal,tangentLight));
fixed3 Specular=_LightColor0.rgb*pow(max(0,dot(tangentNormal,normalize(tangentView+tangentLight))),_Gloss);
fixed3 albedo=tex2D(_MainTex,VToF.MainUV);
return fixed4(Specular+Diffuse*albedo,1.0);
}
ENDCG
}
}
}
我发现从模型空间转入切线空间得到的漫反射贴图要比世界空间要暗一些,并且,从世界空间转入切线空间得到的漫反射贴图要比世界空间更泛白一点。我个人来说可能更倾向于在世界空间中计算一些,因为其他很多变量都在世界空间中计算,相较于其他空间,它相当于一个变量集中地。
下面的图片中图1右边是从世界空间转入切线空间,图2右边是从模型空间中转入切线空间中,两边的图片的左边都是世界空间中计算的,作为对照组,我们可以看到图1右边的比较泛白,图2右边比较暗。
立方体贴图采样
我们刚才采样的都是二维贴图,但我们也可以进一步,采样立方体贴图,我们采样二维贴图的时候,使用的都是模型的表面纹理坐标UV。在采样三维贴图的时候,常见使用两种向量进行立方体的采样:
- 视角的反射方向:使用视角与法线的反射方向对立方体贴图采样,Unity存在内置函数Reflect来计算它。
- 光照的折射方向:使用光照与法线的折射方向对立方体贴图采样。Unity存在内置函数Refract来计算它。
我们计算立体贴图折射的时候必须注意,必须使用_WorldSpaceCameraPos-VToF.worldPos来得到视角方向,而不能使用UnityWorldSpaceLightDir函数来计算,并且,在计算视角的反射时需要将viewDir取反,因为我们求出的是摄像机射入像素的方向,Reflect函数使用的是像素反射到摄像机的方向,这在我们下面的代码中可以看到。
我们使用线性插值的方式写一个反射与折射的混合,混合靠的就是权重比(假设这里值为_Amount),它是Shader中Blender中的一种混合方式,也是Unity脚本中的线性插值的基础方式,我们这里也将它放在这个地方,它会在下面的代码中使用到:
最终颜色 = 颜色A * _Amount + 颜色B * ( 1 - _Amount)
同时,它也要求我们的权重比_Amount必须介于0和1之间。我们用它将反射与折射混合:
Shader "Hidden/CubeCombine"
{
Properties
{
_ReflectionColor("Color",Color)=(1,1,1,1)
_Amount("Amount",Range(0,1))=1
_CubeMap("CubeMap",Cube)="_SkyBox"{}
_RefractRadio("RefractRadio",Range(0.1,1))=0.5
_Gloss("Gloss",Range(8,256))=8
}
SubShader
{
pass
{
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex myVertex
#pragma fragment myFragment
#include "Lighting.cginc"
#include "AutoLight.cginc"
float _Amount;
float _Gloss;
samplerCUBE _CubeMap;
float _RefractRadio;
struct VertexData
{
fixed4 vertex:POSITION;
fixed3 normal:NORMAL;
};
struct VertexToFragment
{
fixed4 pos:SV_POSITION;
fixed3 worldPos:TEXCOORD0;
fixed3 worldNormal:TEXCOORD1;
};
VertexToFragment myVertex(VertexData v)
{
VertexToFragment VToF;
VToF.pos=UnityObjectToClipPos(v.vertex);
VToF.worldPos=mul(unity_ObjectToWorld,v.vertex);
VToF.worldNormal=UnityObjectToWorldNormal(v.normal);
return VToF;
}
fixed4 myFragment(VertexToFragment VToF):SV_TARGET
{
fixed3 worldNormal=normalize(VToF.worldNormal);
fixed3 worldView=normalize(UnityWorldSpaceLightDir(VToF.worldPos));
fixed3 worldLight=normalize(UnityWorldSpaceLightDir(VToF.worldPos));
fixed3 worldReflection=normalize(reflect(-worldView,worldNormal));
fixed3 worldRefraction=normalize(refract(worldLight,worldNormal,_RefractRadio));
fixed3 ReflectCubeAlbedo=texCUBE(_CubeMap,worldReflection).rgb;
fixed3 RefractCubeAlbedo=texCUBE(_CubeMap,worldRefraction).rgb;
fixed3 getCube=lerp(RefractCubeAlbedo,ReflectCubeAlbedo,_Amount);
return fixed4(getCube,1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
菲涅尔反射:
渲染的时候,常常也需要根据视角方向控制反射程度。菲涅尔反射就描述了这种光学现象:
例如在湖边垂直的看水下,那么水下清澈见底,但是如果我们往前面眺望,那么水面就会变得看不见。并不只有水面或者玻璃呈现出这种现象,一般的物体中多少都会呈现菲涅尔现象,菲涅尔反射也是PBR中非常重要的高光反射计算因子。
菲涅尔反射在真实世界中非常复杂,但是在Shader中一般使用近似等式,它往往需要一个反射系数(假设命名为_FresnelScale)和一个N次幂(一般假设次幂为5),
Schlick 菲涅尔近似等式:
Fresnel = _FresnelScale + ( 1 - _FresnelScale ) pow(( 1 - viewDir * normal ) , 5)
我们可以从公式中知道,
- 视角方向与法线方向的夹角越小,菲涅尔值越小,反射越弱,折射越强
- 视角方向与法线方向的夹角越大,菲涅尔值越大,反射越强,折射越弱
我们将混合的权重比设置为菲涅尔反射的值。然后用它来混合漫反射与反射球:
Shader "Hidden/ReflectCombine"
{
Properties
{
_CubeMap("CubeMap",Cube)="_SkyBox"{}
_FresnelScale("FresnelScale",Range(0,1))=0.5
}
SubShader
{
pass
{
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex myVertex
#pragma fragment myFragment
#include "Lighting.cginc"
#include "AutoLight.cginc"
samplerCUBE _CubeMap;
float _FresnelScale;
struct VertexData
{
fixed4 vertex:POSITION;
fixed3 normal:NORMAL;
};
struct VertexToFragment
{
fixed4 pos:SV_POSITION;
fixed3 worldPos:TEXCOORD0;
fixed3 worldNormal:TEXCOORD1;
};
VertexToFragment myVertex(VertexData v)
{
VertexToFragment VToF;
VToF.pos=UnityObjectToClipPos(v.vertex);
VToF.worldPos=mul(unity_ObjectToWorld,v.vertex);
VToF.worldNormal=UnityObjectToWorldNormal(v.normal);
return VToF;
}
fixed4 myFragment(VertexToFragment VToF):SV_TARGET
{
fixed3 worldNormal=normalize(VToF.worldNormal);
fixed3 worldView=normalize(_WorldSpaceCameraPos-VToF.worldPos);
fixed3 worldLight=normalize(UnityWorldSpaceLightDir(VToF.worldPos));
fixed3 worldReflection=reflect(-worldView,worldNormal);
fixed3 fresnel=_FresnelScale+(1-_FresnelScale)*pow(1-dot(worldView,worldNormal),5);
fixed3 ReflectCubeAlbedo=texCUBE(_CubeMap,worldReflection).rgb;
fixed3 Diffuse=_LightColor0.rgb*max(0,dot(worldNormal,worldView));
fixed3 getCube=lerp(Diffuse,ReflectCubeAlbedo,saturate(fresnel));
return fixed4(getCube,1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
我们做一个对照组,可以看出加不加菲涅尔反射的区别,在球体的外轮廓下颜色回归本真:
镜面纹理:
上面的纹理中大部分纹理都是在材质球中指定某一个纹理然后将之渲染出来,但是Shader也支持渲染屏幕图像。它常常用于类似于玻璃的透明材质上,在Shader中可以使用GrabPass在抓取屏幕图像到指定的纹理变量中,然后再使用它进行特殊的处理。
当使用GrabPass抓取屏幕图像时,需要将Shader的渲染队列设置为透明队列,即“Queue”=“Transparent”,这样能保证当抓取屏幕图像时所有处于它后面的透明物体和所有不透明物体都被渲染在了屏幕上(由于透明队列从后向前渲染,所以能保证能抓取到所有在后面的已渲染物体):
GrabPass{ "_自定义纹理名" }
并且,我们在Pass中获得对应的变量时,要像类似与贴图采样一样定义屏幕纹理名和纹理的纹素。就像定义_MainTex_ST一样:
sampler2D _自定义屏幕纹理名;
float4 _自定义屏幕纹理名_TexelSize;
在采样屏幕纹理时,不能单纯地如同采样贴图纹理那样去获得纹理坐标,而是需要使用屏幕坐标:
在Unity中:当我们在顶点着色器中得出裁剪空间中的顶点后,Shader底层自动为我们将裁剪空间通过投影矩阵得出不需要裁剪,那么就会使用齐次除法(也称为透视除法)将该像素点投影到屏幕空间中。即用齐次坐标系的W分量除以xyz分量。这一步完成后的坐标称为归一化的设备坐标(Normalized Device Coordinates)。
在OpenGL中此时的xyz分量都是[-1,1],但是当我们获得屏幕坐标时,需要以左下角为原点(0,0)来构建屏幕坐标系,获得到的xy分量需要都处于[0,1]之间,所以我们需要将设备坐标归一化后得到我们最终的屏幕坐标。
在Shader中,获得正确的屏幕坐标需要两步:1.将裁剪空间中的坐标进行齐次除法,2.得到范围在[-1,1]内的归一化设备坐标NDC。而我们手动计算时需要将两个步骤反过来:
- 将裁减坐标使用映射函数归一化。
- 将得出的归一化裁剪坐标的xy分量除以w分量,获得NDC。
其中,有以下几个要点我们需要注意:
- 对于NDC的w分量来说,如果处于透视投影中它的取值范围为[1/near, 1/far],如果处于正交投影中它的值恒为1。
- 最好将w分量的除法操作放置在片元着色器中进行,因为从顶点着色器到片元着色器中存在插值,但我们得出的屏幕坐标处于并不线性的投影空间中(而插值往往是线性的),所以可能导致结果不准确。(例如下面的屏幕图片根据纹理偏移时,如果在顶点着色器中执行除法,可能导致偏移不明显)。
- ComputeGrabScreenPos的形参是当前屏幕的裁剪坐标(因为NDC就是根据裁剪坐标得出),并且这个函数存在于UnityCG的头文件中。
我们先写一个很简单的抓取屏幕图像实现“伪透明的效果”:
Shader "Hidden/GrabTest"
{
SubShader
{
Tags
{
"Queue"="Transparent"
}
GrabPass
{
"_ScreenTexture"
}
Pass
{
CGPROGRAM
#pragma vertex myVertex
#pragma fragment myFragment
#include "UnityCG.cginc"
sampler2D _ScreenTexture;
float4 _ScreenTexture_TexelSize;
struct VertexData
{
float4 vertex : POSITION;
};
struct VertexToFragment
{
float4 srcPos:TEXCOORD1;
float4 pos : SV_POSITION;
};
VertexToFragment myVertex(VertexData v)
{
VertexToFragment VToF;
VToF.pos=UnityObjectToClipPos(v.vertex);
VToF.srcPos=ComputeGrabScreenPos(VToF.pos);
return VToF;
}
fixed4 myFragment(VertexToFragment VToF) : SV_Target
{
fixed3 ScreenColor=tex2D(_ScreenTexture,VToF.srcPos.xy/VToF.srcPos.w);
return fixed4(ScreenColor,1);
}
ENDCG
}
}
}
这个例子很简单,就是单纯的使用GrabPass获得的纹理然后用屏幕坐标对它采样,结果当然是在其表面渲染了当前屏幕,所以它看来就是透明的。:
使用法线纹理对屏幕纹理进行偏移:
我们实现一个通过法线纹理采样得到的法线方向实现对屏幕坐标的偏移,并且加上菲涅尔着色的效果。,这个代码类似于上文中几个代码的叠加。唯一需要记住的一点是:将法线方向的偏移叠加到屏幕纹理采样坐标时,需要加入屏幕纹理的纹素_ScreenTexture_TexelSize的xy值和自定义的偏移量(我们下文中称为_Disortion)进行叠加。
Shader "Hidden/GlassCombine"
{
Properties
{
_BumpTex("BumpTex",2D)="white"{}
_BumpScale("BumpScale",float)=1.0
_Gloss("Gloss",Range(8,256))=20
_Disortion("Disortion",float)=1.0
_FresnelScale("FresnelScale",Range(0,1))=0.5
}
SubShader
{
Tags
{
"Queue"="Transparent"
//"RenderType"="Opaque"
}
GrabPass{"_GrabTexture"}
pass
{
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex myVertex
#pragma fragment myFragment
#include "Lighting.cginc"
#include "AutoLight.cginc"
sampler2D _BumpTex;
float4 _BumpTex_ST;
sampler2D _GrabTexture;
float4 _GrabTexture_TexelSize;
float _BumpScale;
float _Gloss;
float _Disortion;
float _FresnelScale;
struct VertexData
{
fixed4 vertex:POSITION;
fixed3 normal:NORMAL;
fixed3 tangent:TANGENT;
fixed2 uv:TEXCOORD0;
};
struct VertexToFragment
{
fixed4 pos:SV_POSITION;
fixed3 TangentPos:TEXCOORD0;
fixed3 TangentLight:TEXCOORD1;
fixed2 BumpUV:TEXCOORD2;
fixed3 TangentView:TEXCOORD4;
fixed4 srcPos:TEXCOORD5;
};
VertexToFragment myVertex(VertexData v)
{
VertexToFragment VToF;
VToF.pos=UnityObjectToClipPos(v.vertex);
VToF.BumpUV=TRANSFORM_TEX(v.uv,_BumpTex);
VToF.srcPos=ComputeGrabScreenPos(VToF.pos);
fixed3 worldNormal=UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent=UnityObjectToWorldDir(v.tangent);
fixed3 Bionormal=cross(worldNormal,worldTangent);
float3x3 worldToTangent=float3x3(worldTangent.xyz,Bionormal.xyz,worldNormal.xyz);
fixed3 worldPos=mul(unity_ObjectToWorld,v.vertex);
VToF.TangentPos=mul(worldToTangent,worldPos);
VToF.TangentLight=mul(worldToTangent,UnityWorldSpaceLightDir(worldPos));
VToF.TangentView=mul(worldToTangent,_WorldSpaceCameraPos-worldPos);
return VToF;
}
fixed4 myFragment(VertexToFragment VToF):SV_TARGET
{
fixed3 TangentPos=normalize(VToF.TangentPos);
fixed3 TangentView=normalize(VToF.TangentView);
fixed3 TangentLight=normalize(VToF.TangentLight);
fixed3 tangentNormal=UnpackNormal(tex2D(_BumpTex,VToF.BumpUV));
tangentNormal.xy*=_BumpScale;
//tangentNormal.z=sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
float2 offset=tangentNormal.xy*_Disortion*_GrabTexture_TexelSize;
VToF.srcPos.xy+=offset;
float3 GlassColor=tex2D(_GrabTexture,VToF.srcPos.xy/VToF.srcPos.w);
fixed3 fresnel=_FresnelScale+(1-_FresnelScale)*pow(1-dot(TangentView,tangentNormal),5);
fixed3 Diffuse=_LightColor0.rgb*max(0,dot(tangentNormal,TangentLight));
fixed3 getColor=lerp(GlassColor,Diffuse,fresnel);
fixed3 Specular=_LightColor0.rgb*pow(max(0,dot(tangentNormal,normalize(TangentLight+TangentView))),_Gloss);
return fixed4(getColor+Specular,1);
}
ENDCG
}
}
Fallback "Diffuse"
}
我们这次在切线空间中进行计算,并且将使用法线纹理采样出来的法线方向进行计算光照,效果还是很炫的(说实话我非常非常喜欢这个效果):