したかみ ぶろぐ

Unity成分多め

Unityで雪を作ってみる by Compute Shader

始めに

過去記事です。

shitakami.hatenablog.com

shitakami.hatenablog.com

shitakami.hatenablog.com


前に雪のシェーダーを作成しました。前回はカメラの深度を使用しましたが、今回はComputeShaderを使ってテクスチャに雪の凹凸情報を書き込み、それを元に足跡を付けることを試みました。



凹凸テクスチャを作成

今回はC#とCompute Shaderを使って、テクスチャに凹凸情報を書き込みます。赤が凹みで緑が凸になります。

以下コードです。

C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HeightMapWriter : MonoBehaviour
{
    [SerializeField]
    private ComputeShader m_computeShader;
    [SerializeField]
    private Transform m_Plane;
    
    private RenderTexture m_renderTexture;

    [Space(10)]
    [SerializeField]
    private float m_downRadius;
    [SerializeField]
    private float m_upRadius;

    [SerializeField]
    private string m_texName;

    private int m_width;
    private int m_height;

    private int m_kernelID;
    private ThreadSize m_threadSize;
    

    private Material m_planeMaterial;

    // Start is called before the first frame update
    void Start()
    {

        m_kernelID = m_computeShader.FindKernel("UpdateMap");
        m_computeShader.SetFloat("downRadius", m_downRadius);
        m_computeShader.SetFloat("upRadius", m_upRadius);
        
        uint x, y, z;

        m_computeShader.GetKernelThreadGroupSizes(m_kernelID, out x, out y, out z);

        m_threadSize = new ThreadSize(x, y, z);

        m_renderTexture = new RenderTexture(1024, 1024, 0, RenderTextureFormat.ARGB32);
        m_renderTexture.enableRandomWrite = true;
        m_renderTexture.Create();

        m_width = m_renderTexture.width;
        m_height = m_renderTexture.height;
        m_height = m_renderTexture.height;

        m_computeShader.SetFloat("width", m_width);
        m_computeShader.SetFloat("height", m_height);

        m_computeShader.SetFloat("planeScaleX", m_Plane.localScale.x);
        m_computeShader.SetFloat("planeScaleZ", m_Plane.localScale.z);

        m_planeMaterial = m_Plane.GetComponent<Renderer>().material;
        m_planeMaterial.SetTexture(m_texName, m_renderTexture);
        m_computeShader.SetTexture(m_kernelID, "_textureBuffer", m_renderTexture);
    }

    // Update is called once per frame
    void Update()
    {
        Vector2 pos = CalcPosition();
        m_computeShader.SetFloat("positionX", pos.x);
        m_computeShader.SetFloat("positionY", pos.y);

        m_computeShader.Dispatch(m_kernelID,
            m_width / m_threadSize.x,
            m_height / m_threadSize.y,
            m_threadSize.z);

        
    }

    Vector2 CalcPosition() {
    
        /*
         * Playerの座標をPlane上の座標に変換して
         * 0 ~ 1に値域を変更
         * 最後にRenderTextureの座標と合わせる
         */
        Vector3 planeScale = m_Plane.localScale;
        planeScale.x *= 10;
        planeScale.z *= 10;

        Vector3 pos = transform.position - m_Plane.position;

        pos.x += planeScale.x / 2;
        pos.z -= planeScale.z / 2;

        pos.x /= planeScale.x;
        pos.z /= planeScale.z;

        pos.x = (1 - pos.x);
        pos.z *= -1;
        
        return new Vector2(pos.x, pos.z);

    }

    private void OnDestroy() {

        if(m_planeMaterial != null)
            Destroy(m_planeMaterial);
        
    }

}


Start関数では主に、レンダーテクスチャの作成とComputeShaderの初期設定を行っています。

UpdateではCalcPosition関数でプレイヤーがいる座標をPlane上の座標に変換します。 注意すべきことはPlaneの x, y のScaleはQuadに比べて5倍になっていることです。

CalcPosition関数でプレイヤーがいる座標をPlane上で 0 ~ 1 に正規化して、Compute Shaderでテクスチャの座標に変換しています。



Compute Shader

#pragma kernel UpdateMap
RWTexture2D<float4> _textureBuffer;

#define PI 3.141592 

float positionX;
float positionY;

float downRadius;
float upRadius;

float width;
float height;

float planeScaleX;
float planeScaleZ;

[numthreads(8,8,1)]
void UpdateMap(uint3 id : SV_DispatchThreadID)
{
    float dx = (positionX - id.x / width) * planeScaleX;
    float dy = (positionY - id.y / height) * planeScaleZ;

    float dist = sqrt(dx*dx + dy*dy);
    int isDownCalc = step(dist, downRadius);

    float4 color = _textureBuffer[id.xy];

    // 赤:凹み
    float downValue = (downRadius - dist) / downRadius;
    color.x = max(pow(downValue * isDownCalc, 0.5), color.x);

    // 緑:凸
    int isUpCalc = step(dist, upRadius + downRadius);
    color.y = max(sin((dist - downRadius) / upRadius * PI) * isUpCalc, color.y);

    _textureBuffer[id.xy] = color;

  
}


