本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:
- 书本中句子照抄 + 个人批注
- 项目源码
- 一堆新手会犯的错误
- 潜在的太监断更,有始无终
总之适用于同样开始学习Shader的同学们进行有取舍的参考。
立方体纹理Cube Map
Cubemap是环境映射Environment Mapping的一种实现方法。环境映射可以模拟物体周围的环境,而使用了环境映射的物体可以看起来像镀了层金属一样反射出周围的环境。
和之前的纹理不同,立方体纹理(这里指的是为物体材质应用的纹理)包含了6张图像,这些图像对应了立方体的六个面,即摄像机处于立方体的中心点,并沿着三条轴的六个方向观察所得到的图像。
之前的2D纹理我们通过UV坐标进行采样,但是立方体纹理是一个三维纹理,因此我们若对其进行采样就需要使用一个三维的纹理坐标。这个三维纹理坐标即为世界空间下的一个三维向量(即为上图暗红色箭头表示),我们沿着这个向量向外延伸,那么就会与立方体中的其中一个纹理相交,而采样结果就是由该交点计算而来的。
使用立方体纹理的好处在于,它的实现简单快速,而且得到的效果也比较好。但是缺点是:当场景中引入了新的物体、光源或者物体发生移动的时候,我们就需要重新生成立方体纹理。除此外,立方体纹理仅可以反射环境,而不能反射使用了该立方体纹理的物体本身,也不能模拟多次反射的结果(例如两个金属球互相反射的情况)。因此我们应当要对凸面体使用立方体纹理而不应当为凹面体使用(凹面体凹陷的部分面会反射自身材质)
天空盒
**天空盒(SkyBox)**是游戏中一种用于模拟背景的方法,当我们使用了天空盒子时,整个场景就被包围在一个立方体里。这个立方体里的每个面使用的技术就是立方体纹理映射技术。(所以天空盒本质上是个将场景空间包含在内的巨大立方体纹理?)
只需创建一个SkyBox材质,我们就可以在Unity中使用天空盒子了。
创建一个材质,将其选为SkyBox——6 Sided。注意这六张纹理的位置需要摆放正确,并且为了让天空盒正常渲染,我们需要把这六张纹理的Wrap Mode设置为Clamp,否则接缝处会不匹配。
除了六张纹理图外,还有几个属性——TintColor 是材质的整体颜色 ,Exposure 用于调整天空盒子的亮度;Rotation 用于调整天空盒子沿y轴方向的旋转角度。
在此处进行替换即可替换场景的天空盒
在Lighting中设置的天空盒将会应用于场景中所有的摄像机,如果我们想要一些摄像机使用不同的天空盒,则需要为摄像机添加SkyBox组件来覆盖之前的设置。
在Unity中,天空盒是在所有不透明物体之后渲染的,而其背后使用的网格是一个立方体或者一个细分后的球体。
用于环境映射的立方体纹理
除了天空盒,立方体纹理最场景的用处是环境映射。通过这种方法,我们可以模拟出金属质感的材质。
一般来说创建用于环境映射的立方体纹理有三种方法:第一种方法是直接由一些特殊布局的纹理创建,第二种方法是手动创建一个Cubemap资源,再把六张图赋给它;第三种方法是由脚本生成。
第一种就是使用一些例如立方体展开图的交叉布局等图片,只需将Texture Type设置为CubeMap即可自动生成。
第二种方法是创建一个CubeMap材质,然后把六张纹理图拖拽到面板中。
官方推荐的用法是第一种,因为第一种方法支持对纹理数据进行压缩,而且可以支持边缘修正,光滑反射(glossy reflection)和HDR等功能。
前面两种方法都需要我们提供立方体纹理,这些纹理往往被场景中的所有物体所共用。理想情况下,我们希望根据物体再场景中位置的不同,生成不同的立方体纹理。这是,我们就可以再Unity中使用脚本来创建,这是通过Unity提供的Camera.RenderToCubdemap
函数来实现的,该函数可以把任意位置观察到的场景图象存储到6张图中,从而创建出对应位置上的立方体纹理。
void OnWizardCreate() {
// 临时创建一个摄像机,获取渲染图像并转为Cubemap后销毁
GameObject go = new GameObject("CubemapCamera");
go.AddComponent<Camera>();
go.transform.position = renderFromPosition.position;
go.GetComponent<Camera>().RenderToCubemap(cubemap);
DestroyImmediate(go);
}
我们需要为Cubemap勾选Readable选项。并且选定位置的Poisiton和要赋值的cubeMap就行处理即可。可使用项目扩展的功能选项GameObject->Render Into Cubemap来实现。我们可以设置CubeMap的FaceSize,Facesize越大则纹理分辨率越大,纹理越清晰。
反射
想要模拟反射效果很简单,我们只需要根据入射光线的方向和表面法线方向计算反射光线反向,然后根据这个方向去对立方体纹理进行采样即可:
Shader "Custom/Reflection_Copy"
{
Properties
{
_Color("Color Tint",Color) =(1,1,1,1)
// 反射颜色
_ReflectColor ("Reflection Color",Color) =(1,1,1,1)
// 反射度
_ReflectAmount("Reflect Amount",Range(0,1)) =1
_Cubemap("Reflection Cubemap",Cube) = "_Skybox" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
fixed4 _ReflectColor;
fixed _ReflectAmount;
samplerCUBE _Cubemap;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos:SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldViewDir : TEXCOORD2;
float3 worldRefl : TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex);
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
// reflect使用入射光方向和法线方向计算反射光方向
o.worldRefl = reflect(-o.worldViewDir,o.worldNormal);
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 worldNormlaDir = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(i.worldViewDir);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * saturate(dot(worldNormlaDir,worldLightDir));
// 使用texCUBE,用反射光向量采样Cubemap并乘以反射颜色值
fixed3 reflection = texCUBE(_Cubemap,i.worldRefl).rgb * _ReflectColor.rgb;
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
// 使用lerp函数实现颜色过渡
// lerp(x,y,weight)= (1-weight) * x + weight * y
fixed3 color = ambient + lerp(diffuse,reflection,_ReflectAmount) * atten;
return fixed4(color,1.0);
}
ENDCG
}
}
FallBack "Diffuser"
}
原来金属度(光泽度)和模型本身发光没什么关系,而是和模型材质的反射度相关。
其实就是在之前的光照模型的基础上,获取入射方向和法线方向,并计算反射方向,然后根据反射方向采样CubeMap并使用Lerp实现颜色过渡。
o.worldRefl在使用时没有归一化,因为我们是根据这个向量的方向对CubeMap进行采样,即使归一化了向量方向也不变。
当然反射方向可以在片元着色器中进行计算,比在顶点着色器中计算得到的结果会更好,效果更细腻。但是此法会带来一点性能损失,而且两者其实效果差别不大。
(放在片元着色器计算后似乎没这么亮了?)
折射
在光线传播中的另一种常见现象是折射,折射比反射复杂一点——当光从一种介质斜射入另一种介质时会发生折射。当给定入射角时,我们可以用斯涅尔公式计算反射角,如上图所示,已知 η 1 , η 2 , θ 1 \eta_1,\eta_2,\theta_1 η1,η2,θ1,可以求出 θ 2 \theta_2 θ2,公式为: η 1 s i n θ 1 = η 2 s i n θ 2 \eta_1sin\theta_1 = \eta_2sin\theta_2 η1sinθ1=η2sinθ2
其中 η 1 , η 2 \eta_1,\eta_2 η1,η2分别为两种介质的折射率,折射率是一种重要的物理常数,通常真空的折射率是1,玻璃的折射率是1.5
通常我们处理折射的方法是得到折射方向后对正方体纹理进行采样,然而这样是不对的。对一个透明物体来说,光线再进入时折射一次,出去时再回复原来的入射角,因此一次准确的模拟包含了两次折射。然而想要实现两次折射也是比较复杂的,一次折射的效果看起来也像那么回事,因此我们仅模拟一次折射。
让我们实现一个折射的Shader:
Shader "Custom/Refraction_Copy"
{
Properties
{
_Color("Color Tint",Color) =(1,1,1,1)
_RefractColor("Refraction Color",Color) = (1,1,1,1)
// 折射强度,用于lerp计算
_RefractAmount("Refraction Amount",Range(0,1)) = 1
// 相对折射率 = 射入介质折射率/原介质折射率
_RefractRatio("Refraction Ratio",Range(0.1,1)) = 0.5
_Cubemap("Refraction Cubemap",Cube) = "_Skybox"{}
}
SubShader
{
Tags{"Queue" = "Geometry" "RenderType" = "Opaque"}
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
fixed4 _RefractColor;
fixed _RefractAmount;
fixed _RefractRatio;
samplerCUBE _Cubemap;
struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 worldView : TEXCOORD1;
float3 worldNormal : TEXCOORD2;
float3 worldRefract : TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert(appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld,v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldView = UnityWorldSpaceViewDir(o.worldPos);
// 根据入射方向和法线以及相对折射率计算折射方向
o.worldRefract = refract(-normalize(o.worldView),normalize(o.worldNormal),_RefractRatio);
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 worldNormalDir = normalize(i.worldNormal);
fixed3 worldViewDir = normalize(i.worldView);
// 此处归一化多余了,只需要方向即可
fixed3 worldRefractDir = normalize(i.worldRefract);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT;
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * saturate(dot(worldNormalDir,worldLightDir));
fixed3 refraction = texCUBE(_Cubemap,i.worldRefract).rgb * _RefractColor.rgb;
UNITY_LIGHT_ATTENUATION(atten ,i,i.worldPos);
fixed3 color = ambient+ lerp(diffuse,refraction,_RefractAmount) * atten;
return fixed4(color,1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
折射效果并不好,一次折射只能做到这种程度了
菲涅尔反射
菲涅尔反射(Fresnel reflection) 是一种常见的光学现象,当光线照射到物体表面时,一部分发生折射,一部分发生反射。被反射的光和入射光之间存在一定的比例关系,这个关系可以用菲涅尔公式进行计算。
举一例子:当你站在湖边俯瞰水面时,你会发现水几乎是透明的;但是如果抬头看向远方的水面,你会发现水面波光闪闪,几乎看不见湖面下的东西。这就是菲涅尔效应,这是计算物理渲染中非常重要的一项高光反射计算因子。
要计算菲涅尔反射就需要用到菲涅尔公式,完整的公式是很复杂的,因此我们只采用近似公式——Schlick菲涅尔近似式:
F s c h l i c k ( v , n ) = F 0 + ( 1 − F 0 ) ( 1 − v ⋅ n ) 5 F_{schlick} (v,n)=F_0+(1-F_0)(1-v \cdot n)^5 Fschlick(v,n)=F0+(1−F0)(1−v⋅n)5
其中 F 0 F_0 F0代表一个反射系数,用于控制菲涅尔反射的强度, v v v为视角方向, n n n为法线方向,另一个应用广泛的近似式是Emprical菲涅尔近似式(是不是书中拼错了?Empirical经验的,经验菲涅尔):
F E m p r i c a l ( v , n ) = m a x ( 0 , m i n ( 1 , b i a s + s c a l e × ( 1 − v ⋅ n ) p o w e r ) ) F_{Emprical}(v,n)=max(0,min(1,bias+scale ×(1-v \cdot n)^{power})) FEmprical(v,n)=max(0,min(1,bias+scale×(1−v⋅n)power))
其中bias,scale,power都是控制项
让我们试试用Schlick菲涅尔来计算菲涅尔反射:
Shader "Custom/Chapter9/SchlickFresnel"
{
Properties
{
_Color("Color Tint",Color) =(1,1,1,1)
_FresnelScale ("Fresnel Scale", Range(0, 1)) = 0.5
_Cubemap("Reflection Cubemap",Cube) = "_Skybox" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
fixed _FresnelScale;
samplerCUBE _Cubemap;
struct v2f
{
float4 pos :SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 worldNormal :TEXCOORD1;
float3 worldReflect : TEXCOORD2;
float3 worldView : TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert(appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex);
o.worldView = UnityWorldSpaceViewDir(o.worldPos);
o.worldReflect = reflect(-o.worldView,o.worldNormal);
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 worldNormalDir = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(i.worldView);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT;
fixed3 reflection = texCUBE(_Cubemap,i.worldReflect).rgb;
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * saturate(dot(worldLightDir,worldNormalDir));
fixed3 fresnel = _FresnelScale + (1-_FresnelScale) * pow((1-dot(worldViewDir,worldNormalDir)),5);
fixed3 color = ambient + lerp(diffuse,reflection,saturate(fresnel)) * atten;
return fixed4(color,1.0);
}
ENDCG
}
}
Fallback "Specular"
}
对比反射的代码,我们就是将反射强度用菲涅尔反射进行了代替。
似乎变化不是很明显。