フラグメントシェーダーでルーン文字のようなものをプロシージャルに生成する


この記事はシェーダーアドベントカレンダー Advent Calendar 2019 14日目です！
https://qiita.com/advent-calendar/2019/shader-advent-calender-2019

13日目は@MachiaWorxさんのシェーダで音楽を作るmusic shaderの紹介
https://qiita.com/MachiaWorx/items/fe586e7ceefbf107254f
でした。

# 概要

魔法陣の飾りに、~~ルーン文字~~っぽいものをプロシージャルに生成したい
さやちゃんぐbotからルーン文字っぽくないという指摘を受けて調べたところ
https://coliss.com/articles/freebies/free-font-which-cannot-read.html
どちらかというと、アトランティック言語に似ていますね

■ 使用例
![summon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135381/72229b58-ab45-3eaf-73bd-07571c79c2ef.png)

# 完成図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135381/a8babd25-61c5-512b-ea49-68eb507266aa.png)

```glsl
precision mediump float;
uniform vec2  m;       // mouse
uniform float t;       // time
uniform vec2  r;       // resolution
uniform sampler2D smp; // prev scene

#define saturate(x) clamp(x,0.,1.)
#define _tail2x(p,n) (mod(p,2.)-1.)

float Hash( vec2 p, in float s ){
    return fract(sin(dot(vec3(p.xy,10.0 * abs(sin(s))),vec3(27.1,61.7, 12.4)))*273758.5453123);
}

float noise(in vec2 p, in float s){
  vec2 i = floor(p);
  vec2 f = fract(p);
  return mix(
    mix(Hash(i + vec2(0.,0.), s), Hash(i + vec2(1.,0.), s),f.x),
    mix(Hash(i + vec2(0.,1.), s), Hash(i + vec2(1.,1.), s),f.x),f.y) * s;
}

float fbm(vec2 p){
  float v = 0.0;
  v += noise(p*34., .1);
  v += noise(p*20., .04);
  return v;
}

vec2 tailY2x(vec2 p,float n){p*=n;return vec2(p.x,_tail2x(p.y,n));}

// signed distance
float sd(float d,float r){return r-d;} 
float dot2(vec2 p){return dot(p,p);}

void main(void){
  vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
  float l = saturate(abs(1./((p.x + fbm(p)) * 80.0))*sd(dot2(tailY2x(p+vec2(.1,0.),12.)),.9));
  gl_FragColor = vec4(l*vec3( 0.75, 0.5, .05 )*2., 1.0);
}
```

* [動作サンプル](http://jp.wgld.org/js4kintro/editor/#precision%20mediump%20float%3B%0Auniform%20vec2%20%20m%3B%20%20%20%20%20%20%20%2F%2F%20mouse%0Auniform%20float%20t%3B%20%20%20%20%20%20%20%2F%2F%20time%0Auniform%20vec2%20%20r%3B%20%20%20%20%20%20%20%2F%2F%20resolution%0Auniform%20sampler2D%20smp%3B%20%2F%2F%20prev%20scene%0A%0A%23define%20saturate(x)%20clamp(x%2C0.%2C1.)%0A%23define%20_tail2x(p%2Cn)%20(mod(p%2C2.)-1.)%0A%0Afloat%20Hash(%20vec2%20p%2C%20in%20float%20s%20)%7B%0A%20%20%20%20return%20fract(sin(dot(vec3(p.xy%2C10.0%20*%20abs(sin(s)))%2Cvec3(27.1%2C61.7%2C%2012.4)))*273758.5453123)%3B%0A%7D%0A%0Afloat%20noise(in%20vec2%20p%2C%20in%20float%20s)%7B%0A%20%20vec2%20i%20%3D%20floor(p)%3B%0A%20%20vec2%20f%20%3D%20fract(p)%3B%0A%20%20return%20mix(%0A%20%20%20%20mix(Hash(i%20%2B%20vec2(0.%2C0.)%2C%20s)%2C%20Hash(i%20%2B%20vec2(1.%2C0.)%2C%20s)%2Cf.x)%2C%0A%20%20%20%20mix(Hash(i%20%2B%20vec2(0.%2C1.)%2C%20s)%2C%20Hash(i%20%2B%20vec2(1.%2C1.)%2C%20s)%2Cf.x)%2Cf.y)%20*%20s%3B%0A%7D%0A%0Afloat%20fbm(vec2%20p)%7B%0A%20%20float%20v%20%3D%200.0%3B%0A%20%20v%20%2B%3D%20noise(p*34.%2C%20.1)%3B%0A%20%20v%20%2B%3D%20noise(p*20.%2C%20.04)%3B%0A%20%20return%20v%3B%0A%7D%0A%0Avec2%20tailY2x(vec2%20p%2Cfloat%20n)%7Bp*%3Dn%3Breturn%20vec2(p.x%2C_tail2x(p.y%2Cn))%3B%7D%0A%0A%2F%2F%20signed%20distance%0Afloat%20sd(float%20d%2Cfloat%20r)%7Breturn%20r-d%3B%7D%20%0Afloat%20dot2(vec2%20p)%7Breturn%20dot(p%2Cp)%3B%7D%0A%0Avoid%20main(void)%7B%0A%20%20vec2%20p%20%3D%20(gl_FragCoord.xy%20*%202.0%20-%20r)%20%2F%20min(r.x%2C%20r.y)%3B%0A%20%20float%20l%20%3D%20saturate(abs(1.%2F((p.x%20%2B%20fbm(p))%20*%2080.0))*sd(dot2(tailY2x(p%2Bvec2(.1%2C0.)%2C12.))%2C.9))%3B%0A%20%20gl_FragColor%20%3D%20vec4(l*vec3(%200.75%2C%200.5%2C%20.05%20)*2.%2C%201.0)%3B%0A%7D)

# 解説
実は単なるfbmです。
一般的なfbmの解説はしませんので、以下The Book of Shaderのfbmの解説の方を参考にしてください。
https://thebookofshaders.com/13/?lan=jp

## ノイズ生成で補間しない

重要なのはノイズ発生部分

```glsl
float noise(in vec2 p, in float s){
  vec2 i = floor(p);
  vec2 f = fract(p);
  f *= f * (3.0-2.0*f);//ノイズの頂点を滑らかに補間してつなぐ
  return mix(
    mix(Hash(i + vec2(0.,0.), s), Hash(i + vec2(1.,0.), s),f.x),
    mix(Hash(i + vec2(0.,1.), s), Hash(i + vec2(1.,1.), s),f.x),f.y) * s;
}
```
通常ノイズ生成時に補間関数を使って各ノイズの頂点を滑らかに補間してつなぎますが、文字のカクカクした感じを出したかったので、補間関数を取り除きます。何もしなければ線形補間になるのでカクカクした感じになります。[fbmで良く雷のサンプル](http://www.glslsandbox.com/e#40683.0)が上がってますが、イマイチなのはこのせいです。ノイズ生成時に滑らかに補間していては、どう頑張ってもイナズマのカクカクした感じは出せません。この補間関数部分に、イージング関数を使って変化を強調しても面白い効果が出せます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135381/e804896a-9afa-2423-910e-5359f0322f42.png)

```glsl
float noise(in vec2 p, in float s){
  vec2 i = floor(p);
  vec2 f = fract(p);
  //f *= f * (3.0-2.0*f);//補間しない
  return mix(
    mix(Hash(i + vec2(0.,0.), s), Hash(i + vec2(1.,0.), s),f.x),
    mix(Hash(i + vec2(0.,1.), s), Hash(i + vec2(1.,1.), s),f.x),f.y) * s;
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135381/e28f174d-70e3-0834-ec61-ec114465d631.png)

## fbmの回数は2回まで

次にfbmの回数を2回にします。
fbmの回数が増えるとドンドン角が取れてしまいます。

```glsl
float fbm(vec2 p){
  float v = 0.0;
  v += noise(p*34., .1);
  v += noise(p*20., .04);
  return v;
}
```

## マスクを掛ける

```glsl
void main(void){
  vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
  float l = saturate(abs(1./((p.x + fbm(p)) * 80.0)));
  gl_FragColor = vec4(l*vec3( 0.75, 0.5, .05 )*2., 1.0);
}
```

↓

```glsl
void main(void){
  vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
  float l = saturate(abs(1./((p.x + fbm(p)) * 80.0))*sd(dot2(tailY2x(p+vec2(.1,0.),12.)),.9));
  gl_FragColor = vec4(l*vec3( 0.75, 0.5, .05 )*2., 1.0);
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135381/702538b0-734b-7b3b-7552-4fd0d0347cc3.png)

```glsl
sd(dot2(tailY2x(p+vec2(.1,0.),12.)),.9)
```

最後に、文字の形に見えるように円のグラデーションをタイリングしたマスクを掛けて完成です！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135381/a8babd25-61c5-512b-ea49-68eb507266aa.png)

明日は、さやちゃんぐbotさんの[80年代ゲーセン筐体っぽいレトロ調の画面をUnityシェーダーで作る。](http://sayachang-bot.hateblo.jp/entry/2019/12/11/231351)です。