Shader入门指南

2023-11-14

Unity3D Shader入门指南(一)

分类: Unity3d2014-04-28 17:40 16人阅读 评论(0) 收藏 举报

unity3dShader3d渲染

目录(?)[+]

本文转载自:http://onevcat.com/2013/07/shader-tutorial-1/

 

动机

自己使用Unity3D也有一段时间了,但是很多时候是流于表面,更多地是把这个引擎简单地用作脚本控制,而对更深入一些的层次几乎没有了解。虽然说Unity引擎设计的初衷就是创建简单的不需要开发者操心的谁都能用的3D引擎,但是只是肤浅的使用,可能是无法达到随心所欲的境地的,因此,这种状况必须改变!从哪里开始呢,貌似有句话叫做会写Shader的都是高手,于是,想大概看看从Shader开始能不能使自己到达的层次能再深入一些吧,再于是,有了这个系列(希望我能坚持写完它,虽然应该会拖个半年左右)。

Unity3D的所有渲染工作都离不开着色器(Shader),如果你和我一样最近开始对Shader编程比较感兴趣的话,可能你和我有着同样的困惑:如何开始?Unity3D提供了一些Shader的手册和文档(比如这里这里这里),但是一来内容比较分散,二来学习阶梯稍微陡峭了些。这对于像我这样之前完全没有接触过有关内容的新人来说是相当不友好的。国内外虽然也有一些Shader的介绍和心得,但是也同样存在内容分散的问题,很多教程前一章就只介绍了基本概念,接下来马上就搬出一个超复杂的例子,对于很多基本的用法并没有解释。也许对于Shader熟练使用的开发者来说是没有问题,但是我相信像我这样的入门者也并不在少数。在多方寻觅无果后,我觉得有必要写一份教程,来以一个入门者的角度介绍一些Shader开发的基本步骤。其实与其说是教程,倒不如说是一份自我总结,希望能够帮到有需要的人。

所以,本“教程”的对象是

· 总的来说是新接触Shader开发的人:也许你知道什么是Shader,也会使用别人的Shader,但是仅限于知道一些基本的内建Shader名字,从来没有打开它们查看其源码。

· 想要更多了解Shader和有需求要进行Shader开发的开发者,但是之前并没有Shader开发的经验。

当然,因为我本身在Shader开发方面也是一个不折不扣的大菜鸟,本文很多内容也只是在自己的理解加上一些可能不太靠谱的求证和总结。本文中的示例应该会有更好的方式来实现,因此您是高手并且恰巧路过的话,如果有好的方式来实现某些内容,恳请您不吝留下评论,我会对本文进行不断更新和维护。

一些基本概念

Shader和Material

如果是进行3D游戏开发的话,想必您对着两个词不会陌生。Shader(着色器)实际上就是一小段程序,它负责将输入的Mesh(网格)以指定的方式和输入的贴图或者颜色等组合作用,然后输出。绘图单元可以依据这个输出来将图像绘制到屏幕上。输入的贴图或者颜色等,加上对应的Shader,以及对Shader的特定的参数设置,将这些内容(Shader及输入参数)打包存储在一起,得到的就是一个Material(材质)。之后,我们便可以将材质赋予合适的renderer(渲染器)来进行渲染(输出)了。

所以说Shader并没有什么特别神奇的,它只是一段规定好输入(颜色,贴图等)和输出(渲染器能够读懂的点和颜色的对应关系)的程序。而Shader开发者要做的就是根据输入,进行计算变换,产生输出而已。

Shader大体上可以分为两类,简单来说

· 表面着色器(Surface Shader) - 为你做了大部分的工作,只需要简单的技巧即可实现很多不错的效果。类比卡片机,上手以后不太需要很多努力就能拍出不错的效果。

· 片段着色器(Fragment Shader) - 可以做的事情更多,但是也比较难写。使用片段着色器的主要目的是可以在比较低的层级上进行更复杂(或者针对目标设备更高效)的开发。

因为是入门文章,所以之后的介绍将主要集中在表面着色器上。

Shader程序的基本结构

因为着色器代码可以说专用性非常强,因此人为地规定了它的基本结构。一个普通的着色器的结构应该是这样的: 一段Shader程序的结构

首先是一些属性定义,用来指定这段代码将有哪些输入。接下来是一个或者多个的子着色器,在实际运行中,哪一个子着色器被使用是由运行的平台所决定的。子着色器是代码的主体,每一个子着色器中包含一个或者多个的Pass。在计算着色时,平台先选择最优先可以使用的着色器,然后依次运行其中的Pass,然后得到输出的结果。最后指定一个回滚,用来处理所有Subshader都不能运行的情况(比如目标设备实在太老,所有Subshader中都有其不支持的特性)。

需要提前说明的是,在实际进行表面着色器的开发时,我们将直接在Subshader这个层次上写代码,系统将把我们的代码编译成若干个合适的Pass。废话到此为止,下面让我们真正实际进入Shader的世界吧。

Hello Shader

百行文档不如一个实例,下面给出一段简单的Shader代码,然后根据代码来验证下上面说到的结构和阐述一些基本的Shader语法。因为本文是针对Unity3D来写Shader的,所以也使用Unity3D来演示吧。首先,新建一个Shader,可以在Project面板中找到,Create,选择Shader,然后将其命名为Diffuse Texture

在Unity3D中新建一个Shader

随便用个文本编辑器打开刚才新建的Shader:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

Shader "Custom/Diffuse Texture" {

  Properties {

      _MainTex ("Base (RGB)", 2D) = "white" {}

  }

  SubShader {

      Tags { "RenderType"="Opaque" }

      LOD 200

      

      CGPROGRAM

      #pragma surface surf Lambert

 

      sampler2D _MainTex;

 

      struct Input {

          float2 uv_MainTex;

      };

 

      void surf (Input IN, inout SurfaceOutput o) {

          half4 c = tex2D (_MainTex, IN.uv_MainTex);

          o.Albedo = c.rgb;

          o.Alpha = c.a;

      }

      ENDCG

  }

  FallBack "Diffuse"

}

如果您之前没怎么看过Shader代码的话,估计细节上会看不太懂。但是有了上面基本结构的介绍,您应该可以识别出这个Shader的构成,比如一个Properties部分,一个SubShader,以及一个FallBack。另外,第一行只是这个Shader的声明并为其指定了一个名字,比如我们的实例Shader,你可以在材质面板选择Shader时在对应的位置找到这个Shader。

在Unity3D中找到刚才新建的Shader

接下来我们讲逐句讲解这个Shader,以期明了每一个语句的意义。

属性

Properties{}中定义着色器属性,在这里定义的属性将被作为输入提供给所有的子着色器。每一条属性的定义的语法是这样的:

_Name("Display Name", type) = defaultValue[{options}]

· _Name - 属性的名字,简单说就是变量名,在之后整个Shader代码中将使用这个名字来获取该属性的内容

· Display Name - 这个字符串将显示在Unity的材质编辑器中作为Shader的使用者可读的内容

· type - 这个属性的类型,可能的type所表示的内容有以下几种:

Color - 一种颜色,由RGBA(红绿蓝和透明度)四个量来定义;

2D - 一张2的阶数大小(256,512之类)的贴图。这张贴图将在采样后被转为对应基于模型UV的每个像素的颜色,最终被显示出来;

Rect - 一个非2阶数大小的贴图;

Cube - 即Cube map texture(立方体纹理),简单说就是6张有联系的2D贴图的组合,主要用来做反射效果(比如天空盒和动态反射),也会被转换为对应点的采样;

Range(min, max) - 一个介于最小值和最大值之间的浮点数,一般用来当作调整Shader某些特性的参数(比如透明度渲染的截止值可以是从0至1的值等);

Float - 任意一个浮点数;

Vector - 一个四维数;

· defaultValue 定义了这个属性的默认值,通过输入一个符合格式的默认值来指定对应属性的初始值(某些效果可能需要某些特定的参数值来达到需要的效果,虽然这些值可以在之后在进行调整,但是如果默认就指定为想要的值的话就省去了一个个调整的时间,方便很多)。

Color - 以0~1定义的rgba颜色,比如(1,1,1,1);

2D/Rect/Cube - 对于贴图来说,默认值可以为一个代表默认tint颜色的字符串,可以是空字符串或者”white”,”black”,”gray”,”bump”中的一个

Float,Range - 某个指定的浮点数

Vector - 一个4维数,写为 (x,y,z,w)

· 另外还有一个{option},它只对2D,Rect或者Cube贴图有关,在写输入时我们最少要在贴图之后写一对什么都不含的空白的{},当我们需要打开特定选项时可以把其写在这对花括号内。如果需要同时打开多个选项,可以使用空白分隔。可能的选择有ObjectLinear, EyeLinear, SphereMap, CubeReflect, CubeNormal中的一个,这些都是OpenGL中TexGen的模式,具体的留到后面有机会再说。

所以,一组属性的申明看起来也许会是这个样子的

1

2

3

4

//Define a color with a default value of semi-transparent blue

_MainColor ("Main Color", Color) = (0,0,1,0.5)

//Define a texture with a default of white

_Texture ("Texture", 2D) = "white" {}

现在看懂上面那段Shader(以及其他所有Shader)的Properties部分应该不会有任何问题了。接下来就是SubShader部分了。

Tags

表面着色器可以被若干的标签(tags)所修饰,而硬件将通过判定这些标签来决定什么时候调用该着色器。比如我们的例子中SubShader的第一句

Tags { "RenderType"="Opaque" }

告诉了系统应该在渲染非透明物体时调用我们。Unity定义了一些列这样的渲染过程,与RenderType是Opaque相对应的显而易见的是"RenderType" = "Transparent",表示渲染含有透明效果的物体时调用。在这里Tags其实暗示了你的Shader输出的是什么,如果输出中都是非透明物体,那写在Opaque里;如果想渲染透明或者半透明的像素,那应该写在Transparent中。

另外比较有用的标签还有"IgnoreProjector"="True"(不被Projectors影响),"ForceNoShadowCasting"="True"(从不产生阴影)以及"Queue"="xxx"(指定渲染顺序队列)。这里想要着重说一下的是Queue这个标签,如果你使用Unity做过一些透明和不透明物体的混合的话,很可能已经遇到过不透明物体无法呈现在透明物体之后的情况。这种情况很可能是由于Shader的渲染顺序不正确导致的。Queue指定了物体的渲染顺序,预定义的Queue有:

· Background - 最早被调用的渲染,用来渲染天空盒或者背景

· Geometry - 这是默认值,用来渲染非透明物体(普通情况下,场景中的绝大多数物体应该是非透明的)

· AlphaTest - 用来渲染经过Alpha Test的像素,单独为AlphaTest设定一个Queue是出于对效率的考虑

· Transparent - 以从后往前的顺序渲染透明物体

· Overlay - 用来渲染叠加的效果,是渲染的最后阶段(比如镜头光晕等特效)

这些预定义的值本质上是一组定义整数,Background = 1000, Geometry = 2000, AlphaTest = 2450, Transparent = 3000,最后Overlay = 4000。在我们实际设置Queue值时,不仅能使用上面的几个预定义值,我们也可以指定自己的Queue值,写成类似这样:"Queue"="Transparent+100",表示一个在Transparent之后100的Queue上进行调用。通过调整Queue值,我们可以确保某些物体一定在另一些物体之前或者之后渲染,这个技巧有时候很有用处。

LOD

LOD很简单,它是Level of Detail的缩写,在这里例子里我们指定了其为200(其实这是Unity的内建Diffuse着色器的设定值)。这个数值决定了我们能用什么样的Shader。在Unity的Quality Settings中我们可以设定允许的最大LOD,当设定的LOD小于SubShader所指定的LOD时,这个SubShader将不可用。Unity内建Shader定义了一组LOD的数值,我们在实现自己的Shader的时候可以将其作为参考来设定自己的LOD数值,这样在之后调整根据设备图形性能来调整画质时可以进行比较精确的控制。

