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

pixiv to internetowa społeczność dla ilustratorów i entuzjastów ilustracji, którzy mogą się ze sobą komunikować za pomocą swoich treści. Pozwala ona użytkownikom publikować własne ilustracje. W maju 2023 r. aplikacja miała ponad 84 miliony użytkowników na całym świecie oraz ponad 120 milionów utworzonych prac.

pixiv Sketch to jedna z usług oferowanych przez pixiv. Służy do tworzenia grafik w witrynie za pomocą palców lub rysika. Aplikacja obsługuje wiele funkcji do tworzenia niesamowitych ilustracji, w tym różne rodzaje pędzli, warstw i wypełniania, a także umożliwia prowadzenie transmisji na żywo z procesu tworzenia rysunku.

W tym studium przypadku przyjrzymy się, jak firma Pixiv Sketch poprawiła wydajność i jakość swojej aplikacji internetowej dzięki wykorzystaniu nowych funkcji platformy internetowej, takich jak WebGL, WebAssembly czy WebRTC.

Dlaczego warto tworzyć aplikację do szkicowania w internecie?

pixiv Sketch został po raz pierwszy opublikowany w wersji internetowej i na iOS w 2015 roku. Docelowa grupa odbiorców wersji internetowej to głównie użytkownicy komputerów PC, którzy nadal korzystają z tej platformy, która jest nadal najważniejszą platformą dla społeczności ilustratorów.

Oto 2 główne powody, dla których zespół pixiv zdecydował się opracować wersję internetową zamiast aplikacji na komputer:

  • Tworzenie aplikacji na systemy Windows, Mac, Linux i inne jest bardzo kosztowne. Witryna jest dostępna w dowolnej przeglądarce na komputerze.
  • Internet ma największy zasięg na wszystkich platformach. Z internetu można korzystać na komputerach, urządzeniach mobilnych i w każdym systemie operacyjnym.

Technologia

pixiv Sketch oferuje użytkownikom wiele różnych pędzli. Przed przyjęciem WebGL dostępny był tylko 1 typ pędzla, ponieważ płótno 2D było zbyt ograniczone, aby odwzorować złożoną fakturę różnych pędzli, np. chropowate krawędzie ołówka oraz zmienną szerokość i intensywność koloru zależną od nacisku podczas rysowania.

Typy pędzli używanych w WebGL

Dzięki zastosowaniu WebGL udało się jednak dodać więcej szczegółów do pędzli i zwiększyć liczbę dostępnych pędzli do 7.

7 różnych pędzli w pixiv od cienkich do grubych, ostrych do niewyraźnych, od pikseli do gładkich itp.

W ramach kontekstu 2D canvas można było rysować tylko linie o prostej fakturze o równomiernie rozłożonej szerokości, jak na tym zrzucie ekranu:

Pociąg pędzla z prostą teksturą.

Te linie zostały narysowane przez tworzenie ścieżek i rysowanie pociągnięć, ale WebGL odtwarza je za pomocą punktów sprite’ów i shaderów, jak widać w tych przykładach kodu

Ten przykład pokazuje shader wierzchołkowy.

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 modułu do 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);
}

Użycie sprite'ów punktowych ułatwia zmianę grubości i cieniowania w zależności od siły nacisku. Pozwala to wyrazić następujące silne i słabe linie:

Ostry, równomierny splot z cienkimi końcami.

Nieostry ślad pędzla z większym naciskiem na środku.

Ponadto implementacje korzystające ze sprite’ów punktowych mogą teraz dołączać tekstury za pomocą osobnego shadera, co umożliwia wydajne reprezentowanie pędzli z teksturami, takimi jak ołówek i pisak.

Obsługa rysika w przeglądarce

Korzystanie z cyfrowego piórka stało się bardzo popularne w środowisku artystów cyfrowych. Nowoczesne przeglądarki obsługują interfejs PointerEvent API, który umożliwia użytkownikom korzystanie z rysika na urządzeniu. Za pomocą PointerEvent.pressure możesz mierzyć siłę nacisku pióra, a PointerEvent.tiltX i PointerEvent.tiltY – do mierzenia kąta między rysikiem a urządzeniem.

Aby wykonać uderzenie pędzla za pomocą punktu sprite, musisz interpolować PointerEvent i przekształcić go w bardziej szczegółową sekwencję zdarzeń. W PointerEvent orientację rysika można uzyskać w postaci współrzędnych biegunowych, ale pixiv Sketch przekształca je w wektor reprezentują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 rysunku

Warstwy to jeden z najbardziej unikalnych pojęć w rysowaniu cyfrowym. Umożliwiają one rysowanie różnych elementów ilustracji na sobie nawzajem i edytowanie warstw pojedynczo. pixiv Sketch udostępnia funkcje warstw podobnie jak inne cyfrowe aplikacje do rysowania.

Zazwyczaj warstwy można stosować, używając kilku elementów <canvas> z operacjami drawImage() i kompozytowania. Jest to jednak problematyczne, ponieważ w kontekście obrazu 2D nie ma innego wyjścia, jak użyć trybu kompozycji CanvasRenderingContext2D.globalCompositeOperation, który jest wstępnie zdefiniowany i w dużej mierze ogranicza skalowalność. Dzięki użyciu WebGL i pisania shadera pozwala deweloperom używać trybów kompozycji, które nie są wstępnie zdefiniowane przez interfejs API. W przyszłości pixiv Sketch wprowadzi funkcję warstw za pomocą WebGL, aby zapewnić większą skalowalność i elastyczność.

Oto przykładowy kod tworzenia 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);
  }
}

Malowanie dużych obszarów za pomocą funkcji wiadra

Aplikacje Pixiv Sketch na iOS i Androida już miały tę funkcję, ale w wersji internetowej nie. Wersja aplikacji funkcji zasobnika została wdrożona w C++.

Bazując na kodzie źródłowym w języku C++, zespół pixiv Sketch wykorzystał Emscripten i asm.js do zaimplementowania funkcji zbiornika w wersji internetowej.

bfsQueue.push(startPoint);

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

Użycie pliku asm.js umożliwiło wydajniejsze rozwiązanie. Porównując czas wykonania czystego JavaScriptu z asm.js, można zauważyć, że czas wykonania kodu asm.js jest krótszy o 67%. Zakładamy, że będzie to jeszcze lepsze w przypadku korzystania z WASM.

Szczegóły testu:

  • Jak: maluj obszar 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 Emscriptenowi i asm.js zespół pixiv Sketch mógł opublikować funkcję zasobnika, używając bazy kodu z wersji aplikacji na daną platformę.

Transmisja na żywo podczas rysowania

Pixiv Sketch umożliwia transmitowanie na żywo podczas rysowania za pomocą aplikacji internetowej pixiv Sketch LIVE. Korzysta on z interfejsu API WebRTC, łącząc ścieżkę dźwiękową z 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 tworzyć złożone aplikacje na platformie internetowej i skalować je na dowolne urządzenie. Więcej informacji o technologiach opisanych w tym przypadku znajdziesz pod tymi linkami: