Unity笔记-30-Shader初级篇-01
基础知识
编译指令
#pragma vertex vert
#pragma fragment frag
他们将告诉Unity,那个函数为顶点着色器,哪个函数为片元着色器,理解为定义函数
最简单的顶点着色器&片元着色器函数
顶点着色器函数
float4 vert(float4 v:POSITION):SV_POSITION{
return mul(UNITY_MATRIX_MVP,v);
}
POSITION 语义绑定:模型空间中的顶点坐标
SV_POSITION 语义绑定:裁剪空间中的顶点坐标
UNITY_MATRIX_MVP 为将模型空间转化到裁剪空间的变换矩阵
mul 函数为矩阵的左乘运算
片元着色器函数
fixed4 frag():SV_TARGET{
return fixed(1.0,1.0,1.0,1.0);
}
在本例中,没有任何输入
SV_TARGET 语义绑定:将颜色输出到一个目标上,这里只是返回了一个白色
现在,我们想要得到模型上每个顶点的纹理坐标与法线方向,纹理坐标用于访问纹理,法线则用于计算光照,因此,我们需要为顶点着色器重新定义一个输入参数,这个参数不再是简单的数据类型,而是一个结构体
Shader "Unity shaders/MyShader/01"{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct a2v{
//POSITION 语义告诉Unity,使用模型空间的顶点坐标填充vertex变量
float4 vertex:POSITION
//NORMAL 语义告诉Unity,使用模型空间的法线方向填充normal变量
float3 normal:NORMAL
//TEXCOORD0 语义告诉Unity,使用模型的第一套纹理坐标填充texcoord变量
float4 texcoord:TEXCOORD0
};
float4 vert(a2v v):SV_POSITION{
return mul(UNITY_MATRIX_MVP,v.vertex);
}
fixed4 frag():SV_TARGET{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}
通过语义绑定,Unity会自己把对应的数据填充进去
那么该如何将顶点着色器中的数据传递到片元着色器中呢
Shader "Unity shaders/MyShader/01"{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct a2v{
float4 vertex:POSITION
float3 normal:NORMAL
float4 texcoord:TEXCOORD0
};
struct v2f{
//SV_POSITION 语义告诉Unity,pos里包含了顶点在裁剪空间中的位置信息
float4 pos:SV_POSITION;
//COLOR0 语义用于存储颜色信息
float3 color:COLOR0;
};
v2f vert(a2v v):SV_POSITION{
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
//v.normal包含了顶点的法线方向,分量范围在:[-1.0,1.0]
//下面的代码吧范围映射到了[0,1.0]
//存储到o.color中,传递给片元着色器
o.color=v.normal*0.5+fixed3(0.5,0.5,0.5);
return o;
}
fixed4 frag(v2f i):SV_TARGET{
//将插值后的color显示在屏幕上
return fixed4(i.color,1.0);
}
ENDCG
}
}
}
我们声明了一个新的结构体:v2f,用于在顶点着色器和片元着色器之间传递信息
同样的,v2f中也需要指定每个变量的语义,在本例中,我们使用了SV_POSITION 和COLOR0 语义
顶点着色器的输出结构中,必须包含一个变量他的语义是:SV_POSITION ,否则渲染器无法得到裁剪空间中的顶点坐标,也就无法将顶点渲染到屏幕上,特别说明:从裁剪空间到屏幕坐标的变换,Unity 帮我们完成了,因此我们需要传给他裁剪空间的坐标
外部属性
外部属性的名称要与CG语言里的名称完全相同,可以任意在顶点或片元函数中使用,对结果进行变化,具体不再细讲
Unity内置文件与变量
如何下载
UNITY支持的语义
基础光照
漫反射-逐顶点
预备知识
基本光照模型中的漫反射的计算公式
入射光颜色和强度c-light ,材质漫反射系数m-diffuse ,表面法线方向n ,光源方向I
为防止点积的结果为负,因此使用max 函数约束
主要思路
入射光我们可以通过Unity内置变量_LightColor0 获得(注意:此内置变量必须要定义合适的LightMode 标签),而材质漫反射系数则是属性,因此我们需要获得的仅有法线与光源方向
法线,我们可以通过语义绑定,获得模型空间下的法线,因此我们需要将此法线变换到世界空间下,Unity提供了从世界空间变换到模型空间的矩阵,我们只需要调换法线和矩阵杂戏mul 函数中的位置,即法线左,矩阵右,即可得到和转置矩阵相同的矩阵乘法,由于法线是一个三位矢量,这里只需要去矩阵的3x3即可
光源方向,我们通过_WorldSpaceLightPos0 获得,获得世界空间内的光源方向,归一化。_WorldSpaceLightPos0 只有当场景中只有一个点光源时才能正确拿到对应的光源方向,如果有多个那么拿到的那个可能不是我们想要的。
至此进行计算即可,max 函数可以换成saturate 函数它的作用是把float 类型的直限制在[0,1] 内
逐像素和逐顶点的思路相同,不同的是计算放在了片元着色器中进行,请读者自己完成
半兰伯特模型
由于在光线无法照射到的区域会看起来像一个平面,因此这里对光照效果做出优化,以下这个公式只是为了更好的效果而没有任何物理依据
这是半兰伯特模型,调整计算公式即可
Shader "MyShader/漫反射_01_逐顶点"
{
Properties
{
_Diffuse("Diffuse",Color)=(1,1,1,1)
}
SubShader
{
Pass{
Tags{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
struct a2v{
float4 vertex:POSITION;
float4 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
fixed3 color:COLOR;
};
v2f vert(a2v v){
v2f o;
//将顶点坐标左乘变换矩阵,将顶点坐标转化为裁剪空间的坐标
o.pos=UnityObjectToClipPos(v.vertex);
//通过UNITY内置变量获得环境光部分
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
//获得世界空间内的法线,右乘变换矩阵,相当于左乘这个矩阵的逆转置矩阵,归一化
fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));
//获得世界空间内的光源方向,归一化,注意:_WorldSpaceLightPos0,只有当场景中只有一个点光源时才能
//正确拿到对应的光源方向,如果有多个那么拿到的那个可能不是我们想要的
fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
//世界法线方向与光源方向的点积,角度越小,值越大,并将通过saturate函数将值约束在[0,1]
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));
o.color=ambient+diffuse;
return o;
}
fixed4 frag(v2f i):SV_TARGET{
return fixed4(i.color,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
Shader "MyShader/漫反射_02_逐像素"
{
Properties
{
_Diffuse("Diffuse",Color)=(1,1,1,1)
}
SubShader
{
Pass{
Tags{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
struct a2v{
float4 vertex:POSITION;
float4 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
fixed3 worldNormal:TEXCOORD0;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
return o;
}
fixed4 frag(v2f i):SV_TARGET{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));
fixed3 color=ambient+diffuse;
return fixed4(color,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
Shader "MyShader/HalfLambert"
{
Properties
{
_Diffuse("Diffuse",Color)=(1,1,1,1)
}
SubShader
{
Pass{
Tags{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
struct a2v{
float4 vertex:POSITION;
float4 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
fixed3 worldNormal:TEXCOORD0;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
return o;
}
fixed4 frag(v2f i):SV_TARGET{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
fixed3 halfLambert=dot(worldNormal,worldLight)*0.5+0.5;
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*halfLambert;
fixed3 color=ambient+diffuse;
return fixed4(color,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
高光反射-逐顶点
预备知识
计算公式
主要思路
m-specular 为高光反射系数,v为视角方向,r为反射方向,m-gloss 为高光反射区域控制参数
m-specular 通过属性得到
r 反射方向可以通过以下公式获得
n 为法线,I 为光源方向,也就是入射光线的负方向,当然CG提供了函数reflect 帮助计算
v 视角方向我们通过摄像机位置-顶点位置 获得,注意这是在世界空间下,因此这里需要将模型空间的顶点坐标通过矩阵变换转化为世界空间下的顶点坐标
逐像素和逐顶点的思路相同,不同的是计算放在了片元着色器中进行,请读者自己完成
Blinn-Phong光照模型
他相对于之前的公式,他没有使用反射方向,它引入了一个新矢量I ,它的计算公式如下,将视角方向和光源方向相加再归一化即可
同样,他也只是为了光照效果的优化,而没有任何物理依据,公式如下,代码改一下计算公式即可
Shader "MyShader/高光反射_逐顶点"
{
Properties
{
_Diffuse("Diffuse",Color)=(1,1,1,1)
//控制高光反射颜色
_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 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
float3 color:Color;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));
fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));
fixed3 reflectDir=normalize(reflect(-worldLight,worldNormal));
fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-mul(unity_WorldToObject,v.vertex).xyz);
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
o.color=ambient+diffuse+specular;
return o;
}
fixed4 frag(v2f i):SV_TARGET{
return fixed4(i.color,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
Shader "MyShader/高光反射_逐像素"
{
Properties
{
_Diffuse("Diffuse",Color)=(1,1,1,1)
//控制高光反射颜色
_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 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
// fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
o.worldPos=mul(unity_ObjectToWorld,v.vertex);
// fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
// fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));
// fixed3 reflectDir=normalize(reflect(-worldLight,worldNormal));
// fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-mul(unity_WorldToObject,v.vertex).xyz);
// fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
// o.color=ambient+diffuse+specular;
return o;
}
fixed4 frag(v2f i):SV_TARGET{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));
fixed3 reflectDir=normalize(reflect(-worldLight,worldNormal));
fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos);
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
fixed3 color=ambient+diffuse+specular;
return fixed4(color,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
Shader "MyShader/Blinn"
{
Properties
{
_Diffuse("Diffuse",Color)=(1,1,1,1)
//控制高光反射颜色
_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 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
// fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
o.worldPos=mul(unity_ObjectToWorld,v.vertex);
// fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
// fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));
// fixed3 reflectDir=normalize(reflect(-worldLight,worldNormal));
// fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-mul(unity_WorldToObject,v.vertex).xyz);
// fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
// o.color=ambient+diffuse+specular;
return o;
}
fixed4 frag(v2f i):SV_TARGET{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));
fixed3 reflectDir=normalize(reflect(-worldLight,worldNormal));
fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos);
fixed3 halfDir=normalize(worldLight+viewDir);
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(worldNormal,halfDir)),_Gloss);
fixed3 color=ambient+diffuse+specular;
return fixed4(color,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
帮助函数
基础纹理
单纹理
预备知识
纹理映射坐标
纹理的最初目的就是使用一张图片来控制模型的外观。使用纹理映射,就可以把一张图“黏”在模型表面,逐纹素的控制模型的颜色。而纹理映射技术的作用即是,把纹理映射坐标存储在每个顶点上,纹理映射坐标定义了该顶点在纹理中对应2D坐标。通常这些坐标使用二维变量(u,v) 来表示,u-横向坐标,v-纵向坐标;因此纹理映射坐标也可被称作UV坐标
通过uv坐标,我们才能获得纹理中存储的rgba 值
主要思路
定义属性
_Color _MainTex _Specular _Gloass
分别是:漫反射颜色值,纹理,高光反射颜色值,高光反射区域
并在CG语言内部分别定义,这里需要说明的是:这里还需要为纹理,声明一个_MainTex_ST 的变量,遵循XXX_ST的命名规则,这个变量可以让我们得到纹理的缩放和偏移,_MainTex_ST.xy 存储的是纹理的缩放,_MainTex_ST.zw 存储的是纹理的偏移,也就是材质应用Shader后的下图中的Tiling 与Offset ;
声明顶点着色器的输入与输出
模型顶点坐标与法线就不再多说;详细说说texcoord ,将其语义绑定第一组纹理,Unity会把第一组纹理映射坐标存储在该变量中,但是此时还没有叠加缩放和偏移,因此,我们还需要通过以下公式叠加缩放和偏移,这里就需要使用到之前定义的_MainTex_ST 获得的缩放和偏移
v.texcoord*_MainTex_ST.xy+_MainTex_ST.zw;
做完这些后,获得的才是正确的纹理坐标也就是uv坐标
但此时我们还没有获得纹理的纹素值,只有纹理坐标,因此我们还需要做一项工作:纹理采样,这项工作需要在片元着色器中进行,因为这是逐像素的工作。通过tex2D(纹理,纹理坐标) 对纹理进行纹素值采样,获得纹素值(也就是获得纹理存储的rgb 值)后,将其与漫反射颜色叠加,最终计算到漫反射变量中即可;
Shader "MyShader/单纹理"
{
Properties
{
_Color("Color Tint",Color)=(1,1,1,1)
_MainTex("Main Tex",2D)="White"{}
_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;
float4 _Specular;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 texcoord:TEXCOORD0;
};
struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
float2 uv:TEXCOORD2;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldNormal=UnityObjectToWorldNormal(v.normal);//Unity帮助函数改写
o.worldPos=mul(unity_ObjectToWorld,v.vertex);
//叠加纹理的缩放
o.uv=v.texcoord*_MainTex_ST.xy+_MainTex_ST.zw;
//or
//上面这行代码可以使用内置方法来完成,其中已经自动获得了缩放,并将其叠加,就不需要再次定义_MainTex_ST
//o.uv=TRANFORM_TEX(v.texcoord,_MainTex);
return o;
}
fixed4 frag(v2f i):SV_TARGET{
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLight=normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 albedo=tex2D(_MainTex,i.uv).rgb*_Color.rgb;
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffuse=_LightColor0.rgb*albedo*max(0,dot(worldLight,worldNormal));
fixed3 viewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir=normalize(worldLight+viewDir);
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
凹凸映射-切线空间
凹凸纹理分为高度纹理与法线纹理;这里主要讲解法线纹理,通常来讲,对于一张纹理,美工还会高出这张纹理的高度纹理和法线纹理,例如下面这张图,从左到右分别为:纹理,高度纹理,法线纹理
预备知识
切线空间的法线纹理
模型的切线空间:对于每个顶点,它都有属于自己的切线空间,在这个切线空间内,这个顶点自身为原点,z轴是法线,而x轴则是切线,y轴则是通过二者叉积得到,称作副切线
法线纹理存储的正是切线空间下的法线,被称为切线空间的法线纹理
法线纹理
法线纹理中,他的rgb 分量存储的是表面的法线在切线空间下的xyz 值,但是由于法线分量范围在[-1,1] ,而像素分量则为[0,1] 。因此这里会做一个映射:
pixel=(normal+1)/2
因此法线纹理中真正存储的是把法线经过映射后得到的像素值;
因此,在通过代码获取法线纹理中的法线时,需要进行一次反映射过程;当然,如果不想手动进行反映射的过程,那么可以通过在纹理面板,将法线纹理的Texture Type 设置为Normal Map ,这样就不需要在进行反映射的过程了,他会自动完成这个过程;但是,请注意,设置了Normal Map 后此时法线纹理中存储的rgb 分量,就不再是法线在切线空间下的xyz 分量值了,因此我们就多出了一个工作,利用Unity 内置函数(UnpackNormal) 对法线纹理进行正确采样,那么我们接下来来看看这个函数的内部实现:
简单来说,这么做可以让Unity根据不同平台对纹理进行压缩,某些平台使用了DXT5nm格式因此需要对这种格式对法线进行解码,在DXT5nm格式中,纹素的a 通道对应了法线的x 分量,而g 通道对应了法线的y 分量,r 和g 两个通道被舍弃,法线的z通道可以通过推导得到
那么为什么要使用这种方式压缩呢,我们可以看到,对于法线纹理,我们真正需要的其实只有它的xy 分量的值,z 分量可与通过推导获得,那么在存储的时候,我们只存储它的xy 分量,便可以减少纹理占用的内存空间
主要思路
为了使用切线空间内的法线纹理,我们需要将所有计算切换到切线空间下计算,因此需要获得从世界空间变换到切线空间的变换矩阵,至于使用切线空间或世界空间的优缺点,请阅读《UnityShader入门精要》
如何获得变换矩阵,这里涉及矩阵的知识,不懂的读者请跳过。
对于坐标空间变换一个点,只需要将切线空间内的xyz 轴在世界空间内的xyz 值,按列排序,并将切线空间内的原点放在最后一列,第四行从左到右分别为0-0-0-1 ,具体原因不解释;矩阵4x4
对于坐标空间变换一个矢量,只需要将切线空间内的xyz轴在世界空间内的xyz值,按列排序即可;矩阵3x3
因此这里,只需要将世界空间内的切线,副切线,法线作为按列排序即可获得变换矩阵;
将光线方向和视觉方向通过变换矩阵变换到切线空间下即可,后续使用切线空间下的法线代替原法线进行计算即可
世界空间内的计算和切线空间内的计算思路相同,只是要将法线从切线空间转换到世界空间下,通过矩阵的右乘即可,请读者自己完成
Shader "MyShader/凹凸纹理_切线空间"
{
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;
float4 _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;
//计算副切线
float3 binormal=cross(normalize(v.normal),normalize(v.tangent.xyz));
//从模型空间到切线空间的变换矩阵
float3x3 rotation=float3x3(v.tangent.xyz,binormal,v.normal);
//UNITY内置宏
//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=normalize(i.viewDir);
//采样后的纹理法线
fixed4 packedNormal=tex2D(_BumpMap,i.uv.zw);
//切线空间内的法线
fixed3 tangentNormal;
//如果法线贴图TextureType没有设置为“Normal Map”
//tangentNormal.xy=(packedNormal.xy*2-1);
//如果法线贴图TextureType设置为“Normal Map”
//反映射后的切线空间的法线
tangentNormal.xy=packedNormal.wy*2-1;
//叠加缩放
tangentNormal*=_BumpScale;
//计算z
tangentNormal.z=sqrt(1-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
//纹素值:被采集的纹理,纹理坐标,在叠加颜色值
fixed3 albedo=tex2D(_MainTex,i.uv.xy).rgb*_Color.rgb;
//将纹素值(反射率)叠加给环境光
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
//通过反射率计算漫反射光照
fixed3 diffuse=_LightColor0.rgb*albedo*max(0,dot(tangentNormal,tangentLightDir));
//Blinn视觉优化
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"
}
Shader "MyShader/凹凸纹理_世界空间"
{
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;
float4 _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;
float3 worldNormal=UnityObjectToWorldNormal(v.normal);
float3 worldTangent=UnityObjectToWorldDir(v.tangent.xyz);
float3 worldBinormal=cross(worldNormal,worldTangent)*v.tangent.w;
//充分利用插值存储器的空间,将切线空间的:原点,x(切线),y(副切线),z(法线)
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=UnityWorldSpaceLightDir(worldPos);
fixed3 viewDir=UnityWorldSpaceViewDir(worldPos);
fixed3 bump=UnpackNormal(tex2D(_BumpMap,i.uv.zw));
bump*=_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.xy).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"
}
渐变纹理
主要思路
给予一维纹理,在纹理采用的时候不再使用uv 坐标,而通过半兰伯特模型获得的值(该值已经被映射到[0,1] )
通过一维纹理从0-1的采样获取渐变的颜色值,并将其叠加给漫反射即可;
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "MyShader/渐变纹理"
{
Properties
{
_Color("Color Tint",Color)=(1,1,1,1)
_RampTex("Ramp Tex",2D)="white"{}
_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 _RampTex;
float4 _RampTex_ST;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 texcoord:TEXCOORD0;
};
struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
float2 uv:TEXCOORD2;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
o.uv=v.texcoord*_RampTex_ST.xy+_RampTex_ST.zw;
return o;
}
fixed4 frag(v2f i):SV_TARGET{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
fixed halfLambert=dot(worldNormal,worldLight)*0.5+0.5;
fixed3 diffuseColor=tex2D(_RampTex,fixed2(halfLambert,halfLambert)).rgb*_Color.rgb;
fixed3 diffuse=_LightColor0.rgb*diffuseColor;
fixed3 viewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir=normalize(viewDir+worldLight);
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
遮罩纹理-控制高光反射强度
主要思路
通过一张纹理的某个分量计算掩码值,在与调控属性相乘,一起控制高光反射强度
Shader "MyShader/遮罩纹理"
{
Properties
{
_Color("Color Tint",Color)=(1,1,1,1)
_MainTex("Main Tex",2D)="white"{}
_BumpMap("Bump Map",2D)="bump"{}
_BumpScale("Bump Scale",float)=1.0
_SpecularMask("Specular Mask",2D)="white"{}
_SpecularScale("Specular 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;
float _BumpScale;
sampler2D _SpecularMask;
float _SpecularScale;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 tangent:TANGENT;
float4 texcoord:TEXCOORD0;
};
struct v2f{
float4 pos:SV_POSITION;
float2 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;
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=normalize(i.viewDir);
fixed3 tangentNormal=UnpackNormal(tex2D(_BumpMap,i.uv));
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 specularMask=tex2D(_SpecularMask,i.uv).r*_SpecularScale;
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(tangentNormal,halfDir)),_Gloss)*specularMask;
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
透明效果
基础知识
深度值与深度写入
所谓深度值,即是距离摄像机的距离。深度值越大,距离摄像机越远。Unity通过深度值判断物体的前后排列顺序,请注意这里的顺序是从像素层面上来考虑的。
所谓深度写入,即是在渲染某个像素时,将该像素的深度值写入深度缓存,以便后续再次渲染此像素时,通过当前深度值与深度缓存比较,即可得到覆盖关系;
渲染顺序的意义
因为有深度值的存在,错误的渲染顺序并不会影响到物体正确的渲染覆盖关系。但是如果需要使用到透明的效果,那么渲染顺序就变得十分重要。涉及半透明效果时,往往渲染半透明物体时关闭深度写入。对于一个半透明物体A,和一个不透明物体B,正确的顺序应该是先渲染不透明物体B,在渲染A,接着混合不透明物体A的透明物体B叠加态的颜色,即可获得半透明的效果;而如果错误的先渲染了A在渲染B,由于渲染A的时候深度写入关闭,渲染B的时候并不知道有一个物体在他前面,因此会将其覆盖。
渲染队列标签
透明度测试
预备知识
在片元函数中使用clip 函数来进行透明度测试,它等同于下面的代码
void clip(float4 x){
if(any(x<0){
discard;
}
}
当给定的值小于0时,则舍弃当前的像素,不去渲染;
主要思路
获得纹理的纹素值,把Alpha 通道与给定值相减,如果小于0 则舍弃像素
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "MyShader/透明度测试"
{
Properties
{
_Color("Main Tint",Color)=(1,1,1,1)
_MainTex("Main Tex",2D)="white"{}
_CutOff("Alpha CutOff",Range(0,1))=0.5
}
SubShader
{
Tags{
//渲染队列标签
"Queue"="AlphaTest"
//忽略投影器
"IgnoreProjector"="True"
//将此Shader归入TransparentCutout组
"RenderType"="TransparentCutout"
}
Pass{
Tags{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _CutOff;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 texcoord:TEXCOORD0;
};
struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
float2 uv:TEXCOORD2;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldNormal=UnityObjectToWorldNormal(v.normal);
o.worldPos=mul(unity_ObjectToWorld,v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}
fixed4 frag(v2f i):SV_TARGET{
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLight=normalize(UnityWorldSpaceLightDir(i.worldPos));
float4 texColor=tex2D(_MainTex,i.uv);
//Alpha Test
clip(texColor.r - _CutOff);//与下面这三行等价
// if(texColor.a<0.0){
// discard;
// }
fixed3 albedo=texColor.rgb*_Color.rgb;
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffuse=_LightColor0.rgb*ambient*max(0,dot(worldNormal,worldLight));
return fixed4(ambient+diffuse,1.0);
}
ENDCG
}
}
FallBack "Transparent/Cutout/VertexLit"
}
透明度混合
预备知识
透明度混合命令Blend
只有使用Blend命令打开透明度混合后,在片元着色器中设置透明度通道才有用,否则这些通道不会有任何效果
主要思路
首先关闭深度写入
通过Blend 命令打开透明度混合后,设置透明度通道即可
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "MyShader/透明度混合"
{
Properties
{
_Color("Main Tint",Color)=(1,1,1,1)
_MainTex("Main Tex",2D)="white"{}
_AlphaScale("Alpha Scale",Range(0,1))=1
}
SubShader
{
Tags{
//渲染队列标签
"Queue"="AlphaTest"
//忽略投影器
"IgnoreProjector"="True"
//将此Shader归入TransparentCutout组
"RenderType"="TransparentCutout"
}
Pass{
Tags{
"LightMode"="ForwardBase"
}
//关闭深度写入
ZWrite Off
//开启透明度混合
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 texcoord:TEXCOORD0;
};
struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
float2 uv:TEXCOORD2;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldNormal=UnityObjectToWorldNormal(v.normal);
o.worldPos=mul(unity_ObjectToWorld,v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}
fixed4 frag(v2f i):SV_TARGET{
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLight=normalize(UnityWorldSpaceLightDir(i.worldPos));
float4 texColor=tex2D(_MainTex,i.uv);
fixed3 albedo=texColor.rgb*_Color.rgb;
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffuse=_LightColor0.rgb*albedo*max(0,dot(worldNormal,worldLight));
return fixed4(ambient+diffuse,texColor.a*_AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/Cutout/VertexLit"
}
缺陷
当模型本身有复杂的遮挡关系时,就会产生因错误排序而导致的错误透明效果,例如
开启深度写入的半透明效果
主要思路
针对关闭深度写入导致的错误排序的解决方案,使用两个Pass通道,第一个Pass通道开启深度写入,将模型的深度值写入,但是不输出颜色,通过第二个Pass通道完成透明度混合,这样排序就不会错误;但是这种方法的缺点在于,多使用了一个Pass通道,会对性能造成一定的影响
这里为了不输出颜色,我们需要使用到新的渲染命令:ColorMask
当ColorMask 设置为0 时,意味着该Pass通道不写入任何颜色通道,即不会输出任何颜色,这里只需要写入深度缓存即可
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "MyShader/透明度混合-开启深度测试"
{
Properties
{
_Color("Main Tint",Color)=(1,1,1,1)
_MainTex("Main Tex",2D)="white"{}
_AlphaScale("Alpha Scale",Range(0,1))=1
}
SubShader
{
Tags{
//渲染队列标签
"Queue"="AlphaTest"
//忽略投影器
"IgnoreProjector"="True"
//将此Shader归入TransparentCutout组
"RenderType"="TransparentCutout"
}
Pass{
ZWrite On
ColorMask 0
}
Pass{
Tags{
"LightMode"="ForwardBase"
}
//关闭深度写入
ZWrite Off
//开启透明度混合
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 texcoord:TEXCOORD0;
};
struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
float2 uv:TEXCOORD2;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldNormal=UnityObjectToWorldNormal(v.normal);
o.worldPos=mul(unity_ObjectToWorld,v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}
fixed4 frag(v2f i):SV_TARGET{
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLight=normalize(UnityWorldSpaceLightDir(i.worldPos));
float4 texColor=tex2D(_MainTex,i.uv);
fixed3 albedo=texColor.rgb*_Color.rgb;
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffuse=_LightColor0.rgb*albedo*max(0,dot(worldNormal,worldLight));
return fixed4(ambient+diffuse,texColor.a*_AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/Cutout/VertexLit"
}
ShaderLab混合命令
请阅《UnityShader入门精要》
双面渲染的透明效果
显然,之前所有的Shader我们可以发现,无法看到它的内部或者是背部,这是因为默认情况下,渲染引擎剔除了物体的背部,我们可以通过Cull指令来控制需要剔除的面
剔除背面/前面,关闭剔除
剔除背面,背对摄像机的渲染图元就不会被渲染,正面同理;
关闭剔除,那么所有渲染图元都会被渲染,这样会导致需要渲染的图元数成倍增加,默认不关闭
透明度测试的双面渲染
只需要在Pass通道内,关闭剔除即可
透明度混合的双面渲染
不能使用关闭剔除,因为深度写入关闭,如果直接使用关闭剔除,那么由于模型本身的重叠,可能会导致里外渲染顺序错误,我们无法使得深度缓存按照逐像素的粒度进行深度排序,就无法得到渲染的正确性,无法保证正面和背面的渲染顺序,就可能得到错误的透明效果;
因此,我们将双面渲染的工作分为两个Pass,一个剔除正面,一个剔除背面,也就是第一个渲染背面,第二个渲染正面;
不要反了,如果反了那么正面就变成背面,背面就变成正面,效果就错误了。这样由于Unity执行顺序会先执行第一个Pass,我们就能保证背面渲染在正面渲染之后;
|