UnityShader之漫反射

前言

近些年,电子竞技行业发展迅速,游戏的品质也是逐年提升。从用户体验的角度来讲,画质渲染及画质特效方面非常重要。任何渲染特效都主要是光影的变幻,从而达到美轮美奂的效果。漫反射是最简单的光照效果,所有的光照特效都是以漫反射为基础的。本篇就来详细介绍,漫反射着色器如何实现。

  • 首先我们来看一下什么漫反射
    • 漫反射,是投射在粗糙表面上的光向各个方向反射的现象。当一束平行的入射光线射到粗糙的表面时,表面会把光线向着四面八方反射,所以入射线虽然互相平行,由于各点的法线方向不一致,造成反射光线向不同的方向无规则地反射,这种反射称之为“漫反射”或“漫射”。这种反射的光称为漫射光。很多物体,如植物、墙壁、衣服等,其表面粗看起来似乎是平滑,但用放大镜仔细观察,就会看到其表面是凹凸不平的,所以本来是平行的太阳光被这些表面反射后,弥漫地射向不同方向。


      漫反射
    • 以简单人物为例,在Unity中的漫反射是这样的


      人物漫反射
  • 关于漫反射的光照模型,常见的是兰伯特(Lambert)半兰伯特(Half-Lambert)
    • Lambert的光照公式是(C-light · M-diffuse)max(0,n·I)

    • 光照颜色 * 漫反射颜色 * max(0,法向量*光照方向)

    • 下面以球形2D图像解析一下原理

      • 第一步,我们确认公式中的两个方向向量,注意光照方向是指向光的方向,即入射光向量的逆向量,红色光照方向,蓝色顶点法向量。


        光照方向及法向量方向
      • 第二步,计算不同顶点两向量的夹角,我们可以得到面向光的部分角度在0-90度之间,背向光的角度在90-180度之间。


        计算不同顶点两向量的夹角
      • 第三步,Lambert漫反射需要的面向光的部分渲染,背向光的地方不渲染,且渲染部分需要做曲线性的递减。公式中两个向量做点乘计算,即n · I,点乘公式是|n|*|I|*Conθ,其中θ是两个向量的夹角,下面先看一下余弦图。
        余弦函数曲线
      • 第四步,我们的夹角范围是0-180度,因此我们裁掉后半部分


        0°-180°余弦图
      • 第五步,我们需要0度时颜色最显眼,到90度逐渐递减,余弦前半段得到的结果与我们的预期相符,但90度到180度区间内,漫反射不做任何渲染,即纯黑色代表阴暗面效果,因此得出下图。


        90°--180°渲染比例为0
      • 最终我们在两向量夹角为0°到90°范围内按比例渲染,就形成了最终的漫反射效果。


        最终比例
    • 使用Lambert进行漫反射渲染,在Shader中有两种写法,一种是逐顶点着色,另一种是逐像素着色。从代码中我们可以看出,逐顶点是在顶点着色器中进行漫反射计算,而逐像素则在片元着色器中进行漫反射计算,下面来看逐顶点代码。

      //兰伯特Lambert漫反射(逐顶点)
      Shader "AlbertShader/VertexDiffuse"
      {
          Properties
          {
              //漫反射颜色
              _DiffuseColor("Color",Color)=(1,1,1,1)
          }
          SubShader
          {
              Pass
              {
                  //正向渲染
                  Tags{ "LightMode"="ForwardBase" }
                  CGPROGRAM//------------------CG语言开始-------------------
                  //声明顶点函数
                  #pragma vertex vert
                  //声明片段函数
                  #pragma fragment frag
                  //引入光照函数库
                  #include "Lighting.cginc"
                  //定义外部属性-漫反射颜色
                  float4 _DiffuseColor;
                  //顶点输入结构体
                  struct appdata
                  {
                      //顶点坐标
                      float4 vertex : POSITION;
                      //顶点法线
                      float3 normal : NORMAL;
                  };
                  //顶点输出结构体
                  struct v2f
                  {
                      //像素坐标
                      float4 vertex : SV_POSITION;
                      //临时变量:颜色
                      fixed3 color : COLOR;
                  };
                  //顶点函数实现
                  v2f vert (appdata v)
                  {
                      //定义顶点输出结构体对象
                      v2f o;
                      //顶点坐标转换到屏幕像素坐标
                      o.vertex = UnityObjectToClipPos(v.vertex);
                      //获取环境光
                      float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                      //将顶点法线转换到世界空间下,并做归一化处理
                      float3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
                      //将光照方向转换到世界空间下,并做归一化处理
                      float3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
                      //带入公式运算
                      fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot(worldNormal,worldLight));
                      //结合漫反射和环境光
                      o.color = ambient + diffuse;
                      //返回结果
                      return o;
                  }
                  
      
                  fixed4 frag (v2f i) : SV_Target
                  {
                      //将结果返回
                      return fixed4(i.color,1.0);
                  }
                  ENDCG//------------------CG语言结束-------------------
              }
          }
      }
      
    • 下面来看逐像素代码

      //兰伯特Lambert漫反射(逐像素)
      Shader "AlbertShader/PixelDiffuse"
      {
          Properties
          {
              //漫反射颜色
              _DiffuseColor("Color",Color)=(1,1,1,1)
          }
          SubShader
          {
              Pass
              {
                  //正向渲染
                  Tags{ "LightMode"="ForwardBase" }
                  CGPROGRAM//------------------CG语言开始-------------------
                  #pragma vertex vert
                  #pragma fragment frag
                  
                  #include "Lighting.cginc"
      
                  float4 _DiffuseColor;
                  struct appdata
                  {
                      float4 vertex : POSITION;
                      //顶点法线
                      float3 normal : NORMAL;
                  };
      
                  struct v2f
                  {
                      float4 vertex : SV_POSITION;
                      //世界空间下的顶点法线
                      fixed3 worldNormal : TEXCOORD0;
                  };
      
                  v2f vert (appdata v)
                  {
                      v2f o;
                      o.vertex = UnityObjectToClipPos(v.vertex);
                      //通过矩阵运算,得到世界空间下的顶点法线
                      o.worldNormal = mul(v.normal,(float3x3)unity_WorldToObject);
                      return o;
                  }
                  
                  fixed4 frag (v2f i) : SV_Target
                  {
                      float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                      fixed3 worldNormal = normalize(i.worldNormal);
                      fixed3  worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                      //使用兰伯特光照模型公式
                      fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb *
                          saturate(dot(worldNormal,worldLightDir));
                      fixed3 color = diffuse + ambient;
                      return fixed4(color,1.0);
                  }
                  ENDCG//------------------CG语言结束-------------------
              }
          }
      }
      
  • Lambert漫反射效果不错,但背光的一面全是黑色,若镜头在后面照射,则渲染出来的就是一个黑色平面,视觉效果很差,因此,有时我们也常常使用半兰伯特(Half-Lambert)的光照模型。

  • Half-Lambert的公式与Lambert相似,只是人为改动了公式中的渲染比例,半兰伯特没有太多的物理证明,可以理解为测试出来的一种实现方式。公式即(C-light · M-diffuse)(α(n·I)+β),通常情况下αβ的值为0.5。具体代码如下:

    //半兰伯特HalfLambert漫反射(逐像素)
    Shader "Hidden/HalfPixelDiffuse"
    {
        Properties
        {
            _DiffuseColor("Color",Color)=(1,1,1,1)
        }
        SubShader
        {
            Pass
            {
                Tags{ "LightMode"="ForwardBase" }
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                
                #include "Lighting.cginc"
    
                float4 _DiffuseColor;
                struct appdata
                {
                    float4 vertex : POSITION;
                    float3 normal : NORMAL;
                };
    
                struct v2f
                {
                    float4 vertex : SV_POSITION;
                    //临时变量:世界法线
                    fixed3 worldNormal : TEXCOORD0;
                };
    
                v2f vert (appdata v)
                {
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    //世界法线
                    o.worldNormal = mul(v.normal,(float3x3)unity_WorldToObject);
                    return o;
                }
                
                fixed4 frag (v2f i) : SV_Target
                {
                    //环境光
                    float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                    //世界法线
                    fixed3 worldNormal = normalize(i.worldNormal);
                    //世界入射光
                    fixed3  worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                    //半兰伯特漫反射公式
                    fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb *
                        (0.5 * (dot(worldNormal,worldLightDir)) + 0.5);
                    fixed3 color = diffuse + ambient;
                    return fixed4(color,1.0);
                }
                ENDCG
            }
        }
    }
    
  • Half-Lambert显的更透亮一点,背光面也不是全黑的,三种Shader的渲染效果对比如下:

    逐顶点Lambert、逐像素Lambert、逐像素Half-Lambert

结束语

漫反射是光影效果的基础,通过对漫反射底层实现原理,以及对漫反射公式的图像剖析,大家有没有更清晰的认识呢?Shader就是如此奇妙,这就是所谓的数学之美、编程之美。想成为逻辑开发和图形学开发的双料大师吗?一起加油吧!😋😋😋

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344

推荐阅读更多精彩内容