1. 一个最简单的顶点/片元着色器

现在,我们正式开始学习如何编写Unity Shader,更准确的说是,学习如何编写顶点/片元着色器

2.顶点/片元着色器的基本结构

我们在以前已经讲过了Unity Shader的基本结构。它包含了Shader、Properties、SubShader、Fallback等语义块。顶点/片元着色器的结构与之大体类似,它的结构如下:

Shader "MyShaderName"{
//属性
}
SubShader{
//针对显卡A的SubShader
Pass{
//设置渲染状态和标签
//开始Cg代码片段
CGPROGRAM
//改代码片段的编译指令,例如:
#pragma vertex vert
#pragma fragment frag
//Cg代码写在这里
ENDCG
//其他设置
}
//其它需要的Pass
SubShader{
//针对显卡B的Shader
}
//上述SubShader都失败后用于回调的UnityShader
Fallback "VertexLit"
}

其中,最重要的部分是Pass语义块,我们绝大部分代码都是写在这个语义块里的。下面我们就来创建一个最简单的顶点/片元着色器。

(1)新建一个场景,如下图所示:



可以看到,场景中已经包含了一个摄像机、一个平行光。而且场景的背景不是纯色,而是一个天空盒子(Skybox)。这是因为在Unity5.x中,默认的天空盒子不为空,而是Unity内置的一个天空盒子。为了得到更加原始的效果,我们选择去掉这个天空盒子。做法是,在Unity的菜单中,选择Window->Lighting->Skybox,把该项置空。

(2)新建一个UnityShader

(3)新建一个材质,把新建的UnityShader赋给它

(4)新建一个球体,把刚才的材质赋给它

(5)打开新建的Shader,删除里面的代码,把下面的代码粘进去


保存并返回Unity查看结果。

最后我们得到的结果如图所示:



这就是我们遇见的第一个真正意义上的顶点/片元着色器,我们有必要来详细的解释一下它。

首先,代码的第一行通过Shader语义定义了这个UnityShader的名字。需要注意的是,在上面的代码里,我们并没有用到Properties语义块。Properties语义并不是必须的,我们可以选择不声明任何材质属性。

然后我们声明了SubShader和Pass语义块。在本例中,我们不需要进行任何渲染设置和标签设置,因此SubShader将使用默认的渲染设置和标签设置。在SubShader语义块中,我们定义了一个Pass,在这个Pass中,我们同样没有进行任何自定义的渲染设置和标签设置。

接着就是由CGPROGRAM和ENDCG所包围的CG代码片段。这是我们的重点。首先我们遇到了两条重要的编译指令:

#pragma vertex vert
#pragma fragment frag

它们将告诉Unity,哪个函数包含了顶点着色器的代码,哪个函数包含了片元着色器的代码。更通用的编译指令表如下:

#pragma vertex name
#pragma fragment name

其中name就是我们指定的函数名,这两个函数的名字不一定是vert和frag,它们可以是任意自定义的合发函数名,但我们一般用vert和frag来定义这两个函数,因为它们很直观。

接下来我们来看看vert函数的定义:

float4 vert(float4 v:POSITION):SV_POSITION{
return mul(UNITY_MATRIX_MVP,v);
}

这是本例使用的着色器代码,它是逐顶点执行的。vert函数的输入v包含了这个顶点的位置,这是通过POSITION语义指定的。它的返回值是一个float4类型的变量,它是该顶点在裁剪空间中的位置,POSITION和SV_POSITION都是Cg/HLSL中的语义(semantics),它们是不可省略的,这些语义将告诉系统用户需要哪些输入值,以及用户的输出是什么。例如这里,POSITION将告诉Unity,把模型的顶点坐标填充到输入参数v中,SV_POSITION将告诉Unity,顶点着色器的输出是裁剪空间中的顶点坐标。如果没有这些语义来限定输入和输出参数的话,渲染器就完全不知道用户的输入输出是什么,因此就会得到错误的结果。在后面,我们将总结这些语义。在本例中,顶点着色器只包含了一行代码,这一步就是把顶点坐标从模型空间转换到裁剪空间中。UNITY_MATRIX_MVP矩阵是Unity内置的模型·观察·投影矩阵。

然后我们再来看一下frag函数:

fixed4 frag():SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}

在本例中,frag函数没有任何输入。它的输出是一个fixed4类型变量,并且使用了SV_Target语义进行了限定。SV_Target也是HLSL中的一个系统语义,它等同于告诉渲染器,把用户的输出颜色存储到一个渲染目标(render target)中,这里将输出到默认的帧缓存中。片元着色器的代码很简单,返回了一个表示白色的fixed4类型的变量。片元着色器输出的颜色的每个分量范围在[0,1],其中红(0,0,0)表示黑色,而(1,1,1)表示白色。

至此,我们已经对第一个顶点/片元着色器进行了详细的解释。但是,现在得到的效果实在是太简单了,如何丰富它呢?下面我们将一步步为它添加更多的内容,以得到一个更加具有实践意义的顶点/片元着色器。

3. 模型数据从哪里来

在上面的例子中,在顶点着色器中我们使用POSITION语义得到了模型顶点的位置。那么,如果我们想要得到更多模型的数据要怎么办?

现在,我们想要得到模型上每个顶点的纹理坐标和法线方向。这个需求是很常见的,我们需要使用纹理坐标来访问纹理,而法线可用于计算光照。因此,我们需要为顶点着色器定义一个新的输入参数,这个参数不再是一个简单的数据类型,而是一个结构体。修改后的代码如下:

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/NewSurfaceShader"
{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//使用一个结构体来定义顶点着色器的输入
struct a2v{
//POSITION的语义告诉Unity,用模型空间的顶点坐标填充vertex变量
float4 vertex:POSITION;
//NORMAL语义告诉Unity,用模型空间的法线向量填充normal变量
float4 texcoord:TEXCOORD0;
};
float4 vert(a2v v):SV_POSITION{
//使用v.vertex来访问模型空间的顶点坐标
return UnityObjectToClipPos(v.vertex);
}
fixed frag():SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}

在上面的代码中,我们声明了一个新的结构体a2v,它包含了顶点着色器需要的模型数据。在a2v的定义中,我们用到了更多Unity支持的语义,如NORMAL和TEXCOORD0,当它们作为顶点着色器的输入时都是有特定含义的,因为Unity会根据这些语义来填充这个结构体。对于顶点着色器的输入,Unity支持的语义有:POSITION,TANGENT,NORMAL,TEXCOORD0,TEXCOORD1,TEXCOOR2,TEXCOORD3,COLOR等。

为了创建一个自定义的结构体,我们必须使用如下的格式来定义它:

struct StructName{
Type Name:Semantic;
Type Name:Semantic;
......
}

其中,语义是不可以被省略的。很快,我们将给出这些语义的含义和用法。

然后,我们修改了vert函数的输入参数类型,把它设置为我们新定义的结构体a2v。通过这种自定义结构体的方式,我们可以在顶点着色器中访问模型数据。

读者:a2v的名字是什么意思呢?

我们:a表示应用(application),v表示顶点着色器(vertex shader),a2v的意思就是把数据从应用阶段传递到顶点着色器中。

那么填充到POSITION,TANGENT,NORMAL这些语义中的数据究竟是从哪里来的呢?在Unity中,它们是由使用该材质的Mesh Render组件提供的。在每帧调用Draw Call的时候,Mesh Render组件就会把它负责渲染的模型数据发送给Unity Shader。我们知道,一个模型通常包含了一组三角面片,每个三角面片由3个顶点构成,而每个顶点又包含了一些数据,如顶点位置,法线、切线、纹理坐标、顶点颜色等。通过上面方法,我们可以在顶点着色器中访问顶点的这些模型数据。

