0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Unity】Shaderのスクリプトをどう分割するか

Posted at

はじめに

Shaderのコードは分割したほうが使い回しできる箇所が多くなって便利になると思っていますが具体的にどう分割してるのか書かれた記事を見たことが無いので書こうと思いました。

前提条件として以下があります。

  • Unity環境
  • HLSLを使う(別にCgでも問題ないけど今回の例がHLSL)

ソースコード

いつもこうしてる

  • ○○.shader
  • ○○Core.hlsl
  • ○○Macro.hlsl
  • ○○SDF.hlsl
  • ○○Function.hlsl
  • ○○ForwardPass.hlsl

上から順番にincludeされて行きます。
Unityの標準で存在してるShaderも似たような分割になってるはず…

image.png
このように分割するメリットは機能ごとの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だったりするはずです。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?