Aizu-Progressive xr Lab blog

会津大学のVR部であるA-PxLの部員が持ち回りで投稿していくブログです。部員がそれぞれVRに関する出来事やVRにちなんだことについて学んだことを書いていきます。

連絡はサークルTwitterのDMへお願いします。
「面白法人カヤックVR分室」としても活動しています。詳細はこちら

ComputeShaderで実装するGray-Scotteモデル

f:id:aizu-vr:20200810180146g:plain

始めに

こんにちは、なおしです。


積読していた「作って動かすALife」を読もうとした際にちょうどブログ当番が回ってきたので、復習がてらComputeShaderを使って先ほどの本で最初に紹介されるGray-Scottモデルを実装してみようと思います。


ここではComputeShaderについては解説しませんので、もしわからなかった場合はこれらのサイトを参考にしてください。

neareal.com

neareal.com


また、このプロジェクトはgithubに上げています。

github.com



Gray-Scottモデルについて

書籍の内容を出来るだけ噛み砕いて解説しようと思います。 間違いがあったり、話が飛躍してしまうかもしれませんがご了承下さい。


自己組織化

例えば雪の結晶や雲の形状、地面のひび割れなど完成形がわかっていないにも関わらずある構造を形成することがあります。このことを自己組織化といいます。


反応拡散系

空間に分布された一種あるいは複数種の物質の濃度が、物質がお互いに変化し合うような局所的な化学反応と空間全体に物質が広がる拡散の二つのプロセスの影響によって変化する様子を数理モデル化したものである。(wikipediaより引用)

これにより、自然界の様々なパターンを自己組織的なロジックで作れることが示されています。


Gray-Scottモデル

Gray-Scottモデルは反応拡散系モデルの一つで、空間内で2種類の物質UとVを使用して化学反応と拡散過程を表したモデルです。


この物質Uは一定の補充率で追加され、逆に物質Vは一定の減量率で除かれます。

Gray-Scottモデルでは、1つの物質Uと2つの物質Vが反応して3つの物質Vになります。 また、一定の減量率で除かれた物質Vは不活性生成物Pになり、これ以上の反応をせず変化しない物質になります。


U + 2V \rightarrow 3V \\
V \rightarrow P


2種類の物質UとVは異なる拡散係数を持ち、物質Uは物質Vに比べて素早く空間全体に拡散します。



数式・解説

先程解説した物質Uと物質Vの変化量は次の数式で表されます。

Gray-Scottモデルの反応拡散方程式
\dfrac{\partial u}{\partial t} = Du  \Delta u - uv^2 + f(1 - u) \\
\dfrac{\partial v}{\partial t} = Dv  \Delta v +uv^2 - v(f + k)



\dfrac{\partial u}{\partial t}\dfrac{\partial v}{\partial t}「時間に対するU, Vの変化量」という意味です。

DuDvは拡散定数でUとVの拡散の速さを表します。


次に\Delta u\Delta vについて解説します。

今回想定している空間は2次元なので次のように表せます。

\Delta u = \dfrac{\partial^2}{\partial x^2}u + \dfrac{\partial^2}{\partial y^2}u

  \dfrac{\partial ^2}{\partial ^2 x}u はどういう意味かというと2階微分なのでx軸に対する変化量の変化量となります。

変化量の変化量と言われてもぱっとしないと思うので軽く説明します。

ある地点xの物質Uの濃度を  u(x) とします。

右隣の地点x+1との濃度の変化量は  u(x + 1) - u(x) となります。

同様にして左隣の地点x - 1との変化量は  u(x) - u(x - 1) です。

この2つの変化量の差を求めることで変化量の変化量を求めることが出来ます。


 \dfrac{\partial ^2}{\partial ^2 x}u = (u(x + 1) - u(x)) - (u(x) - u(x - 1)) = u(x + 1) + u(x - 1) - 2u(x)


同様にしてy軸も考慮して以下のようになります。


\Delta u = \dfrac{\partial^2}{\partial x^2}u + \dfrac{\partial^2}{\partial y^2}u \\
= u(x + 1, y) + u(x - 1, y) + u(x, y + 1) +u(x, y - 1) - 4u(x, y)


次に uv ^2について解説します。

先ほど解説したこの式がこの部分に該当します。


U + 2V \rightarrow 3V \\
V \rightarrow P

1つの物質Uと2つの物質Vが反応することにより物質Uは減るので\dfrac{\partial u}{\partial t}では減算、逆に物質Vは増えるので\dfrac{\partial v}{\partial t}では加算します。


最後に f(1 - u) v(f + k)は物質Uの補充率、物質Vの補充率を表します。

物質Uではuの値が1に近づくにつれて値は0になります。物質Vの場合はvの値が0より大きい場合、常に減り続けます。



実装

実装については作って動かすALifeを参考に作成しています。


C#プログラム

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

public class Simulator : MonoBehaviour
{
    private RenderTexture m_uvTexture;

    [SerializeField] 
    private ComputeShader m_grayScottCalculator;

    [Header("モデルの各パラメータ")]
    [SerializeField]
    private float m_f = 0.04f;

    [SerializeField]
    private float m_k = 0.06f;

    [SerializeField]
    private float m_simulateSpeed = 1f;

    [SerializeField]
    private int m_step = 8;

    [SerializeField]
    private float m_gridSize = 0.01f;

    [SerializeField]
    private Texture2D m_noiseTexture;

    [Space(10)]
    [SerializeField]
    private float m_Du = 2e-5f;

    [SerializeField]
    private float m_Dv = 1e-5f;
    

    [SerializeField]
    [Header("初期化項目")] 
    private int m_initialQuadSize;
    

    [SerializeField] 
    private int m_textureSize;

    public int TextureSize { get { return m_textureSize; } }

    private int m_updateGrayScottKernel;
    private Vector3Int m_groupSize;


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

        InitializeTexture();

        InitializeUpdateGrayScott();

        // 生成したRenderTextureをマテリアルに設定
        GetComponent<Renderer>().sharedMaterial.SetTexture("_MainTex", m_uvTexture);
    }

    void Update() {

        for(int i = 0; i < m_step; ++i)
            UpdateGrayScott();

    }

    private void CreateTexture() {

        m_uvTexture = new RenderTexture(m_textureSize, m_textureSize, 0, RenderTextureFormat.RG32);
        m_uvTexture.wrapMode = TextureWrapMode.Clamp;
        m_uvTexture.enableRandomWrite = true;
        m_uvTexture.Create();

    }

    private void InitializeTexture() {
        
        int initializeTextureKernel = m_grayScottCalculator.FindKernel("InitializeTexture");

        // テクスチャの初期状態を設定
        m_grayScottCalculator.SetInt("InitialQuadSize", m_initialQuadSize);
        m_grayScottCalculator.SetInt("minThreadhold", m_textureSize / 2 - m_initialQuadSize / 2);
        m_grayScottCalculator.SetInt("maxThreadhold", m_textureSize / 2 + m_initialQuadSize / 2);
        m_grayScottCalculator.SetTexture(initializeTextureKernel, "Texture", m_uvTexture);
        m_grayScottCalculator.SetTexture(initializeTextureKernel, "NoiseTexture", m_noiseTexture);

        m_grayScottCalculator.GetKernelThreadGroupSizes(initializeTextureKernel, out uint x, out uint y, out uint z);

        m_grayScottCalculator.Dispatch(initializeTextureKernel,
            m_textureSize / (int)x,
            m_textureSize / (int)y,
            (int) z);

    }

    private void InitializeUpdateGrayScott() {

        m_updateGrayScottKernel = m_grayScottCalculator.FindKernel("UpdateGrayScotte");
        
        // 実行する際のグループサイズを求める
        m_grayScottCalculator.GetKernelThreadGroupSizes(m_updateGrayScottKernel, out uint x, out uint y, out uint z);
        m_groupSize = new Vector3Int(m_textureSize / (int)x, m_textureSize / (int)y, (int)z);

        // GrayScottのパラメータを設定
        m_grayScottCalculator.SetFloat("f", m_f);
        m_grayScottCalculator.SetFloat("k", m_k);
        m_grayScottCalculator.SetFloat("dt", m_simulateSpeed);
        m_grayScottCalculator.SetFloat("dx", m_gridSize);
        m_grayScottCalculator.SetFloat("Du", m_Du);
        m_grayScottCalculator.SetFloat("Dv", m_Dv);
        m_grayScottCalculator.SetInt("size", m_textureSize);

        m_grayScottCalculator.SetTexture(m_updateGrayScottKernel, "Texture", m_uvTexture);
        
    }

    private void UpdateGrayScott() {

        m_grayScottCalculator.Dispatch(m_updateGrayScottKernel, m_groupSize.x, m_groupSize.y, m_groupSize.z);

    }

}


