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

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

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

このケーススタディでは、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 には、他のデジタル描画アプリと同様にレイヤ機能が用意されています。

従来、drawImage() と合成オペレーションを使用して複数の <canvas> 要素を使用してレイヤを実装できます。ただし、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 を使用すると、さらに改善される見込みです。

テストの詳細:

  • 方法: バケット関数を使用して 1,180 x 800 ピクセルの領域を塗りつぶす
  • テストデバイス: MacBook Pro(M1 Max)

実行時間:

  • 純粋な JavaScript: 213.8 ミリ秒
  • asm.js: 70.3 ミリ秒

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

描画中のライブ配信

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 を利用すると、ウェブ プラットフォームで複雑なアプリを作成して、あらゆるデバイスにスケーリングできます。このケーススタディで紹介したテクノロジーの詳細については、次のリンクをご覧ください。