8頂点でサイリウムを綺麗に表現する

この記事では以前にちょっとした実験で実装してみた、8頂点でサイリウムを表現する方法をご紹介します。

Unityプロジェクトはこちらから参照できます！

https://github.com/kaneta1992/EightVerticesPsyllium

以下のgif動画はこの記事で紹介する8頂点を利用した方法（ON）と、オーソドックスな方法（OFF）を比較したものです。結構よい感じになっているのではないでしょうか！（サンプルのため動きのクォリティ等はご了承ください :fearful: ）

![image1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136662/5902f743-e1e0-0bbf-e582-3924fbe5c308.gif)

# まずはよくある4頂点で実装する場合

実装はとても簡単で、頂点シェーダーを利用してビルボードのようなものを実装します。ただし通常のビルボードと異なり、Y軸の方向をロックした上で面をカメラ方向に向ける必要があります。

それを頂点シェーダーで実装したのが以下のコードです。

```glsl
v2f vert (appdata v)
{
    v2f o;
    float4x4 mat = unity_ObjectToWorld;
    float3 barUp = mat._m01_m11_m21;
    float3 barPos = mat._m03_m13_m23;

    // Y軸をロックして面をカメラに向ける姿勢行列を作る
    float3 cameraToBar = barPos - _WorldSpaceCameraPos;
    float3 barSide = normalize(cross(barUp, cameraToBar));
    float3 barForward = normalize(cross(barSide, barUp));

    mat._m00_m10_m20 = barSide;
    mat._m01_m11_m21 = barUp;
    mat._m02_m12_m22 = barForward;

    float4 vertex = float4(v.vertex.xyz, 1.0);
    vertex = mul(mat, vertex);

    o.vertex = mul(UNITY_MATRIX_VP, vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}
```

unity_ObjectToWorldから上方向のベクトルとサイリウムの位置を取得して、それを元に新しい姿勢の計算を行い、頂点のワールド変換を行います。

![image8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136662/0b59b2d7-37ba-d582-99ba-f85d88011485.png)

これだけでもそれなりの品質になりますが、視線とサイリウムが平行になるにつれて、ペラペラな板ポリであることがバレてしまいますね。

次の方法で、これをできるだけ目立たないように改善してみます。

# 8頂点にしてペラ感を改善する
ここからは最初に書いた通り、4頂点と少しの工夫を追加して品質を向上します。

## 頂点を追加する
4頂点では表現力に限界があるので、サイリウムの上下の半円の場所に頂点を追加して、8頂点にします。

![image9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136662/6800f624-0722-c682-bfb0-b5036a8400dc.png)

## 頂点にデータを埋め込む
変形で使用するため、先ほど追加した頂点に半円部分の半径をuv2の中に事前に埋め込んでおきます。

頂点シェーダーで変形をする都合で上部は正、下部は負の半径にします。

今回は簡単な形状なのでスクリプトでメッシュを生成しました。

```c#
mesh.uv2 = new Vector2[] {
    new Vector2 (-radius, 0),
    new Vector2 (0, 0),
    new Vector2 (-radius, 0),
    new Vector2 (0, 0),
    new Vector2 (0, 0),
    new Vector2 (0, 0),
    new Vector2 (radius, 0),
    new Vector2 (radius, 0),
};
```

メッシュを生成しているスクリプトの全文は[こちら](https://github.com/kaneta1992/EightVerticesPsyllium/blob/master/Assets/Scripts/CreatePsyllium.cs)です。

![image2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136662/f33857e3-381a-eee6-a765-0b20f40a5a7d.png)

## カメラ位置を利用してメッシュを変形する
この段階ではまだペラペラなので、追加した頂点をビルボードのように、カメラの位置を見て、常に真正面に見えるように変形します。

![image7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136662/3a8a20a9-dacc-468d-225b-abb8007f49c4.png)

さらに視点を固定したまま横から見ると以下のようになってます。

![image6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136662/05bb98c8-9d36-81d3-309f-187411304627.png)

これを実現するために、上で作った4頂点バージョンのサイリウムの頂点シェーダーに二行追加します。

```c
v2f vert (appdata v)
{
    v2f o;
    float4x4 mat = unity_ObjectToWorld;
    float3 barUp = mat._m01_m11_m21;
    float3 barPos = mat._m03_m13_m23;

    // Y軸をロックして面をカメラに向ける姿勢行列を作る
    float3 cameraToBar = barPos-_WorldSpaceCameraPos;
    float3 barSide = normalize(cross(barUp, cameraToBar));
    float3 barForward = normalize(cross(barSide, barUp));

    mat._m00_m10_m20 = barSide;
    mat._m01_m11_m21 = barUp;
    mat._m02_m12_m22 = barForward;

    float4 vertex = float4(v.vertex.xyz, 1.0);
    vertex = mul(mat, vertex);

    // 追加ここから
    // 事前に頂点に入れておいた半径を使って、半円部分がカメラの真正面になるように変形する
    float3 offsetVec = normalize(cross(cameraToBar, barSide));
    vertex.xyz += offsetVec * v.uv2.x;
    // ここまで

    o.vertex = mul(UNITY_MATRIX_VP, vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}
```

8頂点にしてシェーダーを二行加えただけで品質がかなり向上しました！
![image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136662/eb0cbb0d-2483-e91d-bd6a-1d0b1511095e.png)

シェーダーの全文は[こちら](https://github.com/kaneta1992/EightVerticesPsyllium/blob/master/Assets/Shaders/Psyllium.shader)です。

# さいごに
今回実験で実装してみましたが、思いのほか綺麗に見えたので記事にしてみました。

ちょっとした工夫で綺麗に見せることができるものがまだまだあると思うので今後も目を凝らして観察していきたいともいます！