ComputeShader

#pragma kernel InitializeTexture
#pragma kernel UpdateGrayScotte


RWTexture2D<float2> Texture;
Texture2D<float4> NoiseTexture;

int InitialQuadSize;
int minThreadhold;
int maxThreadhold;

[numthreads(8, 8, 1)]
void InitializeTexture(uint3 id : SV_DispatchThreadID) {
    
    float2 initValue;

    if(minThreadhold <= id.x && id.x <= maxThreadhold &&
        minThreadhold <= id.y && id.y <= maxThreadhold)
        initValue = float2(0.5, 0.25);
    else
        initValue = float2(1, 0);

    initValue.x += NoiseTexture[id.xy].x * 0.1;
    initValue.y += NoiseTexture[id.xy].y * 0.1;

    Texture[id.xy] = initValue;

}

float dt;
float dx;
float Du;
float Dv;
float f;
float k;
int size;

[numthreads(8, 8, 1)]
void UpdateGrayScotte(uint3 id : SV_DispatchThreadID) {

    float u = Texture[id.xy].x;
    float v = Texture[id.xy].y;

    float2 laplacian =
        Texture[id.xy + uint2(-1, 0)] +
        Texture[id.xy + uint2(1, 0)] +
        Texture[id.xy + uint2(0, -1)] +
        Texture[id.xy + uint2(0, 1)] - 4 * Texture[id.xy];

    laplacian /= (dx*dx);
    
    float dudt = Du * laplacian.x - u * v * v + f * (1.0 - u);
    float dvdt = Dv * laplacian.y + u * v * v - (f + k) * v;
    
    Texture[id.xy] = float2(u + dt * dudt, v +dt * dvdt);
}



解説


初期化

平面上に2つの物質u、vが存在します。それらを管理するために、RenderTextureを作成します。 フォーマットがRenderTextureFormat.RG32となっている点に注意してください。

また、m_uvTextureのR成分を物質U、G成分を物質Vとします。

        // Simulator.csの74行目から
    private void CreateTexture() {

        m_uvTexture = new RenderTexture(m_textureSize, m_textureSize, 0, RenderTextureFormat.RG32);
        m_uvTexture.wrapMode = TextureWrapMode.Clamp;
        m_uvTexture.enableRandomWrite = true;
        m_uvTexture.Create();

    }



次に、RenderTextureに初期状態を書き込みます。 初期状態はテクスチャの中央に指定された幅で(u, v) = (0.5, 0.25)、それ以外は(u, v) = (1, 0)にします。 下の画像が実行した結果となります。

