MetalとSwiftで遊ぶ「フラグメントシェーダ」

この記事は[NCC Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ncc)の3日目の記事です.

アドベントカレンダー内で勝手にやってるMetalの連載も3日目です．

# 勝手にやってるMetal連載

最近Metalを初めて楽しくなっちゃったのでたくさんoutputしてみる会です．

1. [Rendererを作る](https://qiita.com/micky_dtl/items/7177c667e5a5d6bb777a)
2. [プロシージャルモデリング的な何か](https://qiita.com/micky_dtl/items/711f3f6c86abfe0bea89)
3. フラグメントシェーダで遊ぶ ←　イマココ
4. [テクスチャで遊ぶ](https://qiita.com/micky_dtl/items/f4f8b09e7478187c9ea5)

# フラグメントシェーダで遊ぶ

今回はフラグメントシェーダで遊びます．
ViewControllerからRendererに渡してあげる頂点の情報は[1日目](https://qiita.com/micky_dtl/items/7177c667e5a5d6bb777a)と同じく，View全体をおおうための長方形１つだけです．

第一回でも説明しましたが．iPhoneのスクリーンは縦横比が`1:1`ではありません．
iPhoneXを例にとって見ると，スクリーンは$height:width = 19:9$になっています．なんと，2倍以上歪んでしまいます！
しかしながら，スクリーン座標系はx, y方向ともに $-1.0 \le x, y \le 1.0$ですし，レンダリングする際もそれは変わりません．
なのでvertexシェーダないで，縦に押しつぶすように圧縮してあげる必要があります．

* 縦長なポリゴンを用意する
* vertexシェーダ内でY方向のスケールを$\frac{width}{height}$倍する

この２つの処理で**歪みをなくし**かつ**フルスクリーン**で表示することができます．

これは，次の図のようなのような処理を行います．

<img width="733" alt="スクリーンショット 2018-12-03 23.04.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/76493/b3889201-a69d-7bc7-2748-42e09da79c1f.png">

ViewControllerの中では，このポリゴンの`Position`の情報だけ一度定義して，そこからoffsetと黒色の`Vertex`を作成しています．

``` ViewController.swift

// viewDidLoad内

let y = Float(view.frame.size.height / view.frame.size.width)
let vertices = [Position(-1.0,-y,0),
                Position(1.0,-y,0),
                Position(-1.0,-y,0),
                Position(1.0,-y,0),
                Position(-1.0,-y,0),
                Position(1.0,y,0)]
renderer.setVertices(vertices.map({v -> Vertex in Vertex(offset: Position(0,0,0), position: v, color: Color(0,0,0,1))}))

```

## 用意したUniform変数

* aspectRatio
* time
* touch

一応のため，3つ全部，fragmentシェーダを渡してあげます．

まずは，`touch`を用いて，iPhoneのスクリーン上で触れている部分に白い円を書いてみましょう．
って打ってから作ったのですが，思ってたより大変でした...

円を描くところは，だいぶ後半です

*毎度のように，シンタックスハイライトをつけるためにcppにします．．．*

``` shaders.cpp

#include <metal_stdlib>
using namespace metal;

struct VertexIn {
    float3 offset;
    float3 position;
    float4 color;
};

struct Uniforms {
    float time;
    float aspectRatio;
    float2 touch;
};

struct VertexOut {
    float4 position [[ position ]];
    float4 screenCoord;
    float time;
    float aspectRatio;
    float2 touch;
};

vertex VertexOut vertexDay3(constant VertexIn *vertexIn [[buffer(0)]],
                            constant Uniforms &uniforms [[buffer(1)]],
                            uint vid [[ vertex_id ]]) {
    float3 offset = vertexIn[vid].offset;
    float3 position = vertexIn[vid].position;
    float4 screenCoord = float4(offset, 0.0) + float4(position, 1.0);
    float4 color = vertexIn[vid].color;
    VertexOut out;
    
    float4 pos = float4(screenCoord.x, screenCoord.y*uniforms.aspectRatio, screenCoord.zw);
    
    out.position = pos;
    out.screenCoord = screenCoord;
    out.time = uniforms.time;
    out.aspectRatio = uniforms.aspectRatio;
    out.touch = uniforms.touch;
    return out;
}

//========================================================
//                       ここからが本番                       
//========================================================
float circle(float2 p, float2 center) {
    return 1.0 - smoothstep(0.0, 1.0, length(center-p));
}

fragment half4 fragmentDay3(VertexOut vertexIn [[stage_in]]) {
    float4 p = vertexIn.screenCoord;
    float2 touch = vertexIn.touch;
    float time = vertexIn.time;
    half3 c = half3(step(0.5, circle(p.xy, touch)));
    return half4(c, 1.0);
}

```

最後の方にある`fragmentDay3`の中で円を描いています．

* `circle`関数で各ピクセルが中心(タッチしている部分)からどれくらい近いかを計算しています．
* 各ピクセルの中心までの近さが0.5以上だったら白く塗り，そうでなかったら黒く塗ります．

下がそのbefore afterです

<img width="1024" src="https://gyazo.com/f69bc1fca91518cd3223c4db949a10ca.raw">

もちろん，タッチする場所を動かせば，円も動きます．

## 遊んでたらできたもの

これが，動きます笑

<img width="375" alt="IMG_5266.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/76493/46c722f7-9606-8107-c726-6783637eb34d.png">


こんな感じのフラグメントシェーダを書いてみたのですが．．．
いまだにこんな風になった理由がわからない...

``` shaders.cpp
float re(float2 p, float2 center, float time) {
    float theta = atan2(p.x-center.x, p.y-center.y);
    float2 norm = float2(cos(10*theta + 3*time), sin(20*theta + 2*time));
    return smoothstep(0.0,1.0,length(norm));
}

fragment half4 fragmentDay3(VertexOut vertexIn [[stage_in]]) {
    float4 p = vertexIn.screenCoord;
    float2 touch = vertexIn.touch;
    float time = vertexIn.time;
    float a = 1.0 - re(p.xy, touch, time);
    return half4(a,a,a,1.0);
```

これで画面のいろんなところをタッチしたらこんな感じになりました.

<blockquote class="twitter-tweet" data-conversation="none" data-lang="en"><p lang="ja" dir="ltr">まだまだ止まりません <a href="https://t.co/2PITu0G02x">pic.twitter.com/2PITu0G02x</a></p>&mdash; Mi↑ki↓ (@miki_fms) <a href="https://twitter.com/miki_fms/status/1069620438144212992?ref_src=twsrc%5Etfw">December 3, 2018</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## マンデルブロ集合
とりあえず，シェーダといったらマンデルブロ集合なきがするので，作りました．
ついでにアニメーションさせたりしています！

``` Mandelbrot.cpp
float Mandelbrot(float2 p, float time) {
    int count=0;
    float2 x = p + float2(-0.5,0.0);
    float2 z = float2(cos(time), sin(0.7*time));
    
    for(int i=0; i<360; i++) {
        count++;
        if(length(z) > 2.0) {
            return float(count)/360.0;
        }
        z = float2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y) + x;
    }
    return 1.0;
}

fragment half4 fragmentDay3(VertexOut vertexIn [[stage_in]]) {
    float4 p = vertexIn.screenCoord;
    float2 touch = vertexIn.touch;
    float time = vertexIn.time;
    float3 c = float3(0.30, 0.59, 0.11)*Mandelbrot(p.xy, time);
    return half4(half3(c), 1.0);
}
```
こんな感じです

<img width="375" alt="IMG_600388A1B7E2-1.jpeg" src="https://qiita-image-store.s3.amazonaws.com/0/76493/3d5dce20-634b-d6b4-0e66-21f16c32f2b0.jpeg">


## 波を干渉させる

GLSLスクールのfragmentシェーダの練習で出題されていました．
これもまた動かして楽しんでみました．

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">GLSLスクールのリソースをMetalで書いてしまう病気 <a href="https://t.co/1bWwoHmgXM">pic.twitter.com/1bWwoHmgXM</a></p>&mdash; Mi↑ki↓ (@miki_fms) <a href="https://twitter.com/miki_fms/status/1068139697673711617?ref_src=twsrc%5Etfw">November 29, 2018</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


## また更新します

とりあえず，めっちゃ遅れてるので，投稿して，また面白いのできたら更新します笑

## ソースコード

[連載用のgithub](https://github.com/math-miki/metaladventCalendar/)にあげてあります！Day2です．