大家好,我是阿赵。这里继续讲URP相关的内容。
这次想讲的是CG和HLSL在写法上的一些区别。
一、为什么开始用HLSL
首先,基本上大家都知道的事情再说一遍。
三种Shader编程语言:
1、基于OpenGL的OpenGL Shading Language,缩写GLSL
2、基于DirectX的High Level Shading Language,缩写HLSL
3、基于NVIDIA的C for Graphic,缩写CG
简单来说GLSL和HLSL由于是分别基于不同的接口,所以两者是不能混用的,但CG却是可以同时被两种接口支持。
所以在早期的Unity版本里,最常见的是用CG Program来写Shader。但随着后来各种新技术的出现,CG已经有点跟不上步伐了,所以新版本的Unity里面的支持库渐渐变成了HLSL了。
比如我们之前在导入URP工程时,看到的支持库,全部都是HLSL的。基于这种情况下,我们也应该开始熟悉一下HLSL。
二、从CG转变到HLSL
由于语法是很类似的,所以我先写一个最基础的CG例子,然后再逐步转换成HLSL。
Shader "azhao/CGBase"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
//自己算矩阵转换,把顶点从模型空间转换到裁剪空间,并计算UV坐标
float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
float4 viewPos = mul(UNITY_MATRIX_V, worldPos);
float4 clipPos = mul(UNITY_MATRIX_P, viewPos);
o.pos = clipPos;
o.uv = v.uv*_MainTex_ST.xy + _MainTex_ST.zw;
//上面的计算在导入UnityCG.cginc后可以简化为
//顶点坐标
//o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//或者
//o.pos = UnityObjectToClipPos(v.vertex);
//UV坐标
//o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
half4 frag (v2f i) : SV_Target
{
half4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
这个例子非常简单,标准的顶点片段程序,值得注意的地方有:
1、Unity自带的矩阵,比如unity_ObjectToWorld或者UNITY_MATRIX_MVP之类的,不需要声明,直接就可以使用
2、贴图是用sampler2D 类型来定义的
3、假如引用UnityCG.cginc,那么将会可以使用库里面内置的方法,比如UnityObjectToClipPos或者TRANSFORM_TEX等。
4、我特意不引用UnityCG.cginc,是为了在接下来的转换中,不依赖CG库提供的方法来做对比。
接下来,比较不规范的转换一版HLSL
Shader "azhao/HLSLBase"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4x4 unity_ObjectToWorld;
float4x4 unity_MatrixVP;
v2f vert (appdata v)
{
v2f o;
float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
float4 clipPos = mul(unity_MatrixVP, worldPos);
o.pos = clipPos;
o.uv = v.uv*_MainTex_ST.xy + _MainTex_ST.zw;
return o;
}
half4 frag (v2f i) : SV_Target
{
half4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDHLSL
}
}
}
首先说明的是,这个Shader虽然很多地方不规范,但在Unity里面是完全可以跑起来的。
然后看值得注意的地方:
1、程序体里面不再是使用CGPROGRAM和ENDCG来包裹程序内容了,而是改为了用HLSLPROGRAM和ENDHLSL
2、unity内置的矩阵,不能再直接使用,要先声明再使用了,比如unity_ObjectToWorld和unity_MatrixVP
3、在使用CG的时候,有些矩阵是等价的,比如unity_ObjectToWorld和UNITY_MATRIX_M是一样的,但在HLSL里面,如果没有引用核心库的情况下,拿UNITY_MATRIX_M、UNITY_MATRIX_V、UNITY_MATRIX_P这些矩阵直接计算,不报错,但计算不出正确结果。但类似unity_ObjectToWorld和unity_MatrixVP这类的矩阵是正确的。
下面再来看一个比较正确的版本:
Shader "azhao/HLSLBase2"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"}
LOD 100
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
CBUFFER_END
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
v2f vert (appdata v)
{
v2f o;
VertexPositionInputs vertexInput = GetVertexPositionInputs(v.vertex.xyz);
o.pos = vertexInput.positionCS;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
half4 frag (v2f i) : SV_Target
{
half4 col = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex, i.uv);
return col;
}
ENDHLSL
}
}
}
直接看值得注意的地方:
1、渲染管线可以指定一下"RenderPipeline" = “UniversalPipeline”
2、引入了一个HLSL的核心库Core.hlsl。这个东西是类似于UnityCG.cginc的库,里面带有非常多好用的方法,所以基本上来说,都需要引入的
3、在引入了Core.hlsl之后,那些内置矩阵不需要声明就可以使用了,而且unity_ObjectToWorld和UNITY_MATRIX_M又变成相同的了。这是因为,在核心库里面,对这些矩阵又重新做了一次定义
4、提供了直接转换顶点坐标到裁剪空间的方法GetVertexPositionInputs,这个方法返回的是一个VertexPositionInputs结构体,里面除了有裁剪空间的坐标,还有世界空间坐标和观察空间坐标。甚至还有ndc坐标。
VertexPositionInputs GetVertexPositionInputs(float3 positionOS)
{
VertexPositionInputs input;
input.positionWS = TransformObjectToWorld(positionOS);
input.positionVS = TransformWorldToView(input.positionWS);
input.positionCS = TransformWorldToHClip(input.positionWS);
float4 ndc = input.positionCS * 0.5f;
input.positionNDC.xy = float2(ndc.x, ndc.y * _ProjectionParams.x) + ndc.w;
input.positionNDC.zw = input.positionCS.zw;
return input;
}
5、定义贴图的方式变了
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
6、采样贴图的方式变了
half4 col = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex, i.uv);
SAMPLE_TEXTURE2D方法传入3个参数。
7、在声明变量的时候,通过CBUFFER_START和CBUFFER_END把在Properties里面有声明的变量包裹住,这样的做法是为了SRP Batcher的。需要注意的是,没有在Properties声明的变量,一般就是global全局变量,全局变量是不能包含在CBUFFER_START和CBUFFER_END里面的
8、fixed类型不再被支持,如果使用,会报错unrecognized identifier ‘fixed’