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을 사용하면 이 속도가 더욱 향상될 것으로 예상됩니다.
테스트 세부정보:
- 방법: 버킷 함수로 1180x800픽셀 영역 페인트
- 테스트 기기: MacBook Pro (M1 Max)
실행 시간:
- 순수 JavaScript: 213.8ms
- asm.js: 70.3ms
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를 사용하면 웹 플랫폼에서 복잡한 앱을 만들고 모든 기기에서 확장할 수 있습니다. 이 우수사례에서 소개된 기술에 대해 자세히 알아보려면 다음 링크를 참고하세요.
- WebGL
- WebGL의 후속인 WebGPU도 확인하세요.
- WebAssembly
- WebRTC
- 일본어 원본 기사