· VertexLit及其系列 = 100

· Decal, Reflective VertexLit = 150

· Diffuse = 200

· Diffuse Detail, Reflective Bumped Unlit, Reflective Bumped VertexLit = 250

· Bumped, Specular = 300

· Bumped Specular = 400

· Parallax = 500

· Parallax Specular = 600

Shader本体

前面杂项说完了,终于可以开始看看最主要的部分了,也就是将输入转变为输出的代码部分。为了方便看,请容许我把上面的SubShader的主题部分抄写一遍

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

CGPROGRAM

#pragma surface surf Lambert

 

sampler2D _MainTex;

 

struct Input {

  float2 uv_MainTex;

};

 

void surf (Input IN, inout SurfaceOutput o) {

  half4 c = tex2D (_MainTex, IN.uv_MainTex);

  o.Albedo = c.rgb;

  o.Alpha = c.a;

}

ENDCG

还是逐行来看,首先是CGPROGRAM。这是一个开始标记,表明从这里开始是一段CG程序(我们在写Unity的Shader时用的是Cg/HLSL语言)。最后一行的ENDCG与CGPROGRAM是对应的,表明CG程序到此结束。

接下来是是一个编译指令:#pragma surface surf Lambert,它声明了我们要写一个表面Shader,并指定了光照模型。它的写法是这样的

#pragma surface surfaceFunction lightModel [optionalparams]

· surface - 声明的是一个表面着色器

· surfaceFunction - 着色器代码的方法的名字

· lightModel - 使用的光照模型。

所以在我们的例子中,我们声明了一个表面着色器,实际的代码在surf函数中(在下面能找到该函数),使用Lambert(也就是普通的diffuse)作为光照模型。

接下来一句sampler2D _MainTex;,sampler2D是个啥?其实在CG中,sampler2D就是和texture所绑定的一个数据容器接口。等等..这个说法还是太复杂了,简单理解的话,所谓加载以后的texture(贴图)说白了不过是一块内存存储的,使用了RGB(也许还有A)通道,且每个通道8bits的数据。而具体地想知道像素与坐标的对应关系,以及获取这些数据,我们总不能一次一次去自己计算内存地址或者偏移,因此可以通过sampler2D来对贴图进行操作。更简单地理解,sampler2D就是GLSL中的2D贴图的类型,相应的,还有sampler1D,sampler3D,samplerCube等等格式。

解释通了sampler2D是什么之后,还需要解释下为什么在这里需要一句对_MainTex的声明,之前我们不是已经在Properties里声明过它是贴图了么。答案是我们用来实例的这个shader其实是由两个相对独立的块组成的,外层的属性声明,回滚等等是Unity可以直接使用和编译的ShaderLab;而现在我们是在CGPROGRAM...ENDCG这样一个代码块中,这是一段CG程序。对于这段CG程序,要想访问在Properties中所定义的变量的话,必须使用和之前变量相同的名字进行声明。于是其实sampler2D _MainTex;做的事情就是再次声明并链接了_MainTex,使得接下来的CG程序能够使用这个变量。

终于可以继续了。接下来是一个struct结构体。相信大家对于结构体已经很熟悉了,我们先跳过之,直接看下面的的surf函数。上面的#pragma段已经指出了我们的着色器代码的方法的名字叫做surf,那没跑儿了,就是这段代码是我们的着色器的工作核心。我们已经说过不止一次,着色器就是给定了输入,然后给出输出进行着色的代码。CG规定了声明为表面着色器的方法(就是我们这里的surf)的参数类型和名字,因此我们没有权利决定surf的输入输出参数的类型,只能按照规定写。这个规定就是第一个参数是一个Input结构,第二个参数是一个inout的SurfaceOutput结构。

它们分别是什么呢?Input其实是需要我们去定义的结构,这给我们提供了一个机会,可以把所需要参与计算的数据都放到这个Input结构中,传入surf函数使用;SurfaceOutput是已经定义好了里面类型输出结构,但是一开始的时候内容暂时是空白的,我们需要向里面填写输出,这样就可以完成着色了。先仔细看看INPUT吧,现在可以跳回来看上面定义的INPUT结构体了:

1

2

3

struct Input {

  float2 uv_MainTex;

};

作为输入的结构体必须命名为Input,这个结构体中定义了一个float2的变量…你没看错我也没打错,就是float2,表示浮点数的float后面紧跟一个数字2,这又是什么意思呢?其实没什么魔法,float和vec都可以在之后加入一个2到4的数字,来表示被打包在一起的2到4个同类型数。比如下面的这些定义:

1

2

3

4

5

6

//Define a 2d vector variable

vec2 coordinate;

//Define a color variable

float4 color;

//Multiply out a color

float3 multipliedColor = color.rgb * coordinate.x;

在访问这些值时,我们即可以只使用名称来获得整组值,也可以使用下标的方式(比如.xyzw,.rgba或它们的部分比如.x等等)来获得某个值。在这个例子里,我们声明了一个叫做uv_MainTex的包含两个浮点数的变量。

如果你对3D开发稍有耳闻的话,一定不会对uv这两个字母感到陌生。UV mapping的作用是将一个2D贴图上的点按照一定规则映射到3D模型上,是3D渲染中最常见的一种顶点处理手段。在CG程序中,我们有这样的约定,在一个贴图变量(在我们例子中是_MainTex)之前加上uv两个字母,就代表提取它的uv值(其实就是两个代表贴图上点的二维坐标 )。我们之后就可以在surf程序中直接通过访问uv_MainTex来取得这张贴图当前需要计算的点的坐标值了。

如果你坚持看到这里了,那要恭喜你,因为离最后成功读完一个Shader只有一步之遥。我们回到surf函数,它的两有参数,第一个是Input,我们已经明白了:在计算输出时Shader会多次调用surf函数,每次给入一个贴图上的点坐标,来计算输出。第二个参数是一个可写的SurfaceOutput,SurfaceOutput是预定义的输出结构,我们的surf函数的目标就是根据输入把这个输出结构填上。SurfaceOutput结构体的定义如下