4. 顶点着色器和片元着色器如何通信

在实践中,我们往往希望从顶点着色器中输出一些数据,例如把模型的法线、纹理坐标等传递给片元着色器。这就涉及到顶点着色器和片元着色器之间的通信。

为此,我们需要再定义一个新的结构体。修改后的代码如下:

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/NewSurfaceShader"
{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 texcoord:TEXCOORD0;
};
//使用一个结构体定义顶点着色器的输出
struct v2f{
//SV_POSITION语义告诉Unity.pos里包含了顶点在裁剪空间中的位置信息
float4 pos:SV_POSITION;
//COLOR0语义可以用于存储颜色信息
fixed3 color:COLOR0;
};
v2f vert(a2v v){
//声明输出结构
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//v.normal包含了顶点的法线方向,其分量范围在[-1.0,1.0]
//下面的代码把分量范围映射到了[0.0,1.0]
//存储到o.color中传递给片元着色器
o.color=v.normal*0.5+fixed3(0.5,0.5,0.5);
return o;
}
fixed4 frag(v2f i):SV_Target{
//将插值后的i.color显示到屏幕上
return fixed4(i.color,1.0);
}
ENDCG
}
}
}

5. 如何使用属性

材质提供给我们一个可以方便调节Unity Shader中参数的方式,通过这些参数,我们可以随时调节材质的效果。而这些参数就要写在Properties语义块中。

现在,我们有了新的需求。我们想要在材质面板显示一个颜色拾取器,从而可以直接控制模型在屏幕上显示的颜色。为此我们需要继续修改上面的代码。


在上面的代码中,我们首先添加了Properties语义块,并在其中声明了一个属性_Color,它的类型是_Color,初始值是(1.0,1.0,1.0,1.0),对应白色。为了在Cg代码中可以访问它,我们还需要在Cg代码段中提前定义一个新的变量,这个变量的名称和类型必须与Properties语义块中的属性定义相匹配。

ShaderLab中属性的类型和Cg中变量的类型之间的匹配关系如下表所示:

有时,读者可能会发现在Cg变量前会有一个uniform关键字,例如:

uniform fixed _Color;

uniform关键字是Cg中修饰变量和参数的一种修饰词,它仅仅用于提供一些关于该变量的初始值是如何指定和存储的相关信息(这和其他一些图像编程接口中的uniform关键词的作用不太一样)。在UnityShader中,uniform关键词是可以省略的。

