编写Unity3D着色器的三种方式

不管你会不会写Unity3D的shader,估计你会知道,Unity3D编写shader有三种方式,这篇东西主要就是说一下这三种东西有什么区别,和大概是怎样用的。

先来列一下这三种方式:

fixed function shader

vertex and fragment shader

surface shader

为什么Unity3D要提供三种shader的编写方式呢?那是因为三种方式的编写的难易度有区别,对应着不同的使用人群。其实我觉得这是Uniy3D想得有点多了,着色器不单止是为了实现效果,还要效果实现的效率高,才适合真正使用在项目中,单纯实现一个效果,那只是demo级别的,只适合unity3D自身的产品推广,对实际使用者的意义不会很大。基于这种想法,我下面只会大概的介绍三种方式的使用方法,以后会通过其他文章有针对性的对某种方式进行详细的说明。

先来看看三种方式的说明:

fixed function shader (固定功能着色器)

最简单的着色器类型,只能使用Unity3D自带的固定语法和提供的方法,适用于任何硬件,使用难度最小

 vertex and fragment shader (顶点片段程序着色器)

顶点片段着色器,效果最为丰富的着色器类型,使用Cg/HLSL语言规范,着色器由顶点程序和片段程序组成。所有效果都需要自己编写,使用难度相对较大。

surface shader (表面着色器)

同样使用Cg/HLSL语言规范的着色器类型,不过把光照模型提取出来,可以使用Unity3D自带的一些光照模型,也可以自己编写光照模型,着色器同样由顶点程序和片段程序组成,不过本身有默认的程序方法,使用者可以只针对自己关系的效果部分进行编写。由于选择性比较大,所以可以编写出较为丰富的效果,使用难度相对vertex and fragment shader小。

由上面的三种方式的说明可以看出来了,如果你是由丰富的的Cg开发经验的,好的选择就是直接使用vertex and fragment shader了。fixed function shader虽然简单,但能实现的效果非常有限。而surface shader是Unity3D提供的一种较为折中的方式,同样能实现较丰富的效果,但难度相对小很多。不过surface shader有一个问题,它不支持SubShader内部的多pass,所以某些需要多pass的效果要实现起来会比较困难。

接下来是主要内容,每一种方式的编写规范和举例:

1、Fixed Function shaders

1.标准范例:

 1 Shader "VertexLit" {
 2     Properties {
 3         _Color ("Main Color", Color) = (1,1,1,0.5)
 4         _SpecColor ("Spec Color", Color) = (1,1,1,1)
 5         _Emission ("Emmisive Color", Color) = (0,0,0,0)
 6         _Shininess ("Shininess", Range (0.01, 1)) = 0.7
 7         _MainTex ("Base (RGB)", 2D) = "white" { }
 8     }
 9
10     SubShader {
11         Pass {
12             Material {
13                 Diffuse [_Color]
14                 Ambient [_Color]
15                 Shininess [_Shininess]
16                 Specular [_SpecColor]
17                 Emission [_Emission]
18             }
19             Lighting On
20             SeparateSpecular On
21             SetTexture [_MainTex] {
22                 constantColor [_Color]
23                 Combine texture * primary DOUBLE, texture * constant
24             }
25         }
26     }
27 } 

2.具体说明

具体变量、Material模块参考《Unity3D的着色器介绍(二)——Unity3D的Shader基本结构说明》,主要区别是使用SetTexture 模块来控制贴图

SetTexture的具体写法:

SetTexture [贴图名称] {

ConstantColor color

Combine ColorPart, AlphaPart

}

ConstantColor color

定义一个常量颜色在combine command里面使用。

combine command

combine src1 * src2

将src1和src2相乘,结果会比两者更暗。

combine src1 + src2

将src1和src2相加,结果会比两者更亮

combine src1 - src2

从src1里减去src2

combine src1 +- src2

把src1加到src2,然后减去0.5

combine src1 lerp (src2) src3

src3和src1之间的插值,使用src2的透明值。插值是相对的。

当透明值是1时显示src1,当透明值是0时显示src3

combine src1 * src2 + src3

将src1和src2的透明度相乘,然后加上src3

combine src1 * src2 +- src3

将src1和src2的透明度相乘,然后与src3相加减去0.5

Multiplies src1 with the alpha component of src2, then does a signed add with src3.

combine src1 * src2 - src3

将src1和src2的透明度相乘,然后减去src3

matrix command

matrix [MatrixPropertyName]

