pixiv 是一个在线社区服务,供插画家和插画爱好者通过其内容进行互动。用户可以通过该功能发布自己的插图。截至 2023 年 5 月,他们在全球拥有超过 8, 400 万用户,发布的艺术作品超过 1.2 亿件。
pixiv Sketch 是 pixiv 提供的服务之一。它用于使用手指或触控笔在网站上绘制图形。它支持多种功能,可用于绘制精美的插图,包括多种类型的画笔、图层和填充工具,还允许用户直播绘制过程。
在本案例研究中,我们将了解 pixiv Sketch 如何通过使用 WebGL、WebAssembly 和 WebRTC 等一些新的 Web 平台功能来提升其 Web 应用的性能和质量。
为什么要在 Web 上开发素描应用?
pixiv Sketch 于 2015 年首次发布网页版和 iOS 版。他们面向网站版的目标受众群体主要为桌面设备用户,桌面设备仍然是插图社区最主要的平台。
以下是 pixiv 选择开发 Web 版而非桌面版应用的两个主要原因:
- 为 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 提供的图层功能与其他数字绘图应用大同小异。
传统上,可以通过将多个 <canvas>
元素与 drawImage()
和合成操作结合使用来实现图层。不过,这存在问题,因为对于 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 将存储分区函数实现到 Web 版本中。
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.8 毫秒
- asm.js::70.3 毫秒
借助 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,您可以在 Web 平台上创建复杂的应用,并将其扩展到任何设备。您可以通过以下链接详细了解本案例研究中介绍的技术:
- WebGL
- 另请查看 WebGL 的继任者 WebGPU
- WebAssembly
- WebRTC
- 日语原文