URP常用的方法和结构及其对应的位置

前言

        不知道你们是否和我一样有着这样的经历。明明知道某些东西可以实现个人想要的功能,但是就是记不起函数。或是说对函数名字有些模糊。如果我们使用类似Visual Studio这样的工具,那么它或许会给我们一些提示。但这些大多数是没有做Unity Shader的适配。即使有也是需要花钱的。虽然我个人是使用VSCode来写Unity Shader,但是我也没有找到一个好的插件。

        我个人其实大多数时候不会去记这些函数,且我自身写Shader不多(主要是项目没需求,闲暇时间我也不知道怎么去学这方面的知识)。所以我只好写一篇文章来记录一下,我常用的信息。这里我只做了Unity URP管线下的东西,毕竟内置管线还是有着不错的插件和一些文章总结。

URP版本

        Universal RP 14.0.8

URP文件位置

        我们打开任意一个Unity项目所在的文件位置。我们可以看到其Assets文件。在Assets文件的同目录下,我们可以找到Library\PackageCache\。在PackageCache文件夹下,我们可以找到com.unity.render-pipelines.universal@14.0.8com.unity.render-pipelines.core@14.0.8(这里的14.0.8表示的是我当前URP的版本号)。URP对应的文件就被存储在这里。

各种坐标转变函数

        所有的坐标系转变函数(包括了点和向量)都在下面显示的文件中

1
com.unity.render-pipelines.core@14.0.8\ShaderLibrary\SpaceTransforms.hlsl

这里我列举几个,我认为常用的坐标系转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
float3 TransformObjectToWorld(float3 positionOS)
{
#if defined(SHADER_STAGE_RAY_TRACING)
return mul(ObjectToWorld3x4(), float4(positionOS, 1.0)).xyz;
#else
return mul(GetObjectToWorldMatrix(), float4(positionOS, 1.0)).xyz;
#endif
}

// Transforms position from object space to homogenous space
float4 TransformObjectToHClip(float3 positionOS)
{
// More efficient than computing M*VP matrix product
return mul(GetWorldToHClipMatrix(), mul(GetObjectToWorldMatrix(), float4(positionOS, 1.0)));
}

// Normalize to support uniform scaling
float3 TransformObjectToWorldDir(float3 dirOS, bool doNormalize = true)
{
#ifndef SHADER_STAGE_RAY_TRACING
float3 dirWS = mul((float3x3)GetObjectToWorldMatrix(), dirOS);
#else
float3 dirWS = mul((float3x3)ObjectToWorld3x4(), dirOS);
#endif
if (doNormalize)
return SafeNormalize(dirWS);

return dirWS;
}

// Transforms normal from object to world space
float3 TransformObjectToWorldNormal(float3 normalOS, bool doNormalize = true)
{
#ifdef UNITY_ASSUME_UNIFORM_SCALING
return TransformObjectToWorldDir(normalOS, doNormalize);
#else
// Normal need to be multiply by inverse transpose
float3 normalWS = mul(normalOS, (float3x3)GetWorldToObjectMatrix());
if (doNormalize)
return SafeNormalize(normalWS);

return normalWS;
#endif
}

        如果你看过网络上一些关于URP的文章,你会发现有些作者会使用一些函数如:GetVertexPositionInputs、GetVertexNormalInputs,去获取一些点位和法线相关的信息。这些函数都在下的显示的路径下:

1
com.unity.render-pipelines.universal@14.0.8\ShaderLibrary\ShaderVariablesFunctions.hlsl

示例中的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}

VertexNormalInputs GetVertexNormalInputs(float3 normalOS)
{
VertexNormalInputs tbn;
tbn.tangentWS = real3(1.0, 0.0, 0.0);
tbn.bitangentWS = real3(0.0, 1.0, 0.0);
tbn.normalWS = TransformObjectToWorldNormal(normalOS);
return tbn;
}

于光相关的函数

        光是我们写Shader经常要用到的东西。在URP中于光相关的操作一般而言我们都需要引用下面这段代码:

1
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

往往我们引用这段代码后就可以使用GetMainLight方法去获取。但是如果你翻阅过其中的代码,你就会发现Lighting.hlsl中并没有GetMainLight的实现,且其返回的Light数据也不在其中声明。它们的声明都在Lighting.hlsl同目录下的RealtimeLights.hlsl中。其代码如下:

1
2
3
4
5
6
7
8
9
struct Light
{
half3 direction;
half3 color;
// full-float precision required on some platforms
float distanceAttenuation;
half shadowAttenuation;
uint layerMask;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Light GetMainLight()
{
Light light;
light.direction = half3(_MainLightPosition.xyz);
#if USE_FORWARD_PLUS
#if defined(LIGHTMAP_ON) && defined(LIGHTMAP_SHADOW_MIXING)
light.distanceAttenuation = _MainLightColor.a;
#else
light.distanceAttenuation = 1.0;
#endif
#else
// unity_LightData.z is 1 when not culled by the culling mask, otherwise 0.
light.distanceAttenuation = unity_LightData.z;
#endif
light.shadowAttenuation = 1.0;
light.color = _MainLightColor.rgb;

light.layerMask = _MainLightLayerMask;

return light;
}

GetMainLight还有很多带参数的重载。如果你感兴趣可以翻阅一下RealtimeLights.hlsl中的源码,这里由于篇幅有限我就不在写明了。除了获取主光源的函数外,你也可以在RealtimeLights.hlsl看到获取其他光源的函数————GetAdditionalLight

        这里我在说明一个获取光的坑点。如果你选用的渲染类型是Forward+,那么我们使用GetMainLight是获取不到正确的信息。在Lighting.hlsl的UniversalFragmentBlinnPhong函数中有这样一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#if USE_FORWARD_PLUS
for (uint lightIndex = 0; lightIndex < min(URP_FP_DIRECTIONAL_LIGHTS_COUNT, MAX_VISIBLE_LIGHTS); lightIndex++)
{
FORWARD_PLUS_SUBTRACTIVE_LIGHT_CHECK

Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);
#ifdef _LIGHT_LAYERS
if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
#endif
{
lightingData.additionalLightsColor += CalculateBlinnPhong(light, inputData, surfaceData);
}
}
#endif

这里它通过这样的方式才得到了正确的光信息。如果你只有一个光源的话,那直接使用GetAdditionalLight(0)得到的效果是一样的。

阴影

        在Light结构中,你可以找到名为shadowAttenuation的变量。这个值就可以表示是否有阴影。如果你想得到正确的shadowAttenuation值,那么你就要将着色点的世界坐标转到阴影坐标系(ShadowCoord)中。具体代码如下:

1
Light mainLight = GetMainLight(TransformWorldToShadowCoord(i.worldPos));

在写上面这段代码前,我们还需要一些着色器变体来保证得到正确的数据,声明如下:

1
2
#pragma multi_compile _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile_fragment _SHADOWS_SOFT

有关着色器变体的信息可以查看官方文档

        TransformWorldToShadowCoord看似是一个不同坐标系之间坐标转换的函数。但是在SpaceTransforms.hlsl(请查看本文中各种坐标转变函数)中并不存在这个函数。它被定义在Shadows.hlsl(于RealtimeLights.hlsl在同一文件下)中。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
float4 TransformWorldToShadowCoord(float3 positionWS)
{
#ifdef _MAIN_LIGHT_SHADOWS_CASCADE
half cascadeIndex = ComputeCascadeIndex(positionWS);
#else
half cascadeIndex = half(0.0);
#endif

float4 shadowCoord = mul(_MainLightWorldToShadow[cascadeIndex], float4(positionWS, 1.0));

return float4(shadowCoord.xyz, 0);
}

Shadows.hlsl中定义了URP关于阴影的操作。比如计算级联阴影的函数ComputeCascadeIndex,对阴影贴图的采样SampleShadowmap等。除此之外,Shadows.hlsl中也包含了一些阴影变量的定义,如_ScreenSpaceShadowmapTexture_MainLightShadowmapTexture等。

        如果你只是想要一个简单的阴影实现可以去看一下,我写的这篇文章Unity URP下接受阴影

实例化

        在URP中实现实例化只需要像下面这样进行:

1
2
3
4
5
6
7
8
9
10
#include_with_pragmas "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DOTS.hlsl"
#ifdef UNITY_DOTS_INSTANCING_ENABLED
UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP(float4, _Color)
UNITY_DOTS_INSTANCED_PROP(half , _MainScale)
UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata)

#define _Color UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4 , _Color)
#define _MainScale UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(half , _MainScale)
#endif

更加具体的例子,大家可以去看一下LitInput.hlsl中对应的实例化声明。

1
com.unity.render-pipelines.universal@14.0.8\Shaders\LitInput.hlsl

获取屏幕坐标

        获取屏幕坐标的函数如下:

1
2
3
4
5
6
7
float4 ComputeScreenPos(float4 positionCS)
{
float4 o = positionCS * 0.5f;
o.xy = float2(o.x, o.y * _ProjectionParams.x) + o.w;
o.zw = positionCS.zw;
return o;
}

其在Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderVariablesFunctions.deprecated.hlsl下,但是我们不需要额外的进行引用。因为 #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" 已经帮我们引用好了。

屏幕贴图的采样

        在某些情况下,我们需要对当前渲染出来的信息进行采样。比如屏幕扭曲的效果         屏幕采样的函数,大家只要引入下面这段代码:

1
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareOpaqueTexture.hlsl"

其中的函数如下:

1
2
3
4
5
6
7
8
9
float3 SampleSceneColor(float2 uv)
{
return SAMPLE_TEXTURE2D_X(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, UnityStereoTransformScreenSpaceTex(uv)).rgb;
}

float3 LoadSceneColor(uint2 uv)
{
return LOAD_TEXTURE2D_X(_CameraOpaqueTexture, uv).rgb;
}

函数中uv一般是我们的屏幕坐标的xy。

深度信息

        获取物体的深度也是我们时常需要用到的功能。在URP中,我们只需引入下面这段代码:

1
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareOpaqueTexture.hlsl"

其中的函数如下:

1
2
3
4
5
6
7
8
9
float SampleSceneDepth(float2 uv)
{
return SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, UnityStereoTransformScreenSpaceTex(uv)).r;
}

float LoadSceneDepth(uint2 uv)
{
return LOAD_TEXTURE2D_X(_CameraDepthTexture, uv).r;
}

函数中uv一般是我们的屏幕坐标的xy。

LinearEyeDepth

        LinearEyeDepth也是一个常用的函数。虽然它很常用,但是我到现在也没有搞懂它。这个函数需要两个参数,一个是深度值,另一个是_ZBufferParams(有关_ZBufferParams的信息,Unity官方文档上有说明)。 其函数声明在:

1
PackageCache\com.unity.render-pipelines.core@14.0.8\ShaderLibrary\Common.hlsl

函数实现:

1
2
3
4
float LinearEyeDepth(float depth, float4 zBufferParam)
{
return 1.0 / (zBufferParam.z * depth + zBufferParam.w);
}

这里特别说明一下,这个函数只有在透视投影的时候才能计算出正确的值。如果是在正交投影下,请使用LinearDepthToEyeDepth已获取正确的深度值。 其函数声明在:

1
PackageCache\com.unity.render-pipelines.universal@14.0.8\ShaderLibrary\ShaderVariablesFunctions.hlsl

函数实现:

1
2
3
4
5
6
7
8
9
// Linear depth buffer value between [0, 1] or [1, 0] to eye depth value between [near, far]
half LinearDepthToEyeDepth(half rawDepth)
{
#if UNITY_REVERSED_Z
return half(_ProjectionParams.z - (_ProjectionParams.z - _ProjectionParams.y) * rawDepth);
#else
return half(_ProjectionParams.y + (_ProjectionParams.z - _ProjectionParams.y) * rawDepth);
#endif
}

如果你嫌这个麻烦,你可以直接使用GetLinearEyeDepth。其声明在

1
PackageCache\com.unity.render-pipelines.universal@14.0.8\ShaderLibrary\SSAO.hlsl

函数实现:

1
2
3
4
5
6
7
8
float GetLinearEyeDepth(float rawDepth)
{
#if defined(_ORTHOGRAPHIC)
return LinearDepthToEyeDepth(rawDepth);
#else
return LinearEyeDepth(rawDepth, _ZBufferParams);
#endif
}

碎碎念

        这篇文章的时间跨度也有几个月了。我原本想记录的东西也差不多忘了。上述这些是现在的我觉得需要的。如果后面我发现还有一些函数十分常用,那我再来丰富这篇文章。