【Unity】和紙表現【URP】

はじめに

今回は前回の波の演出を流用し、和紙の表現をポストエフェクトで作ってみました。

環境

Unity 2022.3.4f1
URP14.0.8

ポストエフェクトの掛け方

URPにはURP12からSwapBufferというポストエフェクトをかける時に機能があります。
今回はこちらを使用します。

以下は汎用的に使えるSwapBufferのサンプルです。

public class SamplePostEffectRendererFeature : ScriptableRendererFeature
{
    [SerializeField] private Material _material;

    private SamplePostEffectRenderPass _samplePostEffectRenderPass;

    public override void Create()
    {
        _samplePostEffectRenderPass = new SamplePostEffectRenderPass(_material);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(_samplePostEffectRenderPass);
    }
}

public class SamplePostEffectRenderPass : ScriptableRenderPass
{
    private readonly string _profileName = "SamplePostEffectRenderPass";
    
    private readonly Material _material;

    public SamplePostEffectRenderPass(Material material)
    {
        _material = material;
        renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (renderingData.cameraData.isSceneViewCamera)
        {
            return;
        }

        var cmd = CommandBufferPool.Get(_profileName);

        using (new ProfilingScope(cmd, new ProfilingSampler(_profileName)))
        {
            Blit(cmd, ref renderingData, _material);
        }

        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }
}
Blit(cmd, ref renderingData, _material);

ここでポストエフェクトを適応してます。
これをRendererDataにセットし適応したいポストエフェクトのマテリアルをセットすれば反映されます。

Shader側でカメラのレンダーテクスチャが取りたい場合は

half4 color = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearRepeat, i.texcoord);

のように_BlitTextureに書き込まれてます。

和紙のデコボコ感

和紙は洋紙と違い表面にデコボコがあります。
この表現は前回の記事の水面の光反射表現を用いたら簡単に表現できます。
水面の荒れてる表現を和紙のデコボコに使うと言うことですね。
上記の記事のように法線を作成後

half4 color = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearRepeat, i.texcoord);

float3 toLightDirW = normalize(float3(0.0f, 1.0f, 1.0f));
float diffusePower = dot(normal, toLightDirW) * 2.0f;
color.rgb *= diffusePower;
return color;

のようにディフューズ計算をカメラテクスチャを用いてかけるだけで以下のようになります。

ディフューズをかけた後

デコボコ感が出ましたね。

仕上げ

まずは色をつけます

washiColor.rgb = lerp(washiColor.rgb, _Color.rgb, _ColorRate);

こんな感じになります

色付け

今回は紙が燃えてここエフェクトを消す形にしたいのでディゾルブを追加します。

half dissolveValue = SAMPLE_TEXTURE2D(_DissolveTex, sampler_DissolveTex, i.texcoord).r;
half dissolve = step(dissolveValue, _DissolveRate);
half dissolveOutline2 = step(dissolveValue - _Dissolve2OutlineOffset - 0.05f, _DissolveRate);
half synthesisColorDissolve = smoothstep(_DissolveRate, dissolveValue - _DissolveOutlineOffset,dissolveValue - _Dissolve2OutlineOffset);
half4 outputColor = lerp(washiColor * _DissolveOutlineColor, _Dissolve2OutlineColor, synthesisColorDissolve);
outputColor = lerp(baseColor, outputColor, dissolveOutline2);
outputColor = lerp(outputColor, washiColor, dissolve);

こんな感じになります

ディゾル

燃える際は炭になる黒い箇所と燃えてる箇所の二箇所が出るようにディゾルブのオフセットを二箇所かけてアウトラインを出してます。

また手前に表示したいものと奥に表示したいものの二つが分かれると思います。
その際は一時的にレンダーテクスチャにキャプチャしてポストエフェクト時に合成します。

