おもちゃラボ

Unityで遊びを作ってます

【Unity】LineRendererでは引けない線を引く

Unityで線を引きたいときは、LineRendererを使うのが簡単なのですが、テクスチャの張り方を変えたり、線の太さを動的に変えたりと、アレンジした線を引こうとすると、とたんに難しくなります。

ということで、この記事ではLineRendererを使わずに自力で線を引く方法を紹介します。

f:id:nn_hokuson:20180227213645g:plain:w560

記事の内容は次のとおりです。

線を引くアルゴリズム

基本的には自分で線を描画するためのポリゴンを作って、そこにテクスチャを貼っていく感じです。ポリゴンの形状は線の流れに沿って柔軟に変化させます。

f:id:nn_hokuson:20180227203043j:plain:w500

Unityで自作のポリゴンを作る方法はこちらの記事で紹介しているので、合わせて参考にしてください。

nn-hokuson.hatenablog.com

線に沿ってポリゴンを生成するアルゴリズムは次のような流れになります。まず、最初にタッチされた点に2点ぶんの頂点を生成します。

f:id:nn_hokuson:20180227203626p:plain:w200

タッチの座標が移動したら、前タッチ座標から今タッチ座標までのベクトルを求めて、そのベクトルを90度回した点と-90度回した点に新しい頂点を2つ生成します。

f:id:nn_hokuson:20180227204246p:plain:w400

前回と今回の頂点をあわせて4頂点できるので、これで三角形ポリゴンを2つ描画します。あとはこれを繰り返し、タッチの移動に従って四角形のポリゴンを生成していきます。

f:id:nn_hokuson:20180227204531p:plain:w420

ポリゴンを動的に生成するLineスクリプトを作る

プロジェクトウィンドウで右クリックし、Create→C# Scriptを選択、「Line.cs」というスクリプトを作り、次のプログラムを入力してください。

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

public class Line : MonoBehaviour
{
    public Material _mat;

    List<Vector3> points = new List<Vector3>();
    List<Vector3> vertices = new List<Vector3>();
    List<Vector2> uvs = new List<Vector2>();
    List<int> tris = new List<int>();

    Mesh mesh;
    int offset = 0;
    float xoffset = 0;
    float penSize = 0.6f;          // 筆の太さ
    float uScrollSpeed = 0.18f;  // テクスチャの伸びる速度

    void CreateMesh(float size)
    {
        Vector2 prev = this.points[this.points.Count - 2];
        Vector2 top = this.points[this.points.Count - 1];
        Vector2 dir = (top - prev).normalized;

        Vector2 plus90  = top + new Vector2(-dir.y, dir.x) * size;
        Vector2 minus90 = top + new Vector2(dir.y, -dir.x) * size;

        // 頂点を追加
        this.vertices.Add(minus90);
        this.vertices.Add(plus90);

        // UVを追加
        this.uvs.Add(new Vector2(xoffset, 0));
        this.uvs.Add(new Vector2(xoffset, 1));
        xoffset += (top - prev).magnitude / 6.0f;////uScrollSpeed; 

        // インデックスを追加
        this.tris.Add(offset);
        this.tris.Add(offset + 1);
        this.tris.Add(offset + 2);
        this.tris.Add(offset + 1);
        this.tris.Add(offset + 3);
        this.tris.Add(offset + 2);

        offset += 2;

        mesh.vertices = this.vertices.ToArray();
        mesh.uv = this.uvs.ToArray();
        mesh.triangles = this.tris.ToArray();

        GetComponent<MeshFilter>().sharedMesh = mesh;
        GetComponent<MeshRenderer>().material = _mat;
    }

    public void PenDown(Vector3 tp)
    {
        // 開始点を保存
        this.points.Add(tp);

        // 頂点を2つ生成
        this.vertices.Add(tp);
        this.vertices.Add(tp);

        // uv座標を設定
        this.uvs.Add(new Vector2(0, 1f));
        this.uvs.Add(new Vector2(0, 0));
        this.offset = 0;

        // メッシュ生成
        this.mesh = new Mesh();
    }

    public void PenMove(Vector3 tp, float size)
    {
        this.points.Add(tp);

        CreateMesh(size);
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            this.points.Clear();
            this.vertices.Clear();
            this.uvs.Clear();
            this.tris.Clear();

            Vector3 tp = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            PenDown(tp);
        }
        else if (Input.GetMouseButton(0))
        {
            Vector3 tp = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            PenMove(tp, this.penSize);
        }
    }
}

このプログラムでは次の3つのListを使っています。

  • タッチが移動した座標を保存するpoints
  • ポリゴンの頂点座標を保存するvertices
  • ポリゴンを描画するためのインデックスを保存するindex

CreateMeshメソッドでは現在タッチされている座標をもとに追加すべきポリゴンの頂点座標を2つと、UV座標を2つ、インデックス6つ(三角形のポリゴンを2個描画するため)計算し、リストに追加しています。

