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

本文深入探讨了环境反射在CGShader中的实现原理与实践,详细解析了如何利用CubeMap和反射向量实现镜面效果,包括反射计算公式、纹理采样技巧及UnityCGShader代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载自: https://blog.youkuaiyun.com/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代码,如下:

    


 
  1. Shader "Unlit/EnvReflectUnlitShader"
  2. {
  3. Properties
  4. {
  5. _MainTex ( "Texture", 2D) = "white" {}
  6. _MainCube( "Envirment Cube",CUBE) = "" {}
  7. _ReflectWeight( "Reflect Weight",Range( 0.0, 1.0)) = 0.5
  8. }
  9. SubShader
  10. {
  11. Tags { "RenderType"= "Opaque" }
  12. LOD 100
  13. Cull off
  14. Pass
  15. {
  16. CGPROGRAM
  17. #pragma vertex vert
  18. #pragma fragment frag
  19. #include "UnityCG.cginc"
  20. struct appdata
  21. {
  22. float4 vertex : POSITION; //顶点源坐标
  23. float2 uv : TEXCOORD0; //纹理uv
  24. float3 normal : NORMAL; //顶点源法向量
  25. };
  26. struct v2f
  27. {
  28. float2 uv : TEXCOORD0; //主纹理uv
  29. float3 refl : TEXCOORD1; //计算后的反射向量,使用一个TEXCOORD1语义绑定储存
  30. float4 vertex : SV_POSITION; //变换后的顶点坐标
  31. };
  32. sampler2D _MainTex;
  33. float4 _MainTex_ST;
  34. samplerCUBE _MainCube;
  35. float _ReflectWeight; //反射权重,也就是反射颜色比主颜色强度权重
  36. //反射计算,实际上NvidiaCG提供内置reflect计算函数,且效率比自己实现要高
  37. float3 reflectEx(float3 inLight,float3 norm)
  38. {
  39. return inLight -2.0*norm*dot(norm,inLight);
  40. }
  41. v2f vert (appdata v)
  42. {
  43. v2f o;
  44. //构建y轴随时间time旋转矩阵
  45. float4x4 _mat = float4x4(cos(_Time.y), 0, sin(_Time.y), 0,
  46. 0, 1, 0, 0,
  47. -sin(_Time.y), 0, cos(_Time.y), 0,
  48. 0, 0, 0, 1);
  49. //首先对vertex顶点源坐标进行旋转
  50. float4 rvertex = mul(_mat,v.vertex);
  51. //然后使用MVP矩阵处理顶点到裁剪空间
  52. o.vertex = UnityObjectToClipPos(rvertex);
  53. o.uv = TRANSFORM_TEX(v.uv, _MainTex);
  54. //因为顶点源坐标进行了旋转矩阵处理,所以相应的源法向量也要进行旋转矩阵处理
  55. //处理vertex顶点源法向量进行旋转
  56. float4 rnomal = mul(_mat,v.normal);
  57. //将vertex法向量处理到世界空间
  58. float3 normWorld = normalize(mul(UNITY_MATRIX_M,rnomal)).xyz;
  59. //讲vertex顶点源坐标变换到世界空间
  60. float3 posWorld = mul(UNITY_MATRIX_M,rvertex).xyz;
  61. //在建模空间中计算眼睛到镜面反射点的向量(也就是入射光线))
  62. float3 INlight = posWorld - _WorldSpaceCameraPos.xyz;
  63. //使用反射向量计算公式计算镜面反射点到环境盒子的采样点P的向量
  64. o.refl = reflectEx(INlight,normWorld);
  65. return o;
  66. }
  67. fixed4 frag (v2f i) : SV_Target
  68. {
  69. //根据反射向量采样环境盒子的P点颜色
  70. fixed4 reflCol = texCUBE(_MainCube,i.refl);
  71. //采样主纹理颜色
  72. fixed4 texCol = tex2D(_MainTex, i.uv);
  73. //根据反射权重插值计算最终颜色
  74. fixed4 col = lerp(texCol,reflCol,_ReflectWeight);
  75. return col;
  76. }
  77. ENDCG
  78. }
  79. }
  80. }

          CG shader中已经进行了很详细的注释,但是我还是要着重说明几点,如下:

                ①.appdata结构体,这是通过语义绑定将CG runtime的顶点源坐标,源法向量,主纹理uv传递给vertex顶点函数使用。

                ②.v2f结构体,储存了计算后的主纹理uv,顶点坐标,及使用TEXCOORD1绑定储存的reflect反射向量,以便后续使用。

                ③.vertex顶点函数变换坐标时,顶点源坐标和源法向量必须同步矩阵变换,不然表现效果不一致。

                ④.计算反射向量的一系列操作,全是在世界空间计算完毕,计算的每一步注释都标注了。

                ⑤.在fragment片段函数中,我们分别采样出主纹理颜色和环境反射颜色,通过一个_ReflectWeight进行颜色权重显示。

          最后我来放一个CG shader达到的效果,如下图:

                 

                顺便说一下,因为unity CG提供了很多runtime数据,甚至包含大部分已经计算好的向量,后面我们学习unity的简单高效环境反射的写法。

               so,我们接下来继续。

             

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值