CommandBuffer cmd = CommandBufferPool.Get();
using (new ProfilingScope(cmd, new ProfilingSampler("GrabPass")))
{
     var sortFlags = renderingData.cameraData.defaultOpaqueSortFlags;
     var drawSettings = CreateDrawingSettings(m_ShaderTagId, ref renderingData, sortFlags);
     drawSettings.perObjectData = PerObjectData.None;

     context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref m_FilteringSettings);
}

上記のように特定のShaderTagだけ描画するコードをRenderPassに書き、ここで書き込んだRenderTextureを用いて描画します。

するとこのようにレイヤー分けができるようになります。

レイヤー分け

ディゾルブで消えてる白いキューブと消えない灰色のキューブが確認できると思います。

最後に

ここまでみていただきありがとうございました。
燃える表現はもっとブラッシュアップしたかったのですが、やはり完璧よりまず終わらせろと言うのが最も大事だと思います。
次からは記事に中身を減らしてでも記事を書く回数を増やしていこうと思います。

【URP14】 水表現を作ってみた

はじめに

今回はリアル調の水表現を作成してみました。

環境

Unity 2022.3.4f1
URP14.0.8

水を作ってみる

今回は以下の表現をしてみようと思います。

1.水中の深さ表現
2.水面の環境の反射表現
3.水面の光反射表現
4.水中の屈折表現
5.波の境界表現

水中の深さ表現

こちらは5つの中でも一番簡単です。
高さフォグというものを使用します。
こちら、水面にかけるシェーダーではなく地形の方にかけるシェーダーです。
こちらは簡単に作成できます。

float fogFactor = saturate(input.positionWS.y - _MinHeight);
finalColor = lerp(_FogColor, finalColor, fogFactor);

あらかじめ設定した高さと比べその高さ以下ならフォグの色にする。
これをするだけです。
前回のこの記事のTerrainMeshを使用して作成してみました。
パーリンノイズを用いてスクリプトでフラクタル地形メッシュを作成してみた

これで深い水底の表現が作れました。(谷表現などでも使えそうですね)

水面の環境の反射表現

こちらはCubemapを使って作ろうと思います。
はて、デザイナーでもない僕らがどうやってそんなもの用意するのか?
実はRefrectionProbeを用いたらリアルタイムに作成するのも、事前に画像に出力することも簡単にできます。
まずはTerrainをstaticにします。
RefrectionProbeが出力するCubemapにはstaticなものしか写りません。
そしてRefrectionProbeをシーンに配置します。

スケールやポジションを変更し、地形全体が囲まれるように設定します。

最後にRefrectionProbeコンポーネント内にあるBakeを押すだけです。
そしたらこのような画像がシーンのフォルダ付近にできると思います。

これをシェーダーで使用して反射を作っていこうと思います。

real4 _SpecularCubemap_HDR;
TEXTURECUBE(_SpecularCubemap);
SAMPLER(sampler_SpecularCubemap);

CubeMapはこのようにTEXTURECUBE()で囲むようです。
サンプリングはこのようになります。

// 反射を作成
float3 viewDir = normalize(_WorldSpaceCameraPos - input.positionWS); //視線ベクトルを計算
float3 refDir = reflect(-viewDir, normal); //反射ベクトルを計算
float3 refColor = SAMPLE_TEXTURECUBE_LOD(_SpecularCubemap, sampler_SpecularCubemap, refDir, 1);

視線ベクトルから反射ベクトルを作成し、それを用いてサンプリングするようです。
1と書いてるのはmipmaplevelです。
ぼかしたければ高い値を入れるとボケた反射テクスチャが取得できます。
これで反射することができました。

水面の光反射表現

よく水面がキラキラしてる場面写がドラマやアニメで使われていると思います。
今回はそれになるべく近づけたらいいなと思います。
どうやって表現するかというとheightマップを使います。
これで波の表面を荒立たせ、スペキュラをキラキラにしようと思います。
以下がNormalの生成コードです。

float height = FMB2(input.uv * 400 + _Time.x * 30, 3);
float3 normal = cross( 
float3(0,ddy(height),1),
float3(1,ddx(height),0));

このFMB2は非整数ブラウン運動というノイズ生成アルゴリズムです。
Unityは用意してないのですが、検索すれば沢山出てくると思います。

float3 normal = cross( 
float3(0,ddy(height),1),
float3(1,ddx(height),0));

こちらの箇所はこの記事を参考にして作成しました。
【Unity , shader】原神の海を再現したい
隣接したピクセルの値の勾配を取得し外積を取ると法線情報が取得できるようです。
これを用いたらこのようになりました。

水中の屈折表現

水中表現を作成します。
ディストーションのような表現を行いたいので水中の画像をキャプチャするところから始めようと思います。
URPにはOpaqueTextureなるものがあるのですが、今回は勉強の意味も込め自分でGrabPassを作ってみました。
RendererFeatureは以下の通りです。

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

public class GrabPassRendererFeature : ScriptableRendererFeature
{
    [SerializeField] private RenderPassEvent _renderPassEvent = RenderPassEvent.AfterRenderingOpaques;

    private readonly string _grabPassTextureName = "_GrabPassTexture";

    private RTHandle _grabPassTexture;

    private GrabPassRenderPass _renderPass;


    public override void Create()
    {
        _renderPass = new GrabPassRenderPass();
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(_renderPass);
    }

    public override void SetupRenderPasses(ScriptableRenderer renderer,
        in RenderingData renderingData)
    {
        _renderPass.renderPassEvent = _renderPassEvent;

        var desc = renderingData.cameraData.cameraTargetDescriptor;
        desc.depthBufferBits = 0;
        RenderingUtils.ReAllocateIfNeeded(ref _grabPassTexture, desc, FilterMode.Point, TextureWrapMode.Clamp,
            name: _grabPassTextureName);

        _renderPass.Setup(_grabPassTexture);
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        _grabPassTexture?.Release();
    }
}
var desc = renderingData.cameraData.cameraTargetDescriptor;
desc.depthBufferBits = 0;
RenderingUtils.ReAllocateIfNeeded(ref _grabPassTexture, desc, FilterMode.Point, TextureWrapMode.Clamp, name: _grabPassTextureName);

こんな感じでURP14ではRTHandleを使用し、RenderingUtils.ReAllocateIfNeededで確保するようです。
使い終わったらRelease()も忘れずに行いましょう。

RenderPassはこのようになります。

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

public class GrabPassRenderPass : ScriptableRenderPass
{
    static readonly int _grabPassTextureProperty = Shader.PropertyToID("_GrabPassTexture");

    private RTHandle _grabPassTexture;

    private RTHandle _source;

    public void Setup(RTHandle grabPassTexture)
    {
        _grabPassTexture = grabPassTexture;
    }

    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {
        CameraData cameraData = renderingData.cameraData;
        _source = cameraData.renderer.cameraColorTargetHandle;

        cmd.SetGlobalTexture(_grabPassTextureProperty, _grabPassTexture);
    }

    public override void OnCameraCleanup(CommandBuffer cmd)
    {
        _grabPassTexture = null;
        _source = null;
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (_source == null || string.IsNullOrEmpty(_source.name))
        {
            return;
        }

        CommandBuffer cmd = CommandBufferPool.Get();
        using (new ProfilingScope(cmd, new ProfilingSampler("GrabPass")))
        {
            Blitter.BlitCameraTexture(cmd, _source, _grabPassTexture);
        }
        
        Blitter.BlitCameraTexture(cmd, _source, _grabPassTexture);
        
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }
}
cmd.SetGlobalTexture(_grabPassTextureProperty, _grabPassTexture);

この箇所でシェーダーのグローバルプロパティにテクスチャをセットします。

Blitter.BlitCameraTexture(cmd, _source, _grabPassTexture);

URP14ではBlitter.BlitCameraTextureでレンダーテクスチャのコピーができるようです。
また以下のようにnulチェックをしないとエラーログが出ます。
nullになってしまうフレームがあるようです。