Transforms texture coordinates used in this command with the given matrix

其中Combine部分是可选的,作用是设定颜色部分和透明部分的叠加方式。

其中透明部分如果不写,将会默认使用颜色部分相同的设置。

一般的写法是Combine texture * primary,这样的意思是把贴图乘以顶点颜色作为最终颜色。

也可以写成Combine texture * primary DOUBLE,加一个DOUBLE的作用是让光照强度增幅成2倍。primary代表的是初始的顶点颜色,也就是在没有加贴图之前的顶点颜色。

如果需要叠加第二张或者更多贴图,那么就不能再乘以primary了,要改为previous,也就是Combine texture * previous。previous代表的是之前的颜色,也就是你前一张贴图乘以初始颜色之后的颜色。

从Fixed Function shaders的写法可以看出,这种类型的shader非常简单易用,基本上所有平台设备都能支持,不过能做的东西确实不多。你可以用来简单的表现一些基本的贴图和颜色叠加的效果,也可以使用TexGen的贴图来实现一些贴图特效,不过不能控制顶点,也不能较为复杂的控制表面着色。

2、Vertex and Fragment Shader

1.标准例子:

 1 Shader "Custom/Exam1" {
 2 Properties {
 3 _MainTex ("Texture", 2D) = "white" { }
 4 }
 5 SubShader
 6 {
 7 pass
 8 {
 9 CGPROGRAM
10 #pragma vertex vert
11 #pragma fragment frag
12 #include "UnityCG.cginc"
13 sampler2D _MainTex;
14 float4 _MainTex_ST;
15 struct v2f {
16     float4  pos : SV_POSITION;
17     float2  uv : TEXCOORD0;
18 } ;
19 v2f vert (appdata_base v)
20 {
21     v2f o;
22    o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
23 o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
24     return o;
25 }
26 float4 frag (v2f i) : COLOR
27 {
28 float4 texCol = tex2D(_MainTex,i.uv);
29     float4 outp = texCol;
30     return outp;
31 }
32 ENDCG
33 }
34 }
35 }

2.细节解释:

1、Cg小片段

Cg小片段以CGPROGRAM开头,以ENDCG结尾。中间的内容使用Cg/HLSL语言。

Cg小片段的开始处,可以添加:

#pragma vertex name - 表明这是个以name为名字的函数的顶点程序。

#pragma fragment name- 表明这是个以name为名字的函数的片段程序。

#pragma fragmentoption option - 添加选项到编译的OpenGL 片段程序。 通过 ARB 片段程序 可以查询到所允许的规范的选项列表。 这个指令对顶点程序或者不是以OpenGL为编译目标的程序无效。

#pragma target name - 着色器目标编译。

#pragma target default 编译默认目标:

在Direct3D 9环境中:顶点着色器1.1和像索着色器2.0

ARB顶点程序 有128位指令限制,ARB片段程序 有96位指令限制(32位纹理 + 64位算术运算),16个临时寄存器和4个间接纹理。

#pragma target 3.0 编译着色器模式3.0:

在Direct3D 9环境中:顶点着色器3.0和像索着色器 3.0

ARB顶点程序 没有指令限制,ARB片段程序 有1024位指令限制(512位纹理 + 512位算术运算),32个临时寄存器和4个间接纹理。可以通过使用 #pragma profileoption 命令扩展限制值。例如: #pragma profileoption MaxTexIndirections=256增加间接纹理上限到256个。需要注意的是某些着色器模式3.0中的特性不支持ARB顶点程序和ARB片段程序,比如派生指令这样的。你可以使用 #pragma glsl命令转换到GLSL中,这样子限制较少。

#pragma only_renderers space separated names - 仅用给定的渲染器编译着色器。默认情况下用所有的渲染器都编译着色器。

?d3d9 - Direct3D 9.

?opengl - OpenGL.

?gles - OpenGL ES 2.0.

?xbox360 - Xbox 360.

?ps3 - PlayStation 3.

?flash - Flash.

#pragma exclude_renderers space separated names - 不用给定的渲染器编译着色器。 默认情况下用所有的渲染器都编译着色器。参考上一点。

#pragma glsl - 用桌面OpenGL平台编译着色器时,转换成GLSL里面的Cg/HLSL(而不是默认设置的ARB顶点/片段程序)。

2、参数对应声明

如有以下着色器输入参数

_MyColor ("Some Color", Color) = (1,1,1,1)

