什么是Shader

Shader(着色器)是一段能够针对3D对象进行操作、并被GPU所执行的程序,它负责将输入的Mesh(网格)以指定的方式和输入的贴图或者颜色等组合作用,然后输出。绘图单元可以依据这个输出来将图像绘制到屏幕上。输入的贴图或者颜色等,加上对应的Shader,以及对Shader的特定的参数设置,将这些内容(Shader及输入参数)打包存储在一起,得到的就是一个Material(材质)。之后,我们便可以将材质赋予合适的renderer(渲染器)来进行渲染(输出)了。Shader并不是一个统一的标准,不同的图形接口的Shader并不相同。OpenGL的着色语言是GLSL, NVidia开发了Cg,而微软的Direct3D使用高级着色器语言(HLSL)。而Unity的Shader 是将传统的图形接口的Shader(由 Cg / HLSL编写)嵌入到独有的描述性结构中而形成的一种代码生成框架,最终会自动生成各硬件平台自己的Shader,从而实现跨平台。

Shader种类

OpenGL和Direct3D都提供了三类着色器:

  • 顶点着色器:处理每个顶点,将顶点的空间位置投影在屏幕上,即计算顶点的二维坐标。同时,它也负责顶点的深度缓冲(Z-Buffer)的计算。顶点着色器可以掌控顶点的位置、颜色和纹理坐标等属性,但无法生成新的顶点。顶点着色器的输出传递到流水线的下一步。如果有之后定义了几何着色器,则几何着色器会处理顶点着色器的输出数据,否则,光栅化器继续流水线任务。
  • 像素着色器(Direct3D),常常又称为片断着色器(OpenGL):处理来自光栅化器的数据。光栅化器已经将多边形填满并通过流水线传送至像素着色器,后者逐像素计算颜色。像素着色器常用来处理场景光照和与之相关的效果,如凸凹纹理映射和调色。名称片断着色器似乎更为准确,因为对于着色器的调用和屏幕上像素的显示并非一一对应。举个例子,对于一个像素,片断着色器可能会被调用若干次来决定它最终的颜色,那些被遮挡的物体也会被计算,直到最后的深度缓冲才将各物体前后排序。
  • 几何着色器:可以从多边形网格中增删顶点。它能够执行对CPU来说过于繁重的生成几何结构和增加模型细节的工作。Direct3D版本10增加了支持几何着色器的API, 成为Shader Model 4.0的组成部分。OpenGL只可通过它的一个插件来使用几何着色器。

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

  • 表面着色器(Surface Shader) - 为你做了大部分的工作,只需要简单的技巧即可实现很多不错的效果。类比卡片机,上手以后不太需要很多努力就能拍出不错的效果。
  • 片段着色器(Fragment Shader) - 可以做的事情更多,但是也比较难写。使用片段着色器的主要目的是可以在比较低的层级上进行更复杂(或者针对目标设备更高效)的开发。

Shader程序结构

基本的表面着色器示例:

Shader "Custom/Diffuse Texture" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 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 "VertexInputSimple" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc" struct v2f {
float4 pos : SV_POSITION;
fixed4 color : COLOR;
}; v2f vert (appdata_base v)
{
v2f o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.color.xyz = v.normal * 0.5 + 0.5;
o.color.w = 1.0;
return o;
} fixed4 frag (v2f i) : SV_Target { return i.color; }
ENDCG
}
}
}

下面主要介绍表面着色器

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

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

  • _Name - 属性的名字,简单说就是变量名,在之后整个Shader代码中将使用这个名字来获取该属性的内容
  • Display Name - 这个字符串将显示在Unity的材质编辑器中作为Shader的使用者可读的内容
  • type - 这个属性的类型,可能的type所表示的内容有以下几种:
关键字 类型 对应Cg类型
Float 浮点数 (任意一个浮点数) float _MyFloat (“My float”, Float) = 0.5
Range 浮点数 (在指定范围内) (一个介于最小值和最大值之间的浮点数,一般用来当作调整Shader某些特性的参数(比如透明度渲染的截止值可以是从0至1的值等)) float _MyRange (“My Range”, Range(0.01, 0.5)) = 0.1
Color 浮点四元组 ( 一种颜色,由RGBA(红绿蓝和透明度)四个量来定义) float4 _MyColor (“Some Color”, Color) = (1,1,1,1)
Vector 浮点四元组 (一个四维数) float4 _MyVector(“Some Vector”,Vector) = (1,1,1,1)
2D 2的阶数大小的贴图 (一张2的阶数大小(256,512之类)的贴图。这张贴图将在采样后被转为对应基于模型UV的每个像素的颜色,最终被显示出来) sampler2D _MyTexture (“Texture”, 2D) = “white” {}
Rect 非2的阶数大小的贴图 (一个非2阶数大小的贴图) sampler2D _MyRect(“My Rect”, Rect) = “white” {}
CUBE CubeMap (即Cube map texture(立方体纹理),简单说就是6张有联系的2D贴图的组合,主要用来做反射效果(比如天空盒和动态反射),也会被转换为对应点的采样) samplerCUBE _MyCubemap (“Cubemap”, CUBE) = “” {}
  • 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的模式。

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

//Define a color with a default value of semi-transparent blue
_MainColor ("Main Color", Color) = (,,,0.5)
//Define a texture with a default of white
_Texture ("Texture", 2D) = "white" {}

Tag

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

Tags { "RenderType"="Opaque" }

比较常见的标签有:

  • Queue 
    这个标签很重要,它定义了一个整数,决定了Shader的渲染的次序,数字越小就越早被渲染,早渲染就意味着可能被后面渲染的东西覆盖掉看不见。 
    预定义的Queue有:

    名字 描述
    Background 1000 最早被调用的渲染,用来渲染天空盒或者背景
    Geometry 2000 这是默认值,用来渲染非透明物体(普通情况下,场景中的绝大多数物体应该是非透明的)
    AlphaTest 2450 用来渲染经过Alpha Test的像素,单独为AlphaTest设定一个Queue是出于对效率的考虑
    Transparent 3000 以从后往前的顺序渲染透明物体
    Overlay 4000 用来渲染叠加的效果,是渲染的最后阶段(比如镜头光晕等特效)

  • RenderType 
    “Opaque”或”Transparent”是两个常用的RenderType。如果输出中都是非透明物体,那写在Opaque里;如果想渲染透明或者半透明的像素,那应该写在Transparent中。这个Tag主要用ShaderReplacement,一般情况下这Tag好像也没什么作用。

另外比较有用的标签还有"IgnoreProjector"="True"(不被Projectors影响),"ForceNoShadowCasting"="True"(从不产生阴影)以及"Queue"="xxx"(指定渲染顺序队列)。

LOD

LOD是 Level of Detail的简写,确切地说是Shader Level of Detail的简写,因为Unity中还有一个模型的LOD概念,这是两个不同的东西。我们这里只介绍Shader中LOD,模型的LOD请参考这里

Shader LOD 就是让我们设置一个数值,这个数值决定了我们能用什么样的Shader。可以通过Shader.maximumLOD或者Shader.globalMaximumLOD 设定允许的最大LOD,当设定的LOD小于SubShader所指定的LOD时,这个SubShader将不可用。通过LOD,我们就可以为某个材质写一组SubShader,指定不同的LOD,LOD越大则渲染效果越好,当然对硬件的要求也可能越高,然后根据不同的终端硬件配置来设置 globalMaximumLOD来达到兼顾性能的最佳显示效果。

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的主题部分抄写一遍

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结构体了:

struct Input {
float2 uv_MainTex;
};

作为输入的结构体必须命名为Input,这个结构体中定义了一个float2的变量,表示浮点数的float后面紧跟一个数字2,float和vec都可以在之后加入一个2到4的数字,来表示被打包在一起的2到4个同类型数。比如下面的这些定义:

//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来取得这张贴图当前需要计算的点的坐标值了。

回到surf函数,它的两有参数,第一个是Input:在计算输出时Shader会多次调用surf函数,每次给入一个贴图上的点坐标,来计算输出。第二个参数是一个可写的SurfaceOutput,SurfaceOutput是预定义的输出结构,我们的surf函数的目标就是根据输入把这个输出结构填上。SurfaceOutput结构体的定义如下

struct SurfaceOutput {
half3 Albedo; //像素的颜色
half3 Normal; //像素的法向值
half3 Emission; //像素的发散颜色
half Specular; //像素的镜面高光
half Gloss; //像素的发光强度
half Alpha; //像素的透明度
};

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

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

half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;

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

接下来…

我想现在你已经能读懂一些最简单的Shader了,接下来我推荐的是参考Unity的Surface Shader Examples多接触一些各种各样的基本Shader。

原文链接