if (_source == null || string.IsNullOrEmpty(_source.name))
{
    return;
}

これでGrabPassが追加できました。
これを用いて水面を表示しましょう。
今までは半透明でしたが、このテクスチャを用いるので不透明で半透明の役割が出せます。

TEXTURE2D(_GrabPassTexture);
SAMPLER(sampler_GrabPassTexture);
float4 _GrabPassTexture_ST;

RenderPassでセットしたグローバルプロパティを取得します。
次にテクスチャのサンプルですが、少し工夫しないといけません。
なぜならuvがこのままだと_GrabPassTextureと異なってしまうからです。
この場合UVはスクリーンスペースに変換する必要があります。

// スクリーンスペースにUVを変換
float2 ScreenSpaceUV = input.positionHCS.xy / _ScreenParams.xy;
float4 insideColor = SAMPLE_TEXTURE2D(_GrabPassTexture, sampler_GrabPassTexture, grabTextureUV);

これで取得できました。
反射率もいじった結果こんな感じになりました。

そしてこのテクスチャを歪ませようと思います。
歪ませること自体は簡単で、UVをずらせばいいだけです。
そのUVをずらすかどうかは深度値を参照します。
深度値を比べる方法は以下の通りです。

float4 depthTexture = SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, ScreenSpaceUV);
float terrainDepthLiner = LinearEyeDepth(depthTexture.r, _ZBufferParams);
float4 proPos = ComputeScreenPos(input.positionHCS);
float currentDepth = proPos.z;
float currentDepthLiner = LinearEyeDepth(currentDepth, _ZBufferParams);
    
float depthDiff = terrainDepthLiner - currentDepthLiner;
depthDiff = saturate(depthDiff);
float distortion = sin(depthDiff + _Time.w * 2.0f);
distortion *= _DistortionValue;
    
float2 grabTextureUV = ScreenSpaceUV + distortion;
float4 insideColor = SAMPLE_TEXTURE2D(_GrabPassTexture, sampler_GrabPassTexture, grabTextureUV);
float4 depthTexture = SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, ScreenSpaceUV);
float terrainDepthLiner = LinearEyeDepth(depthTexture.r, _ZBufferParams);

こんな感じで地形の深度値を取得します。
_CameraDepthTextureはUnity側が用意してくれているデフォルトの深度値バッファです。
LinearEyeDepthは深度値を線型補完の形にしてくれるUnityが用意してくれている関数です。
書き込む深度値は以下の通りになります。

float4 proPos = ComputeScreenPos(input.positionHCS);
float currentDepth = proPos.z;
float currentDepthLiner = LinearEyeDepth(currentDepth, _ZBufferParams);

これらを以下のように差分を取り、UVにオフセットをかければ歪み表現ができます。

float depthDiff = terrainDepthLiner - currentDepthLiner;
depthDiff = saturate(depthDiff);
float distortion = sin(depthDiff + _Time.w * 2.0f);
distortion *= _DistortionValue;
    
float2 grabTextureUV = ScreenSpaceUV + distortion;
float4 insideColor = SAMPLE_TEXTURE2D(_GrabPassTexture, sampler_GrabPassTexture, grabTextureUV);

波の境界表現

海岸側だと波が白く泡立っていることがります。
今回はそのような海の境界表現を作ろうと思います。
こちらは水中の屈折表現ができれば簡単にできます。
やることは水と地形の深度値を参照し、差分が少ない箇所を浅瀬と判定すればいいだけです。

float waveOffsetValue = 0.0f;
waveOffsetValue = saturate(waveOffsetValue);
float4 whiteWavesColor = lerp(float4(1.0, 1.0f, 1.0f, 1.0f), finalColor, 0.9f);
whiteWavesColor  = lerp(whiteWavesColor, finalColor,  waveOffsetValue);
depthDiff = step(depthDiff, _WhiteWaveOffset);
finalColor = lerp(finalColor, whiteWavesColor, depthDiff);

白波を作ってる箇所は以下の通りです。