_MyVector ("Some Vector", Vector) = (0,0,0,0)

_MyFloat ("My float", Float) = 0.5

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

_MyCubemap ("Cubemap", CUBE) = "" {}

那么在cg程序里面应该再次声明这些参数,才能在cg程序里面使用

float4 _MyColor;

float4 _MyVector;

float _MyFloat;

sampler2D _MyTexture;

samplerCUBE _MyCubemap;

类型对应关系:

Color和Vector属性对应 float4 类型。

Range和Float属性对应 float 类型。

Texture属性对应 普通2D纹理的sampler2D 类型。

CUBE和RECT纹理对应 samplerCUBE 和 samplerRECT类型。

3、顶点程序

需要获取顶点的信息,可以先引用#include "UnityCG.cginc"

然后在这个包里面有2种类型包含了顶点的信息:

appdata_base: 包含顶点位置,法线 和一个纹理坐标。

appdata_tan:包含顶点位置,切线,法线 和一个纹理坐标。

其中包含以下的参数:

float4 vertex 是顶点位置

float3 normal 是顶点法线

float4 texcoord 是第一UV坐标

float4 texcoord1 是第二UV坐标

float4 tangent 是切线向量(用在法线贴图中)

float4 color 是每个顶点(per-vertex)颜色

当然也可以不引用#include "UnityCG.cginc",直接自己定义appdata

struct appdata {

float4 vertex : POSITION;

float4 texcoord : TEXCOORD0;

};

其中常用类型:

POSITION:顶点位置

COLOR:顶点颜色

NORMAL:法线

TANGENT:切线

TEXCOORD0:UV1

TEXCOORD1:UV2

声明顶点程序名称

#pragma vertex vert

下面就可以写顶点程序vert了

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;

return o;

}

其中v2f是一个自定义块,里面定义了顶点程序返回时包含的信息,也可以传递给片段程序用,其中位置改成SV_POSITION。也可以自定义一些其他的属性。

在顶点程序里面的顶点与矩阵相乘得到最终位置,相关矩阵解释:

UNITY_MATRIX_MVP

当前模型*视*投影矩阵。(注:模型矩阵为 本地->世界)

UNITY_MATRIX_MV

当前模型*视图矩阵

UNITY_MATRIX_V

当前视图矩阵

UNITY_MATRIX_P

当前投影矩阵

UNITY_MATRIX_VP

当前视图*投影矩阵

UNITY_MATRIX_T_MV

转置模型*视图矩阵

UNITY_MATRIX_IT_MV

-逆转置模型*视矩阵

UNITY_MATRIX_TEXTURE0 to UNITY_MATRIX_TEXTURE3

纹理变换矩阵

当每一帧渲染的时候,每一个需要渲染的物体会自动的把顶点信息输入到shader的指定顶点程序,在vert顶点程序里面处理完,把顶点需要的信息赋值之后,就可以把v2f返回,这时候gpu会自动接收到顶点信息,并作处理。这个是GPU编程的特点。

3、片段程序

#pragma fragment frag

然后写

half4 frag (v2f i) : COLOR

{

half4 texcol = tex2D (_MainTex, i.uv);

return texcol * _Color;

}

片段程序不需要自定义块作为返回值,因为片段程序的返回值就直接是颜色了。

不过在片段程序里面,可以通过对之前顶点程序做处理获得的顶点位置、uv、法线等信息进行各种处理,让最终该点的颜色发生各种各样的改变。

3、Surface Shaders

1.标准例子:

Shader "Example/Diffuse Texture" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
          float2 uv_MainTex;
      };
      sampler2D _MainTex;
      void surf (Input IN, inout SurfaceOutput o) {
          o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
      }
      ENDCG
    }
    Fallback "Diffuse"
  }

2.详细说明

程序声明写法:

#pragma surface surfaceFunction lightModel [optionalparams]

surfaceFunction就是程序名称,该程序应该写成:

void surf (Input IN,inout SurfaceOutput o),

Input是你自己定义的结构。Input结构中应该包含所有纹理坐标(texture coordinates)和和表面函数(surfaceFunction)所需要的额外的必需变量。

lightModel 光照模型:

可选:

Lambert

BlinnPhong

或者自定义

Optional parameters:

可选参数:

alpha -透明( Alpha)混合模式。使用它可以写出半透明的着色器。

alphatest:VariableName -透明( Alpha)测试模式。使用它可以写出 镂空效果的着色器。镂空大小的变量(VariableName)是一个float型的变量。

