Cg Programming In Unity Lighting of Bumpy Surfaces


本教程涵盖法线贴图。
这是关于超越二维表面(或表面层)的纹理技术系列教程中的第一篇。 请注意,这些教程旨在教您这些技术是如何工作的。 如果您想在 Unity 中实际使用其中之一,您应该使用内置着色器或表面着色器。
在本教程中,我们从法线贴图开始,这是一种非常成熟的技术,可以模拟小凸起和凹痕的照明——即使是在粗糙的多边形网格上。

基于光照感知形状

在这里插入图片描述左边描绘的卡拉瓦乔的画是关于圣托马斯的怀疑,他不相信基督的复活,直到他将手指放在基督的身边。 使徒们皱起的眉头不仅象征着这种怀疑,而且通过共同的面部表情清楚地表达了这一点。 然而,为什么我们知道他们的额头实际上是皱着的,而不是涂上一些明暗的线条? 毕竟,这只是一幅平面画。 事实上,观众直觉地假设这些是皱眉而不是画眉——尽管这幅画本身允许两种解释。 教训是:在没有任何其他线索(阴影、遮挡、视差效果、立体效果等)的情况下,光亮通常可以令人信服地传达光滑表面上的凹凸。

法线贴图

法线贴图试图通过改变曲面法线向量来表达光滑曲面上(即具有插值法线的粗糙三角形网格)的凹凸。当用这些修改后的法向量计算光照时,观众通常会感觉到虚拟的凹凸-即使渲染了一个完美的平面三角形。这种错觉当然可以被打破(尤其是在轮廓上),但在许多情况下,它是非常令人信服的。
更具体地说,首先在纹理图像(即法线贴图)中生成表示虚拟凹凸的法线向量。然后片段着色器在纹理图像中查找这些向量,并基于它们计算照明。当然,问题是纹理图像中法向量的生成。有不同的方法可以选择,片段着色器必须适应用于生成法线贴图的特定编码。

Unity中的法线贴图

非常好的消息是,您可以使用Unity轻松地从灰度图像创建法线贴图:在您喜爱的绘制程序中创建灰度图像,并使用特定的灰度作为曲面的规则高度,浅灰色用于凹凸,深灰色用于凹痕。确保不同灰度之间的过渡平滑,例如通过模糊图像。导入图像到Unity时,请检查“Inspector”面板中的纹理类型Texture Type更改为法线贴图(Normal map),并勾选“Create from Grayscale”从灰度创建,单击“Apply”后,预览应显示带红色和绿色边缘的蓝色图像。除了生成法线贴图之外,还可以导入编码法线贴图(在这种情况下,不要忘记取消选中“Create from Grayscale”框)。
不太好的消息是片段着色器必须进行一些计算来解码法线。首先,纹理颜色存储在双分量纹理图像中,即只有一个alpha分量 A A A和一个颜色分量可用。颜色组件可以作为红色、绿色或蓝色组件访问—在所有情况下都返回相同的值。这里,我们使用绿色组件 G G G,因为Unity也使用它。两个组件 A A A G G G存储0到1之间的数字;但是,它们表示-1和1之间的坐标 n x n_{x} nx n y n_{y} ny,映射为:
n x = 2 A − 1 n_{x}=2A-1 nx=2A1,
n y = 2 G − 1 n_{y}=2G-1 ny=2G1
根据这两个分量,可以计算三维法向量 n = ( n x , n y , n z ) n=(n{x},n{y},n{z}) n=(nxnynz)的第三分量 n z n_{z} nz,因为对单位长度的归一化为:
n z = ± 1 − n x 2 − n y 2 n_{z} = \pm\sqrt{1-n_{x}^2 - n_{y}^2} nz=±1nx2ny2
如果沿平滑法向量的轴选择z轴(从顶点着色器中设置的法向量插值),则只需要“+”解决方案,因为我们无论如何都无法使用向内指向的法向量渲染曲面。片段着色器中的代码片段可能如下所示:

		    float4 encodedNormal = tex2D(_BumpMap, 
               _BumpMap_ST.xy * input.tex.xy + _BumpMap_ST.zw);
            float3 localCoords = float3(2.0 * encodedNormal.a - 1.0, 
                2.0 * encodedNormal.g - 1.0, 0.0);
            localCoords.z = sqrt(1.0 - dot(localCoords, localCoords));
               // approximation without sqrt:  localCoords.z = 
               // 1.0 - 0.5 * dot(localCoords, localCoords);