C#側でプレイヤーの座標が 0 ~ 1 に正規化されているのでこの値をテクスチャの座標に直します。

そこから凹ませる半径との距離を求めて一定より離れていれば凹ませない、距離の範囲内であれば距離をもとに指数関数を使ってテクスチャのR値に書き込みます。

同じように凸の部分も距離を計算してから三角関数を使って凸の大きさをテクスチャのG値に書き込みます。



結果

f:id:vxd-naoshi-19961205-maro:20191231221204g:plain


初めは小さい赤い円とそれを囲む緑の円がありますが、歩くと混ざって黄色になります。



シェーダーの作成

シェーダーのほとんどは前回作成したシェーダーとほぼほぼ同じですが、大きく変化したドメインシェーダーを解説します。

[domain("tri")]
d2f DS(h2d_const hs_const_data, const OutputPatch<h2d_main, OUTPUT_PATCH_SIZE> i, float3 bary:SV_DomainLocation) {

    d2f o = (d2f)0;
    float3 pos = i[0].pos * bary.x + i[1].pos * bary.y + i[2].pos * bary.z;
    float2 uv = i[0].texcoord * bary.x + i[1].texcoord * bary.y + i[2].texcoord * bary.z;
    float3 normal = i[0].normal * bary.x + i[1].normal * bary.y + i[2].normal * bary.z;
    float3 worldPos = i[0].worldPos * bary.x + i[1].worldPos * bary.y + i[2].worldPos * bary.z
    
    // 表面の凹凸を計算
    float parallax = tex2Dlod(_ParallaxMap, float4(uv.xy, 0, 0));
    float parallaxHeight = (parallax - _ParallaxOffset) * _ParallaxScale
    // 表面の軌跡を計算
    float4 trailColor = tex2Dlod(_TrailTex, float4(uv.xy, 0, 0));
    int IsNotDown = step(trailColor.r, 0);

    pos.y += (parallaxHeight + trailColor.g * _TrailUpScale) * IsNotDown + 
        max(trailColor.r * _TrailDownScale, _TrailMinHeight) * (1 - IsNotDown);
    // 凹凸のテクスチャから隣の高さを取得
    float2 parallaxShiftX = { _ParallaxMap_TexelSize,  0 };
    float2 parallaxShiftZ = { 0, _ParallaxMap_TexelSize };

    half parallaxScale = _ParallaxScale * 2;

    float3 parallaxTexZ = tex2Dlod(_ParallaxMap, float4(uv.xy + parallaxShiftX, 0, 0)) * parallaxScale;
    float3 parallaxTexz = tex2Dlod(_ParallaxMap, float4(uv.xy - parallaxShiftX, 0, 0)) * parallaxScale;
    float3 parallaxTexx = tex2Dlod(_ParallaxMap, float4(uv.xy + parallaxShiftZ, 0, 0)) * parallaxScale;
    float3 parallaxTexX = tex2Dlod(_ParallaxMap, float4(uv.xy - parallaxShiftZ, 0, 0)) * parallaxScale;
    
    // 軌跡のテクスチャから隣の高さを取得
    float2 trailShiftX = { _TrailTex_TexelSize, 0 };
    float2 trailShiftZ = { 0, _TrailTex_TexelSize };
    
    float3 trailTexZColor = tex2Dlod(_TrailTex, float4(uv.xy + trailShiftX, 0, 0));
    float3 trailTexzColor = tex2Dlod(_TrailTex, float4(uv.xy - trailShiftX, 0, 0));
    float3 trailTexxColor = tex2Dlod(_TrailTex, float4(uv.xy + trailShiftZ, 0, 0));
    float3 trailTexXColor = tex2Dlod(_TrailTex, float4(uv.xy - trailShiftZ, 0, 0));
    
    half trailUpScale = _TrailUpScale * 2;
    half trailDownScale = _TrailDownScale * 2;
    
    float texX = trailTexXColor.r * trailDownScale * (1 - step(trailTexXColor.r, 0))
        + step(trailTexXColor.r, 0) * (parallaxTexX.r + trailTexXColor.g * trailUpScale);
    float texx = trailTexxColor.r * trailDownScale * (1 - step(trailTexxColor.r, 0))
        + step(trailTexxColor.r, 0) * (parallaxTexx.r + trailTexxColor.g * trailUpScale);
    float texZ = trailTexZColor.r * trailDownScale * (1 - step(trailTexZColor.r, 0))
        + step(trailTexZColor.r, 0) * (parallaxTexZ.r + trailTexZColor.g * trailUpScale);
    float texz = trailTexzColor.r * trailDownScale * (1 - step(trailTexzColor.r, 0))
        + step(trailTexzColor.r, 0) * (parallaxTexz.r + trailTexzColor.g * trailUpScale);

    float3 du = { 0, _NormalScaleFactor * (texX - texx), 1 };
    float3 dv = { 1, _NormalScaleFactor * (texz - texZ), 0 };

    o.pos = UnityObjectToClipPos(float4(pos, 1));
    o.texcoord = uv;
    o.normal = normal;
    o.worldPos = worldPos;
    
    return o;
}


