イラストレーターのためのウェブの力: ウェブ技術を活用した pixiv の描画アプリ

pixiv は、イラストレーターとイラスト愛好家がコンテンツを通じて交流できるオンライン コミュニティ サービスです。ユーザーが自分のイラストを投稿できるサービスです。世界中で 8,400 万人以上のユーザーがおり、2023 年 5 月時点で 1 億 2,000 万点以上のアート作品が投稿されています。

pixiv Sketch は pixiv が提供するサービスの 1 つです。指やタッチペンを使ってウェブサイトにアートワークを描画するために使用されます。さまざまな種類のブラシ、レイヤ、バケツ塗りなど、素晴らしいイラストを描くためのさまざまな機能をサポートしており、描画プロセスをライブ配信することもできます。

このケーススタディでは、pixiv Sketch が WebGL、WebAssembly、WebRTC などの新しいウェブ プラットフォーム機能を使用して、ウェブアプリのパフォーマンスと品質をどのように改善したかを見ていきます。

ウェブでスケッチ アプリを開発する理由

pixiv Sketch は 2015 年にウェブと iOS で初めてリリースされました。ウェブ版のターゲット ユーザーは主にデスクトップ ユーザーであり、イラスト コミュニティで最も主要なプラットフォームは今もデスクトップです。

pixiv がデスクトップ アプリではなくウェブ版の開発を選択した主な理由は次の 2 つです。

  • Windows、Mac、Linux などのアプリを作成するには、非常にコストがかかります。ウェブはデスクトップの任意のブラウザに到達します。
  • ウェブはプラットフォームを問わず最もリーチが広いです。ウェブはパソコンとモバイルで利用でき、すべてのオペレーティング システムに対応しています。

テクノロジー

pixiv Sketch には、ユーザーが選択できるさまざまなブラシがあります。WebGL を採用する前は、2D キャンバスでは鉛筆の粗いエッジや、スケッチの圧力によって変化する幅や色の濃さなど、さまざまなブラシの複雑なテクスチャを描写するには限界があったため、ブラシの種類は 1 つしかありませんでした。

WebGL を使用したクリエイティブなブラシの種類

しかし、WebGL を採用したことで、ブラシの詳細のバリエーションを増やし、利用可能なブラシの数を 7 つに増やすことができました。

pixiv には、細いものから粗いもの、シャープなものからぼやけたもの、ピクセル化されたものから滑らかなものなど、7 種類のブラシがあります。

2D キャンバス コンテキストを使用すると、次のスクリーンショットのように、幅が均等に分布した単純なテクスチャの線しか描画できませんでした。

シンプルなテクスチャのブラシストローク。

これらの線はパスを作成してストロークを描画することで描画されましたが、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);
}

ポイント スプライトを使用すると、描画圧力に応じて太さと陰影を簡単に変えることができるため、次のような強弱のある線を表現できます。

細い先端でシャープな筆運び。

中央に圧力を加えて描いた、シャープでないブラシストローク。

また、ポイント スプライトを使用する実装では、別のシェーダーを使用してテクスチャを添付できるようになりました。これにより、鉛筆やフェルトペンなどのテクスチャを含むブラシを効率的に表現できます。

ブラウザでのタッチペン サポート

デジタル アーティストの間で、デジタル スタイラスの使用が非常に一般的になっています。最新のブラウザは、ユーザーがデバイスでスタイラスを使用できるようにする PointerEvent API をサポートしています。PointerEvent.pressure を使用してペンの筆圧を測定し、PointerEvent.tiltXPointerEvent.tiltY を使用してデバイスに対するペンの角度を測定します。

ポイント スプライトでブラシストロークを実行するには、PointerEvent を補間して、よりきめ細かいイベント シーケンスに変換する必要があります。PointerEvent では、スタイラスの向きを極座標の形式で取得できますが、pixiv Sketch では、使用前にスタイラスの向きを表すベクトルに変換します。

function getTiltAsVector(event: PointerEvent): [number, number, number] {
  const u = Math.tan((event.tiltX / 180) * Math.PI);
  const v = Math.tan((event.tiltY / 180) * Math.PI);
  const z = Math.sqrt(1 / (u * u + v * v + 1));
  const x = z * u;
  const y = z * v;
  return [x, y, z];
}

function handlePointerDown(event: PointerEvent) {
  const position = [event.clientX, event.clientY];
  const pressure = event.pressure;
  const tilt = getTiltAsVector(event);

  interpolateAndRender(position, pressure, tilt);
}

複数の描画レイヤ

レイヤは、デジタル ドローイングで最もユニークなコンセプトの 1 つです。ユーザーはイラストのさまざまな部分を重ねて描画したり、レイヤごとに編集したりできます。pixiv Sketch には、他のデジタル ドローイング アプリと同様のレイヤ機能が用意されています。

従来は、複数の <canvas> 要素と drawImage() 要素、合成演算を使用してレイヤを実装することができました。ただし、2D キャンバス コンテキストでは CanvasRenderingContext2D.globalCompositeOperation 合成モードを使用するしかなく、このモードは事前定義されており、スケーラビリティを大幅に制限するため、これは問題です。WebGL を使用してシェーダーを記述することで、API で事前定義されていない合成モードを使用できます。今後、pixiv Sketch では、スケーラビリティと柔軟性を高めるために WebGL を使用してレイヤ機能を実装する予定です。

レイヤ構成のサンプルコードを次に示します。

precision highp float;

uniform sampler2D baseTexture;
uniform sampler2D blendTexture;
uniform mediump float opacity;

varying highp vec2 uv;

// for normal mode
vec3 blend(const vec4 baseColor, const vec4 blendColor) {
  return blendColor.rgb;
}

// for multiply mode
vec3 blend(const vec4 baseColor, const vec4 blendColor) {
  return blendColor.rgb * blendColor.rgb;
}

void main()
{
  vec4 blendColor = texture2D(blendTexture, uv);
  vec4 baseColor = texture2D(baseTexture, uv);

  blendColor.a *= opacity;

  float a1 = baseColor.a * blendColor.a;
  float a2 = baseColor.a * (1. - blendColor.a);
  float a3 = (1. - baseColor.a) * blendColor.a;

  float resultAlpha = a1 + a2 + a3;

  const float epsilon = 0.001;

  if (resultAlpha > epsilon) {
    vec3 noAlphaResult = blend(baseColor, blendColor);
    vec3 resultColor =
        noAlphaResult * a1 + baseColor.rgb * a2 + blendColor.rgb * a3;
    gl_FragColor = vec4(resultColor / resultAlpha, resultAlpha);
  } else {
    gl_FragColor = vec4(0);
  }
}

バケツ機能で広い範囲を塗りつぶす

pixiv Sketch の iOS 版アプリと Android 版アプリにはすでにバケツ機能が搭載されていましたが、ウェブ版には搭載されていませんでした。バケット関数のアプリ バージョンは C++ で実装されました。

コードベースが C++ で提供されていたため、pixiv Sketch は Emscripten と asm.js を使用してバケット関数をウェブ版に実装しました。

bfsQueue.push(startPoint);

while (!bfsQueue.empty()) {
  Point point = bfsQueue.front();
  bfsQueue.pop();
  /* ... */
  bfsQueue.push(anotherPoint);
}

asm.js を使用することで、パフォーマンスの高いソリューションを実現できました。純粋な JavaScript と asm.js の実行時間を比較すると、asm.js を使用した場合の実行時間は 67% 短縮されます。WASM を使用すると、さらに改善されることが期待されます。

テストの詳細:

  • 方法: バケツ機能を使用して 1180x800 ピクセルの領域を塗りつぶす
  • テストデバイス: MacBook Pro(M1 Max)

実行時間:

  • Pure JavaScript: 213.8 ミリ秒
  • asm.js: 70.3ms

Emscripten と asm.js を使用して、pixiv Sketch はプラットフォーム固有のアプリ バージョンのコードベースを再利用することで、バケット機能をリリースすることに成功しました。

絵を描きながらライブ配信する

pixiv Sketch では、pixiv Sketch LIVE ウェブアプリを通じて、お絵かき中にライブ配信する機能を提供しています。この機能では、WebRTC API を使用して、getUserMedia() から取得したマイクの音声トラックと <canvas> 要素から取得した MediaStream 動画トラックを組み合わせています。

const canvasElement = document.querySelector('#DrawCanvas');
const framerate = 24;
const canvasStream = canvasElement.captureStream(framerate);
const videoStreamTrack = canvasStream.getVideoTracks()[0];

const audioStream = await navigator.mediaDevices.getUserMedia({
  video: false,
  audio: {},
});
const audioStreamTrack = audioStream.getAudioTracks()[0];

const stream = new MediaStream();
stream.addTrack(audioStreamTrack.clone());
stream.addTrack(videoStreamTrack.clone());

まとめ

WebGL、WebAssembly、WebRTC などの新しい API の力を借りて、ウェブ プラットフォームで複雑なアプリを作成し、あらゆるデバイスにスケーリングできます。このケーススタディで紹介したテクノロジーについて詳しくは、次のリンクをご覧ください。