// GrayScottCalclator.computeの13行目から
void InitializeTexture(uint3 id : SV_DispatchThreadID) {
    
    float2 initValue;

    if(minThreadhold <= id.x && id.x <= maxThreadhold &&
        minThreadhold <= id.y && id.y <= maxThreadhold)
        initValue = float2(0.5, 0.25);
    else
        initValue = float2(1, 0);

f:id:aizu-vr:20200810174051j:plain



最後にテクスチャにランダム性を追加します。

今回はスクリプトからではなく、事前にランダムな値が書かれたノイズテクスチャを作成します。 ノイズテクスチャの作成は以下のサイトのプログラムをお借りして作成しました。

【Unity】ノイズと創造をつなぐ - Qiita

f:id:aizu-vr:20200810174537p:plain



あとはこのノイズテクスチャの値を先程の初期状態に足し合わせます。 結果の画像はあまり違いが分かりにくいのでここでは載せません。

        // GrayScottCalculator.computeの23行目から
    initValue.x += NoiseTexture[id.xy].x * 0.1;
    initValue.y += NoiseTexture[id.xy].y * 0.1;

    Texture[id.xy] = initValue;



また、状態の更新を行う際に必要となる各パラメータは実行前にComputeShaderに設定します。

        // Simulator.csの103行目から
    private void InitializeUpdateGrayScott() {

        m_updateGrayScottKernel = m_grayScottCalculator.FindKernel("UpdateGrayScotte");
        
        // 実行する際のグループサイズを求める
        m_grayScottCalculator.GetKernelThreadGroupSizes(m_updateGrayScottKernel, out uint x, out uint y, out uint z);
        m_groupSize = new Vector3Int(m_textureSize / (int)x, m_textureSize / (int)y, (int)z);

        // GrayScottのパラメータを設定
        m_grayScottCalculator.SetFloat("f", m_f);
        m_grayScottCalculator.SetFloat("k", m_k);
        m_grayScottCalculator.SetFloat("dt", m_simulateSpeed);
        m_grayScottCalculator.SetFloat("dx", m_gridSize);
        m_grayScottCalculator.SetFloat("Du", m_Du);
        m_grayScottCalculator.SetFloat("Dv", m_Dv);
        m_grayScottCalculator.SetInt("size", m_textureSize);

        m_grayScottCalculator.SetTexture(m_updateGrayScottKernel, "Texture", m_uvTexture);
        
    }



状態の更新

C#スクリプトの方ではとても単純でstep回計算を行います。

        // Simulator.csの67行目から
    void Update() {

        for(int i = 0; i < m_step; ++i)
            UpdateGrayScott();

    }


次にComputeShaderの解説します。

// GrayScottCalculator.computeの38行目から
void UpdateGrayScotte(uint3 id : SV_DispatchThreadID) {

    float u = Texture[id.xy].x;
    float v = Texture[id.xy].y;

    float2 laplacian =
        Texture[id.xy + uint2(-1, 0)] +
        Texture[id.xy + uint2(1, 0)] +
        Texture[id.xy + uint2(0, -1)] +
        Texture[id.xy + uint2(0, 1)] - 4 * Texture[id.xy];

    laplacian /= (dx*dx);
    
    float dudt = Du * laplacian.x - u * v * v + f * (1.0 - u);
    float dvdt = Dv * laplacian.y + u * v * v - (f + k) * v;
    
    Texture[id.xy] = float2(u + dt * dudt, v + dt * dvdt);
}

始めに座標 (x, y) に対するUとVの値を取り出します。

次に \Delta u \Delta vを求めて変数laplacianに保存しています。また、計算する際にUとVを一度に計算しています。

求められたlaplacianを隣の成分との距離 dxの2乗で割ります。

後は、前述した計算式通りに進めて最後に1回のシミュレーションの時間 dtを掛けてテクスチャを更新します。



結果

物質Uが赤色、物質Vが緑色です。また、初期化項目のInital Quad Sizeは50、Texture Sizeは256で実行しています。

f:id:aizu-vr:20200811235922g:plain



このままだとおどろおどろしいので、シェーダーで色を変更します。

シェーダープログラム

Shader "Unlit/ColorShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return fixed4(col.r, col.r, col.r, 1);
            }
            ENDCG
        }
    }
}


やっていることは単純で、赤色を白色に変換しているだけです。こうすることで次のように変わります。

f:id:aizu-vr:20200812001455g:plain



様々なパターンの生成

Gray-ScottモデルではパラメータFとKを変更することで様々なパターンを生成することが可能です。 以下書籍で紹介されたパターンです。


F = 0.022, K = 0.051 ストライプ

f:id:aizu-vr:20200812004533j:plain


F = 0.012 K = 0.05 泡

f:id:aizu-vr:20200812005153g:plain


このほかにも生成できるパターンがありますが、詳しくは書籍を参照してください。



まとめ

本来はComputeShaderの復習として勉強したGrayScottでしたがかなり面白い内容でした。

また、テッセレーション+VTFすることで模様に合わせた凸凹を作れて楽しいです。

f:id:aizu-vr:20200812115703j:plain

f:id:aizu-vr:20200812120641g:plain

今はただ書籍通り実装しただけですが、これから少し遊びを追加したりCustomRenderTextureで実装しようかなと思います。


追記

CustomRenderTextureバージョンの記事も書きました。

shitakami.hatenablog.com



参考

言わずもがなこの書籍を何度も読みながら実装を行いました。


Gray-Scottの解説はこちらのサイトも参考にさせて頂いています。

qiita.com


また、数式を理解するために波動方程式の解説を参考にしました。

edom18.hateblo.jp

qiita.com

女の子になってリモート勉強会を行った感想

f:id:aizu-vr:20200710160633p:plain


始めに

こんにちは、なおしです。

コロナウイルスが今年の3月ごろから流行りだし、今現在も大学では部活動が制限されている状況が続いています。