簡単に解説します。

step関数を使用して凹み(赤色)があるかを確かめます。

 int IsNotDown = step(trailColor.r, 0)


そして、この変数を使うことで凹みと凸を分けて計算し、加えて元々の地面の凸凹と合わせています。赤色があると凹みになるので凸の計算はしません。

pos.y += (parallaxHeight + trailColor.g * _TrailUpScale) * IsNotDown + 
        max(trailColor.r * _TrailDownScale, _TrailMinHeight) * (1 - IsNotDown);


法線の計算も先程と同様に隣の色を取得してその色の赤色を調べて計算を行います。

 // 凹凸のテクスチャから隣の高さを取得
    float2 parallaxShiftX = { _ParallaxMap_TexelSize,  0 };
    float2 parallaxShiftZ = { 0, _ParallaxMap_TexelSize };

    half parallaxScale = _ParallaxScale * 2;

    float3 parallaxTexZ = tex2Dlod(_ParallaxMap, float4(uv.xy + parallaxShiftX, 0, 0)) * parallaxScale;
    float3 parallaxTexz = tex2Dlod(_ParallaxMap, float4(uv.xy - parallaxShiftX, 0, 0)) * parallaxScale;
    float3 parallaxTexx = tex2Dlod(_ParallaxMap, float4(uv.xy + parallaxShiftZ, 0, 0)) * parallaxScale;
    float3 parallaxTexX = tex2Dlod(_ParallaxMap, float4(uv.xy - parallaxShiftZ, 0, 0)) * parallaxScale;
    
    // 軌跡のテクスチャから隣の高さを取得
    float2 trailShiftX = { _TrailTex_TexelSize, 0 };
    float2 trailShiftZ = { 0, _TrailTex_TexelSize };
    
    float3 trailTexZColor = tex2Dlod(_TrailTex, float4(uv.xy + trailShiftX, 0, 0));
    float3 trailTexzColor = tex2Dlod(_TrailTex, float4(uv.xy - trailShiftX, 0, 0));
    float3 trailTexxColor = tex2Dlod(_TrailTex, float4(uv.xy + trailShiftZ, 0, 0));
    float3 trailTexXColor = tex2Dlod(_TrailTex, float4(uv.xy - trailShiftZ, 0, 0));
    
    half trailUpScale = _TrailUpScale * 2;
    half trailDownScale = _TrailDownScale * 2;
    
    float texX = trailTexXColor.r * trailDownScale * (1 - step(trailTexXColor.r, 0))
        + step(trailTexXColor.r, 0) * (parallaxTexX.r + trailTexXColor.g * trailUpScale);
    float texx = trailTexxColor.r * trailDownScale * (1 - step(trailTexxColor.r, 0))
        + step(trailTexxColor.r, 0) * (parallaxTexx.r + trailTexxColor.g * trailUpScale);
    float texZ = trailTexZColor.r * trailDownScale * (1 - step(trailTexZColor.r, 0))
        + step(trailTexZColor.r, 0) * (parallaxTexZ.r + trailTexZColor.g * trailUpScale);
    float texz = trailTexzColor.r * trailDownScale * (1 - step(trailTexzColor.r, 0))
        + step(trailTexzColor.r, 0) * (parallaxTexz.r + trailTexzColor.g * trailUpScale);

    float3 du = { 0, _NormalScaleFactor * (texX - texx), 1 };
    float3 dv = { 1, _NormalScaleFactor * (texz - texZ), 0 }; 



結果

f:id:vxd-naoshi-19961205-maro:20191216033503g:plain

ちゃんと雪を押し出した感じの盛り上がりができました。



これからの改良点

f:id:vxd-naoshi-19961205-maro:20191231224904p:plain

凹みの淵の法線が上向き(緑色)になっていたり、

f:id:vxd-naoshi-19961205-maro:20191231225020p:plain

まちまちに途切れていたりする箇所があるので何かしら解決策が見つかれば改善したいです。


また、チカチカ反射させる際にポストエフェクトを使ってもうちょっと良い見栄えに出来ないかなって思ったりしています。



最後に

カメラを使わずにComputeShaderでやる方法を行いました。

これの利点は改良しやすい、Planeの大きさなどを考慮して計算できるなどがありますが、欠点としてはVRChatなどに持っていけない、手形や足型の凹みが作れないことです。

どちらにも良いところ悪いところはありますが、ゲームで使うとしたらComputeShaderではないかなと思います。

最後に、ギリギリ2019年内でまとめきれてよかったと思います。 来年はもっと更新頻度増やしていきたいです。