UnityのLightweight RPを触ってみる

はじめに

Lightweight RPをちょっとだけ触ってみたのでメモします。

  • Unityのバージョン:2018.3.0b8

Scriptable Render Pipeline

Unity 2018.1から追加された機能で、従来のForward/Deferredに加えて 独自のレンダリングパイプラインが作れるものです。

Lightweight RPはこの機能を使って作られた Unity公式が用意しているScriptable Render Pipelineアセットです。

プロジェクトの作成

Lightweight RP用のテンプレートが用意されているので そちらを利用してプロジェクトを作成します。

プロジェクトを立ち上げるとサンプルシーンが表示されます。

Settings内にScriptable Render Pipelineのアセットや ポストプロセスのアセットが配置されています。

Graphics設定を開いてみるとScriptable Render Pipelineが設定されているのがわかります。

フレームデバッガの確認

フレームデバッガを確認してみて通常のレンダーパイプラインと異なることを確認してみます。

最初にShadowCasterパスが呼ばれています。

次にDepthOnlyパスが呼ばれています。

次にStandardLitパスが呼ばれています。 なぜか色が赤いです。

次にSkyboxのレンダリングが行われ、その次に透明オブジェクトの描画が行われます。

その次にPostProcessが行われています。

試しにリアルタイムライトを追加してみます。

加算パスが追加されていないことが確認できます。

LWRP対応マテリアルの作成

試しにLWRP用のマテリアルを作成してみます。 「Create > Material」でLWRP用のマテリアルが作成されます。 「Shader > LightweightPipeline」からシェーダの種類を変更できます。

各マテリアルについての詳しい解説は次のページにあります。

Standard(Physically Based)は ランバート拡散反射とクックトランスの鏡面反射を利用しているようです。

LWRP対応シェーダの確認をしてみる

Standard(Physically Based)のシェーダの中身を確認してみます。 シェーダはパッケージの中に入っています。

Lightweight RPはGitHubで管理されているので、そちらを見るのが楽です。 バージョンごとにブランチが分かれているようなので、 現在入っているバージョンのv3.3.0のものを見ていきます。

LightweightStandard.shaderのリンクは次のとおりです。

次は適当に省略をしたものです。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
Shader "LightweightPipeline/Standard (Physically Based)"
{
Properties
{
...
}

SubShader
{
// Lightweight Pipeline tag is required. If Lightweight pipeline is not set in the graphics settings
// this Subshader will fail. One can add a subshader below or fallback to Standard built-in to make this
// material work with both Lightweight Pipeline and Builtin Unity Pipeline
Tags{"RenderType" = "Opaque" "RenderPipeline" = "LightweightPipeline" "IgnoreProjector" = "True"}
LOD 300

// ------------------------------------------------------------------
// Forward pass. Shades all light in a single pass. GI + emission + Fog
Pass
{
// Lightmode matches the ShaderPassName set in LightweightPipeline.cs. SRPDefaultUnlit and passes with
// no LightMode tag are also rendered by Lightweight Pipeline
Name "StandardLit"
Tags{"LightMode" = "LightweightForward"}

Blend[_SrcBlend][_DstBlend]
ZWrite[_ZWrite]
Cull[_Cull]

HLSLPROGRAM
...
ENDHLSL
}

Pass
{
Name "ShadowCaster"
Tags{"LightMode" = "ShadowCaster"}

ZWrite On
ZTest LEqual
Cull[_Cull]

HLSLPROGRAM
...
ENDHLSL
}

Pass
{
Name "DepthOnly"
Tags{"LightMode" = "DepthOnly"}

ZWrite On
ColorMask 0
Cull[_Cull]

HLSLPROGRAM
...
ENDHLSL
}

// This pass it not used during regular rendering, only for lightmap baking.
Pass
{
Name "Meta"
Tags{"LightMode" = "Meta"}

Cull Off

HLSLPROGRAM
...
ENDHLSL
}

}
FallBack "Hidden/InternalErrorShader"
CustomEditor "LightweightStandardGUI"
}

CGPROGRAM~ENDCGではなくてHLSLPROGRAM~ENDHLSLとなっているのが確認できます。 以前はCgとHLSLの中途半端に混ざったものを使っていたようですが、 新しいレンダーパイプラインに映るタイミングでHLSLに乗り換えたということでしょうか。

このファイルにはプロパティの設定と4つのパスの定義が行われています。 実際のプログラム自体は別ファイルをインクルードするようになっています。 4つのパスは"StandardLit""ShadowCaster""DepthOnly""Meta"です。 "StandardLit"が一番メインの部分です。 "ShadowCaster"は影を落とすパスで、"DepthOnly"はその名のとおり デプスを書き込むパスです。 "Meta"はライトマップのベイクのときに使われるものだそうです。

"StandardLit"を軽くあまり深追いはせずに眺めてみます。

"StandardLit"

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Pass
{
// Lightmode matches the ShaderPassName set in LightweightPipeline.cs. SRPDefaultUnlit and passes with
// no LightMode tag are also rendered by Lightweight Pipeline
Name "StandardLit"
Tags{"LightMode" = "LightweightForward"}

Blend[_SrcBlend][_DstBlend]
ZWrite[_ZWrite]
Cull[_Cull]

HLSLPROGRAM
// Required to compile gles 2.0 with standard SRP library
// All shaders must be compiled with HLSLcc and currently only gles is not using HLSLcc by default
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3d11_9x
#pragma target 2.0

// -------------------------------------
// Material Keywords
#pragma shader_feature _NORMALMAP
#pragma shader_feature _ALPHATEST_ON
#pragma shader_feature _ALPHAPREMULTIPLY_ON
#pragma shader_feature _EMISSION
#pragma shader_feature _METALLICSPECGLOSSMAP
#pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
#pragma shader_feature _OCCLUSIONMAP

#pragma shader_feature _SPECULARHIGHLIGHTS_OFF
#pragma shader_feature _GLOSSYREFLECTIONS_OFF
#pragma shader_feature _SPECULAR_SETUP
#pragma shader_feature _RECEIVE_SHADOWS_OFF

// -------------------------------------
// Lightweight Pipeline keywords
#pragma multi_compile _ _ADDITIONAL_LIGHTS
#pragma multi_compile _ _VERTEX_LIGHTS
#pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE
#pragma multi_compile _ _SHADOWS_ENABLED
#pragma multi_compile _ _LOCAL_SHADOWS_ENABLED
#pragma multi_compile _ _SHADOWS_SOFT
#pragma multi_compile _ _SHADOWS_CASCADE

// -------------------------------------
// Unity defined keywords
#pragma multi_compile _ DIRLIGHTMAP_COMBINED
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile_fog

//--------------------------------------
// GPU Instancing
#pragma multi_compile_instancing

#pragma vertex LitPassVertex
#pragma fragment LitPassFragment

#include "LWRP/ShaderLibrary/InputSurfacePBR.hlsl"
#include "LWRP/ShaderLibrary/LightweightPassLit.hlsl"
ENDHLSL
}

HLSLccをデフォルトでは使わないglesでもHLSLccを使うようにしているようです。

1
#pragma prefer_hlslcc gles

頂点シェーダとフラグメントシェーダの実態は LWRP/ShaderLibrary/LightweightPassLit.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
45
46
47
48
49
50
51
52
// Used in Standard (Physically Based) shader
LightweightVertexOutput LitPassVertex(LightweightVertexInput v)
{
LightweightVertexOutput o = (LightweightVertexOutput)0;

UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

float3 posWS = TransformObjectToWorld(v.vertex.xyz);
o.clipPos = TransformWorldToHClip(posWS);

half3 viewDir = VertexViewDirWS(GetCameraPositionWS() - posWS);

#ifdef _NORMALMAP
o.normal.w = viewDir.x;
o.tangent.w = viewDir.y;
o.binormal.w = viewDir.z;
#else
o.viewDir = viewDir;
#endif

// initializes o.normal and if _NORMALMAP also o.tangent and o.binormal
OUTPUT_NORMAL(v, o);

// We either sample GI from lightmap or SH.
// Lightmap UV and vertex SH coefficients use the same interpolator ("float2 lightmapUV" for lightmap or "half3 vertexSH" for SH)
// see DECLARE_LIGHTMAP_OR_SH macro.
// The following funcions initialize the correct variable with correct data
OUTPUT_LIGHTMAP_UV(v.lightmapUV, unity_LightmapST, o.lightmapUV);
OUTPUT_SH(o.normal.xyz, o.vertexSH);

half3 vertexLight = VertexLighting(posWS, o.normal.xyz);
half fogFactor = ComputeFogFactor(o.clipPos.z);
o.fogFactorAndVertexLight = half4(fogFactor, vertexLight);

#if defined(_SHADOWS_ENABLED) && !defined(_RECEIVE_SHADOWS_OFF)
#if SHADOWS_SCREEN
o.shadowCoord = ComputeShadowCoord(o.clipPos);
#else
o.shadowCoord = TransformWorldToShadowCoord(posWS);
#endif
#endif

#ifdef _ADDITIONAL_LIGHTS
o.posWS = posWS;
#endif

return o;
}

頂点シェーダの最初の部分は次のとおりです。

1
2
3
LightweightVertexOutput LitPassVertex(LightweightVertexInput v)
{
LightweightVertexOutput o = (LightweightVertexOutput)0;

頂点シェーダの入力を引数で受け取っています。 頂点シェーダの出力の初期化を行っています。

LightweightVertexInputLightweightVertexOutputは少し上で定義されています。

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
struct LightweightVertexInput
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 texcoord : TEXCOORD0;
float2 lightmapUV : TEXCOORD1;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct LightweightVertexOutput
{
float2 uv : TEXCOORD0;
DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 1);

#ifdef _ADDITIONAL_LIGHTS
float3 posWS : TEXCOORD2;
#endif

#ifdef _NORMALMAP
half4 normal : TEXCOORD3; // xyz: normal, w: viewDir.x
half4 tangent : TEXCOORD4; // xyz: tangent, w: viewDir.y
half4 binormal : TEXCOORD5; // xyz: binormal, w: viewDir.z
#else
half3 normal : TEXCOORD3;
half3 viewDir : TEXCOORD4;
#endif

half4 fogFactorAndVertexLight : TEXCOORD6; // x: fogFactor, yzw: vertex light

#ifdef _SHADOWS_ENABLED
float4 shadowCoord : TEXCOORD7;
#endif

float4 clipPos : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};

DECLARE_LIGHTMAP_OR_SHShaderLibrary/Lighting.hlslに定義されていました。

1
2
3
4
5
6
7
8
9
#ifdef LIGHTMAP_ON
#define DECLARE_LIGHTMAP_OR_SH(lmName, shName, index) float2 lmName : TEXCOORD##index
#define OUTPUT_LIGHTMAP_UV(lightmapUV, lightmapScaleOffset, OUT) OUT.xy = lightmapUV.xy * lightmapScaleOffset.xy + lightmapScaleOffset.zw;
#define OUTPUT_SH(normalWS, OUT)
#else
#define DECLARE_LIGHTMAP_OR_SH(lmName, shName, index) half3 shName : TEXCOORD##index
#define OUTPUT_LIGHTMAP_UV(lightmapUV, lightmapScaleOffset, OUT)
#define OUTPUT_SH(normalWS, OUT) OUT.xyz = SampleSHVertex(normalWS)
#endif

ライトマップがオンのときとオフのときで異なるものになります。 ライトマップがオンのときにはfloat2TEXCORRDnを定義し、 ライトマップがオフのときにはhalf3TEXCORRDnを定義しています。

_NORMALMAPがオンのときは少しトリッキーな形でviewDirを渡しています。 normaltangentbinormalwviewDirxyzを格納しています。

頂点シェーダの続きを見ていきます。

1
2
3
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

インスタンシングとステレオの初期化を行っています。 UNITY_SETUP_INSTANCE_ID(v)などはUnityCG.cgincのものではなくて、 com.unity.render-pipelines.coreで定義されているもののようです。 com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlslで 定義されていました。

頂点シェーダの続きを見ていきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

float3 posWS = TransformObjectToWorld(v.vertex.xyz);
o.clipPos = TransformWorldToHClip(posWS);

half3 viewDir = VertexViewDirWS(GetCameraPositionWS() - posWS);

#ifdef _NORMALMAP
o.normal.w = viewDir.x;
o.tangent.w = viewDir.y;
o.binormal.w = viewDir.z;
#else
o.viewDir = viewDir;
#endif

UVのスケールとオフセットの補正を行っています。 その後、ワールド座標とクリップスペースの座標を計算しています。 ビューの方向も計算しています。 上で述べたとおりビューの方向はノーマルマップがあるときには normaltangentbinormalw要素に格納しています。

TRANSFORM_TEXcom.unity.render-pipelines.core/ShaderLibrary/Macros.hlslで 定義されています。

1
#define TRANSFORM_TEX(tex, name) ((tex.xy) * name##_ST.xy + name##_ST.zw)

TransformObjectToWorldTransformWorldToHClipGetCameraPositionWScom.unity.render-pipelines.lightweight/LWRP/ShaderLibrary/CoreFunctions.hlslで 定義されています。

1
2
3
4
float3 TransformObjectToWorld(float3 positionOS)
{
return mul(GetObjectToWorldMatrix(), real4(positionOS, 1.0)).xyz;
}
1
2
3
4
5
// Tranforms position from world space to homogenous space
float4 TransformWorldToHClip(float3 positionWS)
{
return mul(GetWorldToHClipMatrix(), float4(positionWS, 1.0));
}
1
2
3
4
float3 GetCameraPositionWS()
{
return _WorldSpaceCameraPos;
}

頂点シェーダの続きを見ていきます。

1
2
// initializes o.normal and if _NORMALMAP also o.tangent and o.binormal
OUTPUT_NORMAL(v, o);

ノーマルの初期化を行っています。

1
2
3
4
5
6
7

// We either sample GI from lightmap or SH.
// Lightmap UV and vertex SH coefficients use the same interpolator ("float2 lightmapUV" for lightmap or "half3 vertexSH" for SH)
// see DECLARE_LIGHTMAP_OR_SH macro.
// The following funcions initialize the correct variable with correct data
OUTPUT_LIGHTMAP_UV(v.lightmapUV, unity_LightmapST, o.lightmapUV);
OUTPUT_SH(o.normal.xyz, o.vertexSH);

DECLARE_LIGHTMAP_OR_SHで一緒に定義されていたマクロを使っています。

1
2
3
half3 vertexLight = VertexLighting(posWS, o.normal.xyz);
half fogFactor = ComputeFogFactor(o.clipPos.z);
o.fogFactorAndVertexLight = half4(fogFactor, vertexLight);

頂点でのライティングとフォグの計算を行っています。

1
2
3
4
5
6
7
#if defined(_SHADOWS_ENABLED) && !defined(_RECEIVE_SHADOWS_OFF)
#if SHADOWS_SCREEN
o.shadowCoord = ComputeShadowCoord(o.clipPos);
#else
o.shadowCoord = TransformWorldToShadowCoord(posWS);
#endif
#endif

シャドウの座標を渡しています。 スクリーンスペースのシャドウかどうかで分岐しています。

1
2
3
4
5
6
#ifdef _ADDITIONAL_LIGHTS
o.posWS = posWS;
#endif

return o;
}

_ADDITIONAL_LIGHTSが定義されている場合は ワールドスペースの座標を渡します。

フラグメントシェーダ

フラグメントシェーダは次のとおりです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Used in Standard (Physically Based) shader
half4 LitPassFragment(LightweightVertexOutput IN) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(IN);

SurfaceData surfaceData;
InitializeStandardLitSurfaceData(IN.uv, surfaceData);

InputData inputData;
InitializeInputData(IN, surfaceData.normalTS, inputData);

half4 color = LightweightFragmentPBR(inputData, surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.occlusion, surfaceData.emission, surfaceData.alpha);

ApplyFog(color.rgb, inputData.fogCoord);
return color;
}

LightweightFragmentPBRがシェーディングの本体のようです。 InitializeStandardLitSurfaceDataLightweightFragmentPBRの入力を計算して渡しているようです。

おわりに

CHANGELOG.mdを見てみると今回ざっと眺めたv3.3.0-previewからいろいろと変更があるようです。

さまざまなAPIのリネームや削除などが行われています。 ここらへんはまだ安定版ではないからというのもあるのでしょう。

ゴリゴリシェーダを書くというのは安定版になってからのほうがよいのかもしれません。