depthDiff = step(depthDiff, _WhiteWaveOffset);
finalColor = lerp(finalColor, whiteWavesColor, depthDiff);

深度値の差分が少ない箇所に色をつけるだけです。
最終的にはこんな感じになります。

最後

白波と屈折表現の箇所をもっと凝りたかったですが、まずはアウトプットをするのが大事ということでこの記事を書かせていただきました。
時間がある時にリベンジしようと思います。
ここまでみていただきありがとうございました。

参考記事まとめ

【Unity , shader】原神の海を再現したい

パーリンノイズを用いてスクリプトでフラクタル地形メッシュを作成してみた

はじめに

今回技術研究を色々行っていくにあたり適当に背景になる使い勝手のいい地形が欲しいなと思いました。
ただデザイナーでもないのでポリゴンをこねこねするのはめんどくさいしUnity以外のツールも手元にない・・
なのでワンボタンで地形メッシュを出せるツールを作ってみました。
今後これを色々改造して様々な表現を作ってみたいと思ってます!

要件

とりあえず今回は地形ぽいメッシュが作れてたらいいと思うので
1.縦横の頂点数が指定できる
2.頂点間隔が指定できる
3.目安の高さが指定できる
4.ランタイムでなくてもいつでも作成できる

この四つを満たす機能を作ってみようと思います!

パーリンノイズを用いて地形を生成する

パーリンノイズは波や雲などゲームでよく使われる乱数生成アルゴリズムの一種です。
今回はこれを用いてなだらかな地形を生成します。
Unityには

Mathf.PerlinNoise

というパーリンノイズを生成する関数が用意されているのでこちらを使いましょう。
Unityでメッシュを生成するにはMeshクラスに頂点情報、インデックス情報を入力して作成します。
まずは頂点を作ってみましょう!

//頂点を設定
for (int i = 0; i < _xVertexNum; i++)
{
   for (int j = 0; j < _zVertexNum; j++)
   {
       var noiseValue = Mathf.PerlinNoise(i * _noiseScale, j * _noiseScale);
       float y = noiseValue * _hegiht;
       vertices.Add(new Vector3(i * _xVertexLength, y, j * _zVertexLength));
        uv.Add(new Vector2(i ,j ));
    }
}

このように縦横の頂点数や頂点間隔の情報を用いて作ります。
またこのUVの箇所に関しては

uv.Add(new Vector2(i ,j ));

一つのメッシュになるので、頂点ごとにテクスチャが繰り返し貼られるように設定します。
(詳しくはリピートテクスチャなどで調べてください)
次はインデックス配列の設定です。

for (int i = 0; i < _xVertexNum - 1; i++)
{
   for (int j = 0; j < _zVertexNum - 1; j++)
   {
       triangles.Add(i * _zVertexNum + j);
       triangles.Add(i * _zVertexNum + j + 1);
       triangles.Add((i + 1) * _zVertexNum + j);
                
       triangles.Add((i + 1) * _zVertexNum + j);
       triangles.Add(i * _zVertexNum + j + 1);
       triangles.Add((i + 1) * _zVertexNum + j + 1);
   }
}

このように設定します。
Unityは左手座標系です。
なので時計回りでインデックスは配置します。
これでメッシュ作成に必要な情報は揃いました。
最終的にはこんな感じになります。

using System.Collections.Generic;
using UnityEngine;

public class TerrainMesh : MonoBehaviour
{
    [SerializeField] private MeshFilter _meshFilter;

    [SerializeField] private float _hegiht = 10.0f;

    [SerializeField] private float _noiseScale = 0.05f;

    [SerializeField] private int _xVertexNum = 100;

    [SerializeField] private int _zVertexNum = 100;

    [SerializeField] private float _xVertexLength = 1.0f;

    [SerializeField] private float _zVertexLength = 1.0f;

    private Mesh _myMesh;

    private void Start()
    {
        CreateMesh();
    }