Unity3D Shader 入门的更多相关文章

  1. 转 猫都能学会的Unity3D Shader入门指南(二)

    猫都能学会的Unity3D Shader入门指南(二) 关于本系列 这是Unity3D Shader入门指南系列的第二篇,本系列面向的对象是新接触Shader开发的Unity3D使用者,因为我本身自己 ...

  2. Unity3D Shader入门指南(二)

    关于本系列 这是Unity3D Shader入门指南系列的第二篇,本系列面向的对象是新接触Shader开发的Unity3D使用者,因为我本身自己也是Shader初学者,因此可能会存在错误或者疏漏,如果 ...

  3. 猫都能学会的Unity3D Shader入门指南(二)

    关于本系列 这是Unity3D Shader入门指南系列的第二篇,本系列面向的对象是新接触Shader开发的Unity3D使用者,因为我本身自己也是Shader初学者,因此可能会存在错误或者疏漏,如果 ...

  4. Unity3D Shader 入门之简单案例的实现(通过法线实现颜色变化)

    在没有接触Unity3D  Shader 之前,总感觉shader特别神奇,因为听说是对渲染流水线进行编程,就是对GPU进行编程.听着特别高大上.这不,最近刚刚接触Shader,学了几个小案例,然后本 ...

  5. Unity3D Shader入门指南(一)

    动机 自己使用Unity3D也有一段时间了,但是很多时候是流于表面,更多地是把这个引擎简单地用作脚本控制,而对更深入一些的层次几乎没有了解.虽然说Unity引擎设计的初衷就是创建简单的不需要开发者操心 ...

  6. (转载)猫都能学会的Unity3D Shader入门指南(一)

    原文地址 http://onevcat.com/2013/07/shader-tutorial-1/ 动机 自己使用Unity3D也有一段时间了,但是很多时候是流于表面,更多地是把这个引擎简单地用作脚 ...

  7. 猫都能学会的Unity3D Shader入门指南(一)

    动机 自己使用Unity3D也有一段时间了,但是很多时候是流于表面,更多地是把这个引擎简单地用作脚本控制,而对更深入一些的层次几乎没有了解.虽然说Unity引擎设计的初衷就是创建简单的不需要开发者操心 ...

  8. 猫都能学会的Unity3D Shader入门指南

    https://onevcat.com/2013/07/shader-tutorial-1/ https://onevcat.com/2013/08/shader-tutorial-2/

  9. 【浅墨Unity3D Shader编程】之二 雪山飞狐篇:Unity的基本Shader框架写法&颜色、光照与材质

    本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/40955607 作者:毛星云(浅墨)  ...

随机推荐

  1. Ubuntu 启动服务 失败 的 可能解决办法

    ➜  ~ service nfs-kernel-server start  Job for nfs-server.service canceled. 如果遇到服务启动失败,请检查有关的文件夹是否创建( ...

  2. minio golang client使用

    初始化 var ( endpoint = "127.0.0.1:8888" accessKeyID = "YXU5IXETKKPX171K4Z6O" secre ...

  3. python -- conda pytorch

    Linux上用anaconda安装pytorch Pytorch是一个非常优雅的深度学习框架.使用anaconda可以非常方便地安装pytorch.下面我介绍一下用anaconda安装pytorch的 ...

  4. 细说shiro之四:在web应用中使用shiro

    官网:https://shiro.apache.org/ 1. 下载在Maven项目中的依赖配置如下: <!-- shiro配置 --> <dependency> <gr ...

  5. jquery.easing 和 jquery.transit 动画插件的使用

    从jQuery API 文档中可以知道,jQuery自定义动画的函数.animate( properties [, duration] [, easing] [, complete] )有四个参数: ...

  6. ext Table中CommandColumn用法

    <ext:CommandColumn Width="250" Header="功能菜单" ColumnID="test"  Butto ...

  7. Newtonsoft.Json添加项

    JObject jo = (JObject)JsonConvert.DeserializeObject(result); ") { string domain=(jo["data& ...

  8. SSH整合方案二(不带hibernate.cfg.xml)

    整体结构: 1.引入相关jar包 2.编写实体类和映射文件 package cn.zqr.domain; public class Customer { private Long cust_id; p ...

  9. MVC设置启动页

    设置启动页需要在路由中添加一段代码: public static void RegisterRoutes(RouteCollection routes)        {            rou ...

  10. mysql 单表更新记录UPDATE

    1.单表更新 (1)mysql> SELECT * FROM users;+----+----------+----------+-----+------+| id   | username | ...