Edited at
NCCDay 3

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

この記事はNCC Advent Calendar 2018の3日目の記事です.

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


勝手にやってるMetal連載

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


  1. Rendererを作る

  2. プロシージャルモデリング的な何か

  3. フラグメントシェーダで遊ぶ ← イマココ

  4. テクスチャで遊ぶ


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

今回はフラグメントシェーダで遊びます.

ViewControllerからRendererに渡してあげる頂点の情報は1日目と同じく,View全体をおおうための長方形1つだけです.

第一回でも説明しましたが.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}$倍する

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

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

スクリーンショット 2018-12-03 23.04.35.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_5266.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);


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


マンデルブロ集合

とりあえず,シェーダといったらマンデルブロ集合なきがするので,作りました.

ついでにアニメーションさせたりしています!


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_600388A1B7E2-1.jpeg


波を干渉させる

GLSLスクールのfragmentシェーダの練習で出題されていました.

これもまた動かして楽しんでみました.


また更新します

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


ソースコード

連載用のgithubにあげてあります!Day2です.