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

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

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

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

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

pixiv Sketch は、2015 年にウェブと iOS で初めてリリースされました。ウェブ版のターゲット ユーザーは主にパソコンであり、今でもイラスト コミュニティが最も多く利用しているパソコンです。

Pixiv でデスクトップ アプリではなくウェブ版を開発する理由として、主に次の 2 つを挙げます。

  • Windows、Mac、Linux などのアプリの作成には多大な費用がかかります。ウェブはデスクトップ上のあらゆるブラウザに到達します。
  • ウェブはプラットフォーム間で最もリーチ率が高い。ウェブはパソコンとモバイルのほか すべてのオペレーティングシステムで利用できます

テクノロジー

pixiv スケッチには、ユーザーが選べるさまざまなブラシが用意されています。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 で定義されていない合成モードを使用できるようになります。将来的には、拡張性と柔軟性を高めるために、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++ で実装されています。

pixiv Sketch では、すでに C++ で利用可能なコードベースを使用して、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.3 ミリ秒

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

描画中のライブ配信

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