2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Unity 6] RenderGraphで実装するBayer Matrix ディザリング【URPポストプロセス】

Last updated at Posted at 2025-12-07

本記事は サムザップ Advent Calendar 2025の7日目の記事です。

はじめに

本記事では、Unity 6環境において、RenderGraphを用いたBayer Matrix ディザリングのポストプロセスエフェクトを実装する手順を紹介します。

完成イメージ

Image Sequence_003_0000.jpg

本記事は下記のような方の参考になればと思います。

  • Unity 6のRenderGraphを活用した実装方法を知りたい
  • URPで独自のポストプロセスエフェクトを作ってみたい
  • Bayer Matrix ディザリングの仕組みに興味がある

検証環境

Unity 6000.0.63f1

RenderGraphの基礎的な解説については、以下の記事が非常に参考になりますので、併せてご覧ください。

Bayer Matrix ディザリングとは?

組織的ディザ法(Ordered Dithering) の一種で、限られた色数(今回は白と黒の2色)で疑似的に階調を表現する手法です。
画像の各ピクセルの明るさと、「Bayer Matrix」と呼ばれる閾値のマップを比較することで、ドットの密度を制御し、人間の目にグレーの濃淡があるように錯覚させます。

1. 数学的定義

Bayer Matrixは、小さい行列から再帰的にサイズを倍にしていくことで生成されます。

基本となる $2 \times 2$ の行列 $M_2$ は以下の通りです。

$$
M_2 = \begin{bmatrix} 0 & 2 \\ 3 & 1 \end{bmatrix}
$$

これを拡張して、サイズ $2n \times 2n$ の行列 $M_{2n}$ を生成する式は以下のようになります。
(ここで $U_n$ は全ての要素が1の $n \times n$ 行列です)

$$
M_{2n} = \begin{bmatrix} 4 M_n & 4 M_n + 2 U_n \\ 4 M_n + 3 U_n & 4 M_n + U_n \end{bmatrix}
$$

2. 今回使用する 4x4 Matrix

上記の式に基づいて $M_2$ から $M_4$ を計算すると、以下の $4 \times 4$ 行列(0〜15の整数値)が得られます。
今回のシェーダー実装では、この配列順序を使用しています。

$$
M_4 = \begin{bmatrix} 0 & 8 & 2 & 10 \\ 12 & 4 & 14 & 6 \\ 3 & 11 & 1 & 9 \\ 15 & 7 & 13 & 5 \end{bmatrix}
$$

3. 判定ロジック

実装においては、この行列の値を16で割って 0.0 ~ 1.0 の範囲に正規化し、画像の各ピクセルの輝度(グレー値)と比較します。

$$
OutputColor = \begin{cases} 1.0 \text{ (White)} & \text{if } PixelLuminance > \frac{Matrix(x, y)}{16.0} \\ 0.0 \text{ (Black)} & \text{if } PixelLuminance \le \frac{Matrix(x, y)}{16.0} \end{cases}
$$

これにより、明るい部分は閾値を超える確率が高くなるため白の密度が増え、暗い部分は黒の密度が増えることで、滑らかなグラデーションを表現できます。

イメージ画像
image.png

実装手順

本記事では、UnityのUniversal Render Pipeline (URP) のRenderGraphを使用し、Global Volumeに追加できるポストプロセスエフェクトとして実装しています。

以下のファイルを作成・実装します。

  1. BayerMatrixEffect.cs - Volume Component(Global Volumeに追加するエフェクト設定)
  2. BayerMatrixPass.cs - RenderGraphを使用したレンダーパス
  3. BayerMatrixRendererFeature.cs - Renderer Feature(レンダーパイプラインへの統合)
  4. BayerMatrixPostEffect.shader - フラグメントシェーダー(Bayer Matrixの計算処理)

1. Volume Componentの作成

BayerMatrixEffect.cs を作成します。

using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

[System.Serializable, VolumeComponentMenu("Custom/Bayer Matrix Effect")]
public class BayerMatrixEffect : VolumeComponent, IPostProcessComponent
{
    public ClampedFloatParameter intensity = new ClampedFloatParameter(0f, 0f, 1f);

    public bool IsActive() => intensity.value > 0f;
    public bool IsTileCompatible() => true;
}
  • VolumeComponentを継承し、IPostProcessComponentを実装
  • intensityパラメータでエフェクトの強度を0-1の範囲で調整可能
  • IsActive()でエフェクトが有効かどうかを判定
  • IsTileCompatible()でタイルレンダリングとの互換性を指定

2. Render Passの作成

BayerMatrixPass.cs を作成します。RenderGraphの中核となる部分です。

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.RenderGraphModule.Util;
using UnityEngine.Rendering.Universal;

public class BayerMatrixPass : ScriptableRenderPass
{
    private Material _material;
    private RenderTargetIdentifier _source;
    
    private static readonly int IdIntensity = Shader.PropertyToID("_Intensity");

    public BayerMatrixPass(Shader shader)
    {
        if (shader == null) return;
        
        _material = CoreUtils.CreateEngineMaterial(shader);
    }
    
    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        if (!_material) return;
           
        //リソース関係のデータ(カメラのテクスチャなど)を取得する。
        UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
        
        var stack = VolumeManager.instance.stack;
        var bayerMatrixEffect = stack.GetComponent<BayerMatrixEffect>();
        
        if (bayerMatrixEffect == null || !bayerMatrixEffect.IsActive()) return;
        
        //Bayer Matrixのパラメータを設定。
        _material.SetFloat(IdIntensity, bayerMatrixEffect.intensity.value);

        //カメラに映すテクスチャの取得。
        TextureHandle cameraTexture = resourceData.activeColorTexture;
           
        //一時的なテクスチャの性質を決めるDescriptorを取得。
        TextureDesc tempDesc = renderGraph.GetTextureDesc(cameraTexture);
        tempDesc.name = "_BayerMatrixTempTexture";
        
        //一時的なテクスチャの取得。
        TextureHandle tempTexture = renderGraph.CreateTexture(tempDesc);
           
        //Blitに必要なデータの用意。
        RenderGraphUtils.BlitMaterialParameters blitParameters = new(cameraTexture, tempTexture, _material, 0);
           
        //カメラバッファから一時テクスチャへのBlit。Bayer Matrixのエフェクトがかかる。 
        renderGraph.AddBlitPass(blitParameters, "Bayer Matrix Effect");
           
        //一時テクスチャからカメラテクスチャへのBlit。ただのコピー。
        renderGraph.AddCopyPass(tempTexture, cameraTexture, "Bayer Matrix Copy");
    }
    
    public void Dispose()
    {
        CoreUtils.Destroy(_material);
    }
}
  • RecordRenderGraph: 従来の Execute ではなく、ここで描画命令を記録

  • renderGraph.AddBlitPass: URPが提供するユーティリティで、簡単にBlit処理を記述

  • renderGraph.CreateTexture: 実際にテクスチャを確保するタイミングはUnity側が管理するため、ここではハンドルの作成のみを行う

3. Renderer Featureの作成

BayerMatrixRendererFeature.cs を作成します。

using UnityEngine;
using UnityEngine.Rendering.Universal;

public class BayerMatrixRendererFeature : ScriptableRendererFeature
{
    [SerializeField] private Shader _shader;
    [SerializeField] private RenderPassEvent _renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
    private BayerMatrixPass _pass;

    public override void Create()
    {
        _pass = new BayerMatrixPass(_shader)
        {
            renderPassEvent = _renderPassEvent,
        };
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (_pass == null) return;
        renderer.EnqueuePass(_pass);
    }
    
    public void OnDestroy() => _pass?.Dispose();
}
  • ScriptableRendererFeatureを継承してレンダーパイプラインに統合
  • Inspectorでシェーダーを割り当て可能に
  • InspectorでRenderPassEventを割り当て可能に(BeforeRenderingPostProcessingをデフォルトにしています。)

4. シェーダーの作成

BayerMatrixPostEffect.shader を作成します。

Shader "Custom/BayerMatrixPostEffect"
{
    Properties
    {
        _Intensity ("Intensity", Range(0, 1)) = 1.0
    }
    
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"}
        
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            HLSLPROGRAM

            #pragma vertex Vert
            #pragma fragment Frag
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"
            
            half _Intensity;

            // 4x4 Bayer Matrix (0-15)
            static const int bayerMatrix[16] = {
                 0,  8,  2, 10,
                12,  4, 14,  6,
                 3, 11,  1,  9,
                15,  7, 13,  5
            };

            float4 Frag(Varyings input) : SV_Target
            {
                // テクスチャの色を取得
                float4 col = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, input.texcoord);
                
                // グレースケール値の計算
                float gray = dot(col.rgb, float3(0.299, 0.587, 0.114));
                
                // UV座標(0-1)に画面解像度(Width, Height)を掛けて、ピクセル座標に変換する
                float2 pixelCoord = input.texcoord * _ScreenParams.xy;
                
                // ピクセル座標を整数に変換し、4で割った余りを計算してインデックス(0-3)を得る
                int x = (int)pixelCoord.x % 4;
                int y = (int)pixelCoord.y % 4;
                
                // 配列は1次元なので、y * 4 + x でインデックス化
                int index = y * 4 + x;
                
                // 閾値を取得 (0-15 の値を 16.0 で割って 0.0-1.0 に正規化)
                float threshold = bayerMatrix[index] / 16.0;
                
                // 比較して白黒判定
                float dithered = step(threshold, gray); 
                
                // 元の色とディザリング結果をIntensityで補間
                float4 result = float4(dithered, dithered, dithered, col.a);
                return lerp(col, result, _Intensity);
            }
            ENDHLSL
        }
    }
}

重要なポイント

  1. Bayer Matrixの定義
    ここでは分かりやすさのために int 配列を使用し、シェーダー内で割り算を行っています。実運用でさらなる最適化が必要な場合は、事前に 1/16.0 を乗算した float 配列を定義すると良いでしょう。

  2. ピクセル座標の計算
    input.texcoord は0〜1の範囲なので、そのまま使うと画面全体で巨大な4マスのパターンになってしまいます。_ScreenParams.xy を掛けてピクセル座標系(例: 1920x1080)に変換することで、ドット単位のディザリングが可能になります。

  3. グレースケール変換
    RGB値をグレースケールに変換しています。(0.299, 0.587, 0.114)の重み付けは、緑色が最も明るく、青色が最も暗く見えるという人間の視覚特性をベースにしたITU-R BT.601規格の重み付けです。

使用方法

1. Renderer Featureの設定

  1. Universal Renderer Asset(例:Assets/Settings/PC_Renderer.asset)を選択
  2. Inspectorで「Add Renderer Feature」をクリック
  3. 「Bayer Matrix Renderer Feature」を選択
  4. 「Shader」フィールドにBayerMatrixPostEffectシェーダーを割り当て

2. Global Volumeへの追加

  1. シーン内でGameObject > Volume > Global Volumeを作成(または既存のものを使用)
  2. Volumeコンポーネントの「Profile」にボリュームプロファイルを割り当て
  3. プロファイルの「Add Override」をクリック
  4. 「Custom > Bayer Matrix Effect」を選択
  5. 「Intensity」パラメータを調整(0-1の範囲)

3. エフェクトの調整

  • Intensity = 0: エフェクト無効(元の画像)
  • Intensity = 1: 完全なハーフトーン効果(白と黒のみ)

元画像
Image Sequence_001_0000.jpg

通常のグレースケール
Image Sequence_002_0000.jpg

Bayer Matrix
規則的なパターンのノイズによって、2色だけで階調が表現されているのが分かります。
Image Sequence_003_0000.jpg
image.png

実行結果(動画)
ポストプロセスなので、Play Mode中もリアルタイムに反映されます。
(※GIF変換の都合で色が滲んで見えますが、Unity上ではシャープな白黒です)
Movie_002.gif

さいごに

今回はUnity 6のRenderGraphを使用して、Bayer Matrix ディザリングを実装しました。

より高品質なハーフトーン手法として「誤差拡散法(Error Diffusion)」がありますが、これは「左のピクセルの誤差を右のピクセルに足す」という直列な依存関係があるため、全ピクセルを並列処理するGPU(フラグメントシェーダー)とは相性が悪く、今回は高速なBayer Matrixを採用しました。

RenderGraphを使うことで、リソース管理やパイプラインへの統合がより体系的に書けるようになっています。独自の絵作りをする際の参考になれば幸いです。

明日は、 @rikua0023 さんの記事になります。お楽しみに!!

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?