そうなると、新入生向けの勉強会の開催が困難になりリモートで行うことになりました。

何か新入生を楽しませることが出来ないかと考えVTuberとなって勉強会を行いました。



これを行おうと決めたきっかけ

今年の3、4月ごろにこれらの記事を拝見しました。

www.moguravr.com

www.moguravr.com


これらの記事を拝見したときは私もやりたいとは思っていませんでした。

しかし新入生歓迎会が中止になったことでやらないといけないと考えなおしました。

例年であれば、大学の様々な部活がこれまでの部活動を紹介し新入生とコミュニケーションをとる機会がありました。

しかし、今年はclusterでのバーチャル開催となり例年より部活と新入生との距離があったのではないかと感じました。

そこで、新入生にA-PxLに興味を持ってもらうこととどんなことが出来るのかという指標を示すためにバ美肉して勉強会をすることに決めました。



作成したもの

以下の動画は会津大学で開催された「コロナウイルスにITで立ち向かおう」ハッカソンに提出した動画です。 こちらの動画は開発途中のものとなります。

https://www.youtube.com/watch?v=D7PUiIpVvzQ


Kawaii-Jyuku 会津大学筋肉増強課


VR空間上にスライドを表示して指し棒を使って説明するようにしました。

また、新入生が書いたプログラムを確認する必要があったのでPC画面をVR空間上に表示しました。



実際に行ってみて

個人的な感想

私自身の感想としては身振り手振りを使って会話ができるのでとても楽しかったです。

ただし、どうしようもない問題がありました。

  • 勉強会中はずっと立ちっぱなし(約3時間)
  • 飲食できない
  • HMDをずっとつけなければいけない

1,2つは頑張れば大丈夫でしたが、何度か気分を悪くしたことはありました。

そこだけは慣れが必要かもしれません。



アンケートで頂いた感想

勉強会終了後アンケートを取りました。 また、新入生だけでなく同席した先輩もアンケートに回答して頂きました。

アンケートで集めた回答は以下の通りです。

  1. 勉強会をVTuberで行って良かったか
  2. 勉強会の内容は良かったか
  3. 資料、サンプルが役に立ったか
  4. 演習の難易度は難しかったか
  5. 勉強会の雰囲気は良かったか
  6. 普通の授業と比べて良かったか、悪かったか等の感想


ここでは、1、2、5、6番の回答についてまとめたいと思います。



勉強会をVTuberで行って良かったか

「勉強会をVTuberで行って良かったか」では悪い回答はなく良かったと回答して頂けました。

f:id:aizu-vr:20200710184428p:plain


勉強会の内容は良かったか

こちらはすべての回答でとても良かったを頂きました。


勉強会の雰囲気は良かったか

こちらは「とても良かった」と「良かった」の回答が半々となりました。勉強会自体でどこかしらの改善点があったかもしれません。

f:id:aizu-vr:20200710185043p:plain


普通の授業と比べて良かったか、悪かったか等の感想

全体的な感想として楽しくできたこととコミュニケーションがしやすかったことが挙げられておりました。

アンケートの回答では悪かった点はなかったです。



バーチャルで勉強会をしての利点と欠点

アンケートと私自身が感じたことを総括すると

  • 新入生と先輩とのコミュニケーションの敷居が下がる
  • バーチャルで行うことで面白みがある

と感じました。ただ個人的な感想ではありますが、授業がわかりやすくなる、内容の理解がしやすくなる等に大きく影響を与えることはないと考えます。

新入生にとってわかりやすいものにするためには資料や説明、解説の仕方によるものであり、それはバーチャルでやろうと対面で授業しようと変わらない部分だと思います。ただし、バーチャルで行うことで画面の変化が増えて勉強会に飽きにくくなるのではないかと感じました。



欠点としてあげられることはこの2つがあると感じました。

  • 資料の準備に加えシステムの調整などの手間が増える
  • 勉強会中に機材のトラブル等でストップすることがある

前者については資料の作成はもちろん、そしてシステム自体に致命的なバグがないか、勉強会が進行できなくなるバグがないかを確認しなくてはいけないのでかなり準備が必要だと感じました。

後者についてはリモートで行う以上起こりうる問題ではありますが、VR機材の接続不良などでさらに勉強会が止まったりすることが何度か起こりました。


まとめ

