Siła internetu dla ilustratorów: jak pixiv wykorzystuje technologie internetowe w swojej aplikacji do rysowania

pixiv to internetowa społeczność entuzjastów ilustratorów i ilustracji do komunikowania się ze sobą za pomocą swoich treści. Pozwala ona publikować własne ilustracje. Z aplikacji korzysta ponad 84 mln użytkowników na całym świecie i do maja 2023 r. opublikowanych jest ponad 120 milionów dzieł sztuki.

Pixiv Sketch to jedna z usług dostarczanych przez pixiv. Służy do rysowania dzieł sztuki na stronie internetowej palcami lub rysikiem. Obsługuje wiele funkcji rysowania niesamowitych ilustracji, w tym wiele rodzajów pędzli, warstw i malowanie wiader, a także umożliwia transmitowanie na żywo procesu rysowania.

W tym studium przypadku przyjrzymy się temu, jak firma Pixiv Sketch zwiększyła wydajność i jakość swojej aplikacji internetowej dzięki nowym funkcjom platformy internetowej, takim jak WebGL, WebAssembly i WebRTC.

Po co tworzyć aplikację do szkicowania w internecie?

Aplikacja Pixiv Sketch została po raz pierwszy udostępniona w internecie i na iOS w 2015 roku. Docelową grupą odbiorców witryny były głównie komputery, które wciąż są najczęściej używaną platformą przez społeczność ilustracyjną.

Oto 2 główne powody, dla których pixiv zdecydowało się utworzyć wersję internetową, a nie komputerową:

  • Tworzenie aplikacji na systemy Windows, Mac, Linux i inne jest bardzo kosztowne. Sieć dociera do każdej przeglądarki na komputerze.
  • Internet ma największy zasięg na różnych platformach. Internet jest dostępny na komputerach, urządzeniach mobilnych i w każdym systemie operacyjnym.

Technologia

Pixiv Sketch oferuje wiele różnych pędzli do wyboru. Przed wprowadzeniem WebGL używał tylko jednego pędzla, ponieważ płótno 2D było zbyt ograniczone do przedstawienia złożonej tekstury różnych pędzli, takich jak grube krawędzie ołówka oraz różne szerokości i intensywności kolorów, które zmieniają się w zależności od nacisku szkicowego.

Kreatywne rodzaje pędzli przy użyciu WebGL

Jednak dzięki wdrożeniu WebGL umożliwiło to dodanie większej liczby szczegółów pędzli i zwiększenie liczby dostępnych pędzli do 7.

7 różnych pędzli pixiv: od cienkich i grubych, ostrych i nieostrych, pikselizowanych po wygładzone itp.

Korzystając z kontekstu kanwy 2D, można było rysować tylko linie o prostej teksturze i równomiernej szerokości, jak na poniższym zrzucie ekranu:

Styl pędzla o prostej teksturze.

Linie te narysowano przez utworzenie ścieżek i kresek, ale WebGL odtworzy to za pomocą sprite’ów punktowych i programów do cieniowania, co widać w przykładach poniżej.

Przykład poniżej pokazuje cieniowanie wierzchołków.

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

Poniżej znajduje się przykładowy kod cieniowania fragmentów.

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

Wykorzystanie punktowych sprite'ów ułatwia zmianę grubości i cieniowania w odpowiedzi na nacisk, co pozwala na wyrażone mocne i słabe linie, jak te:

Ostry, równomierny pociąg pędzlem z cienkimi końcówkami.

Nieostre pociągnięcia pędzla z większym naciskiem pośrodku.

Dodatkowo implementacje używające sprite’ów punktowych mogą teraz dołączać tekstury za pomocą osobnego programu do cieniowania, co umożliwia skuteczne reprezentowanie pędzli za pomocą tekstur, takich jak ołówek i pisak.

Obsługa rysika w przeglądarce

