Shader上で1つ前のフレームの計算結果を使う


概要

Unityでshaderを使って実装しているときに、1つ前のフレームで計算した値を使って今のフレームのピクセルの色を計算する必要がありました
が、shaderの知識がない自分はかなり手こずったので、解決した方法を共有します

実現したかったこと

「《パラメータX》と 《前フレームの計算結果R'》 をごにょごにょして《現フレームのピクセル色》を計算」すること

1つのshaderでは実現できませんでした

shader上の変数に値を入れておけば次のフレームでもその値が使えるというものではなく、shaderはフレーム間で状態を保持しません
となると、値をshaderの外に出しておく必要があるのですが、1つのshaderの出力(frag関数の返り値)は1つだけです
よって、1つのshaderでは計算結果を保存する(出力1)とピクセルの色を指定する(出力2)の両方を実現できません

shaderの分割&データ保持用textureの追加で解決しました

  • shaderを2つに分けました。shader1の結果はRGBのR値(GでもBでも良い)として小さなtextureに出力します
    • shader1: 《パラメータX》 と 《前フレームの計算結果R'》 から 《現フレームの計算結果R》 を計算 → (小さなtextureに)出力
    • shader2: 《現フレームの計算結果R》 から 《現フレームのピクセル色》 を計算 → (色を描画したいtextureに)出力
  • 小さなtextureは2つ用意して、《前フレームの計算結果R'》を持つtextureと《現フレームの計算結果R》を持つtextureが毎フレーム交互に変わるようにスクリプトで操作します

具体的に

必要なもの

  • 値保存用material (_dataMaterial)
    • 値の計算を行って結果を出力するshader (DataCalcShader)を設定する
  • 描画用material (_paintMaterial)
    • 保存された値を取得してピクセル色を出力するshader (PaintShader)を設定する
  • 各materialにパラメータを設定するスクリプト

コード

各materialにパラメータを設定するスクリプト

以下のクラスはStart()内で初期化し、Update()内でCalc()を呼び出して使うことを想定しています
Graphics.Blitでmaterialを指定すると、textureAを_MainTexとしてmaterialに紐づくshaderを実行し結果をtextureBに描画することが可能です

using System;
using UnityEngine;

public class Sample
{
    private Material _dataMaterial;
    private Material _paintMaterial;
    private RenderTexture _dataTex1;
    private RenderTexture _dataTex2;

    public Sample(Material dataMaterial, Material paintMaterial)
    {
        _dataMaterial = dataMaterial;
        _paintMaterial = paintMaterial;

        // 1x1のRenderTextureを2つ作成しておく
        _dataTex1 = new RenderTexture(1, 1, 0, RenderTextureFormat.ARGB32);
        _dataTex1.Create();
        _dataTex2 = new RenderTexture(1, 1, 0, RenderTextureFormat.ARGB32);
        _dataTex2.Create();
    }

    public void Calc(float paramX)
    {
        // 計算のためのパラメータを設定
        _dataMaterial.SetFloat("_ParamX", paramX);

        // フレーム毎に交互に以下のif-elseブロックを呼び出す
        if (Time.frameCount % 2 == 0)
        {
            // _dataTex1を "_MainTex" として_dataMaterialに紐づくshaderの計算を実行し、
            // 結果を_dataTex2に出力(保存)
            Graphics.Blit(_dataTex1, _dataTex2, _dataMaterial, -1);

            // _paintMaterialのshaderの計算では_dataTex2を使う
            _paintMaterial.SetTexture("_DataTex", _dataTex2);
        }
        else
        {
            // _dataTex2を "_MainTex" として_dataMaterialに紐づくshaderの計算を実行し、
            // 結果を_dataTex1に出力(保存)
            Graphics.Blit(_dataTex2, _dataTex1, _dataMaterial, -1);

            // _paintMaterialのshaderの計算では_dataTex1を使う
            _paintMaterial.SetTexture("_DataTex", _dataTex1);
        }
    }

}

DataCalcShaderのfrag関数

float4 frag (v2f i) : SV_Target
{
     // _MainTexのR値 = 前フレームの計算結果
     float previousResult = tex2D(_MainTex, i.uv).r;

     // 計算(ここではlerp)を行う
     float result = lerp(previousResult, _ParamX, 0.5);

     // 結果をR値として保存する
   return float4(result, 0, 0, 0);
}

PaintShaderのfrag関数

float4 frag (v2f i) : SV_Target
{
     // R値として保存されていたデータを取り出す
     float result = tex2D(_DataTex, float2(0, 0)).r;

     // resultからピクセルの色を計算してreturnする
}