【Unity Shaders】學習筆記——SurfaceShader(九)Cubemap
-
如果你想從零開始學習Unity Shader,那么你可以看看本系列的文章入門,你只需要稍微有點編程的概念就可以。
-
水平有限,難免有謬誤之處,望指出。
上一節中講述了制作Cubemap的方法。這一節講講怎么使用它。
Simple Cubemap
先來看一下最簡單的Cubemap。
Shader "Custom/SimpleReflection"
{
Properties
{
_MainTint ("Diffuse Tint", Color) = (1,1,1,1)
_MainTex ("Base (RGB)", 2D) = "white" {}
_Cubemap ("CubeMap", CUBE) = ""{}
_ReflAmount ("Reflection Amount", Range(0.01, 1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Lambert
sampler2D _MainTex;
samplerCUBE _Cubemap;
float4 _MainTint;
float _ReflAmount;
struct Input
{
float2 uv_MainTex;
float3 worldRefl;
};
void surf (Input IN, inout SurfaceOutput o)
{
half4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
o.Emission = texCUBE(_Cubemap, IN.worldRefl).rgb * _ReflAmount;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
其實就是用texCUBE()函數來對Cubemap采樣。
第二個參數應該傳入UV坐標,但實際傳入的是世界反射向量。這是Unity替我們計算了UV坐標。
Cubemap是由六張貼圖構成的,組成了一個類似天空盒的六面體,那這六張貼圖是如何映射到小球上的呢,使小球看起來像倒映着周圍的環境一樣?
將Cubemap想象成一個立方體,包裹着小球,從小球的中心點發射一條射線,穿過小球表面和立方體表面,射線與小球和立方體會各有一個交點,小球上的這個點對應的就是立方體上的這個點的紋理。那要怎樣計算各個點對應的UV坐標呢?從小球中心點發射的一條條射線其實就是法線,有個很明顯的事實就是,法線朝向法線坐標分量最大的坐標軸指向的立方體的面。也就是坐標(1,1,3)的法線朝向的是Z軸指向的立方體的面。這樣就由法線找到了點對應的面。那么UV坐標又該如何計算呢?法線另外兩個較小的坐標值和UV坐標是有關系的。舉個特殊點的例子,小球最頂點的法線的坐標應該是(0,0,1),對應的應該是立方體的頂面的中點,UV坐標應該是(0.5,0.5)。計算UV坐標的方法是(x/z×0.5+0.5,y/z×0.5+0.5),將特殊點代進去,答案是正確的。原理有點難說明,相當於把X、Y坐標投影到了對應的面上。乘0.5加0.5是為了讓法線坐標的區間從[-1,1]變為[0,1]。
在Unity里,我們不必自己計算,可以直接使用內置變量worldRefl來檢索Cubemap。
Normal Cubemap
Shader "Custom/NormalMappedReflection"
{
Properties
{
_MainTint ("Diffuse Tint", Color) = (1,1,1,1)
_MainTex ("Base (RGB)", 2D) = "white" {}
_NormalMap ("Normal Map", 2D) = "bump" {}
_Cubemap ("Cubemap", CUBE) = ""{}
_ReflAmount ("Reflection Amount", Range(0,1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Lambert
samplerCUBE _Cubemap;
sampler2D _MainTex;
sampler2D _NormalMap;
float4 _MainTint;
float _ReflAmount;
struct Input
{
float2 uv_MainTex;
float2 uv_NormalMap;
float3 worldRefl;
INTERNAL_DATA
};
void surf (Input IN, inout SurfaceOutput o)
{
half4 c = tex2D (_MainTex, IN.uv_MainTex);
float3 normals = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap)).rgb;
o.Normal = normals;
o.Emission = texCUBE (_Cubemap, WorldReflectionVector (IN, o.Normal)).rgb * _ReflAmount;
o.Albedo = c.rgb * _MainTint;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
就只是增加了法線貼圖而已。
因為法線信息的改變,所以要重新計算傳入texCUBE()函數的世界反射向量。float3 worldRefl;INTERNAL_DATA變量用於原本的法線信息不使用時,比如使用了法線貼圖,原來的法線信息就不使用了。要根據新的法線信息計算世界反射向量使用WorldReflectionVector()函數。
動態立方圖系統
有時候游戲里的物體從一個環境走到另一個環境,需要更換Cubemap,這樣顯示的反射效果才真實。
有兩種方法更換Cubemap,一種是實時更換Cubemap,這樣的效果最真實,但是要犧牲性能;第二種是當物體走到另一個環境的時候更換Cubemap,這就是我要講的方法。
方法很簡單,就是在C#里用SetTexture的方法動態更換Cubemap。
[ExecuteInEditMode]
public class SwapCubemaps : MonoBehaviour
{
public Cubemap cubeA;
public Cubemap cubeB;
public Transform posA;
public Transform posB;
private Material curMat;
private Cubemap curCube;
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
curMat = renderer.sharedMaterial;
if(curMat)
{
curCube = CheckProbeDistance();
curMat.SetTexture("_Cubemap", curCube);
}
}
private Cubemap CheckProbeDistance() {
float distA = Vector3.Distance(transform.position, posA.position);
float distB = Vector3.Distance(transform.position, posB.position);
if(distA < distB)
{
return cubeA;
}
else if(distB < distA)
{
return cubeB;
}
else
{
return cubeA;
}
}
void OnDrawGizmos() {
Gizmos.color = Color.green;
if(posA)
{
Gizmos.DrawWireSphere(posA.position, 0.5f);
}
if(posB)
{
Gizmos.DrawWireSphere(posB.position, 0.5f);
}
}
}
-
[ExecuteInEditMode]是為了讓腳本在編輯器狀態的時候也能執行,這樣就不必點擊Play調試,比較方便。
-
OnDrawGizmos()是在Scene里畫一些可視化的東西方便調試。
-
CheckProbeDistance()里用Distance判斷物體和A點、B點的距離,根據距離決定返回哪種Cubemap。
-
在Update()設置材質的Cubemap。
Cubemap效果

我有加個金屬的紋理。效果是這樣的。

這是加了法線貼圖的。