插画师必备网络的力量:pixiv 如何利用网络技术打造他们的绘图应用

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 种。

Pixiv 中有七种不同的画笔,从细到粗,从锐到不锐,从像素化到平滑,不一而足。

使用 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.tiltXPointerEvent.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 平台上创建复杂的应用,并将其扩展到任何设备上。您可以通过以下链接详细了解本案例研究中引入的技术: