本系列主要参考《Unity Shaders and Effects Cookbook》一书(感谢原书作者),同时会加上一点个人理解或拓展。

这里是本书所有的插图。这里是本书所需的代码和资源(当然你也可以从官网下载)。

========================================== 分割线 ==========================================

写在前面

照亮的球体(Lit Sphere,翻译过来很怪)类型的光照模型是一种非常有趣的基于图像的光照。实际上,我们可以使用一张2D贴图来完整地烘焙我们的光照。你可以得到Zbrush这个软件实现的相同效果。如果你对Zbrush的MatCaps(Material Captures)很熟悉,那么恭喜你,被照亮的球体是相同的实现原理。我们可以创建一个贴图,然后完全照搬各种烘焙类型,像漫反射,镜面反射,反射以及边缘光照等效果,然后再使用它来创建我们的Shader。这种方法唯一的缺点就是,因为我们将光照完全烘焙到了贴图上,因此无法改变光照,除非你根据你的环境替换另一张贴图,就像我们在反射一章中的简单的Cubemap反射一样。也就是说,这个Shader不会根据你环境中的光照做出任何变化,也不会再你移动你的视角时产生任何改变。下图展示了一个Lit Sphere贴图,它通常被称为一个Sphere Map:
这意味着,这种使用贴图实现的Shader适合创造很好的透视效果,或者甚至可以用于你的游戏中那些像电影画面似的、镜头位置被固定住而你又需要复杂的光照的场景中。用更易懂的话来说,当给定一张贴图后,使用它渲染得到的画面将和贴图看起来一样,就像把贴图投影到了模型上一样。

准备工作

我们需要学习怎样创建一个可以被用于我们的光照模型的贴图。我们可以使用Photoshop,但是我们也可以使用一个更小而且免费的工具,叫做Macrea(仅适用于Windows系统)。这个是一个非常棒的免费软件可以帮你创建这些Lit Sphere maps。作者建议观看Vimeo上的视频(需要翻墙)来帮助你熟悉MaCrea的界面和工作流程。
一旦你熟悉了创建这些Sphere maps的流程,我们可以开始剩下的部分。下面的截图显示了Macrea的界面以及一个用它创建的完整的Lit Sphere。
当然,你也可以在最上面提到的本书资源里找到本章(第五章)使用的贴图。
在Unity里,做如下准备:
  1. 创建一个新的场景以及一些对象,一个平面以及一个平行光;
  2. 创建一个新的Shader 和材质,然后将你的Shader赋给新的材质。

实现和解释

