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를 통해 성능이 뛰어난 솔루션을 사용할 수 있었습니다. 순수 자바스크립트와 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를 사용하여 웹 플랫폼에서 복잡한 앱을 만들고 모든 기기로 확장할 수 있습니다. 다음 링크에서 이 우수사례에서 소개한 기술에 대해 자세히 알아볼 수 있습니다.
- WebGL
- WebGL의 뒤를 잇는 WebGPU도 확인해 보세요.
- WebAssembly
- WebRTC
- 일본어 원본 기사