SceneKitでカスタムジオメトリをつくる

SceneKitにはもともと立方体や球など基本となる形が用意されています。SceneKitを使う場合、これらの基本形状を利用するか、別の3Dモデリングツールで作成したモデルをインポートして利用する場合がほとんどです。

普通にSceneKitを使う分にはカスタムジオメトリをつくる動機はないのですが、ジオメトリについて深く理解したかったので、どのようにジオメトリを作成するのか調べてみました。例としてはSCNBoxと似た直方体を作成してみます。

カスタムジオメトリを作成するSCNGeometryのイニシャライザはSCNGeometry(sources: [SCNGeometrySource], elements: SCNGeometryElement]?)となっていてSCNGeometrySourceのArrayとSCNGeometryElementのArrayを渡して作成する仕様となっています。

SCNGeometrySourceはいくつかイニシャライザが用意されていますが、頂点(vertices)のみでも作成できるので今回は頂点のみ作成します。他に法線(normal)やテクスチャを作成することができます。

以下のコードで作成したカスタムジオメトリをSCNNodeに渡せば直方体を描画できます。

static func Box(width: CGFloat, height: CGFloat, length: CGFloat) -> SCNGeometry {
    let w = width / 2
    let h = height / 2
    let l = length / 2

    // 8つの頂点の座標を指定しています。
    let vertices = SCNGeometrySource(vertices: [
        // bottom
        SCNVector3(-w, -h, -l),
        SCNVector3(w, -h, -l),
        SCNVector3(w, -h, l),
        SCNVector3(-w, -h, l),
        // top
        SCNVector3(-w, h, -l),
        SCNVector3(w, h, -l),
        SCNVector3(w, h, l),
        SCNVector3(-w, h, l), 
    ])
    
    // 各頂点を結ぶ順番を指定します。8つの頂点を結ぶと12個の三角形で作成されます。
    // 0,1,3はインデックスが0,1,3の頂点を結んで三角形を描画することを意味します。
    // SceneKitでは反時計回りに頂点を結ぶ必要があります。
    let indices: [UInt32] = [
        // bottom
        0, 1, 3,
        3, 1, 2,

        // left
        0, 3, 4,
        4, 3, 7,

        // right
        1, 5, 2,
        2, 5, 6,

        // top
        4, 7, 5,
        5, 7, 6,

        // front
        3, 2, 7,
        7, 2, 6,

        // back
        0, 4, 1,
        1, 4, 5,
    ]

    // SCNGeometryElementは頂点(vertices)のインデックスとプリミティブのタイプを選択して作成します。
    let inds = SCNGeometryElement(indices: indices, primitiveType: .triangles)

    return SCNGeometry(sources: [vertices], elements: [inds])
}

UnityでPlaneオブジェクトの両面を描画する

UnityのPlaneオブジェクトはデフォルトだと裏から見ると何も見えません。

裏から見ても見えるようにするには、シェーダーを作ってSubShaderにCull offと追記します。 1行追加するだけで出来るのですが、知らなくて以前はまりました。

このCullというのはCullingのことでカメラに映らないときはレンダリングを無効にする機能のことです。デフォルトではCull backになっていて裏側をレンダリングしない設定になっています。

Cull Frontとすると表をレンダリングしないで裏側をレンダリングします。これをBoxに適用すると部屋をつくることができます。

シェーダーのUV座標についておおまかに理解する

シェーダーでUV座標の扱いが良くわからなかったので調べてまとめる。

これはシェーダーの本などを読むと最初に載っているHelloWorld的なサンプル、「面に色のグラデーションを表示させる」を試してみると理解できた。

シェーダーでUV座標をレンダリングパイプラインから取得するには構造体にTEXCOORDセマンティクスを適用した変数を用意することで受け取れる。

struct appdata {
    float2 uv: TEXCOORD;
}

変数uvをfrag関数の中で各ピクセルの色を決める部分で使用すると、色をグラデーションさせることができる。

 fixed4 frag(v2f i): SV_Target {
     fixed4 color = fixed4( i.uv.x, i.uv.y, 0, 1);
     return color;
 }

このように書くとuv座標のxの値大きくなるにつれて黒から赤色に、yの座標が大きくなるにつれて黒から緑にグラデーションするので全体では左下が黒、右下が赤、左上が緑、右上が黄色になる。

全体では以下のコードになる。

Shader "Custom/Gradation"
{
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

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

            fixed4 frag(v2f i): SV_Target {
                fixed4 color = fixed4(i.uv.x, i.uv.y, 0, 1);
                return color;
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}

サーフェスシェーダーで頂点を動かす

去年からちょこちょことシェーダーを勉強しているのだけれど、なかなか思ったように書けるようにならないので、2020年は本腰を入れてシェーダーを勉強しようと思う。

勉強するためにUdemy、書籍、同人誌、ブログなどを参考に進めているが、自分がアウトプットしていかないと憶えないので、ここにはやりたいこととその実装、メモを書いていく。

まずシェーダーで頂点を動かすのをやりたかったので、書いてみた。

Shader "Custom/SimpleVertex"
{
    Properties {
        _BendWeight("Bend Scale", Range(0.0, 1.0)) = 0.2
        _MainTex("Main Texture", 2D) = "white" {}
    }
    SubShader {
        CGPROGRAM
        
        // vertex: vertと指定すると、SurfaceShaderで頂点をいじれる
        #pragma surface surf Lambert vertex:vert
        #define PI 3.14159

        float _BendWeight;
        sampler2D _MainTex;

        struct Input {
            float4 color: Color;
        };

        void vert(inout appdata_full v) {
            // _Timeを使って動きをつくる
            float bend = sin(PI * _Time.x * 20);

            // v.texcoordを使うことでTextureの位置によって動く大きさが変えることができる
            float x = sin(v.texcoord.x * PI);
            float y = sin(v.texcoord.y * PI);
            v.vertex.y +=  _BendWeight * bend * (x+y);
        }

        void surf(Input IN, inout SurfaceOutput o) {
            o.Albedo = IN.color.rgb;
        }

        ENDCG
    }
    FallBack "Diffuse"
}

こちらを参考にさせていただきました。

IoTのハッカソンに参加してきたよ

本とIoTハッカソンに参加してきた。

f:id:mio_kt:20170130120514j:plain

参加者としてハッカソンに行ってきたのはとても久しぶりだ。1年ぶりくらいかもしれない。本もIoTも興味の惹かれる分野だったので、たまには参加してみようっていう軽い気持ちで参加してきた。

軽い気持ちで参加したので、誰も知った人がいない空間がしんどくて、開始1時間後くらいには帰りたくなってきた。でもせっかく来たんだしと思って、がんばって交流しているとだんだんと楽しくなってきて、終了したときは来てよかったーと思ったので記録しておく。

ハッカソンに参加しての気づき。

気づき① 役割ごとの重要性

ワークショップとかハッカソンとかに参加すると、プランナーがやたら多くて、アイデアはあるけど実装できないみたいな話をよく聞く。それで絵に書いた餅にならないように、つくることが出来るエンジニアリングが超重要と思ってた。でも今回のハッカソンに参加している人はほぼエンジニアで、企画やデザイン、UXを考えることが出来る人が少なかった。そうすると、それらスキルを持った人が重宝される。結局バランスが大事なんだなーと思う。ドラクエのパーティでも戦士ばかりだとダメ出し、魔法使いばかりでもダメだ。それぞれのスキルを組み合わせてパーティを組むことが大事だよね。当たり前のことかもしれないけど、今回体感してハッとした。

気づき② 新しい出会い

上にも書いたけど、久しぶりにハッカソンに参加したので、最初は誰も知った人がいなくて、とてもしんどい気持ちになってきた。開始1時間後くらいの説明の時間はもう帰ろうかなと思っていた。でもそこから少しづつ初対面の参加者の人たちと話していて、打ち解けてくると居心地がだんだん良くなってきた。よく知った間柄ではない人と話すのは、自分の好きなことや得意なことを再認識させてくれる。グループの中で当たり前となった前提がなにもないので、改めて自分がどういう人か話さなきゃならない。これは面倒でもあるんだけど、自分は何が好きで、どうしたいのか話すとことによって、自分はこういうことが好きだったんだなと、改めて気づかせてくれる。最近技術力を高めたい気持ちが強かったけど、自分の興味の範囲は、ただ技術力を高めて仕事をするだけではなくて、現在の社会問題を考えることや、技術によって人の生活がどう変わるかと言った側面にもあるようだ。普段まわりの人からそういう話ばかり聞くので、少し食傷気味になってたみたい。

気づき③ つくるはたのしい

ハッカソンでは二日間ひたすらつくるところを担当していた。これはひたすら楽しい。また自分ひとりで開発してるわけではないので、チームのために良いサービスにしなければならないという適度なプレッシャーがあり、これが自分にとってはとても良い。仕事以外で一人でやってると手を抜いてしまう面もあるので、プレッシャーって心地よいわけではないけど、必要だと感じる。

結果的に今回4人のチームでつくったサービスは特別賞をいただきました。頑張ったせいかとして表彰されるのはうれしいものですなー。

15 : 言語処理100本ノックでPythonのお勉強

第二章 : 15 末尾のN行をを出力

自然数Nをコマンドライン引数などの手段で受け取り,入力のうち末尾のN行だけを表示せよ.確認にはtailコマンドを用いよ.

Pythonコードはこんな感じ。

# tail.py
file = 'hightemp.txt'

N = int(input('文の末尾から切取りたい行数を選択してください。\n'))

with open(file) as data:
    lines = data.readlines()

for line in lines[-N:]:
    print(line.strip())

コードをターミナルから実行。

$ python tail.py
文の末尾から切取りたい行数を選択してください。
5

Unixコマンドで確認。

$ tail -5 hightemp.txt

Qiitaのこちらの記事を参考にしております。

14 : 言語処理100本ノックでPythonのお勉強

第二章 : 14 先頭からN行目を出力

自然数Nをコマンドライン引数などの手段で受け取り,入力のうち先頭のN行だけを表示せよ.確認にはheadコマンドを用いよ.

コードはこんな感じ。

file = 'hightemp.txt'
N = 4

with open(file) as f:
    for i, line in enumerate(f):
        if i < N:
            line = line.rstrip()
            print(line)

enumerate()関数を使うとfor文回すときに中身とインデックスを両方取得できるので便利ですね。 line.rstrip()のところは改行コードを削除するために使っています。

unixコマンドではこんな感じで確認できます。
head -4 hightemp.txt'

コマンドラインからファイルと表示したい行数を指定して実行する場合はこんな感じになります。 sysモジュールを使うとargvのリストとしてコマンドライン引数を受け取ることができます。 第2引数は行数を指定するために渡していますが、文字列として受け取っているので、関数の中で整数に変換して使っています。

import sys

def get_head_line(file, N):
    N = int(N)
    with open(file) as f:
        for i, line in enumerate(f):
            if i < N:
                line = line.rstrip()
                print(line)

if __name__ == '__main__':
    get_head_line(sys.argv[1], sys.argv[2])