小强学渲染之Unity Shader编程HelloWorld

2023-03-10,,

  第一个简单的顶点vert/片元frag着色器

     1)打开Unity 5.6编辑器,新建一个场景后ctrl+s保存命名为Scene_5。默认创建的场景是包含了一摄像机,一平行光,且场景背景是一天空盒而非纯色。在这里菜单中选择 Window->lighting->settings,会弹出一个光照选项设置框如下图:

     

     点击箭头处选择“None”资源即可去掉天空盒,看到一个纯色背景。

     2)右键create一C# script,命名为shader_5,放置脚本到shader文件夹。(相当于定义了如何“穿衣服”)

     3)右键新建shader/Standard Surface Shader,命名为shaderMat_5,放置到Material文件夹。(相当于定了一“外表衣服”) 然后把2)中的shader文件赋给它(注意:这里我们的shader文件全部都放到shader文件夹统一管理,那每个shader代码编写命名要“shader/xxx”,如下图)。

  

  

     4)右键create一个Capsule柱体,把第3)的材质拖到这个Sphere球体,渲染这个柱体的时候就能运行自定义的shader代码。(相当于告诉对象,按照shader定义的‘穿衣方式’给对象穿上材质衣服)

     5)使用standard shader原有默认代码,渲染的效果如下:

    

     复制粘贴以下的代码到shader文件,

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

Shader "shader/shader_5" {
SubShader {
Pass {
CGPROGRAM #pragma vertex vert
#pragma fragment frag float4 vert(float4 v : POSITION) : SV_POSITION {
return UnityObjectToClipPos(v);
} fixed4 frag() : SV_Target {
return fixed4(1.0, , , );
} ENDCG
}
}
}

     运行看到的效果如下:

    

     下面详细分析,代码第一行通过“../文件夹名/shader文件名”形式定义了每个u shader的名字,有利于为材质球选择shader时右键快速找到自定义的shader文件(“/”控制shader文件在材质面板中出现的位置)。在Unity ShaderLab结构中,主要是PropertiesSubShaderFallback等。

     <1>Properties,顾名思义即“属性”,通俗点说就是提供在编辑器材质面板上让开发者输入接收属性调整各种材质属性的直接调试的机会。 因需要在shader程序中访问这些属性赋值,所以每个需要有一唯一的名字(Name),这个Name的用途仅是在shader程序中做区分。在Unity中,属性名字通常由一下划线开始。显示的名称display name)则是出现在材质面板上的名字,显然要对其指定类型(PropertyType)。为了第一次把Unity shader赋给某材质时,材质面板上能显示初始值,所以需要为每个属性指定一个默认值。

    

     如在上述代码中添加属性Property,