バ美肉で勉強会をするのは予想以上に大変でした。しかし、それによって対面での授業が出来ない環境でも楽しく勉強会を行うことが出来たかと思います。

これからはリモートでの発表や別の勉強会等でバ美肉をするかもしれませんので引き続き改良をしていこうかと思います。


また、このバ美肉についてはnoteや個人のブログにまとめておりますので拝見して頂くと幸いです。

note.com

note.com

shitakami.hatenablog.com

Unityでオンラインゲームを作れるPhotonのすすめ

はじめに

こんにちは、学部2年の星野です。タイトルにもある通り、春休みの間Photonというサービスを使いUnityでオンラインゲームを作っていました。サーバーの知識もほとんど持たない自分でも短期間でオンラインゲームを作ることができました。Photonの使い方についての記事はネット上にたくさんあるので、今回はPhotonを使ってみての感想を中心に書いていきたいと思います。

Photonとは

f:id:aizu-vr:20200628225108j:plain

オンラインゲームを簡単に作れるサービス
ゲームサーバーを自分で構築する必要がない
無料プランもある

なぜPhotonを選んだか

Photonと同じようなサービスは他にも存在しています。(モノビットエンジンなど) その中からPhotonを選んだ一番の理由は

ネット上に情報が多い

ということです。 自分の調べ方が悪いせいかもしれませんが、同じようなサービスの中でネット上にある情報量がダントツでした。

Photonを使ってみた感想

分かりやすくて簡単

Photonへの接続やルーム処理などは数行のスクリプトで行うことができます。 ネット上に使用例がたくさんある上に、非常に分かりやすい仕組みになっていたので簡単に使うことができました。

サーバーの知識が必要ない

サーバーについて考える時間をゲーム作りの方へまわすことができます。

安心のサポート

無料プランで分からないところがあったのでメールで質問を送ったところ、すぐに返信が来て疑問を解消することができました。

Photonを使う上での注意点

無料プランについて

Photonの無料プランには「20CCU(同時接続人数の単位)まで」「月の通信料は60GBまで」という制限があり、これを超えなければ無料で使うことができます。 CCUは超えようとすると21番目以降の人が接続されなくなり、通信料が超えてしまうと超過して接続してしまい料金が請求される可能性があります。 無料でPhotonを使いたい人は通信料には注意しましょう。(通信料は自分で確認することができます)

まとめ

分かりやすくて簡単
サーバー関係の知識はないけどオンラインゲームを作ってみたいという人にはピッタリ
困ったときはネットで調べれば大体解決する。どうしても解決しないときは直接質問もできる。
無料プランを使う場合、制限に注意する。

アバターの服の一部に動く絵をかく

はじめに

こんにちは, 学部3年の木村です.
この記事では, 私がよく使わせて頂いているこのアバターの着物の模様を自分でもかきたくなったのでシェーダーで袖に絵を描けるようにしたことを書きます.

豊姫うか -Toyohime Uka- ver.2.00

試したこと

袖の領域をマスクする

まずは袖のマスク用のテクスチャを用意しました.

こんな感じで袖の領域を切り抜けます.

シェーダーはArktoon-Shadersを少し改変して使いました.
arcludeFrag.cgincのフラグメントシェーダーでDiffuseが宣言している箇所を以下のように変更し, float3 customColor(float2 uv, float3 color)関数内で絵を描きます.

float3 Diffuse = (customColor(i.uv0, _MainTex_var.rgb)*REF_COLOR.rgb);

上の画像のようにマスクした領域をピンクに塗る場合はこのようになります.

float3 customColor(float2 uv, float3 color)
{
    float mask = UNITY_SAMPLE_TEX2D(REF_SHADERMASK, TRANSFORM_TEX(uv, REF_SHADERMASK)).r;
    return lerp(color, float3(1, 0, 1), mask);
}


ここで試しに袖に絵を描いてみましたが, 境界線が目立ってしまってよくなかったです.

UV座標を見て頑張る

次に試したのは絵を描きたい領域をUV座標で直接切り分ける方法です.