有了我们的场景资源,我们就可以开始编写我们的Lit Sphere shading model了。
  1. 和之前一样,我们需要一些properties传递给Surface Shader,以便我们可以让这个Shader的用户更改贴图以及一些变量。因此,我们首先添加下面的代码到Properties块:
    	Properties {
    _MainTint ("Diffuse Tint", Color) = (1,1,1,1)
    _MainTex ("Base (RGB)", 2D) = "white" {}
    _NormalMap ("Normal Map", 2D) = "bump" {}
    }
  2. 因为我们的Shader仅使用贴图来照亮我们的模型,因此我们不需要内置的Lambert光照函数,而需要声明我们自己的Unlit光照函数。我们还需要写一个顶点函数:
    		CGPROGRAM
    #pragma surface surf Unlit vertex:vert

    解释:鉴于有些童鞋忘了或者没有看过之前的内容,这里说明一下上面这句声明的意思。它表明我们将使用名为surf的Surface Shader function,以及名为Unlit的自定义光照函数,还有一个名为vert的顶点函数。本书第一次提到这个知识点是在这一节

  3. 和前面一样,我们需要在块中声明之前的properties,以便我们可以利用Inspector中的各个用户给定的数据。
    		float4 _MainTint;
    sampler2D _MainTex;
    sampler2D _NormalMap;
  4. 接下来,我们创建一个新的光照函数,它对应上面提到的名为Unlit的光照模型。这样做是因为我们不想使用场景中的灯光影响我们的Shader。我们仅仅想要得到对象的阴影,其他的交给贴图来照亮整个对象。因此,我们需要添加下面的光照函数(这里跟原书有一点不一样,我根据官方文档修改了返回类型):
    		inline half4 LightingUnlit (SurfaceOutput s, fixed3 lightDir, fixed atten)
    {
    half4 c = half4(1,1,1,1);
    c.rgb = s.Albedo;
    c.a = s.Alpha;
    return c;
    }

    解释:同样,关于这里的内容可以参见这一节

  5. 现在,我们需要将一些额外的属性加入到Input结构体中,以便我们可以将这些信息从vert()函数传递给surf()函数:
    		struct Input {
    float2 uv_MainTex;
    float2 uv_NormalMap;
    float3 tan1;
    float3 tan2;
    };

    解释:你可以把Input结构体当成是顶点函数和Surface shading函数之间的纽带,它们之间的沟通都是靠Input结构体进行传递的。

  6. 为了正确的在Sphere map中查找,我们需要将切线旋转矩阵(the rotated tangent vector)和当前模型的逆转置模型视图矩阵(the inverse transpose model view matrix)相乘。这将会给你合适的想来应用到Sphere map贴图中。如果你还是不明白也不要紧,在后面我们会进一步说明。
    		void vert (inout appdata_full v, out Input o)
    {
    UNITY_INITIALIZE_OUTPUT(Input,o); TANGENT_SPACE_ROTATION;
    o.tan1 = mul(rotation, UNITY_MATRIX_IT_MV[0].xyz);
    o.tan2 = mul(rotation, UNITY_MATRIX_IT_MV[1].xyz);
    }

    解释:vert()函数是这个光照模型横纵真正起作用的地方。我们使用模型的切线旋转矩阵和它的逆转置模型视图进行相乘,它的结果将在下面被用于查找Sphere map中对应的位置。那么,逆转置模型视图矩阵是从哪里产生的呢?其实这是Unity提供的另一个内置参数,因此我们不需要再自己计算它了。

    实际上Unity提供了绝大多数在标准的CGFX shaders中常见的转换矩阵。这是我们使用Surface Shader的一个好处之一,我们不需要自己计算它们了,而仅仅需要调用内置的参数。

    原书并没有给出这一步的解释,我这里补充一下。这个shader的精髓就在于它是像投影一样,完全平铺在Sphere上的。我们可以想象它的本质,就是在Eye Space中,根据顶点法线在X和Y轴上的投影作为UV坐标,对纹理进行采样。而作者在这里选择把计算统一到Tangent Space中。因此,这里的tan1和tan2就分别对应了在Tangent Space中,Eye Space的X轴和Y轴方向。这种写法是推导了一步后的结果,原始的计算代码如下:

    o.tan1 = mul(rotation, mul(float3(1.0f, 0.0, 0.0f), (float3x3)UNITY_MATRIX_IT_MV));
    o.tan2 = mul(rotation, mul(float3(0.0f, 1.0, 0.0f), (float3x3)UNITY_MATRIX_IT_MV));

    rotation负责把方向从Object Space转换到Tangent Space。因此我们首先需要把Eye Space的X和Y轴转换到Object Space中, mul(float3(1.0f, 0.0, 0.0f), (float3x3)UNITY_MATRIX_IT_MV)负责这个转换步骤。这是因为,如果我们想要把X轴从Eye Space转换到Tangent Space,就需要从Eye Space到Tangent Space的变换矩阵,即我们希望有UNITY_MATRIX_MV的逆矩阵。然而,Unity没有直接提供这个矩阵么认识提供了UNITY_MATRIX_IT_MV,即UNITY_MATRIX_MV的逆转置矩阵。但我们仍然可以通过mul中交换变换矩阵和向量的位置,来得到相同的效果。然后在通过外层的mul把方向从Object Space转换到Tangent Space即可。还是不明白的可以看一下这篇文章

  7. 最后,我们需要实现我们的surf()函数,进行一些计算来产生对应的Sphere map中的查找值,并把它们赋给我们的结构体。同样,这个部分将在后面进行说明:
    		void surf (Input IN, inout SurfaceOutput o)
    {
    float3 normals = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap));
    o.Normal = normals; float2 litSphereUV;
    litSphereUV.x = dot(IN.tan1, o.Normal);
    litSphereUV.y = dot(IN.tan2, o.Normal); half4 c = tex2D (_MainTex, litSphereUV*0.5+0.5);
    o.Albedo = c.rgb * _MainTint;
    o.Alpha = c.a;
    }
下图显示了上述Shader的渲染结果。

完整代码

Shader "Custom/LitSphere" {
Properties {
_MainTint ("Diffuse Tint", Color) = (1,1,1,1)
_MainTex ("Base (RGB)", 2D) = "white" {}
_NormalMap ("Normal Map", 2D) = "bump" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200 CGPROGRAM
#pragma surface surf Unlit vertex:vert float4 _MainTint;
sampler2D _MainTex;
sampler2D _NormalMap; inline half4 LightingUnlit (SurfaceOutput s, fixed3 lightDir, fixed atten)
{
half4 c = half4(1,1,1,1);
c.rgb = s.Albedo;
c.a = s.Alpha;
return c;
} struct Input {
float2 uv_MainTex;
float2 uv_NormalMap;
float3 tan1;
float3 tan2;
}; void vert (inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input,o); TANGENT_SPACE_ROTATION;
o.tan1 = mul(rotation, UNITY_MATRIX_IT_MV[0].xyz);
o.tan2 = mul(rotation, UNITY_MATRIX_IT_MV[1].xyz);
} void surf (Input IN, inout SurfaceOutput o)
{
float3 normals = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap));
o.Normal = normals; float2 litSphereUV;
litSphereUV.x = dot(IN.tan1, o.Normal);
litSphereUV.y = dot(IN.tan2, o.Normal); half4 c = tex2D (_MainTex, litSphereUV*0.5+0.5);
o.Albedo = c.rgb * _MainTint;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}