    private void CreateMesh()
    {
        if (_myMesh != null)
        {
            DestroyImmediate(_myMesh);
        }

        _myMesh = new Mesh();
        _myMesh.name = "TerrainMesh";


        List<Vector3> vertices = new List<Vector3>();
        List<Vector2> uv = new List<Vector2>();
        List<int> triangles = new List<int>();

        //頂点を設定
        for (int i = 0; i < _xVertexNum; i++)
        {
            for (int j = 0; j < _zVertexNum; j++)
            {
                var noiseValue = Mathf.PerlinNoise(i * _noiseScale, j * _noiseScale);
                float y = noiseValue * _hegiht;
                vertices.Add(new Vector3(i * _xVertexLength, y, j * _zVertexLength));

                uv.Add(new Vector2(i, j));
            }
        }

        _myMesh.SetVertices(vertices);
        _myMesh.SetUVs(0, uv);

        //インデックスを設定
        for (int i = 0; i < _xVertexNum - 1; i++)
        {
            for (int j = 0; j < _zVertexNum - 1; j++)
            {
                triangles.Add(i * _zVertexNum + j);
                triangles.Add(i * _zVertexNum + j + 1);
                triangles.Add((i + 1) * _zVertexNum + j);

                triangles.Add((i + 1) * _zVertexNum + j);
                triangles.Add(i * _zVertexNum + j + 1);
                triangles.Add((i + 1) * _zVertexNum + j + 1);
            }
        }

        _myMesh.SetTriangles(triangles.ToArray(), 0);

        _myMesh.RecalculateNormals();

        _meshFilter.mesh = _myMesh;
    }
}

これをこんな感じにアタッチします。
メッシュを表示する場合MeshFilterとMeshRenderer及びMaterialが必要なので忘れずアタッチしましょう(RequireComponentなど使用するのもあり)
実行するとこのように表示されると思います。
ちゃんと作成できました。
しかし実行しないと表示されなかったり、パラメーターの調整がめんどくさかったりするのでエディタ拡張で1ボタンでメッシュを作成できるようにしようと思います。
そのエディタ拡張コードはこんな感じです。

#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(TerrainMesh))]
public class TerrainMeshEditor : Editor
{
    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        
        var meshFilterProperty = serializedObject.FindProperty("_meshFilter");
        EditorGUILayout.PropertyField(meshFilterProperty);
        
        var hegihtProperty = serializedObject.FindProperty("_hegiht");
        EditorGUILayout.PropertyField(hegihtProperty);
        
        var noiseScaleProperty = serializedObject.FindProperty("_noiseScale");
        EditorGUILayout.PropertyField(noiseScaleProperty);
        
        var xVertexNumProperty = serializedObject.FindProperty("_xVertexNum");
        EditorGUILayout.PropertyField(xVertexNumProperty);
        
        var zVertexNumProperty = serializedObject.FindProperty("_zVertexNum");
        EditorGUILayout.PropertyField(zVertexNumProperty);
        
        var xVertexLengthProperty = serializedObject.FindProperty("_xVertexLength");
        EditorGUILayout.PropertyField(xVertexLengthProperty);
        
        var zVertexLengthProperty = serializedObject.FindProperty("_zVertexLength");
        EditorGUILayout.PropertyField(zVertexLengthProperty);
        
        if (GUILayout.Button("メッシュ作成"))
        {
            var terrainMesh = target as TerrainMesh;
            terrainMesh.CreateMesh();
        }

        serializedObject.ApplyModifiedProperties();
    }
}
#endif

またTerrainMeshのCreateMeshをエディタ拡張でもアクセスできるようにパブリックメソッドに変更しておきます。

public void CreateMesh()

*注意:エディタ拡張のコードはEditorフォルダ配下に置くようにしましょう。
これでこのようなボタンを押すと実行中でなくとも生成できるようになりました。
TerrainMeshのStartメソッドも必要なくなったので消しちゃいましょう。
次はこれを使って色々な表現をしていこうと思います。
ここまで見て頂きありがとうございました。