float2 st = uv*2. - 1.;
float3 col = color;
if(color.r >= .1 && color.g >= .1 && color.b >= .1) return float3(st, 0);   // 襟の部分
if(st.x >= 0.) return float3(st, 0);  // 胴の部分
if(st.y <= 0.) 
{
   // 左袖
   st = mul(st + 1., R(-.74));
   float i = smoothstep(.5, 1., st.x);
   st.x -= _Time.x*.5;
   st.y += _Time.x;
   col = lerp(col, float3(st.x-.83, st.y+.2, 0), i);
} else
{
   // 右袖
   st = mul(st, R(-.85));
   float i = smoothstep(.5, 1.,st.y);
   st.x += _Time.x;
   st.y -= _Time.x*.5;
   col = lerp(col, float3(st.x + .25, st.y-.8, 0), i);
}
return col;

無理矢理感しかないですが, とりあえず部位ごとに色を塗れるようになりました.
あとは袖にだけ絵を描きます.

最終的にこうなりました

花びらの距離関数

float sdFlower(float2 p, float size)
{
    p+= .5;
    p*=.7;
    float a = atan2(p.y, p.x);  // -PI~PI
    a+= sin(_Time.y);
    float d = min(abs(cos(a * 5.) + .4), abs(sin(a * 5.)) + 1.1) * .32*size;
    return step(length(p), d);
}

今後

この記事を書いてる途中でマスクを使っても境界線は誤魔化せそうだなと思いました.
また, 花びらの動き方など単調なのでもっと自然の花びらのような動きをさせたいです.

Rayを使って壁に弾痕を残したい+α

f:id:aizu-vr:20200427225115p:plain こんにちは。学部3年の山田です。 今回はUnityで壁とかに弾痕を残すやつをいろんなサイトをコピペ参考にしながら作ったので紹介します。

今回使用したAssetとモデル紹介

使わせていただいた銃の3Dモデル  かっこいい

製作者:GHOST EMPIRE

BOOTH↓ booth.pm

アニメーション作成に使ったAsset↓

assetstore.unity.com

血のエフェクト↓

assetstore.unity.com

Rayを使う

弾痕を残すやつを作るうえでどんな実装方法にするか考えてみた結果、まず”弾が当たった場所に弾痕の画像を置く”という機能を作れば良いと思い調べたところUnityのRayという機能が引っかかり、実装している先駆者様もいました。

Rayの主な使い方は当たり判定を飛ばしてcolliderにぶつけて何かするというものです。今回は銃の先端からRayを飛ばし、壁にぶつかったらその座標を取得し、取得した座標に弾痕を置く、という実装方法になっています。

実装

早速ですが下記サイトにある記事を参考にして銃の先端からRayを飛ばし、オブジェクトと接触した座標に弾痕を表示させるものを作ります。

qiita.com

こちらで紹介されているものはカメラの中心からRayを飛ばして画像をオブジェクトの表面に表示させるものです。このままカメラオブジェクトを銃の子として銃の先端に配置するだけでもいいですが、必要なものはRayの初期位置と方向なので他のオブジェクトでも代用できます。


UnityのRayを使って壁に弾痕を残すやつテスト

デバッグ用にRayを可視化してあります。Rayとオブジェクトが接触した座標に弾痕が表示されています。

あとはボタンを押したら壁に弾痕が現れるような処理を追加するだけです。下記サイトを参考に弾痕を配置する処理を追加します。配置する座標と角度は上記サイトを参考にしました。

nopitech.com

これで銃口からRayを飛ばして、当たった場所に弾痕を配置させることができるようになりました。

あとはキャラクターに持たせるなりカメラの子にするなりすればそれっぽいものは作れます。

今回はUnityChanに持たせてみました。


UnityのRayを使って壁に弾痕を残すやつ

無事、壁に弾痕が残るのを確認できました。

おまけ 人と壁を区別できるようにする

Raycastの要素の中にRaycastHitというものがあります。これはRayが当たったオブジェクトの情報を読み取ることができ、今回もオブジェクトにRayが当たった場所などの情報を読み取っています。RaycastHitで読み取ることができる情報の中に、オブジェクトのレイヤー情報があります。これを使って人と壁を区別します。

人として扱うオブジェクトのレイヤー番号を8、壁として扱うオブジェクトのレイヤーをDefault(番号0)とします。 下記サイトを参考に、レイヤー番号が0なら弾痕を、レイヤー番号が8なら血のパーティクルを配置するようにします。

teratail.com

実装した結果↓


UnityのRayを使って弾痕を残すやつ2

人(豆腐)を撃った時には血が出ます。 終

会津大学VR部の部員が持ち回りで投稿していくブログです。特にテーマに縛りを設けずに書いていきます!