写在最后

上述是一种简单但从视觉上看起来不错的方法来得到复杂的光照效果。这种技术的缺点就是它不能根据真实的灯光实时更新。它的光源看起来就像是被固定在摄像机朝向的某个位置上,就像贴图在视图上被投影到对象上一样。
下面给出一些关于本小节内容的连接。

【Unity Shaders】Lighting Models —— 光照模型之Lit Sphere的更多相关文章

  1. 【Unity Shaders】Lighting Models —— 灯型号Lit Sphere

    考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同一时候会加上一点个人理解或拓展. 这里是本书全部的插图.这里是本书所需的代码和资源(当然你 ...

  2. 【Unity Shaders】Lighting Models 介绍

    本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源 ...

  3. Unity Shader——Writing Surface Shaders(2)——Custom Lighting models in Surface Shaders

    Surface Shader中的自定义光照模型 当你在编写 Surface Shaders 时,是在描述一个表面的属性(反射颜色.法线……),而且光的交互过程是由一个光照模型来计算的.内建的光照模型有 ...

  4. 【Unity Shaders】Lighting Models —— 衣服着色器

    本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源 ...

  5. 【Unity Shaders】Diffuse Shading——创建一个自定义的diffuse lighting model(漫反射光照模型)

    本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源 ...

  6. 【Unity Shaders】使用CgInclude让你的Shader模块化——创建CgInclude文件存储光照模型

    本系列主要參考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同一时候会加上一点个人理解或拓展. 这里是本书全部的插图. 这里是本书所需的代码 ...

  7. 【Unity Shaders】学习笔记——SurfaceShader(十一)光照模型

    [Unity Shaders]学习笔记——SurfaceShader(十一)光照模型 转载请注明出处:http://www.cnblogs.com/-867259206/p/5664792.html ...

  8. 【Unity Shaders】学习笔记——SurfaceShader(三)BasicDiffuse和HalfLambert

    [Unity Shaders]学习笔记——SurfaceShader(三)BasicDiffuse和HalfLambert 转载请注明出处:http://www.cnblogs.com/-867259 ...

  9. 【Unity Shaders】法线纹理(Normal Mapping)的实现细节

    写在前面 写这篇的目的是为了总结我长期以来的混乱.虽然题目是"法线纹理的实现细节",但其实我想讲的是如何在shader中编程正确使用法线进行光照计算.这里面最让人头大的就是各种矩阵 ...

随机推荐

  1. C语言程序设计第二次作业—————顺序结构改

    1.输出带框文字:在屏幕上输出以下3行信息. ************* Welcome ************* 源程序 #include <stido.h> int mian() { ...

  2. python基础(1)

    1.python中三元表达式 类比于C.C++.Java中都有的三目运算符,python中使用下面语句实现三元表达式 true_part if condition else false_part. c ...

  3. log4j的终极封装

    通用型(再也不用每个类new一个logger了) public class Log { private static Map<String,Logger> loggerMap = new ...

  4. 简介JSP与FreeMarker及Volicity区别

    FreeMarker FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页.电子邮件.配置文件.源代码等)的通用工具. 它不是面向最终用户的,而是一个 ...

  5. 修改原生单选框样式(vue单选组件)

    一.效果如图 二.实现 修改单选样式 //html <div class="radio-wrap"> <input type="radio" ...

  6. Vue-起步篇:Vue与React、 Angular的区别

    毋庸置疑,Vue.React. Angular这三个是现在比较火的前端框架.这几个框架都各有所长,选择学习哪种就得看个人喜好或者实际项目了.相比之下, Vue 是轻量级且容易学习掌握的. 1.Vue和 ...

  7. 吴恩达深度学习第1课第4周-任意层人工神经网络(Artificial Neural Network,即ANN)(向量化)手写推导过程(我觉得已经很详细了)

    学习了吴恩达老师深度学习工程师第一门课,受益匪浅,尤其是吴老师所用的符号系统,准确且易区分. 遵循吴老师的符号系统,我对任意层神经网络模型进行了详细的推导,形成笔记. 有人说推导任意层MLP很容易,我 ...

  8. Node.js系列文章:利用console输出日志文件

    通常我们在写Node.js程序时,都习惯使用console.log打印日志信息,但这也仅限于控制台输出,有时候我们需要将信息输出到日志文件中,实际上利用console也可以达到这个目的的,今天就来简单 ...

  9. 网页底部广告悬浮弹窗(css)

    有的单页面需要添加广告等悬浮div. 部分代码: <div class="flex"> 内容.... </div> 主要css代码: .flex{posit ...

  10. Java中的内存分配

    Java程序在运行时,需要在内存中分配空间,为了提高效率,就对空间进行了不同区域的划分,因为每一片区域否有特定的处理数据方式和内存管理方式. 1.栈存储局部变量 2.堆存储new出来的东西 3.方法区 ...