Shader "shader/shader_5" {
Properties {
_testInt("fuckInt",Int) =
_testFloat("fuckFloat",Float) = 1.68
_testRange("fuckRange",Range(0.0, 10.0)) = 6.8 _testColor("fuckColor",Color) = (0.0, 1.0, 0.0, 0.0)
_testVector("fuckVector",Vector) = (,,,)
//定义2D贴图 2的阶数大小(256*318)之类的贴图,这张贴图在采样后被转为对应基于模型UV的每个像素的颜色最终显示
_test2D("2D贴图",2D) = ""{}
//定义立方体贴图 简单说就是6张有联系的贴图的组合,主要用来做反射效果(比如天空盒动态反射),也会被转换为对应点的采样
_testCube("Cube贴图",Cube) = "white"{}
//_test3D("fuck3D",3D) = "black"{}
} SubShader {
Pass {
CGPROGRAM fixed4 _testColor;//注意要使用Properties属性值时必须先声明 #pragma vertex vert
#pragma fragment frag float4 vert(float4 v : POSITION) : SV_POSITION {
return UnityObjectToClipPos(v);
} fixed4 frag() : SV_Target {
return _testColor;
} ENDCG
}
}
}

     效果如下图所示,颜色可直接在材质面板中调整:

    

     <2>SubShader和Pass语义块,用来说明如何渲染,每个shader文件可以包含多个SubShader语义块,当Unity加载这个shader文件时会按顺序从头扫描所有SubShader,然后选择第一个平台支持的SubShader文件,若都不支持则选择Fallback指定的shader。而每个Pass定义了一次完整的渲染流程,Pass数目较多会导致渲染性能的下降,所以尽量使用最少数量的Pass。  在SubShader和Pass中都可以设置渲染相关的状态(如是否开启混合和测试等)标签,但各自的标签内容不同的故不能共用,后面会分开说明。当在SubShader中设置了渲染状态,则会应用到所有的Pass块,否则要单独在Pass中声明。

    

     SubShader的标签是字符串组成的键值对,用来告诉unity引擎希望怎样以及何时渲染这个对象,标签值和Pass的不一样。编写格式是:  Tags {"tagName" = "value"  "tagName2" = "value2"}

    

     Pass语义块多了个name,用来区分每一个pass块,并且通过这个名称,可以使用UsePass命令来直接使用其他Unity Shader中的Pass块。例如在Pass中添加 Name "MyPassName",那在别的模块就能通过 UsePass “MyShader/MYPASSNAME" 进行一样的渲染处理,有利于提高代码的复用性。 要注意一点,使用UsePass指令时Pass的名称必须所有字母都是大写模式!

    

    

     在HelloWorld工程中不需要任何渲染设置和标签设置,只需要编写CG代码块片段,由CGPROGRAM开始到ENDCG结束包围。 首先,是两条以"#pragma标志的编译指令,将告诉unity哪个函数包含了顶点着色器的代码,哪个函数包含了片段着色器代码。 #pragma vertex name 这里name就是我们指定的函数名,不一定是vert或frag,可以是任何自定义的合法函数名。

     重点分析vert函数的定义:

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

      顶点着色器代码,是逐顶点执行的。这里POSITION和SV_POSITION都是CG中的语义,是不可忽略的,这些语义告诉unity用户需要哪些输入值,以及用户输出是什么。如POSITION将告诉Unity,把模型的顶点坐标填充到输入参数v中;SV_POSITION将告诉Unity顶点着色器的输出是裁剪空间中的顶点坐标 。如果没有语义说明,渲染器就完全不知道用户的输入输出是什么,会得到错误的效果。  运行程序,会发现unity会自动替换mul()函数为UnityObjectToClipPos,原因在于:所有内建的矩阵名字在Instanced Shader中都是被重定义过的,如果直接使用UNITY_MATRIX_MVP,会引入一个额外的矩阵乘法运算。推荐使用UnityObjectToClipPos / UnityObjectToViewPos函数,会把这一次额外的矩阵乘法优化为向量-矩阵乘法。

      同理,frag函数SV_Target语义告诉渲染器,把用户输出颜色存储到一个渲染目标中,这里将默认输出到默认的帧缓存中

     扩展模型输入数据

      上面的代码只能通过POSITION语义获得模型的顶点坐标,要是还想获得模型每个顶点的纹理坐标和法线方向,该怎么办呢?因为使用纹理坐标来访问对纹理采样,而用法线数据计算光照等都是很正常的需求,这就要求定义一新的输入参数(如结构体等),而不再是简单的一个数据类型。同理希望输出纹理坐标,法线等数据到fragment片元着色器,这也需要结构体。 修改的代码如下:

    SubShader {
Pass {
CGPROGRAM fixed4 _testColor;//注意要使用Properties属性值时必须先声明 #pragma vertex vert
#pragma fragment frag struct a2v
{
//POSITION语义:用模型空间的顶点坐标填充vertex变量
float4 vertex:POSITION;
//NORMAL语义:用模型空间的法线方向填充normal变量
float3 normal:NORMAL;
//TEXCOORD0语义:用模型的第一套纹理坐标填充texcoord变量
float4 texcoord:TEXCOORD0;
}; struct v2f
{
//SV_POSITION语义:把顶点在裁剪空间中位置信息填充pos变量
float4 pos:SV_POSITION;
//COLOR0语义:存储颜色信息
float3 color:COLOR0;
}; float4 vert(a2v v) : SV_POSITION {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.nonrmal * 0.5 + fixed3(0.5,0.5,0.5)
return o
} fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
} ENDCG
}
}

      其中,a2v表示把数据从应用阶段(application)传递到顶点着色器中。那填充到POSITION,NORMAL等语义中的数据是从哪来的呢?在Unity中,它们是由使用该材质的Mesh Render组件提供。在每帧调用draw Call的时候,Mesh Render组件会把它负责渲染的模型数据发送给Unity Shader。而一个模型通常包含一组三角面片,每个三角面片由3个顶点构成,每个顶点又包含一些数据,如顶点位置,法线,纹理坐标,顶点颜色等等。

小强学渲染之Unity Shader编程HelloWorld的相关教程结束。

《小强学渲染之Unity Shader编程HelloWorld.doc》

下载本文的Word格式文档,以方便收藏与打印。