finalcolor:ColorFunction - 自定义的最终颜色函数(final color function)。 请参考范例:表面着色器例子(Surface Shader Examples)。

exclude_path:prepass 或者 exclude_path:forward - 使用指定的渲染路径,不需要生成通道。

addshadow - 添加阴影投射 & 收集通道(collector passes)。通常用自定义顶点修改,使阴影也能投射在任何程序的顶点动画上。

dualforward - 在正向(forward)渲染路径中使用 双重光照贴图(dual lightmaps)。

fullforwardshadows - 在正向(forward)渲染路径中支持所有阴影类型。

decal:add - 添加贴花着色器(decal shader) (例如: terrain AddPass)。

decal:blend - 混合半透明的贴花着色器(Semitransparent decal shader)。

softvegetation - 使表面着色器(surface shader)仅能在Soft Vegetation打开时渲染。

noambient - 不适用于任何环境光照(ambient lighting)或者球面调和光照(spherical harmonics lights)。

novertexlights - 在正向渲染(Forward rendering)中不适用于球面调和光照(spherical harmonics lights)或者每个顶点光照(per-vertex lights)。

nolightmap - 在这个着色器上禁用光照贴图(lightmap)。(适合写一些小着色器)

nodirlightmap - 在这个着色器上禁用方向光照贴图(directional lightmaps)。 (适合写一些小着色器)。

noforwardadd - 禁用正向渲染添加通道(Forward rendering additive pass)。 这会使这个着色器支持一个完整的方向光和所有光照的per-vertex/SH计算。(也是适合写一些小着色器).

approxview - 着色器需要计算标准视图的每个顶点(per-vertex)方向而不是每个像索(per-pixel)方向。 这样更快,但是视图方向不完全是当前摄像机(camera) 所接近的表面。

halfasview - 在光照函数(lighting function)中传递进来的是half-direction向量,而不是视图方向(view-direction)向量。 Half-direction会计算且会把每个顶点(per vertex)标准化。这样做会很快,但不完全准确。

注意:surface由于使用了光照模型,所以是不能写pass,直接写在subshader里面的。

输入端:

uv_贴图变量名称 - 贴图的uv

float3 viewDir - 视图方向( view direction)值。为了计算视差效果(Parallax effects),边缘光照(rim lighting)等,需要包含视图方向( view direction)值。

float4 with COLOR semantic -每个顶点(per-vertex)颜色的插值。

float4 screenPos - 屏幕空间中的位置。 为了反射效果,需要包含屏幕空间中的位置信息。比如在Dark Unity中所使用的 WetStreet着色器。

float3 worldPos - 世界空间中的位置。

float3 worldRefl - 世界空间中的反射向量。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。 请参考这个例子:Reflect-Diffuse 着色器。

float3 worldNormal - 世界空间中的法线向量(normal vector)。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。

float3 worldRefl; INTERNAL_DATA - 世界空间中的反射向量。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。为了获得基于每个顶点法线贴图( per-pixel normal map)的反射向量(reflection vector)需要使用世界反射向量(WorldReflectionVector (IN, o.Normal))。请参考这个例子: Reflect-Bumped着色器。

float3 worldNormal; INTERNAL_DATA -世界空间中的法线向量(normal vector)。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。为了获得基于每个顶点法线贴图( per-pixel normal map)的法线向量(normal vector)需要使用世界法线向量(WorldNormalVector (IN, o.Normal))。

输出端:

struct SurfaceOutput {

half3 Albedo;

half3 Normal;

half3 Emission;

half Specular;

half Gloss;

half Alpha;

};

最后说两句我觉得比较重要的话:

1、着色器都有它规定的指令限制、寄存器、和纹理的限制,有一些复杂的效果需要使用到较多的寄存器和指令,可能会超出限制,比如使用着色器来实现骨骼动画就很容易超出限制了。不论在什么平台编写着色器(unity3D或者stage3D之类)都需要先了解清楚有什么限制,不然等到编译的时候就哭了。

2、着色器的限制有时候是跟发布平台的硬件支持有关系的,某些发布的平台(比如手机端)不支持你指定的高级别着色器模式,它有可能会根据实际情况降低级别,或者甚至显示不出来。所以编写着色器效果之前要考虑一下需要发布的平台,不要老是问一些诸如“为什么我在Unity3D的编辑器看的效果是正常的,为什么发布到手机就看不到效果”之类的问题了。