使用 OpenGL ES的设备的解码实际上更简单,因为在这种情况下Unity不使用双组件纹理。因此,对于移动平台,代码变成:

			float4 encodedNormal = tex2D(_BumpMap, 
               _BumpMap_ST.xy * input.tex.xy + _BumpMap_ST.zw);
            float3 localCoords = 
               2.0 * encodedNormal.rgb - float3(1.0, 1.0, 1.0);

但是,本教程的其余部分(以及“Projection of Bumpy Surfaces”一节)将只介绍桌面平台。
Unity为曲面的每个点使用局部曲面坐标系来指定法线贴图中的法向量。该局部坐标系的z轴由世界空间中平滑的插值法向量N给出,x-y平面是与曲面相切的平面。具体来说,x轴由Unity提供给顶点的切线参数T指定。给定x和z轴,y轴可以通过顶点着色器中的叉积计算,例如B=N× T(字母B表示此向量的传统名称“副法线”。)
注意,法线向量N是由逆模型矩阵从对象空间转换为世界空间(因为它与曲面正交);而切线向量T指定曲面上点之间的方向,因此将使用模型矩阵进行转换。副法线向量B表示第三类不同变换的向量。因此,最好的选择是先将N和T变换为世界空间,然后利用变换向量的交叉积在世界空间中计算B。
这些计算由顶点着色器执行,例如:

 	     struct vertexInput {
            float4 vertex : POSITION;
            float4 texcoord : TEXCOORD0;
            float3 normal : NORMAL;
            float4 tangent : TANGENT;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 posWorld : TEXCOORD0;
               // position of the vertex (and fragment) in world space 
            float4 tex : TEXCOORD1;
            float3 tangentWorld : TEXCOORD2;  
            float3 normalWorld : TEXCOORD3;
            float3 binormalWorld : TEXCOORD4;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
            
            float4x4 modelMatrix = unity_ObjectToWorld;
            float4x4 modelMatrixInverse = unity_WorldToObject;
 
            output.tangentWorld = normalize(
               mul(modelMatrix, float4(input.tangent.xyz, 0.0)).xyz);
            output.normalWorld = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            output.binormalWorld = normalize(
               cross(output.normalWorld, output.tangentWorld) 
               * input.tangent.w); // tangent.w is specific to Unity

            output.posWorld = mul(modelMatrix, input.vertex);
            output.tex = input.texcoord;
            output.pos = UnityObjectToClipPos(input.vertex);
            return output;
         }

在计算binormalWorld时,因子input.tangent.w是特定于Unity的,即Unity提供了切线向量和法线映射,因此我们必须进行乘法。
利用世界空间中的归一化方向T、B和N,我们可以很容易地形成一个矩阵,将法线映射的任意法向量N从局部曲面坐标系映射到世界空间,因为这样一个矩阵的列就是轴的向量;因此,3×3将n映射到世界空间的矩阵为:
M s u r f a c e → w o r l d = [ T x B x N x T y B y N y T z B z N z ] M_{surface \to world}= \begin{bmatrix} T_x&B_x &N_x \\ T_y&B_y &N_y\\ T_z&B_z &N_z \end{bmatrix} Msurfaceworld=TxTyTzBxByBzNxNyNz

在Cg中,由于矩阵是逐行构造的,所以构造转置矩阵实际上更容易

M s u r f a c e → w o r l d T = [ T x T y T z B x B y B z N x N y N z ] M_{surface \to world}^T= \begin{bmatrix} T_x&T_y &T_z \\ B_x&B_y &B_z\\ N_x&N_y &N_z \end{bmatrix} MsurfaceworldT=TxBxNxTyByNyTzBzNz

构造是在片段着色器中完成的,例如:

float3x3 local2WorldTranspose = float3x3(input.tangentWorld,
               input.binormalWorld, input.normalWorld);

