pixiv เป็นบริการชุมชนออนไลน์สำหรับนักวาดภาพประกอบและผู้สนใจภาพประกอบเพื่อสื่อสารกันผ่านเนื้อหา ผู้คนสามารถโพสต์ภาพของตนเองได้ โดยมีผู้ใช้กว่า 84 ล้านคนทั่วโลก และมีจำนวนงานศิลปะมากกว่า 120 ล้านรายการที่โพสต์ตั้งแต่เดือนพฤษภาคม 2023
pixiv Sketch เป็นหนึ่งในบริการของ pixiv ใช้ในการวาดงานศิลปะ บนเว็บไซต์โดยใช้นิ้วมือหรือสไตลัส แพลตฟอร์มนี้รองรับฟีเจอร์มากมายสำหรับการวาดภาพที่น่าตื่นตาตื่นใจ ไม่ว่าจะเป็นการใช้พู่กัน เลเยอร์ และการเพ้นท์สีแบบถัง นอกจากนี้ ยังให้ผู้ใช้สตรีมขั้นตอนการวาดภาพแบบสดได้ด้วย
ในกรณีศึกษานี้ เราจะดูว่า pixiv Sketch ปรับปรุงประสิทธิภาพและคุณภาพของเว็บแอปโดยใช้ฟีเจอร์ของแพลตฟอร์มเว็บใหม่ๆ บางอย่าง เช่น WebGL, WebAssembly และ WebRTC ได้อย่างไร
ทำไมต้องพัฒนาแอปร่างภาพบนเว็บ
pixiv Sketch เปิดตัวครั้งแรกบนเว็บและ iOS ในปี 2015 กลุ่มเป้าหมายของเว็บไซต์เวอร์ชันเว็บส่วนใหญ่คือเดสก์ท็อป ซึ่งยังคงเป็นแพลตฟอร์มหลักที่ชุมชนภาพตัวอย่างใช้มากที่สุด
เหตุผลหลัก 2 ข้อของ pixiv ที่เลือกพัฒนาเวอร์ชันเว็บแทนแอปบนเดสก์ท็อปมีดังนี้
- การสร้างแอปสำหรับ Windows, Mac, Linux และอื่นๆ มีค่าใช้จ่ายสูง เว็บจะเข้าถึงเบราว์เซอร์ใดก็ได้บนเดสก์ท็อป
- เว็บมีการเข้าถึงที่ดีที่สุดในแพลตฟอร์มต่างๆ เว็บมีให้บริการทั้งในเดสก์ท็อปและอุปกรณ์เคลื่อนที่และทุกระบบปฏิบัติการ
เทคโนโลยี
pixiv Sketch มีแปรงมากมายให้ผู้ใช้เลือก ก่อนใช้ WebGL เราใช้แปรงเพียงประเภทเดียวเนื่องจาก Canvas แบบ 2 มิตินั้นจำกัดเกินกว่าที่จะแสดงภาพพื้นผิวที่ซับซ้อนของแปรงต่างๆ เช่น ขอบหยาบของดินสอ และความกว้างและความเข้มของสีที่ต่างกันซึ่งเปลี่ยนแปลงไปตามแรงกดในการสเก็ตช์
ประเภทของแปรงสุดสร้างสรรค์ที่ใช้ WebGL
อย่างไรก็ตาม การใช้ WebGL ช่วยให้บริษัทสามารถใส่รายละเอียดแปรงที่หลากหลายและเพิ่มจำนวนแปรงที่ใช้ได้เป็น 7 แบบ
เมื่อใช้บริบท Canvas 2 มิติ คุณจะวาดเส้นที่มีพื้นผิวเรียบง่ายและมีความกว้างกระจายเท่าๆ กัน ดังภาพหน้าจอต่อไปนี้
เส้นเหล่านี้วาดโดยการสร้างเส้นทางและเส้นวาด แต่ WebGL จำลองเส้นนี้โดยใช้ Point Sprite และเฉดสี ดังแสดงในตัวอย่างโค้ดต่อไปนี้
ตัวอย่างต่อไปนี้จะแสดงตัวปรับเฉดสีเวอร์เท็กซ์
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()
และการดำเนินการประกอบกัน แต่เรื่องนี้ไม่ค่อยดีเนื่องจากบริบทของ Canvas แบบ 2 มิตินั้นไม่มีทางเลือกอื่นนอกจากการใช้โหมดการเรียบเรียง 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 โดยการดำเนินการนี้ใช้ WebRTC API ซึ่งรวมแทร็กเสียงจากไมโครโฟนที่ได้รับจาก getUserMedia()
และแทร็กวิดีโอ MediaStream
ที่ดึงมาจากองค์ประกอบ <canvas>
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());
บทสรุป
ศักยภาพของ API ใหม่อย่าง WebGL, WebAssembly และ WebRTC ช่วยให้คุณสร้างแอปที่ซับซ้อนบนแพลตฟอร์มเว็บและปรับขนาดในอุปกรณ์ใดก็ได้ ดูข้อมูลเพิ่มเติมเกี่ยวกับเทคโนโลยีที่นำมาใช้ในกรณีศึกษานี้ได้ที่ลิงก์ต่อไปนี้
- WebGL
- และดู WebGPU ที่พัฒนาต่อจาก WebGL
- WebAssembly
- WebRTC
- บทความต้นฉบับภาษาญี่ปุ่น