原文地址:http://blog.csdn.net/a6627651/article/details/50545643

时间: 2024-08-02 06:58:39

编写Unity3D着色器的三种方式的相关文章

DirectX11--HLSL编译着色器的三种方法

前言 本文不考虑Effects11(FX11),而是原始的HLSL语言. 该文章从教程02单独抽离出来作为单独的教程. 目前编译与加载着色器的方法如下: 使用Visual Studio中的HLSL编译器,随项目编译期间一同编译,并生成.cso(Compiled Shader Object)对象文件,在运行期间加载该文件以读取字节码. 使用Visual Studio中的HLSL编译器,随项目编译期间一同编译,并生成.inc或.h的头文件,着色器字节码在编译期间就可以确定. 在程序运行期间编译着色器

于Unity3D动态创建对象和创建Prefab三种方式的原型对象

u3d在动态创建的对象,需要使用prefab 和创建时 MonoBehaviour.Instantiate( GameObject orignal) 需要的对象为原型. 文提供三种方式获得prefab对象. 方式一:使用脚本的public字段 直接在Project视图里找到做好的prefab,将其拖拽到指定脚本的指定public GameObject 字段. 方式二:Resource类 1.在Assets目录下的任何位置创建一个名为resources的目录.将做好的prefab放到这个目录下,p

Spring bean管理器 bean实例化的三种方式

bean实例化的三种方式实现 第一种:使用类的无参数构造方法创建(常用 重要) 第一种实例化方式最常用,实例化类时会通过调用无参构造方法创建.示例代码如下: package spring.com.UserService; public class UserService { public UserService() { //该方法是无参方法 } public void AddUser(){ System.out.println("Add........................."

(五十七)android解析XML总结(SAX、Pull、Dom三种方式)

一.SAX.DOM.PULL的比较 SAX.DOM.PULL各有自己的特点,具体操作XML的时候该如何选择呢? 1.内存占用 这是一个根本性问题.由于Android手机性能相对于现在的应用操作还是有限的,程序对内存的占用直接影响到了解析XML的速度.在这点上,SAX.Pull以它们比DOM占用更少的内存的解析方式,更适合于Android手机开发. 2.编程方式 SAX采用事件驱动,在相应事件触发的时候,会调用用户编写好的方法.也就是说,每解析一类XML,就要编写一个新的适合该类XML的处理类.这

Java Class类以及获取Class实例的三种方式

T - 由此 Class 对象建模的类的类型.例如,String.class 的类型是Class<String>.如果将被建模的类未知,则使用Class<?>. [java] view plain copy print? public final class Class<T> extends Object  implements java.io.Serializable, java.lang.reflect.GenericDeclaration, java.lang.r

java创建线程的三种方式及其对比

Java中创建线程主要有三种方式: 一.继承Thread类创建线程类 (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务.因此把run()方法称为执行体. (2)创建Thread子类的实例,即创建了线程对象. (3)调用线程对象的start()方法来启动该线程. package com.thread; public class FirstThreadTest extends Thread{ int i = 0; //重写run方法,run方法的方

[OpenSource]浅谈.Net和Java互相调用的三种方式

在很多的大型系统开发中,开发工具往往不限制于同一种开发语言,而是会使用多种开发语言的混合型开发.目前Java和.Net都声称自己占85%的市场份额,不管谁对谁错,Java和.Net是目前应用开发的两个主要阵营,所以Java.和Net之间的整合是大型应用开发过程中经常会面临一个问题. 目前Java和.Net之间的整合主要有三种思路和做法(经过几天的查阅,目前我就看到了这三种方式,可能还有其他的方法): 1)基于通讯协议的整合 基于通讯协议的整合方式,最容易被人首先想到,简单的方式可以通过Web S

spring配置datasource三种方式

1.使用org.springframework.jdbc.datasource.DriverManagerDataSource 说明:DriverManagerDataSource建立连接是只要有连接就新建一个connection,根本没有连接池的作用. <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">           

android解析XML总结-SAX、Pull、Dom三种方式

在android开发中,经常用到去解析xml文件,常见的解析xml的方式有一下三种:SAX.Pull.Dom解析方式.最近做了一个android版的CSDN阅读器,用到了其中的两种(sax,pull),今天对android解析xml的这三种方式进行一次总结. xml示例(channels.xml)如下: <?xml version="1.0" encoding="utf-8"?> <channel> <item id="0&q