Używanie rysika cyfrowego stało się niezwykle popularne wśród artystów cyfrowych. Nowoczesne przeglądarki obsługują interfejs PointerEvent API, który umożliwia użytkownikom korzystanie z rysika na urządzeniu. PointerEvent.pressure do pomiaru nacisku pióra, a PointerEvent.tiltX, PointerEvent.tiltY – do mierzenia kąta nachylenia pióra względem urządzenia.

Aby można było przesuwać pędzlem ze sprite’em punktowym, element PointerEvent musi być interpolowany i przekształcony w bardziej szczegółową sekwencję zdarzeń. W funkcji PointerEvent orientację rysika można uzyskać w postaci współrzędnych biegunowych, ale program Pixiv Sketch konwertuje je na wektor odzwierciedlający orientację rysika przed ich użyciem.

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

Wiele warstw rysowania

Warstwy to jedna z najbardziej wyjątkowych koncepcji w rysunkach cyfrowych. Pozwalają użytkownikom rysować na sobie różne ilustracje i wprowadzać zmiany w kolejnych warstwach. Pixiv Sketch oferuje funkcje warstwy podobnie jak inne aplikacje do rysowania cyfrowego.

Konwencjonalnie implementuje warstwy przy użyciu kilku elementów <canvas> za pomocą operacji drawImage() i operacji komponowania. Jest to jednak problem, ponieważ w przypadku kontekstu kanwy 2D nie ma innego wyjścia niż użycie trybu kompozycji CanvasRenderingContext2D.globalCompositeOperation, który jest wstępnie zdefiniowany i w dużym stopniu ogranicza skalowalność. Dzięki użyciu WebGL i napisaniu cieniowania programiści mogą korzystać z trybów kompozycji, które nie są wstępnie zdefiniowane w interfejsie API. W przyszłości pixiv Sketch wdroży funkcję warstwy z użyciem WebGL, aby zapewnić większą skalowalność i elastyczność.

Oto przykładowy kod kompozycji warstwy:

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

Obraz na dużym obszarze z funkcją wiadra

Aplikacje pixiv Sketch na iOS i Androida już udostępniały tę funkcję, ale wersja internetowa już nie. Wersja aplikacji funkcji zasobnika została zaimplementowana w języku C++.

Ponieważ baza kodu jest już dostępna w języku C++, firma pixiv Sketch wykorzystała Emscripten i asm.js do zaimplementowania funkcji zasobnika w wersji internetowej.

bfsQueue.push(startPoint);

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

Użycie kodu asm.js umożliwiło wydajne rozwiązanie. W porównaniu z czasem wykonywania czystego JavaScriptu i kodu asm.js czas wykonywania przy użyciu pliku asm.js jest skrócony o 67%. Wyniki powinny być jeszcze lepsze w standardzie WASM.

Szczegóły testu:

  • Jak: malowanie obszaru o wymiarach 1180 x 800 pikseli za pomocą funkcji zasobnika
  • Urządzenie testowe: MacBook Pro (M1 Max)

Czas wykonania:

  • Czysty JavaScript: 213,8 ms
  • asm.js: 70,3 ms

Dzięki wykorzystaniu Emscripten i asm.js pixiv Sketch udało się udostępnić funkcję zasobnika, wykorzystując bazę kodu z wersji aplikacji na danej platformie.

Transmisja na żywo podczas rysowania

Pixiv Sketch umożliwia transmitowanie na żywo podczas rysowania za pomocą aplikacji internetowej Pixiv Sketch LIVE. Wykorzystuje ona interfejs API WebRTC, który łączy ścieżkę dźwiękową mikrofonu uzyskaną z getUserMedia() i ścieżkę wideo MediaStream pobraną z elementu <canvas>.

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

Podsumowanie

Dzięki nowym interfejsom API, takim jak WebGL, WebAssembly i WebRTC, możesz stworzyć złożoną aplikację na platformie internetowej i skalować ją na dowolnym urządzeniu. Więcej informacji o technologiach przedstawionych w tym studium przypadku znajdziesz pod tymi linkami: