C for Graphic:语言(环境反射)

版权声明: https://blog.csdn.net/yinhun2012/article/details/82932930

       之前我们学习了一系列CG shader制作效果,主要围绕的是vertex顶点和frag片段函数能做的一些"动态"的事情,比如顶点位移啊,像素插值变化啊什么的,这些都属于一种主观的状态变化,什么意思呢?通俗一点来说就是CG shader所作用的物体,自发的在变化着,从而影响场景的周围环境,有种"一枝独秀"的感觉,亦或者说是"自身影响环境"。

       而这次呢,我们就反过来学习一种"环境影响自身"的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,我们接下来继续。

             

猜你喜欢

转载自blog.csdn.net/yinhun2012/article/details/82932930