CreateMeshメソッド中の次のコードでは、現在のタッチ座標と1フレーム前のタッチ座標から移動ベクトルdirを計算しています。dirはnormalizedで正規化されていることに注意して下さい。

        Vector2 prev = this.points[this.points.Count - 2];
        Vector2 top = this.points[this.points.Count - 1];
        Vector2 dir = (top - prev).normalized;

次のスクリプトでは回転行列を使って、現在のタッチ座標(top)を中心に、移動ベクトル(dir)を+90度回した点の座標と-90度回した点の座標を計算しています。

        Vector2 plus90  = top + new Vector2(-dir.y, dir.x) * size;
        Vector2 minus90 = top + new Vector2(dir.y, -dir.x) * size;

ポリゴンの生成プログラムについて詳しくは次の記事を合わせて参照ください。

nn-hokuson.hatenablog.com

このプログラムでは、1本の線しか描画出来ないプログラムになっていますが、Meshを動的に生成することで、簡単に複数の線を同時に引けるようになります。

Lineスクリプトをアタッチする

プログラムができたら、Pen.csをアタッチするためのGameObjectを作ります。ヒエラルキーウインドウからCreate→Empty Objectを選択して名前をLineに変更します。生成したPenオブジェクトにLine.csをドラッグ&ドロップします。

f:id:nn_hokuson:20180227211410j:plain

また、描画にはMesh FilterコンポーネントとMesh Rendererコンポーネントが必要になるため、インスペクタのAdd Componentからこれらのコンポーネントを追加して下さい。

f:id:nn_hokuson:20180227211603p:plain:w300

マテリアルをアタッチする

線を描画するために使用するテクスチャを指定します。プロジェクトウィンドウで右クリックしてCreate→Materialを選択し、インスペクタからMaterialのシェーダを「Unlit/Transparent」に変更したうえで、テクスチャをセットしてください。

f:id:nn_hokuson:20180227212225p:plain:w300

最後にPenのスクリプトにいま作成したテクスチャをセットします。ヒエラルキーウインドウでPenを選択し、インスペクタの「mat」の欄にマテリアルをドラッグ&ドロップしてください。

f:id:nn_hokuson:20180227212541j:plain

線を描いてみる

Unityを実行して画面上をドラッグしてみてください。ドラッグした軌跡に従って線が描けると思います。

f:id:nn_hokuson:20180227213645g:plain:w560

テクスチャの伸び方を変えたい場合はPen.csのuspeedを変更してください。これによりテクスチャのu方向へのサンプリング速度が変化します。

また、線の太さを変えたい場合はpenSizeのパラメータを変更してください。このpenSizeをペンの移動に応じて変更することで、動的に太さが変化する線を書くことが出来ます。

f:id:nn_hokuson:20180227214211j:plain:w300

【Unityシェーダ入門】ポリゴンの表面と裏面に別テクスチャを貼る

UnityでPlaneにテクスチャを貼って普通に回転させると、裏面が透明になってしまいます。これを防ぐためにカリングをオフにしたシェーダを作りました。

nn-hokuson.hatenablog.com

このように、カリングをオフにすることで、両面ともテクスチャを表示することができます。この記事ではもう一歩すすめて、表面と裏面で違うテクスチャを貼る方法を紹介したいと思います。
f:id:nn_hokuson:20180220203042p:plain

ポリゴンが裏面か表面かを判定する

ポリゴンが表向きか裏向きかはポリゴンの頂点を指定する順番で決まります。詳しくは次の記事を参考にして下さい。

nn-hokuson.hatenablog.com

Unityのシェーダでポリゴンが表向きか裏向きかを判定するには、フラグメントシェーダにfacingという引数を追加します。facingはfixed型の値で0以上なら表面、0未満なら裏面を表します。
f:id:nn_hokuson:20180220200802p:plain:w550

このfacing変数を使ってポリゴンの向きに応じてテクスチャを割り当てます。

ポリゴンの向きを判定するシェーダを作る

まずはシェーダファイルを作成しましょう。プロジェクトウィンドウで右クリックし、Create→Shader→Unlit Shaderを選択して、reverseible.shaderという名前で保存してください。また、作成したreversible.shaderを右クリックしてCreate→MaterialでUnlit_reversible.matというマテリアルを作成しておきます。

f:id:nn_hokuson:20180220200817p:plain:w200

続いて作成したreversible.shaderに次のプログラムを入力してください。

Shader "Unlit/reversible"
{
    Properties
    {
        _MainTex ("Texture", 2D)    = "white" {}
        _MainTex2("Texture2", 2D) = "white"{}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Cull off

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            sampler2D _MainTex2;
            float4 _MainTex_ST;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos( v.vertex );
                o.uv = v.uv;
                return o;
            }
            
            fixed4 frag (v2f i, fixed facing : VFACE) : SV_Target
            {                
                return (facing > 0 ) ? tex2D(_MainTex, i.uv) : tex2D(_MainTex2, i.uv);
            }
            ENDCG
        }
    }
}

ポリゴンの両面を表示するため、今回も「Cull off」を指定してカリングをしないように設定しています。

頂点シェーダでは頂点座標とテクスチャのUV座標を受け取り、ワールド座標に変換して出力しています。UV座標はフラグメントシェーダに受け渡しています。

