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개로 늘릴 수 있었습니다.

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를 사용하면 성능이 우수한 솔루션을 사용할 수 있습니다. 순수 JavaScript와 asm.js의 실행 시간을 비교하면 asm.js를 사용한 실행 시간이 67% 단축됩니다. WASM을 사용하면 훨씬 더 나을 것으로 예상됩니다.
테스트 세부정보:
- 방법: 버킷 기능으로 1180x800px 영역을 페인트합니다.
- 테스트 기기: MacBook Pro (M1 Max)
실행 시간:
- 순수 JavaScript: 213.8ms
- asm.js: 70.3ms
Emscripten과 asm.js를 사용하여 pixiv Sketch는 플랫폼별 앱 버전의 코드베이스를 재사용하여 버킷 기능을 성공적으로 출시할 수 있었습니다.
그림을 그리면서 라이브 스트리밍
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를 사용하여 웹 플랫폼에서 복잡한 앱을 만들고 모든 기기에서 확장할 수 있습니다. 이 케이스 스터디에 소개된 기술에 대해 자세히 알아보려면 다음 링크를 참고하세요.
- WebGL
- WebGL의 후속 버전인 WebGPU도 확인해 보세요.
- WebAssembly
- WebRTC
- 일본어 원본 기사