我们想用local2WorldTranspose的转置(即未转置的原始矩阵)来变换n;因此,我们把左边的n与矩阵相乘。例如,使用以下行:

 float3 normalDirection = 
               normalize(mul(localCoords, local2WorldTranspose));

利用世界空间中的新法向量,我们可以计算“Smooth Specular Highlights”部分中的光照。

完整的Shader代码

此着色器代码简单地集成了所有片段,并使用我们的标准像素光双通道方法。它还演示了使用CGINCLUDE … ENDCG块,由所有Subshader的所有Pass隐式共享。

Shader "Cg normal mapping" {
   Properties {
      _BumpMap ("Normal Map", 2D) = "bump" {}
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
      _SpecColor ("Specular Material Color", Color) = (1,1,1,1) 
      _Shininess ("Shininess", Float) = 10
   }

   CGINCLUDE // common code for all passes of all subshaders

      #include "UnityCG.cginc"
      uniform float4 _LightColor0; 
      // color of light source (from "Lighting.cginc")

      // User-specified properties
      uniform sampler2D _BumpMap;   
      uniform float4 _BumpMap_ST;
      uniform float4 _Color; 
      uniform float4 _SpecColor; 
      uniform float _Shininess;

      struct vertexInput {
         float4 vertex : POSITION;
         float4 texcoord : TEXCOORD0;
         float3 normal : NORMAL;
         float4 tangent : TANGENT;
      };
      struct vertexOutput {
         float4 pos : SV_POSITION;
         float4 posWorld : TEXCOORD0;
         // position of the vertex (and fragment) in world space 
         float4 tex : TEXCOORD1;
         float3 tangentWorld : TEXCOORD2;  
         float3 normalWorld : TEXCOORD3;
         float3 binormalWorld : TEXCOORD4;
      };

      vertexOutput vert(vertexInput input) 
      {
         vertexOutput output;

         float4x4 modelMatrix = unity_ObjectToWorld;
         float4x4 modelMatrixInverse = unity_WorldToObject;

         output.tangentWorld = normalize(
            mul(modelMatrix, float4(input.tangent.xyz, 0.0)).xyz);
         output.normalWorld = normalize(
            mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
         output.binormalWorld = normalize(
            cross(output.normalWorld, output.tangentWorld) 
            * input.tangent.w); // tangent.w is specific to Unity

         output.posWorld = mul(modelMatrix, input.vertex);
         output.tex = input.texcoord;
         output.pos = UnityObjectToClipPos(input.vertex);
         return output;
      }

      // fragment shader with ambient lighting
      float4 fragWithAmbient(vertexOutput input) : COLOR
      {
         // in principle we have to normalize tangentWorld,
         // binormalWorld, and normalWorld again; however, the 
         // potential problems are small since we use this 
         // matrix only to compute "normalDirection", 
         // which we normalize anyways

         float4 encodedNormal = tex2D(_BumpMap, 
            _BumpMap_ST.xy * input.tex.xy + _BumpMap_ST.zw);
         float3 localCoords = float3(2.0 * encodedNormal.a - 1.0, 
             2.0 * encodedNormal.g - 1.0, 0.0);
         localCoords.z = sqrt(1.0 - dot(localCoords, localCoords));
         // approximation without sqrt:  localCoords.z = 
         // 1.0 - 0.5 * dot(localCoords, localCoords);

         float3x3 local2WorldTranspose = float3x3(
            input.tangentWorld, 
            input.binormalWorld, 
            input.normalWorld);
         float3 normalDirection = 
            normalize(mul(localCoords, local2WorldTranspose));

         float3 viewDirection = normalize(
            _WorldSpaceCameraPos - input.posWorld.xyz);
         float3 lightDirection;
         float attenuation;

         if (0.0 == _WorldSpaceLightPos0.w) // directional light?
         { 
            attenuation = 1.0; // no attenuation
            lightDirection = normalize(_WorldSpaceLightPos0.xyz);
         } 
         else // point or spot light
         {
            float3 vertexToLightSource = 
               _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
            float distance = length(vertexToLightSource);
            attenuation = 1.0 / distance; // linear attenuation 
            lightDirection = normalize(vertexToLightSource);
         }

         float3 ambientLighting = 
            UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;

         float3 diffuseReflection = 
            attenuation * _LightColor0.rgb * _Color.rgb
            * max(0.0, dot(normalDirection, lightDirection));

         float3 specularReflection;
         if (dot(normalDirection, lightDirection) < 0.0) 
            // light source on the wrong side?
         {
            specularReflection = float3(0.0, 0.0, 0.0); 
            // no specular reflection
         }
         else // light source on the right side
         {
            specularReflection = attenuation * _LightColor0.rgb 
               * _SpecColor.rgb * pow(max(0.0, dot(
               reflect(-lightDirection, normalDirection), 
               viewDirection)), _Shininess);
         }
         return float4(ambientLighting + diffuseReflection 
            + specularReflection, 1.0);
      }
      
      // fragment shader for pass 2 without ambient lighting 
      float4 fragWithoutAmbient(vertexOutput input) : COLOR
      {
        // in principle we have to normalize tangentWorld,
        // binormalWorld, and normalWorld again; however, the  
        // potential problems are small since we use this 
        // matrix only to compute "normalDirection", 
        // which we normalize anyways

        float4 encodedNormal = tex2D(_BumpMap, 
           _BumpMap_ST.xy * input.tex.xy + _BumpMap_ST.zw);
        float3 localCoords = float3(2.0 * encodedNormal.a - 1.0, 
            2.0 * encodedNormal.g - 1.0, 0.0);
        localCoords.z = sqrt(1.0 - dot(localCoords, localCoords));
           // approximation without sqrt:  localCoords.z = 
           // 1.0 - 0.5 * dot(localCoords, localCoords);

        float3x3 local2WorldTranspose = float3x3(
           input.tangentWorld,
           input.binormalWorld, 
           input.normalWorld);
        float3 normalDirection = 
           normalize(mul(localCoords, local2WorldTranspose));

        float3 viewDirection = normalize(
           _WorldSpaceCameraPos - input.posWorld.xyz);
        float3 lightDirection;
        float attenuation;

        if (0.0 == _WorldSpaceLightPos0.w) // directional light?
        {
           attenuation = 1.0; // no attenuation
           lightDirection = normalize(_WorldSpaceLightPos0.xyz);
        } 
        else // point or spot light
        {
           float3 vertexToLightSource = 
              _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
           float distance = length(vertexToLightSource);
           attenuation = 1.0 / distance; // linear attenuation 
           lightDirection = normalize(vertexToLightSource);
        }

        float3 diffuseReflection = 
           attenuation * _LightColor0.rgb * _Color.rgb
           * max(0.0, dot(normalDirection, lightDirection));

        float3 specularReflection;
        if (dot(normalDirection, lightDirection) < 0.0) 
           // light source on the wrong side?
        {
           specularReflection = float3(0.0, 0.0, 0.0); 
              // no specular reflection
        }
        else // light source on the right side
        {
           specularReflection = attenuation * _LightColor0.rgb 
              * _SpecColor.rgb * pow(max(0.0, dot(
              reflect(-lightDirection, normalDirection), 
              viewDirection)), _Shininess);
        }
        return float4(diffuseReflection + specularReflection, 1.0);
      }
   ENDCG

   SubShader {
      Pass {      
         Tags { "LightMode" = "ForwardBase" } 
            // pass for ambient light and first light source
 
         CGPROGRAM
            #pragma vertex vert  
            #pragma fragment fragWithAmbient  
            // the functions are defined in the CGINCLUDE part
         ENDCG
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         Blend One One // additive blending 
 
         CGPROGRAM
            #pragma vertex vert  
            #pragma fragment fragWithoutAmbient
            // the functions are defined in the CGINCLUDE part
         ENDCG
      }
   }
}

请注意,我们使用了平铺和偏移均匀凹凸贴图,如“纹理球体”一节中所述,因为此选项通常对凹凸贴图特别有用。

总结

本节中,我们看到了:

  • 人类对形状的感知往往依赖于光线。
  • 什么是法线映射。
  • Unity如何编码法线贴图。
  • 片段着色器如何解码Unity的法线贴图,并使用它们进行逐像素照明。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值