第四章 开始Unity Shader学习之旅(1)的更多相关文章

  1. 第四章 开始Unity Shader学习之旅(2)

    目录 1. 强大的援手:Unity提供的内置文件和变量 1.1 内置的包含文件 1.2 内置的变量 2. Unity提供的Cg/HLSL语义 2.1 什么是语义 2.2 Unity支持的语义 2.3 ...

  2. 第四章 开始Unity Shader学习之旅(3)

    1. 程序员的烦恼:Debug 调试(debug),大概是所有程序员的噩梦.而不幸的是,对一个Shader进行调试更是噩梦中的噩梦.这也是造成Shader难写的原因之一--如果发现得到的效果不对,我们 ...

  3. Unity Shader入门精要学习笔记 - 第5章 开始 Unity Shader 学习之旅

    一个顶点/片元 着色器的结构大概如下: Shader "MyShaderName" { Properties { //属性 } SubShader { //针对显卡A的SubSha ...

  4. Unity Shader 学习之旅

    Unity Shader 学习之旅 unityshader图形图像 纸上学来终觉浅,绝知此事要躬行 美丽的梦和美丽的诗一样 都是可遇而不可求的——席慕蓉 一.渲染流水线 示例图 Tips:什么是 GP ...

  5. Unity Shader 学习之旅之SurfaceShader

    Unity Shader 学习之旅之SurfaceShader unity shader 图形图像  如果大地的每个角落都充满了光明 谁还需要星星,谁还会 在夜里凝望 寻找遥远的安慰——江河 官方文档 ...

  6. Unity Shader学习笔记-1

    本篇文章是对Unity Shader入门精要的学习笔记,插图大部分来自冯乐乐女神的github 如果有什么说的不正确的请批评指正 目录 渲染流水线 流程图 Shader作用 屏幕映射 三角形遍历 两大 ...

  7. 【Unity Shader学习笔记】Unity基础纹理-单张纹理

    1 单张纹理 1.1 纹理 使用纹理映射(Texture Mapping)技术,我们把一张图片逐纹素(Texel)地控制模型的颜色. 美术人员建模时,会在建模软件中利用纹理展开技术把纹理映射坐标(Te ...

  8. Unity shader学习之屏幕后期处理效果之高斯模糊

    高斯模糊,见 百度百科. 也使用卷积来实现,每个卷积元素的公式为: 其中б是标准方差,一般取值为1. x和y分别对应当前位置到卷积中心的整数距离. 由于需要对高斯核中的权重进行归一化,即使所有权重相加 ...

  9. 第四章:重构代码[学习Android Studio汉化教程]

    第四章 Refactoring Code The solutions you develop in Android Studio will not always follow a straight p ...

随机推荐

  1. K近邻(k-Nearest Neighbor,KNN)算法,一种基于实例的学习方法

    1. 基于实例的学习算法 0x1:数据挖掘的一些相关知识脉络 本文是一篇介绍K近邻数据挖掘算法的文章,而所谓数据挖掘,就是讨论如何在数据中寻找模式的一门学科. 其实人类的科学技术发展的历史,就一直伴随 ...

  2. [Neo4j]Conda虚拟环境中安装python-igraph

    neo4j算法需要用到python-igraph包,但试过很多方法,都失败了 pip install python-igraph 安装失败, 提示C core of igraph 没有安装. 在con ...

  3. 关于mimikatz lazagne免杀方法

    其实现在的杀软还是玩的老一套,改改特征字符就能过了,最新的defender能用这个方法过 文章直接从笔记复制出来的,有需要的自己看情况用 git clone https://github.com/ge ...

  4. java核心编程书上的一个错误

    书上说这段代码说明了java对对象不是采用的按引用调用 这明显错了,java还是引用传递,只是把引用对象的变量复制了,互换了x,y所指的对象,对a,b没有影响

  5. JavaSE语法

    二.JavaSE语法(上) 1.Java 有没有 goto 语句? goto 是 Java 中的保留字,在目前版本的 Java 中没有使用.根据 James Gosling(Java 之父)编写的&l ...

  6. RocketMQ 消息发送system busy、broker busy原因分析与解决方案

    目录 1.现象 2.原理解读 2.1 RocketMQ 网络处理机制概述 2.2 pair.getObject1().rejectRequest() 2.3 漫谈transientStorePoolE ...

  7. NOIP模拟 20

    来自liu_runda的善意 T1 周 究级难题,不可做,咕了. T2  任 他为什么总强调没环啊? 他为什么总强调没环啊? 他为什么总强调没环啊? ...... QAQ 因为他总是棵树,所以点的数量 ...

  8. 使用Typescript重构axios(九)——异常处理:基础版

    0. 系列文章 1.使用Typescript重构axios(一)--写在最前面 2.使用Typescript重构axios(二)--项目起手,跑通流程 3.使用Typescript重构axios(三) ...

  9. 前端与算法 leetcode 48. 旋转图像

    目录 # 前端与算法 leetcode 48. 旋转图像 题目描述 概要 提示 解析 解法一:转置加翻转 解法二:在单次循环中旋转 4 个矩形 算法 传入测试用例的运行结果 执行结果 GitHub仓库 ...

  10. day5-列表专区

    list 列表.类li = [1, 12, 9, "age", ["88", ["19", 10], "方法"], &q ...