Aizu-Progressive xr Lab blog

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

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

三次元極座標系でパーティクル

学部4年のユムルです。

最近Shaderをかいたり、VRのMV (Music Video) を作ったりして遊んでます。
作った物をVRSNSのVRChatにて人に見せたりして楽しんでいるのですが、最近ブタジエン(https://twitter.com/butadiene121)さんのシェーダーパーティクルに魅せられて、真似して次のようなシェーダーパーティクルを作りました。

これの解説をします。

パーティクルとして使うメッシュと表現の決定

今回はCubeのメッシュをもとに、次の表現を使うことにします。

f:id:aizu-vr:20200904223913p:plain
一つのパーティクル表現

書いたShaderは次のものです。

Shader "PolarParticle/Cube1" {
  Properties { }
  SubShader {
    Tags { "RenderType"="Transparent" "Queue"="Transparent" } // 透明
    Cull off
    Blend SrcAlpha One
    ZWrite Off
    LOD 100

    Pass {
      CGPROGRAM
      #pragma target 5.0
      #pragma vertex VS
      #pragma fragment FS
      
      #include "UnityCG.cginc"

      struct VS_OUT {
        float4 vertex : SV_Position;
        float4 texcoord : TEXCOORD0;
        float4 color : COLOR;
      };

      VS_OUT VS(appdata_full v) { 
        VS_OUT o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.texcoord = v.texcoord;
        o.color = float4(0, 1, 1, 1);
        return o;
      }

      float4 FS(VS_OUT i) : SV_Target {
        float2 uv = i.texcoord.xy;
        float2 p = uv * 2.0 - 1.0;
        float t = 0.01 / min(abs(abs(p.x) - 1.0), abs(abs(p.y) - 1.0));
        t = saturate(t);
        return float4(i.color.xyz, t);
      }
      ENDCG
    }
  }
}

メッシュの大量複製

メッシュをパーティクルとして扱うために、メッシュを大量にコピーしたいです。

私はこれをするために2種類の方法を知っています。 普通は次の方法1を使えば良いと思いますが、VRChatではそれをすることはできないので方法2を使いました。

それぞれの方法で試しに100万個のCubeを表示してみますが、後につくる極座標のパーティクルは1024個で作成します。

方法1

次の記事を参考にします。 shitakami.hatenablog.com feelingames.hatenablog.com

次のコードを書きます。

using UnityEngine;

public class GPUInstance : MonoBehaviour {
  public Mesh mesh;
  public Material material;
  public Bounds bounds;
  public int instanceNum;
  public ComputeBuffer buffer;

  void Start() {
    var args = new uint[5] { 0, 0, 0, 0, 0 };
    args[0] = mesh != null ? (uint) mesh.GetIndexCount(0) : 0;
    args[1] = (uint) instanceNum;
    buffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
    buffer.SetData(args);
  }

  void Update() {
    Graphics.DrawMeshInstancedIndirect(
      mesh,
      0,
      material,
      bounds,
      buffer);
  }

  void OnDestroy() {
    if (buffer != null) buffer.Release();
  }
}

取り敢えず大量のCubeを規則正しく並べるシェーダーを書きます、

Shader "PolarParticle/ManyCube" {
  Properties { }
  SubShader {
    Tags { "RenderType"="Transparent" "Queue"="Transparent" }
    Cull off
    Blend SrcAlpha One
    ZWrite Off
    LOD 100

    Pass {
      CGPROGRAM
      #pragma target 5.0
      #pragma vertex VS
      #pragma fragment FS
      
      #include "UnityCG.cginc"

      struct VS_OUT {
        float4 vertex : SV_Position;
        float4 texcoord : TEXCOORD0;
        float4 color : COLOR;
      };

      static uint particleNum = 1000000; // パーティクルの数
      VS_OUT VS(appdata_base v, uint instanceId : SV_InstanceID) {
        VS_OUT o;
        uint id = instanceId;  // パーティクルのID
        uint n = pow(particleNum, 1.0 / 3.0);
        float particleSize = 0.1; // パーティクルのサイズ
        float interval = 1; // パーティクル間の距離
        float zoneLength = (n-1) * interval; // 全体の範囲の長さ
        uint3 axisId = uint3( // 極ごとのID
          id % n,
          id / n % n,
          id / (n * n));
        float3 pos = (float3)(-zoneLength / 2) + axisId * interval;
        v.vertex.xyz = v.vertex.xyz * particleSize + pos;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.texcoord = v.texcoord;
        o.color = float4(0, 1, 1, 1);
        return o;
      }

      float4 FS(VS_OUT i) : SV_Target {
        float2 uv = i.texcoord.xy;
        float2 p = uv * 2.0 - 1.0;
        float a = 0.01 / min(abs(abs(p.x) - 1.0), abs(abs(p.y) - 1.0));
        a = saturate(a);
        return float4(i.color.rgb, a);
      }
      ENDCG
    }
  }
}

書いたシェーダからマテリアルを作成し、GameObjectに先ほどのC#のコードをアタッチして、次のように100万個のCubeが出るように設定します。

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

シーンを再生すると、100万個のCubeの位置が頂点シェーダーによって決められているのを確認できます。 f:id:aizu-vr:20200905185554p:plain

後のパーティクルの為、今回はInstance Numを1024に設定してください。

方法2

VRChatでメッシュのパーティクルを使う場合は、C#によるスクリプトを組み込んだ作品をアップロードできません。したがって、方法1も使えません。 代わりに間接的にC#を用いて、Meshを複製してアセットファイル化するスクリプトを書き、更にジオメトリシェーダーの[instance(31)]にて31倍にメッシュを増やします。

メッシュを複製したアセットファイルを作成するため、C#のエディタ拡張を書きます

using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;

public class MeshCopy : EditorWindow {
  [MenuItem("MyEditor/MeshCopy")]
  static void Open() {
    GetWindow<MeshCopy>("MeshCopy");
  }

  Mesh useMesh;  // コピー元のメッシュ
  int copyN; // 複製する数

  void OnGUI() {
    useMesh = (Mesh) EditorGUILayout.ObjectField("Use Mesh", useMesh, typeof(Mesh), false);
    copyN = EditorGUILayout.IntField("Copy Num", copyN);
    if (GUILayout.Button("Mesh N Copy")) {
      MeshNCopy();
    }
  }

  void MeshNCopy() {
    var vertices = new List<Vector3>();
    var texcoord = new List<Vector3>();
    var triangles = new List<int>();
    var normals = new List<Vector3>();
    var tangents = new List<Vector4>();
    var colors = new List<Color>();
    // ここのループでメッシュをコピー、texcoordのz値にidを入れておく
    for (int i = 0; i < copyN; i++) {
      vertices.AddRange(useMesh.vertices);
      texcoord.AddRange(useMesh.uv.Select(v => new Vector3(v.x, v.y, i)));
      triangles.AddRange(useMesh.triangles
        .Select(index => index + i * useMesh.vertexCount));
      normals.AddRange(useMesh.normals);
      tangents.AddRange(useMesh.tangents);
      colors.AddRange(useMesh.colors);
    }
    var mesh = new Mesh();
    mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
        // indexFormatを変えておかないとtrianglesの値の上限を余裕で超えてしまう
    mesh.SetUVs(0, texcoord);
    mesh.triangles = triangles.ToArray();
    mesh.normals = normals.ToArray();
    mesh.tangents = tangents.ToArray();
    mesh.colors = colors.ToArray();
    string path = string.Format("Assets/{0}x{1}.asset", useMesh.name, copyN);
    AssetDatabase.CreateAsset(mesh, path);
    AssetDatabase.SaveAssets();
  }
}
#endif

Unity上部のメニューにある、MyEditor -> MeshCopyを開き、Cubeのメッシュを設定し、後でメッシュを更に31倍するので、Copy Numを100万 / 31 = 約33000個 に設定しボタンを押します。
すると、Assets直下に33000個のCubeメッシュのアセットファイルが生成されます。
とはいえこの時点で、メッシュファイルのサイズは81MBになってしまうので、実用的に考えるならCube1024個で2MBくらいのものになるでしょう。

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

Shaderの方は次のようです。こちらもcubeを規則正しく並べておきます。 メッシュのサイズを抑え、ジオメトリシェーダのinstanceにて更に31倍までポリゴンを増やすことができます。 頂点シェーダーVSは値をそのまま返すのみにし、ジオメトリシェーダGSにて頂点位置を決定します。

Shader "PolarParticle/ManyCube2" {
  Properties { }
  SubShader {
    Tags { "RenderType"="Transparent" "Queue"="Transparent" }
    Cull off
    Blend SrcAlpha One
    ZWrite Off
    LOD 100

    Pass {
      CGPROGRAM
      #pragma target 5.0
      #pragma vertex VS
      #pragma geometry GS
      #pragma fragment FS
      
      #include "UnityCG.cginc"

      struct GS_OUT {
        float4 vertex : SV_Position;
        float4 texcoord : TEXCOORD0;
        float4 color : COLOR;
      };

      appdata_base VS(appdata_base v) { return v; }

      #define instanceN 31
      [instance(instanceN)]
      [maxvertexcount(3)]
      void GS(triangle appdata_base i[3], inout TriangleStream<GS_OUT> stream, uint gsid : SV_GSInstanceID) {
        for (int j = 0; j < 3; j++) {
          appdata_base v = i[j];
          GS_OUT o;
          uint id = gsid + instanceN * v.texcoord.z;  // パーティクルのIDを計算
          uint n = pow(33000 * instanceN, 1.0/3.0);
          float particleSize = 0.1;
          float interval = 1;
          float zoneLength = (n-1) * interval;
          uint3 axisId = uint3(
            id % n,
            id / n % n,
            id / (n * n));
          float3 pos = (float3)(-zoneLength / 2) + axisId * interval;
          v.vertex.xyz = v.vertex.xyz * particleSize + pos;
          o.vertex = UnityObjectToClipPos(v.vertex);
          o.texcoord = v.texcoord;
          o.color = float4(0, 1, 1, 1);
          stream.Append(o);
        }
        stream.RestartStrip();
      }

      float4 FS(GS_OUT i) : SV_Target {
        float2 uv = i.texcoord.xy;
        float2 p = uv * 2.0 - 1.0;
        float a = 0.01 / min(abs(abs(p.x) - 1.0), abs(abs(p.y) - 1.0));
        a = saturate(a);
        return float4(i.color.rgb, a);
      }
      ENDCG
    }
  }
}

SkinnedMeshRendererにてBoundsを設定しつつ使います。

方法1との違いは、頂点シェーダーでなくジオメトリシェーダを使うこと、idの設定に一手間加えることです。
以降のシェーダーのコードは方法1を使う場合のコードとします。もし、VRChatのために方法2を使う人は読み変えてください。

後のパーティクルの為、今回は1024個のパーティクルを表示する為に1024/16 = 64個のCubeメッシュを生成し、後でジオメトリのinstanceにて16倍するように設定してください。

極座標系によるパーティクルの位置決定

x座標、y座標をもとに、座標を決定する座標系を直交座標と言いますが、
極座標系という座標系も存在します。

極座標系は原点からの距離rと、x軸から反時計回りへの角度θによる(r, θ)で座標を決定します。

mathtrain.jp

原点からのrをsinやcosなどの三角関数と共に角度によって適当に決めるようにすると、綺麗な模様が描けます

www.desmos.com

二次元の極座標系(r, θ)を直交座標系(x, y)に変換する式は次の通りです

x = r cosθ
y = r sinθ

今回のパーティクルではこれを三次元で考えます。

mathtrain.jp

三次元極座標は、原点からの距離r、角度はz軸からの角度θとx軸からの角度φによる(r, θ, Φ)によって決まるようです。 今回はUnityのシェーダーで実装しますから、数学でz軸であるところはy軸、y軸のところがz軸であると考えます。 したがって、原点からの距離r、y軸からの角度θ, x軸からの角度Φで考え、 三次元直交座標系(x, y, z)への変換は次のようです。

x = r sinθ cosφ
y = r cosθ
z = r sinθ sinφ

ではシェーダを書いていきます。
パーティクルのidをもとに、パーティクルの位置を極座標系を用いて算出する関数を用意します。
最初に極座標系(r, θ, Φ)の変数と直交座標系への変換のコードを書いておきます。

#define PI UNITY_PI
void particleTransform(uint id, out float3 pos) {
  float r = 1;
  float theta = 0;
  float phi = 0;
  pos = float3(
    r * sin(theta) * cos(phi),
    r * cos(theta),
    r * sin(theta) * sin(phi));
}

さて、二次元極座標において適当なグラフを示したときはr をθをもとに決定していましたが、
今回はidの値が元になります。
したがって、idの値を適当に変数 tに入れて扱うことにします。 `` float t = id * 0.01; θをπ / 2にしておくと、phiの値によりxz座標上の値になります

float t = id * 0.01;
float r = 1;
float theta = PI / 2;
float phi = t;

また、作成した関数を頂点シェーダに適応します。

VS_OUT VS(appdata_base v, uint instanceId : SV_InstanceID) {
  VS_OUT o;
  uint id = instanceId;
  float particleSize = 0.05;
  float3 pos;
  particleTransform(id, pos);
  v.vertex.xyz = v.vertex.xyz * particleSize + pos;
  o.vertex = UnityObjectToClipPos(v.vertex);
  o.texcoord = v.texcoord;
  o.color = float4(0, 1, 1, 1);
  return o;
}

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

θの値を適当なサイン波にしてみます。
float theta = PI / 2 + sin(5.5 * t) * 0.2;

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

Φの値も適当なサイン波にしてみます。
float phi = sin(t) * 3 * PI;

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

θのサイン波の大きさを敢えて小さくしていましたが、試しに大きくしてみます。
float theta = PI / 2 + sin(5.5 * t) * 0.7;

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

止まっていてもつまらないので、時間によって変化するようにします。
Φの値を時間によって変化させてみます。
float phi = sin(t) * 3 * PI + sin(t + _Time.y * 0.3) * 3;

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

なかなかかっこよくなってきました。

tの値、rの値も時間によって変化させてみます。
float t = id * 0.01 + _Time.y * 0.1;
float r = 0.2 + sin(3 * t);

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

パーティクルが全て同じ方向を向いているので、回転の設定をします。

wgld.org

この記事を参考に、回転の関数を書きます。行列のままの方が扱いやすいので行列のまま値の受け渡しを行います。

float3x3 rotate(float angle, float3 axis) {
  float3 a = normalize(axis);
  float s = sin(angle);
  float c = cos(angle);
  float r = 1.0 - c;
  return float3x3(
    a.x * a.x * r + c, a.y * a.x * r + a.z * s, a.z * a.x * r - a.y * s,
    a.x * a.y * r - a.z * s, a.y * a.y * r + c, a.z * a.y * r + a.x * s,
    a.x * a.z * r + a.y * s, a.y * a.z * r - a.x * s, a.z * a.z * r + c);
}

また、サイズと色の設定も適当に設定します。

#define PI UNITY_PI
void particleTransform(uint id, out float3 pos, out float3x3 rot, out float size, out float3 color) {
    // out 変数追加
  float t = id * 0.01 + _Time.y * 0.1;;
  float r = 0.2 + sin(3 * t);
  float theta = PI / 2 + sin(5.5 * t) * 0.7;
  float phi = sin(t) * 3 * PI + sin(t + _Time.y * 0.3) * 3;
  pos = float3(
    r * sin(theta) * cos(phi),
    r * cos(theta),
    r * sin(theta) * sin(phi));
  // rot, size, color設定
  rot = mul(rotate(theta, float3(1, 0, 0)), rotate(phi - _Time.y * 3, float3(0, 1, 0)));
  size = cos(5.3 * t + _Time.y * 3.0) * 0.05;
  color = float3(sin(2.4 + t + _Time.y * 0.6), sin(0.7 + 2 * t - _Time.y * 0.6), sin(3.0 * t + _Time.y * 0.7)) / 2 + 0.5;
}

頂点シェーダも書き変えます。

VS_OUT VS(appdata_base v, uint instanceId : SV_InstanceID) {
  VS_OUT o;
  uint id = instanceId;
  float particleSize = 0.05;
  float3 pos;
  float3x3 rot;
  float size;
  float3 color;
  particleTransform(id, pos, rot, size, color);
  v.vertex.xyz = mul(rot, v.vertex.xyz * size) + pos;
  o.vertex = UnityObjectToClipPos(v.vertex);
  o.texcoord = v.texcoord;
  o.color = float4(color, 1);
  return o;
}

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

かっこいいのができあがりました。

コードの全体は次のようです。

Shader "PolarParticle/Test" {
  Properties { }
  SubShader {
    Tags { "RenderType"="Transparent" "Queue"="Transparent" }
    Cull off
    Blend SrcAlpha One
    ZWrite Off
    LOD 100

    Pass {
      CGPROGRAM
      #pragma target 5.0
      #pragma vertex VS
      #pragma fragment FS
      
      #include "UnityCG.cginc"

      struct VS_OUT {
        float4 vertex : SV_Position;
        float4 texcoord : TEXCOORD0;
        float4 color : COLOR;
      };

      float3x3 rotate(float angle, float3 axis) {
        float3 a = normalize(axis);
        float s = sin(angle);
        float c = cos(angle);
        float r = 1.0 - c;
        return float3x3(
          a.x * a.x * r + c, a.y * a.x * r + a.z * s, a.z * a.x * r - a.y * s,
          a.x * a.y * r - a.z * s, a.y * a.y * r + c, a.z * a.y * r + a.x * s,
          a.x * a.z * r + a.y * s, a.y * a.z * r - a.x * s, a.z * a.z * r + c);
      }

      #define PI UNITY_PI
      void particleTransform(uint id, out float3 pos, out float3x3 rot, out float size, out float3 color) {
        float t = id * 0.01 + _Time.y * 0.1;;
        float r = 0.2 + sin(3 * t);
        float theta = PI / 2 + sin(5.5 * t) * 0.7;
        float phi = sin(t) * 3 * PI + sin(t + _Time.y * 0.3) * 3;
        pos = float3(
          r * sin(theta) * cos(phi),
          r * cos(theta),
          r * sin(theta) * sin(phi));
        rot = mul(rotate(theta, float3(1, 0, 0)), rotate(phi - _Time.y * 3, float3(0, 1, 0)));
        size = cos(5.3 * t + _Time.y * 3.0) * 0.05;
        color = float3(sin(2.4 + t + _Time.y * 0.6), sin(0.7 + 2 * t - _Time.y * 0.6), sin(3.0 * t + _Time.y * 0.7)) / 2 + 0.5;
      }

      VS_OUT VS(appdata_base v, uint instanceId : SV_InstanceID) {
        VS_OUT o;
        uint id = instanceId;
        float particleSize = 0.05;
        float3 pos;
        float3x3 rot;
        float size;
        float3 color;
        particleTransform(id, pos, rot, size, color);
        v.vertex.xyz = mul(rot, v.vertex.xyz * size) + pos;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.texcoord = v.texcoord;
        o.color = float4(color, 1);
        return o;
      }

      float4 FS(VS_OUT i) : SV_Target {
        float2 uv = i.texcoord.xy;
        float2 p = uv * 2.0 - 1.0;
        float a = 0.01 / min(abs(abs(p.x) - 1.0), abs(abs(p.y) - 1.0));
        a = saturate(a);
        return float4(i.color.rgb, a);
      }
      ENDCG
    }
  }
}

A-PxLが誕生してから、今まで。

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

こんにちは、ぶろっくのいずです。

初代部長である秋山さん達がこの部を作ってくださった時から3年以上が経ちました。 A-PxLの成り立ちを知る人もいなくなってきた事もあり、今までの活動をまとめました。 (※この記事について修正、追記出来る部員の方がいましたら気軽に変更をお願いします)

目次

A-PxL(旧 会津大学VR部)のこれまでの活動

2016年度

会津大VR部(会津大学) - Japan-VR-circles

3月5日:はてなブログ開設

2017年度

4月6日:会津大学内にカヤックVR分室を設立しました! | 面白法人カヤック

4月28日:Twitter開設

8月11日:会津大学オープンキャンパス

8月中旬:面白法人カヤックインターン(学部1年生2人)

9月上旬:面白法人カヤックインターン(学部3年生3人)

9月29日〜10月1日:VR部 開発合宿

10月7日〜10月8日:蒼翔祭(学祭)

12月:初代部長秋山さんが株式会社AnostVRを設立

2018年度

4月10日:新入生説明会

4月17日:今年の方針の説明 VR概論1

4月24日:VR概論2

5月8日:ゲームサウンドワークショップ

5月15日:3Dモデルワークショップ

5月22日:Git勉強会1

5月29日:Git勉強会2

5月27日:会津マルシェにてVR雪合戦を展示

6月30日:ブレスト研修会

7月3日:チーム進捗報告及びチーム活動

8月11日:オープンキャンパス

9月14日〜9月16日:開発合宿(南会津の宿泊施設CloudCamp)

10月6日〜10月7日:蒼翔祭(学祭)

10月16日:チーム進捗報告及びチーム活動

11月4日:デジゲー博2018(秋葉原UDX)

11月16日:IVRC決勝大会ユース部門銀賞を獲得

youtu.be

11月18日:パソコン甲子園にてゲームの展示

12月1日〜12月22日:健康づくりハッカソン

12月:部内面談

1月:部内面談

2月:次年度の役員決め

2月16日〜2月17日:新潟・山形・会津大学共同ハッカソンに参加naoto80840.github.io

2019年度

4月:役員の分担(代表者の他に副代表者、会計、自治会担当者を増員)

4月5日:部名を「Aizu-Progressive xr Lab(略称:A-PxL)」に変更

5月18日〜5月19日:VRキャラバン活動

6月16日:Oculus Quest体験会

7月13日〜7月14日:新入生対抗ハッカソン

8月11日:オープンキャンパス VR作品体験会

8月23日〜9月14日:学祭に向けてのPR動画制作

10月12日〜10月13日:蒼翔祭(学祭)は台風により中止

12月:部内VRコンテンツ体験会

1月31日:来年度の活動についてミーティング

2月14日:勉強会についての会議

3月1日:追いコン

2020年度

4月5日:clusterにて会津大学VRサークル説明会

4月6日:部室があるAiCTを3Dスキャン

4月15日:clusterにてA-PxL新入生向けの説明会

4月29日〜5月10日:バーチャルマーケット4に出展

5月14日〜6月11日毎週木曜日:新入生向けのプログラミング勉強会

5月15日〜6月12日毎週金曜日:新入生向けのUnity勉強会

5月16日〜5月30日毎週土曜日:新入生向けのBlender勉強会

6月6日〜6月13日毎週土曜日:新入生向けのVFX勉強会

6月17日:OpenAppLab "2020"

7月11日:Git勉強会1

7月14日:部内でVR体験会

7月16日:Git勉強会2

7月17日:Git勉強会3

7月21日:チーム決め

9月:夏休み

10月10日:オンラインの蒼翔祭(学祭)

活動内容の変化

2016年度:?

2017年度:?

2018年度:チームに別れてゲームを制作。

2019年度:ゲーム部と新規開発部(NDD)に別れて活動。ゲーム部は毎週火曜日19:00〜21:00に中講義室M5にて定例会。新規開発部は毎週金曜日19:00〜21:00に研究棟北ラウンジにて定例会。

2020年度:ゲーム部と新規開発部を統合。毎週火曜日19:00からUBICの3Dシアターにて定例会(大学のテスト期間や大型の休みを除く)。周一でチームごとに進捗を報告しゲームを制作。

部員数の推移

(最大アクティブ数はSlackのアナリティクスより集計)

2016年度:総数11人→13人

2017年度:総数33人、最大アクティブ数33人、新部員20?人

2018年度:総数49?人、最大アクティブ数44人、新部員16人

2019年度:総数63?人、最大アクティブ数43人、新部員6人

2020年度:総数77人、最大アクティブ数38人、新部員6人

役員の推移

顧問:マイケル・コーエン先生

2017年度

代表者:Achu→Achu (@Achu_retro) | Twitter

2018年度

代表者:ゆべねこ→ゆべねこ (@yubeshineko) | Twitter

2019年度

代表者(新規開発部リーダー):Bigdra→bigdra (@bigdra50) | Twitter

副代表者(ゲーム部リーダー):なめろうNameChung (@sketch_namerou) | Twitter

会計:ヤミみみ→ヤミみみ (@MtYamaniko) | Twitter

自治会担当者:ぶろっくのいず→ぶろっくのいず (@blocknoise26) | Twitter

2020年度前半(学祭前)

代表者:Bigdra→bigdra (@bigdra50) | Twitter

副代表者:TC→TC (@Meancore226) | Twitter

会計:ヤミみみ→ヤミみみ (@MtYamaniko) | Twitter

自治会担当者:ぶろっくのいず→ぶろっくのいず (@blocknoise26) | Twitter

2020年度後半(学祭後)

???

A-PxLメンバー達のTwitter

twitter.com

部室の変化

2017年度:?

2018年度:会津大学UBIC分室

2019年度:AiCT(あゆむCafe)の展示スペース

2020年度:無し

部名変更の経緯

最初は会津大学VR部という名であったA-PxL。(結局今もVR部と呼んだり呼ばれることも) 2019年3月頃、VRだけでなくARなどxRを扱う部であるのに対し、部名がそぐわないという意見が出る。(※部名の変更がちょうど年度始まりから可能) 「VRやxR関連であること」、「他と被らないオリジナリティのあること」という条件を元に30個?ほどの部名案が出る。その中から「なんちゃらxRなんちゃら(どんな名前だっけ?)」という部名がアンケートで選ばれ、最終的に「Aizu-Progressive xr Lab(略してA-PxL)」という部名になる。A-PxLの読み方は「アイヅピクセル」。

(2020年9月7日更新)

スクリプトからゲームオブジェクトのマテリアルの色を変える覚え書き

こんにちは、学部4年の杉山です。ネタが尽きたので覚え書きです。

はじめに

最近論文執筆を始めまして、その際にオブジェクトのマテリアルの色をスクリプトから変えているのでせっかくなのでメモ書き程度に紹介したいと思います。

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

カラーネームを使う

Unityには9(10)のカラーネームがあります。

  1. black
  2. blue
  3. (clear)
  4. cyan
  5. gray(grey)
  6. green
  7. magenta
  8. red
  9. white
  10. yellow

clearは透明であるから色として含めるかはおいておくとして、これらのカラーネームを使うなら。

 GetComponent<Renderer>().material.color = Color.black

 のように記述すれば好きな色に変更できます。

RGBAを使う

RGBAを使うこともできます。

Color(float r, float g, float b, float a); //それぞれ r=赤、g=緑、b=青、a=アルファ値

RGBだけでも使えます。

Color(float r, float g, float b);

例文としては

GetComponent<Renderer>().material.color = Color( 0.1f, 0.2f, 0.3f, 0.4f );

アルファ値は指定しなければ1として扱われます。

おまけ

色を変更する際に便利なメソッドがあったので紹介します

Color Lerp( Color a, Color b, float t); // t は0~1の間、0の時はa、1の時はbを返す

使うときは

GetComponent<Renderer>().material.color = Color.Lerp( Color.white, Color.black, t); // t は任意の数

最後に

わざわざスクリプトから色を変更する機会もそうそうないとは思いますが、お役に立てば幸いです。色を変えるのが億劫なときなどにどうぞ。

参考

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

Githubで一貫してタスク管理

皆さんこんにちは学部4年の森口です。 8月に入り急に気温が上がったことで外出自粛が促進されていてある意味感染症対策になっておりますww

さて今回はGithubの機能であるissueとprojectなどを使ってGithub内で完結したタスク管理方法についてまとめていきたいと思います。

Githubでタスク管理

Githubにおけるタスク管理で主に使う機能はissueとprojectの二つになります。これらの二つでタスクの管理をしつつPullRequestなどを組み合わせることでさらに効果的にプロジェクトの管理を進めることができます

issue

issueはGithubのタスク管理機能の一つであり、バグ報告や機能追加などのチケットを作成したり編集することができます

project

projectはいわゆるカンバン方式のタスク管理機能でTrelloに近い使用感で誰がどのタスクを行なっているのか、対応中やレビュー待ちといったタスクの進捗状況を確認することができます

無論TrelloやGoogleスプレッドシートなど他サービスを使用してのタスク管理もできますが可能な部分はGithub内で完結させることでサービスを跨いだときに起こる弊害をなくすことができより快適に開発ができると考えています。

ではここからGithubでタスク管理をしていく一連の流れと手順を説明していきます。

projectの作成

まずはprojectを新しく作成します。Githubのページ上でProjectsタブをクリックすると以下のような画面になります。 f:id:aizu-vr:20200813152912p:plain そして右上にあるCreate a ProjectをクリックするとProjectの詳細を設定する画面に移ります(ここの内容は後から変更できます) f:id:aizu-vr:20200813153116p:plain Project templateの部分はGithub側から予め用意されているテンプレートを使用することができます。
今回はAutomated kanbanを使用していますがこれはissueを登録したり、完了した際などに自動的にカードを移動してくれます(自分で設定することで同じような物を作れますがこっちの方が楽です)
名前や概要などを記述したらCreateProjectをクリックしてProjectを作成しましょう
projectを作成するとカンバン形式の画面が表示されます f:id:aizu-vr:20200813153916p:plain この画面でタスクの進捗状況などを確認できます。初めはTo Doにあるカードを開発を開始するときはInProgressに移動させるといった感じです。
また、かくカラムの右上にある3つの点をクリックしManage automationを選択するとどのアクションを行なったときにカードを自動的に移動させるか設定することができます。

f:id:aizu-vr:20200813154605p:plainf:id:aizu-vr:20200813154601p:plain

これでprojectの設定は完了したので次に移りましょう

Milestone

issueの設定をする前にMilestoneという機能について紹介したいと思います。これはその名の如くマイルストーンを設定することができてこれをissueに紐づけることでマイルストーン毎にタスクを管理することができます。
マイルストーンの作成はIssueタブをクリックして画像内で赤く囲ったMilestonesを選択します。
f:id:aizu-vr:20200813155614p:plain そうするとマイルストーンの管理画面になるのでNewmilestoneを選択してマイルストーンを作成します。
f:id:aizu-vr:20200813155938p:plain 作成画面ではタイトルと期限、詳細を設定するように言われるので記述します。
f:id:aizu-vr:20200813155944p:plain

それでは今回の本題となるissueを作成していきましょう!

issueの作成

先ほどマイルストーンを作成した時と同様にIssueタブを選択し、今度はNewIssueを選択します
f:id:aizu-vr:20200813160553p:plain Issueの作成画面は以下のようになっておりここでバグ報告や新機能追加のためのissueを作成します。
GithubではMarkdownが対応しているので見出しなどを利用して概要、issue内の細々としたタスクなどを分けて記述するとよりわかりやすいと思います。
また、以下の画像でタスクリストの部分で使用している記法はGithubのタスクリストの書き方で、この記法で記述しておくとこのタスクリストの進捗状況がissueに表示されるので便利です。 f:id:aizu-vr:20200813160654p:plain 詳細が書き終わったらProjectやマイルストーンを紐付けましょう。紐付けは画面右側のProjectやMilestoneの欄にある歯車を選択して該当の物を選択します。 その他の項目はAssigneesはそのチケットに関わる人、Labelはどのような種類のissueなのかを指定できます。
設定が完了したらSubmit new issueを選択してissueを作成しましょう
f:id:aizu-vr:20200813161637p:plain 作成したissueには番号が割り当てられ(タイトル横の#1)この番号をコミットメッセージやプルリクエストの詳細部分に記述するとそれらからこのissueへのリンクが作成されます。

いざ開発

自分宛のIssueができたらいざ開発に移ります。まずは、自分が特定のタスクを進行中であることを他のメンバーにもわかるようにProjectのカードを移動させます
Projectを見にいくとTodoのところにカードが作成されています。これは先ほどIssueを作成したときにProjectを紐づけたので自動的に生成されました。これをドラッグアンドドロップでInProgressへ移動させましょう。
f:id:aizu-vr:20200813162255p:plain

プルリクエス

さて、開発が終わったらあとは自分の成果をマージしてもらうためプルリクエストを作成します。
プルリクエストとは自分の作業ブランチにおけるコミットをマージしてくださいと要請する機能です。個人開発では特別なものではありませんが、チーム開発ではコードレビューなどが行われるタイミングでもあり、自分のコミットに対し問題がなければある程度権限のある人が承認しマージされます。
この工程では特別することは変わらないのでプルリクエストの大まかな説明は省略し、今回のタスク管理と関係のある部分のみ説明します。
と言ってもポイントは一点だけで詳細を記述する部分でCloses (issue番号) のように記述するとそのissueはマージされた時点でCloseされます。もちろん手動でもCloseはできるので絶対描かなければならないわけではないですがそのIssueの内容が全て完了しているのであれば書いておいた方がいいでしょう。
また、プルリクエストを作成しレビューを待つ場合はProjectにRevewingのようなカラムを作成しカードを移動しておきましょう。 f:id:aizu-vr:20200813163751p:plain

クローズ&リオープン

プルリクエストが承認されIssueの内容を完了したらIssueをクローズしましょう。Issueタブを選択し自分の担当していたIssueを選択したらCloseIssueを選択することでクローズすることができます。また、IssueをクローズするとProjectのカードはDoneへ移動します。 f:id:aizu-vr:20200813164229p:plain

f:id:aizu-vr:20200813164229p:plainf:id:aizu-vr:20200813164233p:plain
また、後々の仕様変更で以前クローズしたIssueの部分を変更したりしたい場合が出てくるかもしれません。新しくIssueを作成しても問題ありませんが一度クローズしたissueを再度開くこともできます。その場合はクローズしたIssueを選択しReopen issueを選択します。 f:id:aizu-vr:20200813164240p:plain

最後に

いかがでしたでしょうか。現在私の所属しているチームでタスク管理をするのに前々から気になっていたので使ってみようと思い、使い方などを調べたことを記事にしましたが非常に使い勝手が良さそうな印象を受けました。
また、今回紹介した方法は一例ですのでさらに使いやすい方法もあるかと思いますのでチームの形態によって色々とカスタマイズしてチームとして最も使いやすい形で管理を行うのが一番いいのかなと思います。それでは今回はこの辺で失礼します。

参考

docs.github.com

www.atmarkit.co.jp www.atmarkit.co.jp

qiita.com

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

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