而这次呢,我们就反过来学习一种"环境影响自身"的CG shader效果,我们生活在大都市中,基本上就是头顶蓝天脚踩大地,周围则都是厚厚的高楼墙壁,假如我是一面镜子,反射着周遭一切的物体,我运动一下,镜中反射的物体就跟着不断的变化,这种效果就是这次我们主要学习的对象,对,没错,这次我们就来学习一种"环境反射"镜面效果。
首先我们来确定一下什么是"环境",从小范围来说,我们坐在办公室上班,"环境"就是天花板地板和前后左右的同事,那么从大范围来说,"环境"就是头顶的星空,脚下的大地和四周的花草树木大街小巷。图形学中,将"环境"具象出一个正方体"盒子",如下图:
假设我们就是处在(0,0,0)原点坐标,那么看到的正方体内部上下+前后左右的六面景象就是"环境"了,在图形学中,我们称这个"环境正方体"为立方贴图(CubeMap)。CubeMap的六个面(top bottom left right front back)对应六张纹理贴图,也可以称为环境贴图。
那么问题来了,假设我是一个处在xyz坐标系的原点的"镜子",我该怎么表现自己才能达到"反射"环境的效果呢?先来说一下反射这个概念(前面我们在几何向量讲过反射,这里我只讲解重要的地方),反射主要包含入射向量,入射点法向量和反射向量三个重要因素,那么观察者眼睛坐标点eyePosition观察"我这面镜子"上的某一点MirrorPosition,实际上观察的是"环境盒子"某一面贴图上的坐标点P的采样颜色,这两个向量就是反射关系,如下图:
实际上呢,现实中我们通过镜面看到的物体object是光源(比如太阳)发出光线照射到物体object上经过漫反射等将光线反射到镜面,再次经过"完美"的镜面反射后,光线进入人眼,所以才看到镜面反射的物体。省去前面那一次太阳光漫反射阶段,那么就意味着P点发射的光线,射入镜面MirrorPosition点后反射到eyePostion点。
那么实际上观察者eye观察到的镜面MirrorPosition点的颜色为"环境盒子"的某个面的P点的纹理采样颜色。
这个时候,我们整理一下已知量,我这里标明一下:
①.观察者eye的源坐标,这个unity CG运行时提供,_WorldSpaceCameraPos这个字段就是,光照模型栏目有说过,不记得可以回过去看。
②.反射点MirrorPosition的源坐标和法向量,这个也是CG语义绑定提供的数据。
③."环境盒子"的纹理采样函数texCube,这个就非常重要了,可以说我们实现环境反射CG shader最关键的就是这个函数,这个函数一共两个参数texCube(cubeMap,ReflectVector3),第一个就是"环境盒子立方贴图cubemap",第二个就是"环境盒子"内的反射向量,这个反射向量和我上面画的真实反射刚好相反,这个ReflectVector3是观察者眼睛eyePosition发出一束光线到镜面观察点MirrorPosition的向量(MirrorPosition-eyePosition)经过反射后到达P点的向量(P-MirrorPosition),这样的话我们通过eye发出的入射光经过镜面入射点产生反射光和反射向量计算查找"环境盒子"内部对应的采样点颜色就很方便了。
这里会引发一个问题,texCube函数第二个参数只是一个三维向量vector3,那么这个向量我们就只知道朝向而不知道具体的位置,因为它并不是齐次四维形式的,那么texCube通过第二个反射三维向量计算采样,不就会很奇怪?因为在texCube函数内部实现中根本就无法通过这个参数进行数学计算。这里图形学中给出一种假设,假设"环境盒子"是无限大的,无限大哟!那么对于无限大的"环境盒子"正方体中心的"反射镜面体",我们就可以认为"反射镜面体"的每个顶点都在"环境盒子"中心(大家请理解这种解释,因为无限大所以无限远,所以网格的每个顶点都接近于中心点),那么只需要一个三维向量vector3就能表示位置了,因为这个三维向量的起始反射点就是"环境盒子"的中心点,这样texCube通过反射向量采样就能实现了。
以上是我必须强调的理解方式。
接下来我们就来实际验证一下,首先创建"环境盒子",这是我在南澳旅游拍的一点照片,顺便创建对应的CubeMap,如下:
创见CubeMap的操作是右键create-legacy-cubemap。
然后实现我们的CG shader代码,如下:
-
Shader
"Unlit/EnvReflectUnlitShader"
-
{
-
Properties
-
{
-
_MainTex (
"Texture",
2D) =
"white" {}
-
_MainCube(
"Envirment Cube",CUBE) =
"" {}
-
_ReflectWeight(
"Reflect Weight",Range(
0.0,
1.0)) =
0.5
-
}
-
SubShader
-
{
-
Tags {
"RenderType"=
"Opaque" }
-
LOD
100
-
Cull off
-
-
Pass
-
{
-
CGPROGRAM
-
-
#pragma vertex vert
-
#pragma fragment frag
-
-
#include "UnityCG.cginc"
-
-
struct appdata
-
{
-
float4 vertex : POSITION;
//顶点源坐标
-
float2 uv : TEXCOORD0;
//纹理uv
-
float3 normal : NORMAL;
//顶点源法向量
-
};
-
-
struct v2f
-
{
-
float2 uv : TEXCOORD0;
//主纹理uv
-
float3 refl : TEXCOORD1;
//计算后的反射向量,使用一个TEXCOORD1语义绑定储存
-
float4 vertex : SV_POSITION;
//变换后的顶点坐标
-
};
-
-
sampler2D _MainTex;
-
float4 _MainTex_ST;
-
samplerCUBE _MainCube;
-
float _ReflectWeight;
//反射权重,也就是反射颜色比主颜色强度权重
-
-
//反射计算,实际上NvidiaCG提供内置reflect计算函数,且效率比自己实现要高
-
float3 reflectEx(float3 inLight,float3 norm)
-
{
-
return inLight
-2.0*norm*dot(norm,inLight);
-
}
-
-
v2f vert (appdata v)
-
{
-
v2f o;
-
-
//构建y轴随时间time旋转矩阵
-
float4x4 _mat = float4x4(cos(_Time.y),
0, sin(_Time.y),
0,
-
0,
1,
0,
0,
-
-sin(_Time.y),
0, cos(_Time.y),
0,
-
0,
0,
0,
1);
-
//首先对vertex顶点源坐标进行旋转
-
float4 rvertex = mul(_mat,v.vertex);
-
//然后使用MVP矩阵处理顶点到裁剪空间
-
o.vertex = UnityObjectToClipPos(rvertex);
-
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
-
-
//因为顶点源坐标进行了旋转矩阵处理,所以相应的源法向量也要进行旋转矩阵处理
-
//处理vertex顶点源法向量进行旋转
-
float4 rnomal = mul(_mat,v.normal);
-
//将vertex法向量处理到世界空间
-
float3 normWorld = normalize(mul(UNITY_MATRIX_M,rnomal)).xyz;
-
//讲vertex顶点源坐标变换到世界空间
-
float3 posWorld = mul(UNITY_MATRIX_M,rvertex).xyz;
-
//在建模空间中计算眼睛到镜面反射点的向量(也就是入射光线))
-
float3 INlight = posWorld - _WorldSpaceCameraPos.xyz;
-
//使用反射向量计算公式计算镜面反射点到环境盒子的采样点P的向量
-
o.refl = reflectEx(INlight,normWorld);
-
-
return o;
-
}
-
-
fixed4 frag (v2f i) : SV_Target
-
{
-
//根据反射向量采样环境盒子的P点颜色
-
fixed4 reflCol = texCUBE(_MainCube,i.refl);
-
//采样主纹理颜色
-
fixed4 texCol = tex2D(_MainTex, i.uv);
-
//根据反射权重插值计算最终颜色
-
fixed4 col = lerp(texCol,reflCol,_ReflectWeight);
-
return col;
-
}
-
ENDCG
-
}
-
}
-
}
CG shader中已经进行了很详细的注释,但是我还是要着重说明几点,如下:
①.appdata结构体,这是通过语义绑定将CG runtime的顶点源坐标,源法向量,主纹理uv传递给vertex顶点函数使用。
②.v2f结构体,储存了计算后的主纹理uv,顶点坐标,及使用TEXCOORD1绑定储存的reflect反射向量,以便后续使用。
③.vertex顶点函数变换坐标时,顶点源坐标和源法向量必须同步矩阵变换,不然表现效果不一致。
④.计算反射向量的一系列操作,全是在世界空间计算完毕,计算的每一步注释都标注了。
⑤.在fragment片段函数中,我们分别采样出主纹理颜色和环境反射颜色,通过一个_ReflectWeight进行颜色权重显示。
最后我来放一个CG shader达到的效果,如下图:
顺便说一下,因为unity CG提供了很多runtime数据,甚至包含大部分已经计算好的向量,后面我们学习unity的简单高效环境反射的写法。
so,我们接下来继续。