일러스트레이터를 위한 웹의 힘: pixiv가 그리기 앱에 웹 기술을 활용하는 방법

pixiv는 일러스트레이터와 일러스트 애호가가 콘텐츠를 통해 서로 소통할 수 있는 온라인 커뮤니티 서비스입니다. 사람들이 자신만의 삽화를 게시할 수 있습니다 전 세계적으로 8,400만 명 이상의 사용자가 있으며 2023년 5월 현재 1억 2,000만 개가 넘는 예술작품을 게시했습니다.

pixiv Sketch는 pixiv의 서비스 중 하나입니다. 손가락이나 스타일러스로 웹사이트에 그림을 그리는 데 사용됩니다. 다양한 종류의 브러시, 레이어, 버킷 페인팅 등 멋진 일러스트레이션을 그리는 다양한 기능을 지원하고 그리기 과정을 실시간 스트리밍할 수도 있습니다.

이 우수사례에서는 pixiv Sketch가 WebGL, WebAssembly, WebRTC와 같은 새로운 웹 플랫폼 기능을 사용하여 웹 앱의 성능과 품질을 개선한 방법을 살펴봅니다.

웹에서 스케치 앱을 개발해야 하는 이유

pixiv Sketch는 2015년에 웹과 iOS에서 처음 출시되었습니다. 웹 버전의 타겟층은 주로 데스크톱이었으며, 이 플랫폼은 여전히 일러스트레이션 커뮤니티에서 가장 많이 사용하는 플랫폼입니다.

pixiv가 데스크톱 앱이 아닌 웹 버전을 개발하는 두 가지 주요 이유는 다음과 같습니다.

  • Windows, Mac, Linux 등을 위한 앱을 만드는 데에는 비용이 매우 많이 듭니다. 웹은 데스크톱의 모든 브라우저에 도달합니다.
  • 웹은 여러 플랫폼에서 도달범위가 가장 넓습니다. 웹은 데스크톱, 모바일, 모든 운영체제에서 사용할 수 있습니다

기술

pixiv Sketch에는 사용자가 선택할 수 있는 다양한 브러시가 있습니다. WebGL을 채택하기 전에는 연필의 거친 가장자리와 스케치 압력에 따라 변화하는 너비와 색상 강도와 같은 여러 브러시의 복잡한 텍스처를 표현하기에 2D 캔버스가 너무 제한되었기 때문에 한 가지 유형의 브러시만 있었습니다.

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.tiltX, PointerEvent.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);
}

다중 그리기 레이어

레이어는 디지털 그리기에서 가장 고유한 개념 중 하나입니다. 사용자는 서로 다른 일러스트레이션을 겹쳐서 그릴 수 있고 레이어별로 수정할 수 있습니다. pixiv Sketch는 다른 디지털 그리기 앱과 매우 유사한 레이어 기능을 제공합니다.

일반적으로 drawImage() 및 합성 연산과 함께 여러 <canvas> 요소를 사용하여 레이어를 구현할 수 있습니다. 하지만 2D 캔버스 컨텍스트에서는 사전 정의되어 있고 확장성이 크게 제한되는 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를 통해 성능이 뛰어난 솔루션을 사용할 수 있었습니다. 순수 자바스크립트와 asm.js의 실행 시간을 비교하면 asm.js를 사용한 실행 시간이 67% 단축되었습니다. 이는 WASM을 사용할 때 훨씬 더 좋을 것으로 예상됩니다.

테스트 세부정보:

  • 방법: 버킷 기능으로 1180x800px 영역 그리기
  • 테스트 기기: MacBook Pro (M1 Max)

실행 시간:

  • 순수 자바스크립트: 213.8ms
  • 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를 사용하여 웹 플랫폼에서 복잡한 앱을 만들고 모든 기기로 확장할 수 있습니다. 다음 링크에서 이 우수사례에서 소개한 기술에 대해 자세히 알아볼 수 있습니다.