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

pixiv 是一个在线社区服务,供插画家和插画爱好者通过其内容进行互动。用户可以通过该功能发布自己的插图。截至 2023 年 5 月,他们在全球拥有超过 8,400 万用户,发布的艺术作品超过 1.2 亿件

pixiv Sketch 是 pixiv 提供的服务之一。它用于使用手指或触控笔在网站上绘制图形。它支持多种用于绘制精美插图的功能,包括多种类型的画笔、图层和水桶绘制,并且用户还可以直播其绘制过程。

在本案例研究中,我们将了解 pixiv Sketch 如何通过使用 WebGL、WebAssembly 和 WebRTC 等一些新的 Web 平台功能来提升其 Web 应用的性能和质量。

为什么要在网络上开发素描应用?

pixiv Sketch 于 2015 年首次发布网页版和 iOS 版。他们面向网站版的目标受众群体主要是桌面设备用户,桌面设备仍然是插图社区最主要的平台。

以下是 pixiv 选择开发 Web 版而非桌面版应用的两个主要原因:

  • 为 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 提供的图层功能与其他数字绘图应用大同小异。

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