フラグメントシェーダでは引数に頂点シェーダからの構造体のほか、facing引数を追加しています。このfacing > 0の場合はポリゴンが表面、facing <= 0の場合はポリゴンが裏面を向いています。

そこで、facingの値をみて、ポリゴンが表向きであれば_MainTexを使用し、裏向きであれば_MainTex2を使用しています。

マテリアルとテクスチャを割り当てる

シェーダプログラムができたら、マテリアルをPlaneにドラッグ&ドロップして割り当ててください。

f:id:nn_hokuson:20180220201116j:plain

また、マテリアルには表面用のテクスチャを裏面用のテクスチャを指定する必要があります。ヒエラルキーウインドウでPlaneを選択した状態で、インスペクタの_MainTexと_MainTex2にテクスチャをドラッグ&ドロップしてください。

f:id:nn_hokuson:20180220201822j:plain

実行して、Planeを回してみるとポリゴンの表面と裏面で別々のテクスチャが表示されていることが確認できると思います。

f:id:nn_hokuson:20180220202752g:plain:w500

7日間でマスターするUnityシェーダ入門

この記事は、これまでにおもちゃラボで紹介してきたUnityのシェーダ入門記事40本のまとめです。

1日に5記事読めば7日間で読み切れるはず...今のところ(笑)

シェーダって時々聞くけど難しそう・・・というイメージをお持ちの方も多いと思います。でも、Unityを使えばかなりのメンドウな部分はUnityにおまかせできちゃうので、本当に必要な部分のシェーダを書くだけでイメージ通りの絵作りができるようになります。

f:id:nn_hokuson:20180214201002j:plain

使用するシェーダ

Unityで使えるシェーダにはsurfaceシェーダと頂点/フラグメントシェーダの2種類があります。ここではこの2つのシェーダの他、ライティング・ポストエフェクトの内容も解説しています。それぞれの各記事へのリンクを下にまとめておきます。また、Unity2018からはノードベースでシェーダを作成できるShader Graphという機能も提供されるようになりました。これからこちらも合わせてドシドシ追加予定です!

surfaceシェーダ入門

【Unityシェーダ入門】透明なシェーダを作る - おもちゃラボ
【Unityシェーダ入門】氷のような半透明シェーダを作る - おもちゃラボ
【Unityシェーダ入門】リムライティングのシェーダを作る - おもちゃラボ
【Unityシェーダ入門】テクスチャを表示する - おもちゃラボ
【Unityシェーダ入門】ステンドグラスのシェーダを作る - おもちゃラボ
【Unityシェーダ入門】uvスクロールで水面を動かす - おもちゃラボ
【Unityシェーダ入門】テクスチャをブレンドして自然な地形を表示する - おもちゃラボ
【Unityシェーダ入門】円やリングをかっこよく動かす方法 - おもちゃラボ
【Unityシェーダ入門】シェーダで作るノイズ5種盛り - おもちゃラボ
【Unityシェーダ入門】粘性のある液体をシェーダで作る - おもちゃラボ
【Unityシェーダ】テクスチャの両面を描画する方法 - おもちゃラボ
【Unityシェーダ入門】トゥーンシェーダを自作してみる - おもちゃラボ
【Unityシェーダ入門】頂点カラーを表示するシェーダを作る - おもちゃラボ
【Unityシェーダ入門】シェーダで旗や水面をなびかせる - おもちゃラボ
【Unityシェーダ入門】Dissolve(溶けるような)シェーダをつくる - おもちゃラボ
【Unityシェーダ入門】シェーダを使って世界に雪を降らせよう - おもちゃラボ
【Unityシェーダ入門】オブジェクトが重なった部分をくり抜く - おもちゃラボ
【Unityシェーダ入門】ポリゴンをポイント(点)で表現する - おもちゃラボ
【Unityシェーダ入門】スパイクノイズを作る - おもちゃラボ
【Unityシェーダ入門】綺麗に半透明のモデルが表示できるシェーダを作る - おもちゃラボ

シェーダプログラミングおすすめ参考書

シェーダで使う数学や物理の理論から、シェーダプログラミングの基礎、グラフィックスプログラミング、画像処理までひとつひとつ丁寧に解説してくれている超良書。使用言語はDirectXだけれども、Unityでも普通に役立ちます!ただ絶版・・・
[asin:4839912475:detail]
Unityのシェーダを解説している本の中では一番サンプルが豊富で分かりやすいです。surfaceシェーダも頂点・フラグメントシェーダだけでなくポストエフェクトについても解説されています。洋書なので英語ですが、だいたい雰囲気でわかります(笑)

シェーダとGPUは切っても切り離せない関係。グラフィックスプログラミングをやる人は読んでおきたい一冊。英語で良ければ無料で公開されていますが、上のUnityシェーダ本と違って結構ボリュームがあるから、できれば日本語で読みたい・・・
GPU Gems 3 日本語版

GPU Gems 3 日本語版

Amazon
シェーダは出てこないけど拙書「Unityの教科書」もよろしくです!