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 na publikowanie własnych ilustracji. W maju 2023 r. aplikacja miała ponad 84 miliony użytkowników na całym świecie i ponad 120 milionów utworów.

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 opracowaniu przyjrzymy się, jak zespół pixiv Sketch poprawił wydajność i jakość swojej aplikacji internetowej, korzystając z nowych funkcji platformy internetowej, takich jak WebGL, WebAssembly i 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. Internet jest dostępny na komputerach, urządzeniach mobilnych i w każdym systemie operacyjnym.

Technologia

pixiv Sketch oferuje użytkownikom wiele różnych pędzli do wyboru. Przed przyjęciem WebGL istniał tylko jeden typ pędzla, ponieważ płótno 2D było zbyt ograniczone, aby odwzorować złożoną fakturę różnych pędzli, np. ostre krawędzie ołówka i różną szerokość oraz intensywność koloru, która zmienia się w zależności od nacisku na rysik.

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ągnięcie pędzla z prostą fakturą.

Te linie zostały narysowane przez tworzenie ścieżek i rysowanie pociągnięć, ale WebGL odtwarza to 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;
}

Przykład poniżej zawiera przykładowy kod fragment shadera.

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

Dzięki punktowym sprite’om można łatwo zmieniać grubość i cieniowanie w reakcji na nacisk podczas rysowania, co pozwala na tworzenie linii o różnej grubości, takich jak te:

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: PointerEvent.pressure służy do pomiaru nacisku na pióro, a PointerEvent.tiltXPointerEvent.tiltY do pomiaru kąta nachylenia pióra względem urządzenia.

Aby wykonać uderzenie pędzla za pomocą punktu sprite, musisz interpolować PointerEvent i przekształcić go w bardziej szczegółową sekwencję zdarzeń. W zdarzeniu PointerEvent orientację pióra można uzyskać w postaci współrzędnych biegunowych, ale przed użyciem pixiv Sketch przekształca je w wektory reprezentujące orientację pióra.

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 do tworzenia warstw:

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 wiaderka

Aplikacje pixiv Sketch na iOS i Androida oferowały już funkcję kosza, ale nie było jej w wersji internetowej. Wersja funkcji zbiornika w aplikacji została zaimplementowana 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 asm.js umożliwiło uzyskanie wydajnego rozwiązania. 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%. Oczekujemy, że będzie to jeszcze lepsze w przypadku korzystania z WASM.

Szczegóły testu:

  • Sposób wykonania: pomaluj obszar 1180 x 800 pikseli za pomocą funkcji zasobnika
  • Urządzenie testowe: MacBook Pro (M1 Max)

Czas wykonania:

  • Sam 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 oferuje funkcję transmisji na żywo podczas rysowania za pomocą aplikacji internetowej pixiv Sketch LIVE. Wykorzystuje ona interfejs WebRTC API, łącząc ścieżkę dźwiękową z mikrofonu uzyskaną z getUserMedia() oraz ścieżkę wideo z MediaStream.<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: