はじめに
Shaderのコードは分割したほうが使い回しできる箇所が多くなって便利になると思っていますが具体的にどう分割してるのか書かれた記事を見たことが無いので書こうと思いました。
前提条件として以下があります。
- Unity環境
- HLSLを使う(別にCgでも問題ないけど今回の例がHLSL)
ソースコード
いつもこうしてる
- ○○.shader
- ○○Core.hlsl
- ○○Macro.hlsl
- ○○SDF.hlsl
- ○○Function.hlsl
- ○○ForwardPass.hlsl
上から順番にincludeされて行きます。
Unityの標準で存在してるShaderも似たような分割になってるはず…
このように分割するメリットは機能ごとのShaderでよく使うメソッドたちを何回も書かなくても良くなることです。
図のよく使うメソッド達に該当するのは
- ○○Macro.hlsl
- ○○Function.hlsl
の2つです。
具体的にどんなことを記述していくのかは下で細かく紹介します。
何をどこに書くか
○○.shader
Properties
と必要な数のPass
○○Core.hlsl
CBUFFER_START(UnityPerMaterial)
で書く内容、struct
○○Macro.hlsl
マクロ関係
○○SDF.hlsl
SDF関係
○○Function.hlsl
どこでも使いそうなメソッド
○○ForwardPass.hlsl
頂点シェーダーとフラグメントシェーダー
○○.shader
Shader "Universal Render Pipeline/Pn/SpriteSimpleLit"
{
Properties
{
// Main
_MainTex("Sprite Texture", 2D) = "white" {}
// Outline
_UseOutline("Use Outline", int) = 0
_OutlineColor("Outline Color", Color) = (1,1,1,1)
}
SubShader
{
Tags
{
// 省略
}
Pass
{
Name "Universal2D"
Tags
{
"LightMode" = "Universal2D"
}
HLSLPROGRAM
// 一部省略
#include "Assets/AyahaShader/PnShader/Shader/SpriteSimpleLit/Pn_SpriteSimpleLitCore.hlsl"
#include "Assets/AyahaShader/PnShader/Shader/SpriteSimpleLit/Pn_SpriteLitForwardPass.hlsl"
ENDHLSL
}
Pass
{
Name "UniversalForward"
Tags
{
// 省略
}
HLSLPROGRAM
// 一部省略
#include "Assets/AyahaShader/PnShader/Shader/SpriteSimpleLit/Pn_SpriteSimpleLitCore.hlsl"
#include "Assets/AyahaShader/PnShader/Shader/SpriteSimpleLit/Pn_SpriteLitForwardPass.hlsl"
ENDHLSL
}
}
}
Properties
と必要な数のPass
を書きます。
CBUFFER_START(UnityPerMaterial)
の中の内容は他のPass
で使わない変数があったとしてもすべてのPass
で共通して書いてないとうまく働かないのでここでは書かず、○○Core.hlsl
に書いて共通化します。
○○Core.hlsl
#ifndef PN_SPRITE_SIMPLELIT_CORE_INCLUDED
#define PN_SPRITE_SIMPLELIT_CORE_INCLUDED
#include "Assets/AyahaShader/PnShader/Shader/Pn_Macro.hlsl"
#include "Assets/AyahaShader/PnShader/Shader/SpriteSimpleLit/Pn_SpriteFunction.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
// Texture
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
CBUFFER_START(UnityPerMaterial)
// Main
uniform half4 _MainTex_ST;
// Outline
uniform int _UseOutline;
uniform float4 _OutlineColor;
CBUFFER_END
struct Attributes
{
// 省略
};
struct Varyings
{
// 省略
};
#endif
前述のとおり、CBUFFER_START(UnityPerMaterial)
を切り離してこのようにすれば一部のPassで変数を書き忘れてうまく動かない事故を防げます
Macroやよく使うメソッドたちはこのタイミングでincludeします。
○○Macro.hlsl
#ifndef PN_MACRO
#define PN_MACRO
#define PN_EPS .000001
#define PN_COMPARE_EPS(n) max(n, PN_EPS)
#endif
どこでも良く使うかもしれないマクロをここにまとめます。
○○SDF.hlsl
#ifndef PN_SDF
#define PN_SDF
float sdCircle(float2 p, float r)
{
return length(p) - r;
}
float opUni( float d1, float d2 )
{
return min(d1,d2);
}
float opSub( float d1, float d2 )
{
return max(-d1,d2);
}
float opInt( float d1, float d2 )
{
return max(d1,d2);
}
#endif
SDF式だったり、それらを合成するためのメソッドをここに書きます。
○○Function.hlsl
// Outline
// ex) float outline = Outline(_MainTex, sampler_MainTex, i.uv, mainTex.a, width);
float Outline(Texture2D tex, SamplerState state, float2 uv, float alpha, float2 outlineWidth)
{
float leftShift = SAMPLE_TEXTURE2D(tex, state, float2(uv.x + outlineWidth.x, uv.y)).a;
float rightShift = SAMPLE_TEXTURE2D(tex, state, float2(uv.x - outlineWidth.x, uv.y)).a;
float upShift = SAMPLE_TEXTURE2D(tex, state, float2(uv.x, uv.y + outlineWidth.y)).a;
float downShift = SAMPLE_TEXTURE2D(tex, state, float2(uv.x, uv.y - outlineWidth.y)).a;
return saturate((leftShift - alpha) + (rightShift - alpha) + (upShift - alpha) + (downShift - alpha));
}
// https://docs.unity3d.com/ja/Packages/com.unity.shadergraph@10.0/manual/Colorspace-Conversion-Node.html
float3 RGBtoHSV(float3 In)
{
float4 K = float4(0.0, -0.333333333, 0.666666667, -1.0);
float4 P = lerp(float4(In.bg, K.wz), float4(In.gb, K.xy), step(In.b, In.g));
float4 Q = lerp(float4(P.xyw, In.r), float4(In.r, P.yzx), step(P.x, In.r));
float D = Q.x - min(Q.w, Q.y);
float E = 1e-10;
return float3(abs(Q.z + (Q.w - Q.y)/(6.0 * D + E)), D / (Q.x + E), Q.x);
}
// https://docs.unity3d.com/ja/Packages/com.unity.shadergraph@10.0/manual/Colorspace-Conversion-Node.html
float3 HSVtoRGB(float3 In)
{
float4 K = float4(1.0, 0.666666667, 0.333333333, 3.0);
float3 P = abs(frac(In.xxx + K.xyz) * 6.0 - K.www);
return In.z * lerp(K.xxx, saturate(P - K.xxx), In.y);
}
float Monochrome(float3 col)
{
return dot(col, float3(0.299f, 0.587f, 0.114f));
}
色を変換するメソッドやアウトライン機能はメソッドにしたほうが便利なので分けます。
Macro
SDF
Function
はよく使う機能の集合なので頂点シェーダーやフラグメントシェーダーなどよりも早くincludeします。
3Dのシェーダーを作るときはFunction
にスペキュラを作るためのメソッドや陰影を作るメソッドなどを書いておき、複数Passになった時に同じ仕組みを何回も書かないでも良くなるようにします。
○○ForwardPass.hlsl
#ifndef PN_SPRITE_SIMPLELIT_FORWARDPASS
#define PN_SPRITE_SIMPLELIT_FORWARDPASS
Varyings UnlitVertex(Attributes v)
{
Varyings o = (Varyings)0;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
// 一部省略
// Lighting
float3 dlColor = _MainLightColor.rgb;
float3 dlColorHSV = RGBtoHSV(dlColor);
dlColorHSV.z = lerp(dlColorHSV.z, 1.0, _DirectionalLightPower);
dlColorHSV.z = PN_COMPARE_EPS(dlColorHSV.z);
dlColor = HSVtoRGB(dlColorHSV);
o.color.rgb *= dlColor;
return o;
}
float4 UnlitFragment(Varyings i) : SV_Target
{
// 一部省略
// Outline
float4 outlineCol = (float4)0.0;
if(_UseOutline)
{
float2 width = max(_Width, 0.0) / _WidthMult;
width /= _MainTex_TexelSize.zw;
float outline = Outline(_MainTex, sampler_MainTex, i.uv, mainTex.a, width);
outlineCol.rgb = outline.xxx * _OutlineColor.rgb;
outlineCol.a = outline.x * _OutlineColor.a * i.color.a;
}
// LastColor
float alpha = (mainTex.a * i.color.a) + outlineCol.a;
float3 lastColor = (mainTex.rgb * i.color.rgb) + outlineCol.rgb;
lastColor = saturate(lastColor + pixelLightColor);
return float4(lastColor, alpha);
}
#endif
頂点シェーダーとフラグメントシェーダーを書いてます。
そしてMacro
SDF
Function
で用意したマクロやメソッドを使っています。
Pass
ごとに多少挙動が変わってもアウトラインを作ったりの基本的な仕組みを書き間違える事故を防げます。
おわりに
メリットは何回も書いてますが
- 複数Passになった時に同じ処理を何回も書かなくてもいい
- SRP Batcherに対応させやすい
です。
命名は人それぞれ?だったりすると思います。今回CoreとなってるところがInputだったりするはずです。