1

2

3

4

5

6

7

8

struct SurfaceOutput {

    half3 Albedo;     //像素的颜色

    half3 Normal;     //像素的法向值

    half3 Emission;   //像素的发散颜色

    half Specular;    //像素的镜面高光

    half Gloss;       //像素的发光强度

    half Alpha;       //像素的透明度

};

这里的half和我们常见float与double类似,都表示浮点数,只不过精度不一样。也许你很熟悉单精度浮点数(float或者single)和双精度浮点数(double),这里的half指的是半精度浮点数,精度最低,运算性能相对比高精度浮点数高一些,因此被大量使用。

在例子中,我们做的事情非常简单:

1

2

3

half4 c = tex2D (_MainTex, IN.uv_MainTex);

o.Albedo = c.rgb;

o.Alpha = c.a;

这里用到了一个tex2d函数,这是CG程序中用来在一张贴图中对一个点进行采样的方法,返回一个float4。这里对_MainTex在输入点上进行了采样,并将其颜色的rbg值赋予了输出的像素颜色,将a值赋予透明度。于是,着色器就明白了应当怎样工作:即找到贴图上对应的uv点,直接使用颜色信息来进行着色,over。

接下来…

我想现在你已经能读懂一些最简单的Shader了,接下来我推荐的是参考Unity的Surface Shader Examples多接触一些各种各样的基本Shader。在这篇教程的基础上,配合一些google的工作,完全看懂这个shader示例页面应该不成问题。如果能做到无压力看懂,那说明你已经有良好的基础可以前进到Shader的更深的层次了(也许等不到我的下一篇教程就可以自己开始动手写些效果了);如果暂时还是有困难,那也没有关系,Shader学习绝对是一个渐进的过程,因为有很多约定和常用技巧,多积累和实践自然会进步并掌握。

在接下来的教程里,打算通过介绍一些实际例子以及从基础开始实际逐步动手实现一个复杂一点的例子,让我们能看到shader在真正使用中的威力。我希望能尽快写完这个系列,但是无奈时间确实有限,所以我也不知道什么时候能出炉…写好的时候我会更改这段内容并指向新的文章。您要是担心错过的话,也可以使用邮件订阅或者订阅本站的rss(虽然Google Reader已经关了- -)。

Unity3D Shader入门指南(二)

关于本系列

这是Unity3D Shader入门指南系列的第二篇,本系列面向的对象是新接触Shader开发的Unity3D使用者,因为我本身自己也是Shader初学者,因此可能会存在错误或者疏漏,如果您在Shader开发上有所心得,很欢迎并恳请您指出文中纰漏,我会尽快改正。在之前的开篇中介绍了一些Shader的基本知识,包括ShaderLab的基本结构和语法,以及简单逐句地讲解了一个基本的shader。在具有这些基础知识后,阅读简单的shader应该不会有太大问题,在继续教程之前简单阅读一下Unity的Surface Shader Example,以检验您是否掌握了上一节的内容。如果您对阅读大部分示例Shader并没有太大问题,可以正确地指出Shader的结构,声明和使用的话,就说明您已经准备好继续阅读本节的内容了。

法线贴图(Normal Mapping)

法线贴图是凸凹贴图(Bump mapping)的一种常见应用,简单说就是在不增加模型多边形数量的前提下,通过渲染暗部和亮部的不同颜色深度,来为原来的贴图和模型增加视觉细节和真实效果。简单原理是在普通的贴图的基础上,再另外提供一张对应原来贴图的,可以表示渲染浓淡的贴图。通过将这张附加的表示表面凸凹的贴图的因素于实际的原贴图进行运算后,可以得到新的细节更加丰富富有立体感的渲染效果。在本节中,我们将首先实现一个法线贴图的Shader,然后对Unity Shader的光照模型进行一些讨论,并实现一个自定义的光照模型。最后再通过更改shader模拟一个石头上的积雪效果,并对模型顶点进行一些修改使积雪效果看起来比较真实。在本节结束的时候,我们就会有一个比较强大的可以满足一些真实开发工作时可用的shader了,而且更重要的是,我们将会掌握它是如何被创造出来的。

关于法线贴图的效果图,可以对比看看下面。模型面数为500,左侧只使用了简单的Diffuse着色,右侧使用了法线贴图。比较两张图片不难发现,使用了法线贴图的石头在暗部和亮部都有着更好的表现。整体来说,凸凹感比Diffuse的结果增强许多,石头看起来更真实也更具有质感。

image

本节中需要用到的上面的素材可以在这里下载,其中包括上面的石块的模型,一张贴图以及对应的法线贴图。将下载的package导入到工程中,并新建一个material,使用简单的Diffuse的Shader(比如上一节我们实现的),再加上一个合适的平行光光源,就可以得到我们左图的效果。另外,本节以及以后都会涉及到一些Unity内建的Shader的内容,比如一些标准常用函数和常量定义等,相关内容可以在Unity的内建Shader中找到,内建Shader可以在Unity下载页面的版本右侧找到。

接下来我们实现法线贴图。在实现之前,我们先简单地稍微多了解一些法线贴图的基本知识。大多数法线图一般都和下面的图类似,是一张以蓝紫色为主的图。这张法线图其实是一张RGB贴图,其中红,绿,蓝三个通道分别表示由高度图转换而来的该点的法线指向:Nx、Ny、Nz。在其中绝大部分点的法线都指向z方向,因此图更偏向于蓝色。在shader进行处理时,我们将光照与该点的法线值进行点积后即可得到在该光线下应有的明暗特性,再将其应用到原图上,即可反应在一定光照环境下物体的凹凸关系了。关于法向贴图的更多信息,可以参考wiki上的相关条目

一张典型的法线图

回到正题,我们现在考虑的主要是Shader入门,而不是图像学的原理。再上一节我们写的Shader的基础上稍微做一些修改,就可以得到适应并完成法线贴图渲染的新Shader。新加入的部分进行了编号并在之后进行说明。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

