運用網路技術輔助插圖:pixiv 如何在繪圖應用程式上運用網頁技術

pixiv 是一個線上社群服務,適合插圖者和插畫愛好者透過內容交流。可讓使用者發布自己的插圖目前全球擁有 8, 400 萬名使用者,截至 2023 年 5 月發布的藝術品已突破 1.2 億

pixiv Sketch 是 pixiv 提供的其中一項服務。用於在網站上使用手指或觸控筆繪製藝術品。這項功能支援各種繪製精美插圖的功能,包括多種筆刷、圖層和值區繪製,而且還能直播繪圖程序。

在本個案研究中,我們將探討 pixiv Sketch 如何使用 WebGL、WebAssembly 和 WebRTC 等新的網路平台功能,改善自家網頁應用程式的效能和品質。

為什麼要在網路上開發草圖應用程式?

pixiv Sketch 於 2015 年首次在網路和 iOS 上推出。他們使用網頁版服務的目標對象主要是電腦,仍是插圖社群最常使用的主要平台。

以下是 pixiv 選擇開發網頁版 (而非電腦版應用程式) 的兩大理由:

  • 建立 Windows、Mac、Linux 等平台的應用程式,所費不貲。網路可透過任何電腦版瀏覽器進入。
  • 網路能在所有平台上觸及最高。您可以在電腦、行動裝置和所有作業系統上使用網路

科技

pixiv Sketch 提供多種筆刷,供使用者選擇。採用 WebGL 前,由於 2D 畫布無法呈現不同筆刷的複雜紋理 (例如鉛筆的粗略邊緣,以及因素描壓力而改變的寬度和色彩強度),因此在採用 WebGL 前就只採用一種筆刷。

使用 WebGL 處理筆刷的廣告素材類型

不過,採用 WebGL 後,他們得以在筆刷詳細資料中加入更多不同的項目,將可用的筆刷數量增加至 7 個。

Pixiv 裡七種不同的筆刷,範圍從精細、粗糙、銳利、不銳利、像素化到平滑等。

使用 2D 畫布結構定義時,只能繪製具有平均平均寬度的簡單紋理的線條,如下方螢幕截圖所示:

採用簡單紋理的筆觸。

這些線條都是透過建立路徑和繪圖筆觸的方式繪製,但 WebGL 會使用點 Sprite 和著色器重現這個情況,如以下程式碼範例所示

以下範例說明頂點著色器。

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);
}

使用點 Sprite 可輕鬆調整粗細和陰影,以回應繪圖壓力,從而顯示下列強和弱線,如以下所示:

在細緻度下銳利,甚至筆刷細緻。

取消銳化筆觸,中間套用更多壓力。

此外,使用點 Sprite 的實作現在可使用獨立的著色器,附加紋理,以便使用鉛筆和油滴筆等紋理以高效地呈現筆刷。

瀏覽器支援觸控筆

對數位藝人來說,數位觸控筆變得非常熱門。新型瀏覽器支援 PointerEvent API,以便讓使用者在裝置上使用觸控筆:使用 PointerEvent.pressure 測量筆壓,並使用 PointerEvent.tiltXPointerEvent.tiltY 測量筆對裝置的角度。

如要使用點 Sprite 執行筆刷筆劃,則必須插入 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);
}

多個繪圖圖層

圖層是數位繪圖領域中最獨特的概念之一。可讓使用者繪製不同的插圖,並逐層編輯。pixiv Sketch 提供的圖層功能與其他數位繪圖應用程式十分相似。

傳統上,您可以使用多個 <canvas> 元素搭配 drawImage() 和合成作業來實作層。不過這會造成問題,因為使用 2D Canvas 結構定義時,無法選擇其他選擇,而是要使用 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)

執行時間:

  • 純 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,您就能在網路平台上建立複雜的應用程式,並在任何裝置上調度資源。如要進一步瞭解本個案研究中介紹的技術,請造訪下列連結: