pixiv 是一项在线社区服务,旨在让插画家和插画爱好者通过自己的内容相互交流。让用户可以发布自己的插图。截至 2023 年 5 月,他们在全球拥有 8, 400 多万用户,发布的艺术作品超过 1.2 亿件。
pixiv Sketch 是由 pixiv 提供的服务之一。它可用于使用手指或触控笔在网站上绘制艺术作品。它支持使用多种功能(包括多种类型的画笔、图层和水桶绘画)绘制令人惊艳的插图,并且允许用户直播他们的绘制过程。
在本案例研究中,我们将了解 pixiv Sketch 如何使用一些新的 Web 平台功能(如 WebGL、WebAssembly 和 WebRTC)改善其 Web 应用的性能和质量。
为什么要在网络上开发素描应用?
pixiv Sketch 于 2015 年首次在网络上和 iOS 平台上发布。其网页版的目标受众群体主要是桌面设备,这仍然是插图社区使用的主要平台。
以下是 pixiv 选择开发网页版而不是桌面应用的两大原因:
- 开发适用于 Windows、Mac、Linux 等版本的应用程序的成本很高。网络可到达桌面版的任何浏览器。
- 网络在各平台上的覆盖面最广。Web 既适用于桌面设备、移动设备,也适用于所有操作系统。
技术
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;
}
以下示例展示了 fragment 着色器的示例代码。
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.8 毫秒
- asm.js::70.3 毫秒
借助 Emscripten 和 asm.js,pixiv Sketch 能够重复使用平台专用应用版本中的代码库,成功发布存储分区功能。
在绘画时进行直播
pixiv Sketch 提供了通过 pixiv Sketch LIVE Web 应用进行直播的同时进行直播的功能。该应用使用 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
- 日语原创文章