Shader "Custom/Normal Mapping" {

  Properties {

      _MainTex ("Base (RGB)", 2D) = "white" {}

      

      //1

        _Bump ("Bump", 2D) = "bump" {}

  }

  SubShader {

      Tags { "RenderType"="Opaque" }

      LOD 200

      

      CGPROGRAM

      #pragma surface surf Lambert

      

      sampler2D _MainTex;

      

      //2

        sampler2D _Bump;

 

      struct Input {

          float2 uv_MainTex;

          

          //3

            float2 uv_Bump;

      };

 

      void surf (Input IN, inout SurfaceOutput o) {

          half4 c = tex2D (_MainTex, IN.uv_MainTex);

          

          //4

            o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump);

 

          o.Albedo = c.rgb;

          o.Alpha = c.a;

      }

      ENDCG

  }

  FallBack "Diffuse"

}

1. 声明并加入一个显示名称为Bump的贴图,用于放置法线图

2. 为了能够在CG程序中使用这张贴图,必须加入一个sample,希望你还记得~

3. 获取Bump的uv信息作为输入

4. 从法线图中提取法线信息,并将其赋予相应点的输出的Normal属性。UnpackNormal是定义在UnityCG.cginc文件中的方法,这个文件中包含了一系列常用的CG变量以及方法。UnpackNormal接受一个fixed4的输入,并将其转换为所对应的法线值(fixed3)。在解包得到这个值之后,将其赋给输出的Normal,就可以参与到光线运算中完成接下来的渲染工作了。

现在保存并且编译这个Shader,创建新的material并使用这个shader,将石头的材质贴图和法线图分别拖放到Base和Bump里,再将其应用到石头模型上,应该就可以看到右侧图的效果了。

光照模型

在我们之前的看到的Shader中(其实也就上一节的基本diffuse和这里的normal mapping),都只使用了Lambert的光照模型(#pragma surface surf Lambert),这是一个很经典的漫反射模型,光强与入射光的方向和反射点处表面法向夹角的余弦成正比。关于Lambert和漫反射的一些详细的计算和推论,可以参看wiki(Lambert漫反射)或者其他地方的介绍。一句话的简单解释就是一个点的反射光强是和该点的法线向量和入射光向量和强度和夹角有关系的,其结果就是这两个向量的点积。既然已经知道了光照计算的原理,我们先来看看如何实现一个自己的光照模型吧。

在刚才的Shader上进行如下修改。

· 首先将原来的#pragma行改为这样

1

#pragma surface surf CustomDiffuse

· 然后在SubShader块中添加如下代码

1

2

3

4

5

6

7

inline float4 LightingCustomDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten) {

    float difLight = max(0, dot (s.Normal, lightDir));

    float4 col;

    col.rgb = s.Albedo * _LightColor0.rgb * (difLight * atten * 2);

    col.a = s.Alpha;

    return col;

}

· 最后保存,回到Unity。Shader将编译,如果一切正常,你将不会看到新的shader和之前的在材质表现上有任何不同。但是事实上我们现在的shader已经与Unity内建的diffuse光照模型撇清了关系,而在使用我们自己设定的光照模型了。

喵的,这些代码都干了些什么!相信你一定会有这样的疑惑…没问题,没有疑惑的话那就不叫初学了,还是一行行讲来。首先正像我们上一篇所说,#pragma语句在这里声明了接下来的Shader的类型,计算调用的方法名,以及指定光照模型。在之前我们一直指定Lambert为光照模型,而现在我们将其换为了CustomDiffuse。

接下来添加的代码是计算光照的实现。shader中对于方法的名称有着比较严格的约定,想要创建一个光照模型,首先要做的是按照规则声明一个光照计算的函数名字,即Lighting<Your Chosen Name>。对于我们的光照模型CustomDiffuse,其计算函数的名称自然就是LightingCustomDiffuse了。光照模型的计算是在surf方法的表面颜色之后,根据输入的光照条件来对原来的颜色在这种光照下的表现进行计算,最后输出新的颜色值给渲染单元完成在屏幕的绘制。

也许你已经猜到了,我们之前用的Lambert光照模型是不是也有一个名字叫LightingLambert的光照计算函数呢?Bingo。在Unity的内建Shader中,有一个Lighting.cginc文件,里面就包含了LightingLambert的实现。也许你也注意到了,我们所实现的LightingCustomDiffuse的内容现在和Unity内建中的LightingLambert是完全一样的,这也就是使用新的shader的原来视觉上没有区别的原因,因为实现确实是完全一样的。

首先来看输入量,SurfaceOutput s这个就是经过表面计算函数surf处理后的输出,我们讲对其上的点根据光线进行处理,fixed3 lightDir是光线的方向,fixed atten表示光衰减的系数。在计算光照的代码中,我们先将输入的s的法线值(在Normal mapping中的话这个值已经是法线图中的对应量了)和输入光线进行点积(dot函数是CG中内置的数学函数,希望你还记得,可以参考这里)。点积的结果在-1至1之间,这个值越大表示法线与光线间夹角越小,这个点也就应该越亮。之后使用max来将这个系数结果限制在0到1之间,是为了避免负数情况的存在而导致最终计算的颜色变为负数,输出一团黑,一般来说这是我们不愿意看到的。接下来我们将surf输出的颜色与光线的颜色_LightColor0.rgb(由Unity根据场景中的光源得到的,它在Lighting.cginc中有声明)进行乘积,然后再与刚才计算的光强系数和输入的衰减系数相乘,最后得到在这个光线下的颜色输出(关于difLight * atten * 2中为什么有个乘2,这是一个历史遗留问题,主要是为了进行一些光强补偿,可以参见这里的讨论))。

在了解了基本实现方式之后,我们可以看看做一些修改玩玩儿。最简单的比如将这个Lambert模型改亮一些,比如换成Half Lambert模型。Half Lambert是由Valve创造的可以使物体在低光线条件下增亮的技术,最早被用于半条命(Half Life)中以避免在低光下物体的走形。简单说就是把光强系数先取一半,然后在加0.5,代码如下:

1

2

3

4

5

6

7

8

inline float4 LightingCustomDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten) {

    float difLight = dot (s.Normal, lightDir);

    float hLambert = difLight * 0.5 + 0.5;

    float4 col;

    col.rgb = s.Albedo * _LightColor0.rgb * (hLambert * atten * 2);

    col.a = s.Alpha;

    return col;

}

这样一来,原来光强0的点,现在对应的值变为了0.5,而原来是1的地方现在将保持为1。也就是说模型贴图的暗部被增强变亮了,而亮部基本保持和原来一样,防止过曝。使用Half Lambert前后的效果图如下,注意最右侧石头下方的阴影处细节更加明显了,而这一切都只是视觉效果的改变,不涉及任何贴图和模型的变化。

Half Lambert下发现贴图的表现

表面贴图的追加效果

OK,对于光线和自定义光照模型的讨论暂时到此为止,因为如果展开的话这将会一个庞大的图形学和经典光学的话题了。我们回到Shader,并且一起实现一些激动人心的效果吧。比如,在你的游戏场景中有一幕是雪地场景,而你希望做一些石头上白雪皑皑的覆盖效果,应该怎么办呢?难道让你可爱的3D设计师再去出一套覆雪的贴图然后使用新的贴图?当然不,不是不能,而是不该。因为新的贴图不仅会增大项目的资源包体积,更会增大之后修改和维护的难度,想想要是有好多石头需要实现同样的覆雪效果,或者是要随着游戏时间堆积的雪逐渐变多的话,你应该怎么办?难道让设计师再把所有的石头贴图都盖上雪,然后再按照雪的厚度出5套不同的贴图么?相信我,他们会疯的。

于是,我们考虑用Shader来完成这件工作吧!先考虑下我们需要什么,积雪效果的话,我们需要积雪等级(用来表示积雪量),雪的颜色,以及积雪的方向。基本思路和实现自定义光照模型类似,通过计算原图的点在世界坐标中的法线方向与积雪方向的点积,如果大于设定的积雪等级的阈值的话则表示这个方向与积雪方向是一致的,其上是可以积雪的,显示雪的颜色,否则使用原贴图的颜色。废话不再多说,上代码,在上面的Shader的基础上,更改Properties里的内容为

1

2

3

4

5

6

7

Properties {

    _MainTex ("Base (RGB)", 2D) = "white" {}

    _Bump ("Bump", 2D) = "bump" {}

    _Snow ("Snow Level", Range(0,1) ) = 0

    _SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0)

    _SnowDirection ("Snow Direction", Vector) = (0,1,0)

}

没有太多值得说的,唯一要提一下的是_SnowDirection设定的默认值为(0,1,0),这表示我们希望雪是垂直落下的。对应地,在CG程序中对这些变量进行声明:

1

2

3

4

5

sampler2D _MainTex;

sampler2D _Bump;

float _Snow;

float4 _SnowColor;

float4 _SnowDirection;

接下来改变Input的内容:

1

2

3

4

5

struct Input {

    float2 uv_MainTex;

    float2 uv_Bump;

    float3 worldNormal; INTERNAL_DATA

};

相对于上面的Shader输入来说,加入了一个float3 worldNormal; INTERNAL_DATA,如果SurfaceOutput中设定了Normal值的话,通过worldNormal可以获取当前点在世界中的法线值。详细的解说可以参见Unity的Shader文档。接下来可以改变surf函数,实装积雪效果了。

1

2

3

4

5

6

7

8

9

10

11

12

void surf (Input IN, inout SurfaceOutput o) {

    half4 c = tex2D (_MainTex, IN.uv_MainTex);

    o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump));

 

    if (dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) > lerp(1,-1,_Snow)) {

        o.Albedo = _SnowColor.rgb;

    } else {

        o.Albedo = c.rgb;

    }

 

    o.Alpha = c.a;

}

和上面相比,加入了一个if…else…的判断。首先看这个条件的不等式的左侧,我们对雪的方向和和输入点的世界法线方向进行点积。WorldNormalVector通过输入的点及这个点的法线值,来计算它在世界坐标中的方向;右侧的lerp函数相信只要对插值有概念的同学都不难理解:当Snow取最小值0时,这个函数将返回1,而Snow取最大值时,返回-1。这样我们就可以通过设定Snow的值来控制积雪的阈值,要是积雪等级Snow是0时,不等式左侧不可能大于右侧,因此完全没有积雪;相反要是_Snow取最大值1时,由于左侧必定大于-1,所以全模型积雪。而随着取中间值的变化,积雪的情况便会有所不同。

应用这个Shader,并且适当地调节一下积雪等级和颜色,可以得到如下右边的效果。

添加了积雪效果的Shader

更改顶点模型

到现在位置,我们还仅指是在原贴图上进行操作,不管是用法线图使模型看起来凸凹有致,还是加上积雪,所有的计算和颜色的输出都只是“障眼法”,并没有对模型有任何实质的改动。但是对于积雪效果来说,实际上积雪是附加到石头上面,而不应当简单替换掉原来的颜色。但是具体实施起来,最简单的办法还是直接替换颜色,但是我们可以稍微变更一下模型,使原来的模型在积雪的方向稍微变大一些,这样来达到一种雪是附加到石头上的效果。

我们继续修改之前的Shader,首先我们需要告诉surface shadow我们要改变模型的顶点。首先将#param行改为

#pragma surface surf CustomDiffuse vertex:vert

这告诉Shader我们想要改变模型顶点,并且我们会写一个叫做vert的函数来改变顶点。接下来我们再添加一个参数,在Properties中声明一个_SnowDepth变量,表示积雪的厚度,当然我们也需要在CG段中进行声明:

1

2

3

4

5

//In Properties{…}

_SnowDepth ("Snow Depth", Range(0,0.3)) = 0.1

 

//In CG declare

float _SnowDepth;

接下来实现vert方法,和之前积雪的运算其实比较类似,判断点积大小来决定是否需要扩大模型以及确定模型扩大的方向。在CG段中加入以下vert方法

1

2

3

4

5

6

void vert (inout appdata_full v) {

    float4 sn = mul(transpose(_Object2World) , _SnowDirection);

    if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow * 2) / 3)) {

        v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;

    }

}

和surf的原理差不多,系统会输入一个当前的顶点的值,我们根据需要计算并填上新的值作为返回即可。上面第一行中使用transpose方法输出原矩阵的转置矩阵,在这里_Object2World是Unity ShaderLab的内建值,它表示将当前模型转换到世界坐标中的矩阵,将其与积雪方向做矩阵乘积得到积雪方向在物体的世界空间中的投影(把积雪方向转换到世界坐标中)。之后我们计算了这个世界坐标中实际的积雪方向和当前点的法线值的点积,并将结果与使用积雪等级的2/3进行比较lerp后的阈值比较。这样,当前点如果和积雪方向一致,并且积雪较为完整的话,将改变该点的模型顶点高度。

加入模型更改前后的效果对比如下图,加入模型调整的右图表现要更为丰满真实。

image

这节就到这里吧。本节中实现的Shader可以在这里找到完整版本进行参考,希望大家周末愉快~

3ds Max制作真实的雪场景效果

 

软件简介:
本教程为大家讲解了真实的雪材质制作过程,非常简单的教程,但个人觉得讲解的非常好,希望这套比较老的教程能给你带来帮助。

 

 

 

 

下面教程开始

雪的材质看似简单,其实它的制做并不是想像中的那么容易。许多人在设置材质的时候往往只是简单的指定一个白色,然后给个凹凸,此教程由软件自学网首发,然后就把精力放到灯光设置上了。

其实恰恰相反!白色的是一种对漫反射极为敏感的色彩,环境对它的影响很强烈,所以从这方面看雪应该是一种色彩很“丰富”的物质。

对于室外的场景,雪的色彩主要取决于太阳和天空,而且雪花结是一种半透明物质,仅仅通过灯光来模拟这种效果很难达到令人满意的效果,而且对于大的场景要使灯光均匀的散布在雪上,设置起来非常麻烦,而且非常占用系统资源,所以雪的制做重点应放到以材质为主的原则上,以弥补灯光的不足!

这个教程就是给大家一个简单的做雪的思路,看看从材质上怎样和灯光来相辅相成。

首先要注意的是,这里所讲的内容都是基于VRay渲染系统的,你也可以使用Brazil,3ds Max的渲染系统无法渲染出这么细腻的图像,对于这个Shader不太适用!

第一步:新建一个标准材质,命名为Snow。将阴影、过渡和高光全部设为纯白色。

第二步:这是最关键的一步!钩选自发光通道,添加一个Mask贴图,Map设置为Gradient Ramp,Mask设置为Falloff。(图01)

 

图01

第三步:设置Gradient Ramp。注意Gradient Ramp的图Type是Mapped,Source Map是Falloff,渐变色彩设置如图。靠右的深色控制的是雪的暗部色彩,靠中和左的浅色是雪的亮部和高光色,需要注意的是这些色彩的调整应该符合你制做的场景的整体色彩,如果是黄昏的场景,那么这几个色彩就要相应的有变化!这里需要自己多琢磨一下!Source Map设置也如图,用Mix Curve曲线可以控制雪的明暗,切记!(图02)

 

图02

第四步:然后返回Mask层,设置Mask的Falloff贴图,如下图。(图03)

 

图03

第五步:给高光强度通道一个Cellular贴图,将Size设置为0.4,钩选Fractal。将Glossiness值设为99。同时将这个Cellular贴图复制到凹凸贴图通道。(提示:凹凸贴图可以用刚才的Cellular贴图混合一个另外的贴图从而得到更自然的效果!)(图04)

 

图04

第六步:好了现在就大功告成了,最后的样子应该是这样,如下图。(图05)

 

图05

第七步:不是太象,别着急,把它合并到顶底材质中,赋予给你制做好的带有置换的场景,并建立一个主光,(注意只用一盏灯即可)提高渲染采样,渲染!不出意外,应该就是以下的效果了!更多的效果都可以通过Gradient Ramp贴图来实现,开始研究吧!(图06)

 

积雪

 

 

Shader "Custom/VVSnowShader" {

    Properties {

        _MainTex ("Base (RGB)", 2D) = "white" {}

        _Bump ("Bump", 2D) = "bump" {}

        _Snow ("Snow Level", Range(0,1) ) = 0

        _SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0)

        _SnowDirection ("Snow Direction", Vector) = (0,1,0)

        _SnowDepth ("Snow Depth", Range(0,0.3)) = 0.1

    }

    SubShader {

        Tags { "RenderType"="Opaque" }

        LOD 200

 

        CGPROGRAM

        #pragma surface surf CustomDiffuse vertex:vert

 

        sampler2D _MainTex;

        sampler2D _Bump;

        float _Snow;

        float4 _SnowColor;

        float4 _SnowDirection;

        float _SnowDepth;

 

        struct Input {

            float2 uv_MainTex;

            float2 uv_Bump;

            float3 worldNormal;

            INTERNAL_DATA

        };

 

        inline float4 LightingCustomDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten) {

            float difLight = dot (s.Normal, lightDir);

            float hLambert = difLight * 0.5 + 0.5;

            float4 col;

            col.rgb = s.Albedo * _LightColor0.rgb * (hLambert * atten * 2);

            col.a = s.Alpha;

            return col;

        }

 

        void vert (inout appdata_full v) {

            float4 sn = mul(transpose(_Object2World) , _SnowDirection);

            if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow * 2) / 3)) {

                v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;

            }

        }

 

        void surf (Input IN, inout SurfaceOutput o) {

            half4 c = tex2D (_MainTex, IN.uv_MainTex);

 

            o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump));

 

            if (dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) > lerp(1,-1,_Snow)) {

                o.Albedo = _SnowColor.rgb;

            } else {

                o.Albedo = c.rgb;

            }

            

            o.Alpha = 1;

        }

        ENDCG

    }

    FallBack "Diffuse"

}

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Shader入门指南 的相关文章

随机推荐

  • 什么是AQS?

    AQS AbstractQueuedSynchronizer 是 Java 中用于构建同步器的抽象基类 它提供了一种强大的框架 使得可以相对容易地构建各种同步工具 如锁 信号量 倒计数器等 AQS 是 Java 并发包中的核心组件之一 它在
  • JAVA--Map集合详解

    特点 该集合存储键 key 值 value 对 一对一对往里存 而且要保证键 key 的唯一性 Map集合和Set集合很像 其实Set集合底层就是使用了Map集合 什么时候使用Map集合 当数据之间存在映射关系时 优先考虑Map集合 Map
  • MySQL的基础部分(基础部分完结)

    MySQL的基础部分 基础部分完结 文章目录 MySQL的基础部分 基础部分完结 知识小回顾 小案例部分 分页查询 总结多子句查询 多表查询 重点 难点 自连接 多行子查询 小结 子查询临时表 any all关键字的使用 mysql多列子查
  • Shell脚本交互之:自动输入密码

    平时在控制台输入指令如 sudo ssh ftp或者修改admin权限的文件时候都会要求输入password 但是在she ll脚本运行过程中该如何交互实现自动输入密码呢 下面总结三种实现方法 一 重定向 用重定向方法实现交互的前提是指令需
  • 录播系统服务器大全,专业高清录播服务器 全高清录播系统 系统设计精美

    高清录播服务器 支持多个会议 多速率 多种录制模式对会议随心所欲的进行录制点播 可以配合当前各种标准H 323 SIP的MCU和终端产品 表现出良好的兼容性 标配5组20路同时录制 大可支持20组视频会议录制 录制中的视频会议的音频 视频和
  • 服务部署之每个主机的单个服务实例

    背景 您已应用微服务架构模式并将系统架构为一组服务 每个服务都部署为一组服务实例 以实现吞吐量和可用性 问题 如何打包和部署服务 诉求 服务使用各种语言 框架和框架版本编写 每个服务由多个服务实例组成 用于吞吐量和可用性 服务必须可独立部署
  • 【实例分割】4、YOLACT: Real-time Instance Segmentation

    文章目录 摘要 1 引言 2 相关工作 3 YOLACT 3 1 模板的产生 3 2 Mask 系数 3 3 Mask集成 3 4 其他情况 4 检测器 5 其他贡献 6 结果 6 1 实例分割结果 6 2 Mask质量 6 3 动态稳定性
  • SnakeGame(贪吃蛇游戏)

    目录 一 前言 二 项目介绍 1 游戏的操作方式 2 开发的过程中的注意事项 1 图像的左右问题 2 摄像头的画面尺寸问题 三 游戏的实现要点 1 选择第三方库 2 找到关键点并标记 3 创建一个类来保存关于游戏的所有功能 4 定义函数进行
  • spring & bean

    1 spring的三种实例化bean方法 2 配置spring管理bean的作用域 3 spring管理的bean的生命周期 bean什么时候进行实例化 spring容器启动的时候 还是在调用geatBean 方法的时候 延时初始化 指定b
  • chatgpt赋能python:Python创建5×5矩阵(Matrix)教程

    Python创建5 5矩阵 Matrix 教程 Python是一种广泛使用的高级编程语言 其具有简单易用 可读性强 支持多种编程范式等特点 已经成为数据分析 科学计算与机器学习等领域必备的编程工具之一 在Python的诸多应用场景中 创建矩
  • OpenCV4-图像二值化

    OpenCV4 图像二值化 图像二值化 1 环境配置 OpenCV版本 OpenCV4 1 2 图像二值化 二值图像 图像中的像素灰度值无论在什么数据类型中都只有最大值和最小值两种取值 这种 非黑即白 的图像称为二值图像 将非二值图像经过计
  • Java基础——函数与数组

    上一节中给大家留得习题都完成的怎么样呀 有什么问题就给阿Q留言吧 阿Q会知无不言 言无不尽的 今天让我们进一步学习一下java中的函数与数组吧 函数 也称之为方法 1 定义在类中 有特定功能的一段小程序 可以提高代码的复用性和维护性 2 函
  • 毕业项目SSM框架配置文件之SqlMapConfig.xml

  • Registry学习资料

    官网 https registry project readthedocs io en latest index html
  • npm安装、使用方法

    文章目录 npm安装 使用方法 1 npm介绍 2 npm查看版本 3 npm查看所有命令列表 4 npm查看所有命令用法 5 npm查看配置 6 npm永久换源 7 npm查看换源是否成功 8 npm创建模块 9 npm查看项目已安装的所
  • 【springboot】@RequestBody和@ResponseBody以及@PathVariable的使用及区别

    1 RequestBody和 ResponseBody RequestBody 通过 requestBody可以将请求体中的JSON字符串绑定到相应的bean上 当然 也可以将其分别绑定到对应的字符串上 实质上接受的是一个JSON的字符串
  • 基于Linux用C语言实现TCP/UDP图片和文件传输(socket)

    目录 一 TCP实现 1 服务端 2 客户端 二 UDP实现 1 服务端 2 客户端 一 TCP实现 传输控制协议 TCP Transmission Control Protocol 是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设
  • Python-turtle库

    star py import turtle turtle pensize 5 turtle color red turtle penup turtle goto 150 150 turtle pendown turtle begin fil
  • Opencv之Unsharp Mask(USM)锐化

    1 介绍 USM技术是通过增强图像的高频部分的内容来提高视觉效果 用具体的公式表达即为 y n m x n m z n m 其中 x n m 为输入图像 y n m 为输出图像 而z n m 为校正信号 一般是通过对x进行高通滤波获取 是用
  • Shader入门指南

    Unity3D Shader入门指南 一 分类 Unity3d2014 04 28 17 40 16人阅读 评论 0 收藏 举报 unity3dShader3d渲染 目录 本文转载自 http onevcat com 2013 07 sha