前言
这个并不是技术分享的教程类博客,只是我记录的笔记而已。不,不要误会了!(虽然溜一遍下来法线好像也能看
Shader基础
顶点着色器和片元着色器
顶点着色器
顶点着色器是流水线的第一个阶段。它负责坐标变换以及逐顶点光照。比如把顶点坐标从模型空间转换到齐次裁剪空间,逐顶点光照在后面会说。
片元着色器
片元着色器又称为像素着色器,它的输入是上一个阶段对定点信息插值得到的结果,输出是一个或者多个颜色值。在这个阶段可以完成很多重要渲染技术(纹理采样)。但是它只能影响单个片元,无法将任何结果发给其他人。
Shader的结构
Shader"MyShaderName" //Shader的名字,里面添加/可以创建文件夹
{
Properties{
//属性,声明一些着色需要的变量
_Color("Color Tint",Color)=(1,1,1,1)
//代码内叫啥("窗口里叫啥",啥类型)=赋值
}
SubShader{
//针对显卡A的SubShader
Pass{
//设置渲染状态和标签,此时声明Properties的变量
Tags{"LightMode"="ForwardBase"}
CGPROGRAM //开始CG代码的片段
//该代码片段的编译指令:
#pragma vertex vert //vert函数为顶点着色方式
#pragma fragment frag //frag函数为片元着色方式
#include "Lighting.cginc" //声明引用的头文件
struct a2v{
//可以在这里声明结构体
};
struct v2f{
};
v2f vert(a2v v){
//进行顶点着色器的相关操作
}
fixed4 frag(v2f i):SV_Target{
//进行片元着色器的相关操作
}
ENDCG //结束CG代码的片段
}
//其它需要的Pass
}
FallBack "Diffuse"//如果上面SubShader全寄了,就回调双引号内的Unity Shader
}
变量声明形式
fixed4 :什么精度的数字+多少个连一起
精度: fixed<half<float
标准光照模型
在标准光照模型中,把进入摄像机的模型分成四部分,每个部分分别计算贡献度。四个部分分别为自发光、高光反射、漫反射、环境光。
环境光(ambient)
用来模拟间接光照。简介光照指在多个物体反射后进入摄像机的光线。环境光为一个全局变量,计算等式为:
$$ c_{ambient}=g_{ambient} $$
在Shader中,我们通过Unity内置变量UNITY_LIGHTMODEL_AMBIENT直接获得环境光颜色和强度信息。
自发光(emissive)
直接从光源进入摄像机的光线,计算等式为:
$$ c_{emissive}=m_{emissive} $$
由于大部分物体没有自发光特性,所以不存在Unity变量。对于存在自发光特性的物体,我们要先把材质的自发光颜色添加到输出颜色上再输出就行了。
漫反射(diffuse)
用于对那些被物体表面随机散射到各个方向的辐射度进行建模。该光照符合兰伯特定律:
$$ c_{diffuse}=(c_{light}*m_{diffuse})max(0,\hat{n}*\hat{l}) $$
其中clight是光源颜色,mdiffuse是材质的漫反射颜色,n为表面法线,l为指向光源的单位矢量。当两者夹角为0时,说明光源直射,为最大值1.当两者垂直时值为0,说明光源射不到该表面。而大于90度已经是负值(没有光线反射),所以也算作是0.
高光反射(specular)
指完全沿着镜面反射方向被反射的光线,让物体看起来是有光泽的。
Phong模型
计算高光反射所需信息有表面法线,视角方向,光源方向和反射方向等。其中反射方向可以通过前两个计算而得出:
$$ \hat{r}=2*(\hat(n)*\hat(l))*\hat(n)-\hat(l)$$
(知道法线方向和入射方向求反射方向属于是初中知识了。。。)
这样可以通过上述信息,使用Phong模型计算高光反射的部分:
$$ c_{specular}=(c_{light}*m_{specular})max(0,\hat{v}*\hat{r})^m_{gloss}$$
其中mgloss为光泽度,用于控制高光区域“亮点”的宽度。光泽度越大亮点越小。(毕竟下面范围是[0,1],肯定越大是越小的)mspecular是材料的高光反射,控制强度和颜色。clight是光源颜色和强度。
Blinn模型
另一种高光反射模型为Blinn模型。它与Phong模型的区别在于避免计算反射方向,而是在对光源方向和视角方向取平均后再归一化后,引入新的矢量得到的。即
$$\hat(h)={\frac {\hat(v)+\hat(l)}{\lvert \hat(v)+\hat(l) \rvert}}$$
后面就和Phong模型差不多,但是是拿n和h之间的夹角计算:
$$ c_{specular}=(c_{light}*m_{specular})max(0,\hat{n}*\hat{h})^m_{gloss}$$
逐像素还是逐顶点
计算光照可以在两个着色器中计算。其中在片元着色器计算叫做逐像素光照(Phong着色),在顶点着色器中计算叫做逐顶点光照(高洛德着色)。
由于逐顶点光照只需在每个顶点上计算光照,然后再渲染图元内部线性插值,所以计算量小于逐像素光照。但由于存在非线性计算时,过度依赖线性插值的逐顶点光照就会产生问题。(特别是在高光反射上)
漫反射光照模型实践
逐顶点光照计算
Shader "Custom/Diffuse Vertex-Level"
{
Properties
{
_Diffuse("Diffuse",Color)=(1,1,1,1)
//控制材质的漫反射颜色,可以在hierachy窗口调整
}
SubShader
{
pass{
//定义正确的LightMode才能得到Unity的内置光照变量(如_LightColor0)
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//需要使用下面的内置文件
#include "Lighting.cginc"
//只有定义一个和Properties类型匹配的变量才能使用Properties内变量
fixed4 _Diffuse;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL; //告诉Unity将模型顶点法线信息存储到normal变量内
};
struct v2f{
float4 pos:SV_POSITION;
fixed3 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));
//我们选用世界坐标下计算,而v.normal为模型空间下的,所以需要转化为世界坐标
//转化流程为得到两空间的变换矩阵的逆矩阵unity_WorldToObject,然后和法线相乘
//由于法线为一个三维向量,所以只需要截取前三行前三列
fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
//将光源方向归一化处理
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"
}
saturate(x):把x锁定在[0,1]范围内;
normalize(x):将x归一化,只留下方向;
**UnityObjectToClipPos(v)**:将顶点位置从模型空间转换到裁剪空间;
mul(x,y):计算两向量/一向量一矩阵/两矩阵相乘;
dot(x,y):计算x,y的点积。
逐像素光照计算
Shader"Custom/Diffuse Pixel-Level{
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;
float3 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXTCOORD0;
};
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;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
fixed3 color=ambient+diffuse;
return fixed4(color,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
完 全 相 同
半兰伯特模型
由于兰伯特光照模型没照到的地方全是0,莫得光暗变化,所以万能的v社在开发《半条命》的时候整了个半兰伯特光照模型,公式如下:
$$c_{diffuse}=(c_{light}*m_{diffuse})(0.5(\hat(n)*\hat(l))+0.5)$$
这样背光面也有明暗变化,视觉上加强了不少。
代码方面和逐像素计算差不多,但是修改的地方有:
fixed4 frag(v2f i):SV_TARGET{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
fixed halfLambert=saturate(dot(worldNormal,worldLightDir))*0.5+0.5;
//这里单独拎出来算了
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*halfLambert;
fixed3 color=ambient+diffuse;
return fixed4(color,1.0);
}
高光反射光照模型实践
之前提到过反射方向的计算公式:
$$ \hat{r}=2*(\hat(n)*\hat(l))*\hat(n)-\hat(l)$$
在Unity里,你只需要**reflect(i,n)**就可以(i入射方向,n法线方向)
逐顶点光照计算
Shader "Custom/Specular Vertex-Level"
{
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 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
//算漫反射光照
fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
//计算反射方向,由于reflect函数入射方向要求从光源指向交点,所以worldLightDir要取反
fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-mul(unity_ObjectToWorld,v.vertex).xyz);
//_WorldSpaceCameraPos为摄像机位置,将顶点位置先转换为世界空间,然后与摄像机相减就是视角位置
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
//最后进行Phong模型的计算,得出高光反射的贡献度
o.color=ambient+diffuse+specular;
//与环境光、漫反射光相加,存储到最后的颜色里
return o;
}
fixed4 frag(v2f i):SV_Target{
//由于在顶点着色器都算完了,所以这里摸大鱼
return fixed4(i.color,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
这时逐顶点计算高光反射的弊端就出现了:阴处都是一片片的。
这是因为高光反射的计算是非线性的,而在顶点着色器里计算光照再插值是线性的。这样破坏了原来计算的非线性关系。
逐像素光照计算
Shader "Unity Shader Book/Chapter 6/Specular Pixel-Level"
{
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);
o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag(v2f i):SV_Target{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
虽然计算过程基本相同,但是得出的效果更令人满意:
Blinn-Phong模型计算
由于逐顶点计算高光反射的拉跨程度,所以我们只用逐像素的方法来实践
Shader "Custom/BlinnPhong"
{
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);
o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag(v2f i):SV_Target{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
fixed3 halfDir=normalize(worldLightDir+viewDir);
//扣扣大,计算新矢量h
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(worldNormal,halfDir)),_Gloss);
//然后改成h和n点乘
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
使用Blinn模型的高光反射部分看起来更大、更亮一些,所以大多数情况都会采用Blinn模型。
- 本文链接:https://karmotrine.fun/2022/01/11/Unity-Shader01%E2%80%94%E2%80%94%E5%85%89%E7%85%A7/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。
若没有本文 Issue,您可以使用 Comment 模版新建。