シェーダーでルーン文字書いてみた
こんにちは, 今月からA-PxL代表になりました学部2年の木村です. これからA-PxL をもっとxR技術に触れたり, 開発や勉強などしやすい場にすることを目指していきたいと思います!
最近シェーダーの勉強をして遊んでおり, 今回はルーン文字を表示するシェーダーを書いたのでその紹介をしたいと思います.
ルーン文字とは
ルーン文字は「呪術や儀式に用いられた神秘的な文字」と紹介されることもあるが、実際には日常の目的で使われており、ルーン文字で記された書簡や荷札なども多数残されている。呪術にも用いられていたが、それが盛んに行われるようになったのは、むしろラテン文字が普及しルーン文字が古めかしくいかにも神秘的に感じられるようになった時代に入ってからである。
ルーン文字はずっと昔に使われていた文字で, ラテン文字が普及した頃には呪術などにも使われるようになった文字とのことです. アニメやゲームでの魔法演出などによく使われており, 昨年の部内でチーム開発した「DESIGN」という作品でも使っていました.
https://youtu.be/UG9Lq4oFoQk?t=142
木片などにナイフで刻みつけて表記していたようなので, 直線を組み合わせた字形となっていてシェーダーでも書きやすそうです.
実装
前述の通り, ルーン文字は直線を組み合わせた字形のため, 長方形を組み合わせればよさそうです. レイマーチングで使っていた距離関数で2Dの絵も描けるようなのでやってみました. 距離関数とは, 相手との距離を返す関数のことです. こちらに詳しく解説されていましたのでご参照ください.
この表の左上の「フェオ(Feoh)」という文字を書いてみます.
float dRoundRect(float2 pos, float2 size){ float2 d = abs(pos) - size; return length(max(d, .0)) + min(max(d.x, d.y), .0); } float dFeoh(float2 pos, float size){ pos *= 100. / size; float width = .1; pos.x += 1.; float rect1 = dRoundRect(pos, float2(width, 10)); return rect1; } float3 map(float2 uv){ float2 fPos = frac(uv) * 2 -1; return dFeoh(fPos, 5); } fixed4 frag (v2f i) : SV_Target{ fixed4 col = 1; col.rgb = step(.5, map(i.uv)); if(col.r >= 1. && col.g >= 1. && col.b >= 1.){ discard; } return col; }
↑は丸みのある長方形の距離関数をサイズ調整しただけです.
static const float PI = 3.141592; float2 rotate(float2 pos, float angle){ angle = angle * PI / 180.; float2 a = normalize(angle); float s = sin(angle); float c = cos(angle); return float2(pos.x * c - pos.y * s, pos.x * s + pos.y * c); } // 略 float dFeoh(float2 pos, float size){ pos *= 100. / size; float width = .1; pos.x += 1.; float rect1 = dRoundRect(pos, float2(width, 10)); float rect2 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 6.8), 45.), float2(width, 4.)); return rect2; }
次は長方形を回転させたものです. 変更のない関数などは省略しました.
これで長方形を好きな角度で表示することができるようになったので, 次は組み合わせてみます.
float opUnion(float d1, float d2){ return min(d1, d2); } // 略 float dFeoh(float2 pos, float size){ pos *= 100. / size; float width = .1; pos.x += 1.; float rect1 = dRoundRect(pos, float2(width, 10)); float rect2 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 6.8), 45.), float2(width, 4.)); float rect3 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 1.8), 45.), float2(width, 4.)); return opUnion(rect1, opUnion(rect2, rect3)); }
opUnion
は和集合の演算をしています. このように, 和集合や差集合などを使って距離関数の合成ができます.
あとは同様に24個分のルーン文字を作成するだけです.
// ----------------- Rune ------------------------- // フェオ float dFeoh(float2 pos, float size){ pos *= 100. / size; float width = .1; pos.x += 1.; float rect1 = dRoundRect(pos, float2(width, 10)); float rect2 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 6.8), 45.), float2(width, 4.)); float rect3 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 1.8), 45.), float2(width, 4.)); return opUnion(rect1, opUnion(rect2, rect3)); } // ウル float dUr(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(float2(pos.x + 5., pos.y + .5), float2(width, 9.5)); float rect2 = dRoundRect(float2(pos.x - 5., pos.y + 2.), float2(width, 8)); float rect3 = dRoundRect(rotate(float2(pos.x, pos.y - 7.5), 106), float2(width, 5.3)); return opUnion(rect1, opUnion(rect2, rect3)); } // ソーン float dThorn(float2 pos, float size){ pos *= 100. / size; float width = .1; pos.x += 1.; float rect1 = dRoundRect(pos, float2(width, 10)); float rect2 = dRoundRect(rotate(float2(pos.x - 5.5, abs(pos.y)), 130), float2(width, 7)); return opUnion(rect1, rect2); } // アンスール float dAnsur(float2 pos, float size){ pos *= 100. / size; float width = .1; pos.x += 1.; float rect1 = dRoundRect(pos, float2(width, 10)); float rect2 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 7.), -45.), float2(width, 4.)); float rect3 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 2.), -45.), float2(width, 4.)); return opUnion(rect1, opUnion(rect2, rect3)); } // ラド float dRad(float2 pos, float size){ pos *= 100. / size; float width = .1; pos.x += 1.; float rect1 = dRoundRect(pos, float2(width, 10)); float rect2 = dRoundRect(rotate(float2(pos.x - 5.5, abs(pos.y - 5.45)), 130), float2(width, 7)); float rect3 = dRoundRect(rotate(float2(pos.x - 4, pos.y + 3.), 130), float2(width, 5.)); return opUnion(rect1, opUnion(rect2, rect3)); } // ケン float dKen(float2 pos, float size){ pos *= 100. / size; float width = .1; pos.x += 1.; float rect1 = dRoundRect(rotate(float2(pos.x, abs(pos.y)), 45.), float2(width, 10)); return rect1; } float dGeofu(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(rotate(float2(pos.x, pos.y), 45.), float2(width, 10)); float rect2 = dRoundRect(rotate(float2(pos.x, -pos.y), 45.), float2(width, 10)); return opUnion(rect1, rect2); } float dWynn(float2 pos, float size){ pos *= 100. / size; float width = .1; pos.x += 1.; float rect1 = dRoundRect(pos, float2(width, 10)); float rect2 = dRoundRect(rotate(float2(pos.x - 5.5, abs(pos.y - 5.45)), 130), float2(width, 7)); return opUnion(rect1, rect2); } float dHagall(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(float2(pos.x + 6., pos.y + .5), float2(width, 9.5)); float rect2 = dRoundRect(float2(pos.x - 6., pos.y + .5), float2(width, 9.5)); float rect3 = dRoundRect(rotate(float2(pos.x, pos.y), 120), float2(width, 6.5)); return opUnion(rect1, opUnion(rect2, rect3)); } float dNied(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(pos, float2(width, 10)); float rect2 = dRoundRect(rotate(float2(pos.x, pos.y - 1.), 120), float2(width, 6.5)); return opUnion(rect1, rect2); } float dIs(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect = dRoundRect(pos, float2(width, 10)); return rect; } float dJara(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(rotate(float2(pos.x + 6., abs(pos.y - 2.)), 50.), float2(width, 7.)); float rect2 = dRoundRect(rotate(float2(pos.x - 6., abs(pos.y + 2.)), -50.), float2(width, 7.)); return opUnion(rect1, rect2); } float dYr(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(pos, float2(width, 10)); float rect2 = dRoundRect(rotate(float2(pos.x - 3.1, pos.y - 7.35), -50.), float2(width, 4.)); float rect3 = dRoundRect(rotate(float2(pos.x + 3.1, pos.y + 7.35), -50.), float2(width, 4.)); return opUnion(rect1, opUnion(rect2, rect3)); } float dPeorth(float2 pos, float size){ pos *= 100. / size; float width = .1; pos.x += 1.; float rect1 = dRoundRect(float2(pos.x + 5., pos.y), float2(width, 8.)); float rect2 = dRoundRect(rotate(float2(abs(pos.x - 1.4), pos.y - 3.15), 53.), float2(width, 8.)); float rect3 = dRoundRect(rotate(float2(abs(pos.x - 1.4), pos.y + 3.15), -53.), float2(width, 8.)); return opUnion(rect1, opUnion(rect2, rect3)); } float dEolh(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(float2(pos.x, pos.y), float2(width, 10.)); float rect2 = dRoundRect(rotate(float2(abs(pos.x), pos.y - 2.), 45.), float2(width, 8.)); return opUnion(rect1, rect2); } float dSigel(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(rotate(float2(pos.x - .7, pos.y + 4.), 53.), float2(width, 4.)); float rect2 = dRoundRect(rotate(float2(pos.x - .7, pos.y - .7), -53.), float2(width, 4.)); float rect3 = dRoundRect(rotate(float2(pos.x - .7, pos.y - 5.5), 53.), float2(width, 4.)); return opUnion(rect1, opUnion(rect2, rect3)); } float dTir(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(float2(pos.x, pos.y), float2(width, 10.)); float rect2 = dRoundRect(rotate(float2(pos.x - 3.3, pos.y - 7.5), -53.), float2(width, 4.)); float rect3 = dRoundRect(rotate(float2(pos.x + 3.3, pos.y - 7.5), 53.), float2(width, 4.)); return opUnion(rect1, opUnion(rect2, rect3)); } float dBeorc(float2 pos, float size){ pos *= 100. / size; float width = .1; pos.x += 2.; float rect1 = dRoundRect(float2(pos.x, pos.y), float2(width, 10.)); float rect2 = dRoundRect(rotate(float2(pos.x - 3.3, pos.y - 7.5), -53.), float2(width, 4.)); float rect3 = dRoundRect(rotate(float2(pos.x - 3.3, pos.y - 2.7), 53.), float2(width, 4.)); float rect4 = dRoundRect(rotate(float2(pos.x - 3.3, pos.y + 7.5), 53.), float2(width, 4.)); float rect5 = dRoundRect(rotate(float2(pos.x - 3.3, pos.y + 2.7), -53.), float2(width, 4.)); return opUnion(rect1, opUnion(rect2, opUnion(rect3, opUnion(rect4, rect5)))); } float dEoh(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(float2(pos.x - 7, pos.y + .5), float2(width, 9.5)); float rect2 = dRoundRect(float2(pos.x + 7., pos.y + .5), float2(width, 9.5)); float rect3 = dRoundRect(rotate(float2(pos.x - 3.5, pos.y - 6.4), 54.), float2(width, 4.3)); float rect4 = dRoundRect(rotate(float2(pos.x + 3.5, pos.y - 6.4), -54.), float2(width, 4.3)); return opUnion(rect1, opUnion(rect2, opUnion(rect3, rect4))); } float dMann(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(float2(pos.x - 7, pos.y + .5), float2(width, 9.5)); float rect2 = dRoundRect(float2(pos.x + 7., pos.y + .5), float2(width, 9.5)); float rect3 = dRoundRect(rotate(float2(pos.x - 3.5, pos.y - 6.9), 60.), float2(width, 4.1)); float rect4 = dRoundRect(rotate(float2(pos.x + 3.5, pos.y - 6.9), -60.), float2(width, 4.1)); float rect5 = dRoundRect(rotate(float2(pos.x + 3.5, pos.y - 3.), 60.), float2(width, 4.1)); float rect6 = dRoundRect(rotate(float2(pos.x - 3.5, pos.y - 3.), -60.), float2(width, 4.1)); return opUnion(rect1, opUnion(rect2, opUnion(rect3, opUnion(rect4, opUnion(rect5, rect6))))); } float dLagu(float2 pos, float size){ pos *= 100. / size; float width = .1; pos.x += 1.; float rect1 = dRoundRect(float2(pos.x, pos.y), float2(width, 9.5)); float rect2 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 6.5), -45.), float2(width, 4.3)); return opUnion(rect1, rect2); } float dIng(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(rotate(float2(pos.x - 3.5, pos.y + 3.5), 45.), float2(width, 5.)); float rect2 = dRoundRect(rotate(float2(pos.x + 3.5, pos.y + 3.5), -45.), float2(width, 5.)); float rect3 = dRoundRect(rotate(float2(pos.x + 3.5, pos.y - 3.5), 45.), float2(width, 5.)); float rect4 = dRoundRect(rotate(float2(pos.x - 3.5, pos.y - 3.5), -45.), float2(width, 5.)); return opUnion(rect1, opUnion(rect2, opUnion(rect3, rect4))); } float dOthel(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(rotate(float2(pos.x - .5, pos.y + 3.5), 45.), float2(width, 6.)); float rect2 = dRoundRect(rotate(float2(pos.x + .5, pos.y + 3.5), -45.), float2(width, 6.)); float rect3 = dRoundRect(rotate(float2(pos.x + .5, pos.y - 3.3), 45.), float2(width, 3.5)); float rect4 = dRoundRect(rotate(float2(pos.x - .5, pos.y - 3.3), -45.), float2(width, 3.5)); return opUnion(rect1, opUnion(rect2, opUnion(rect3, rect4))); } float dDaeg(float2 pos, float size){ pos *= 100. / size; float width = .1; float rect1 = dRoundRect(float2(pos.x - 7, pos.y), float2(width, 9.5)); float rect2 = dRoundRect(float2(pos.x + 7., pos.y), float2(width, 9.5)); float rect3 = dRoundRect(rotate(float2(pos.x, pos.y ), 36.5), float2(width, 11.7)); float rect4 = dRoundRect(rotate(float2(pos.x, pos.y ), -36.5), float2(width, 11.7)); return opUnion(rect1, opUnion(rect2, opUnion(rect3, rect4))); } float dBlank(float2 pos, float size){ pos *= 100. / size; float width = .1; return 1.; } // ---------------------------------------
ルーン文字の距離関数だけで300行近くなってしまいました. しんどい...
これでとりあえずすべての文字がシェーダーで書けるようにはなりましたが, 他にもいろいろ詰め込んで最終的にできたものはこちら↓です
— ryudai (@bigdra50) August 24, 2019
https://github.com/bigdra50/Shaders/blob/master/ShaderLab/Rune.shader
最後に
これをGPUパーティクルにしたものを記事にしたくて実装中でしたが, まだまだ時間がかかりそうだったのでここまでにしました.
だいぶ無理やりな実装になってる気がするのでもっといいやり方あれば教えて頂けると嬉しいです...
参考
ルーン文字関連 https://happy-runes.jimdo.com/%E3%83%AB%E3%83%BC%E3%83%B3%E6%96%87%E5%AD%97%E4%B8%80%E8%A6%A7-%E6%84%8F%E5%91%B3/ https://ja.wikipedia.org/wiki/%E3%83%AB%E3%83%BC%E3%83%B3%E6%96%87%E5%AD%97 距離関数 https://qiita.com/7CIT/items/fe33b9b341b9918b6c3d#%E8%A7%92%E4%B8%B8%E9%95%B7%E6%96%B9%E5%BD%A2 http://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm https://www.slideshare.net/shohosoda9/threejs-58238484 図形や文字の描画 http://karanokan.info/2019/03/31/post-2465/ http://www.shaderslab.com/demo-75---matrix-pattern.html http://wordpress.notargs.com/blog/blog/2015/08/21/pixel-shaderglsl-sandbox%e3%81%a7%e3%83%87%e3%82%b8%e3%82%bf%e3%83%ab%e3%81%aa%e3%82%bf%e3%82%a4%e3%83%9e%e3%83%bc%e3%82%92%e4%bd%9c%e3%82%8b/