こんにちは。pixiv Sketch(ピクシブスケッチ)チームでアルバイトをしているnontanです。pixiv Sketchは日々のお絵かきを今までよりもっと手軽に楽しめるコミュニケーションプラットフォームです。きちんとした絵だけではなく、落書きや描き途中の絵も気軽にシェアできることを目指して、日々開発をしています。
そのためにpixiv Sketchではブラウザ上でお絵描きができ、そのまま投稿できる機能を提供しています。このお絵かき機能は以前までCanvas 2D Contextベースで実装されていましたが、今年の3月にWebGLとEmscriptenを使って、よりパワフルにリニューアルされました。この記事では、WebGLとEmscriptenのそれぞれがお絵かき機能を実装する上で、どのように活用されているのか紹介します。
WebGL
pixiv Sketchのお絵かき機能では、ブラシによる描画、レイヤー合成等、画像の処理をする部分ほとんどすべてと、キャンバス上に現れるUIパーツにWebGLが用いられています。WebGLによって、API的にはスマートフォンやPCといった、ネイティブ環境でのプログラミングとほぼ同等の自由度でプログラミングすることが可能となっています。
ブラシ
WebGL導入によって最も表現力が高まった部分がブラシです。
上のようなザラザラとした質感を持ったブラシや、筆圧に対応して色の濃さと線の太さの変化のついたブラシ、縁の部分がぼやけたブラシなどが追加されました。 これらはWebGLによるポイントスプライトの高速な合成と、プログラマブルシェーダによって実現されています。
ここで、ブラシに使われているシェーダの中から少し変わったものを紹介したいと思います。
precision highp float; | |
attribute vec2 pos; | |
attribute float thicknessFactor; | |
attribute float opacityFactor; | |
uniform float pointSize; | |
varying float varyingOpacityFactor; | |
varying float hardness; | |
// Calculate hardness from actual point size | |
float calcHardness(float s) { | |
float h0 = .1 * (s - 1.); | |
float h1 = .01 * (s - 10.) + .6; | |
float h2 = .005 * (s - 30.) + .8; | |
float h3 = .001 * (s - 50.) + .9; | |
float h4 = .0002 * (s - 100.) + .95; | |
return min(h0, min(h1, min(h2, min(h3, h4)))); | |
} | |
void main() { | |
float actualPointSize = pointSize * thicknessFactor; | |
varyingOpacityFactor = opacityFactor; | |
hardness = calcHardness(actualPointSize); | |
gl_Position = vec4(pos, 0., 1.); | |
gl_PointSize = actualPointSize; | |
} |
precision highp float; | |
const float strength = .8; | |
const float exponent = 5.; | |
uniform vec4 color; | |
varying float hardness; | |
varying float varyingOpacityFactor; | |
float fallOff(const float r) { | |
// w is for width | |
float w = 1. - hardness; | |
if (w < 0.01) { | |
return 1.; | |
} else { | |
return min(1., pow(1. - (r - hardness) / w, exponent)); | |
} | |
} | |
void main() { | |
vec2 texCoord = (gl_PointCoord - .5) * 2.; | |
float r = length(texCoord); | |
if (r > 1.) { | |
discard; | |
} | |
float brushAlpha = fallOff(r) * varyingOpacityFactor * strength * color.a; | |
gl_FragColor = vec4(color.rgb, brushAlpha); | |
} |
これは、一番右側のブラシプレビューのような、アンチエイリアスがきいたブラシを実現するためのシェーダです。ベクタ的なAPIを持つ、Canvas 2D Contextで実装されていた頃は、アンチエイリアスを有効化するだけで実現可能だったこのブラシですが、ラスタとして処理しているSketchのWebGL実装では、現在の線の太さにおおじてハードネスをうまく変化させながら円を描画することによって実現しました。
レイヤー合成
WebGL版お絵かき機能のもう一つの目玉がレイヤー機能です。
陰を描いたり線画上に下塗りする際に使える乗算モードや、ハイライトを書き込む際に便利なオーバーレイなど、様々な合成モードがあります。
2D Contextを使うことでも複数のcanvasとdrawImage, CompositeOperationを組み合わせたり、CSSのmix-blend-modeなどを使って実現可能ですが、WebGLのオフスクリーンレンダリング機能を用いることによって単一のcanvasのみで実現されています。 また、まだ実装されていませんが、クリッピングやアルファロックといった、2D Contextでは実現しづらいような合成もプログラマブルシェーダを用いることで実現可能となります。
Emscripten
pixiv SketchのiOS, Androidアプリでは、既にバケツ機能が提供されていましたが、Web版では提供していませんでした。また、アプリ版のバケツ機能はC++で実装されていました。そこでWeb版でもバケツ機能を実装するにあたって、白羽の矢が立ったのがEmscriptenとasm.jsです。
実は、pixiv Sketchのバケツ機能は単に同じ色の部分を塗りつぶすだけではなく、
- 染み出し機能
- 隙間埋め
- アンチエイリアス
といった機能が控えめにですが実装されています。これらの機能を実現するためにはビットマップ上のピクセルを探索したり、塗りつぶし領域の境界をトラバースしたりする必要があります。このような処理をGCが発生しないように意識して書いたJavaScriptのコードとC++のコードを比較すると次のようになります。
bfsQueue.push(startPoint); | |
while (!bfsQueue.empty()) { | |
Point point = bfsQueue.front(); | |
bfsQueue.pop(); | |
/* ... */ | |
bfsQueue.push(anotherPoint); | |
} |
const bfsQueue = []; | |
while (bfsQueue.length > 0) { | |
const pointY = bfsQueue.pop(); | |
const pointX = bfsQueue.pop(); | |
/* ... */ | |
bfsQueue.unshift(anotherPointX, anotherPointY); | |
} |
JavaScriptではオブジェクトがヒープにアロケートされることを回避するために、配列のインデックスの偶奇などを用いて構造をエンコードするなど、不自然な書き方をする必要があります。対して、C++では自然な記述でもパフォーマンスに問題が出ることがありません。
また、asm.jsを解釈することができるブラウザであれば、境界のトラバースなどの処理を追加した場合でも、比較的実行時間の増加を抑えることができる点も大きなメリットです。
このように、Emscriptenを用いることによって、アプリ版で利用しているコードにJavaScript向けのバインディングコードを追加するだけで、バケツ機能を移植することが可能になりました。ロジック部分は同一のままであるため、Web版に移植する際に行った修正などを、すばやくアプリ版へフィードバックすることもできました。
まとめ
いかがでしたか。
ネイティブアプリとの差を詰めていくWeb版pixiv Sketchのお絵かき機能と、WebGLやEmscriptenといった技術はとても相性が良く、様々な面で活用されています。
pixiv SketchチームではWebGLやEmscriptenに興味があるアルバイトやエンジニアを募集しています。