ทำให้ตัวอักษรนับล้านเคลื่อนไหวโดยใช้ Three.js

เกริ่นนำ

เป้าหมายของฉันในบทความนี้คือการวาดตัวอักษรเคลื่อนไหวเป็นล้านๆ ตัวบนหน้าจอที่อัตราเฟรมราบรื่น งานนี้น่าจะเป็นไปได้ด้วย GPU ที่ทันสมัย แต่ละตัวอักษรประกอบด้วยรูปสามเหลี่ยมที่มีพื้นผิว 2 รูป ดังนั้นเราจะพูดถึงรูปสามเหลี่ยม 2 ล้านรูปต่อเฟรม

หากคุณมาจากพื้นหลังภาพเคลื่อนไหว JavaScript แบบเดิม ทั้งหมดนี้จะฟังดูบ้าบิ่น แน่นอนว่าไม่มีรูปสามเหลี่ยม 2 ล้านรูปที่อัปเดตทุกเฟรมกับ JavaScript ในปัจจุบัน แต่โชคดีที่เรามี WebGL ที่ช่วยให้เราใช้ประโยชน์จาก GPU สมัยใหม่ได้อย่างยอดเยี่ยม และรูปสามเหลี่ยมแบบเคลื่อนไหว 2 ล้านรูปก็ทำได้ง่ายๆ ด้วย GPU ที่ทันสมัยและเวทมนตร์ของตัวปรับแสงเงา

การเขียนโค้ด WebGL ที่มีประสิทธิภาพ

การเขียนโค้ด WebGL ที่มีประสิทธิภาพจำเป็นต้องมีแนวคิดบางอย่าง วิธีปกติในการวาดภาพโดยใช้ WebGL คือการตั้งค่าเครื่องแบบ บัฟเฟอร์ และตัวปรับแสงเงาสำหรับแต่ละวัตถุ แล้วตามด้วยข้อความให้วาดวัตถุ การวาดภาพลักษณะนี้จะใช้งานได้เมื่อวาดวัตถุจำนวนไม่มาก หากต้องการวาดวัตถุจำนวนมาก คุณควรลดปริมาณการเปลี่ยนแปลงสถานะ WebGL ให้เหลือน้อยที่สุด ในการเริ่มต้น ให้วาดวัตถุทั้งหมดโดยใช้ตัวปรับเฉดสีเดียวกันต่อกันเพื่อที่คุณจะได้ไม่ต้องเปลี่ยนตัวให้เฉดสีระหว่างวัตถุ สำหรับวัตถุทั่วไป เช่น อนุภาค คุณสามารถรวมวัตถุหลายรายการไว้ในบัฟเฟอร์เดียวและแก้ไขวัตถุโดยใช้ JavaScript ได้ ด้วยวิธีนี้ คุณจะต้องอัปโหลดบัฟเฟอร์จุดยอดมุมอีกครั้งแทนที่จะเปลี่ยนเครื่องแบบให้เฉดสีสำหรับทุกอนุภาค

แต่การจะประมวลผลให้เร็วมากๆ คุณต้องขับเคลื่อนการคำนวณส่วนใหญ่ไปยังตัวปรับเฉดสี นี่คือสิ่งที่เราพยายามจะทำที่นี่ ทำให้ตัวอักษรนับล้านเคลื่อนไหวได้โดยใช้ตัวให้เฉดสี

โค้ดของบทความใช้ไลบรารี Three.js ซึ่งตัดตอนสำเร็จรูปที่น่าเบื่อทั้งหมดออกจากการเขียนโค้ด WebGL แทนที่จะต้องเขียนการตั้งค่าสถานะ WebGL และการจัดการข้อผิดพลาดหลายร้อยบรรทัด เมื่อใช้ Three.js คุณจะต้องเขียนโค้ดเพียง 2-3 บรรทัด นอกจากนี้ การแตะลงในระบบตัวปรับแสงเงา WebGL จาก Three.js ก็ทำได้ง่ายเช่นกัน

การวาดวัตถุหลายชิ้นโดยใช้การเรียกครั้งเดียว

ต่อไปนี้เป็นตัวอย่างรหัสเทียมเล็กๆ ที่แสดงวิธีการวาดวัตถุหลายชิ้นโดยใช้การเรียกครั้งเดียว วิธีดั้งเดิมคือการวาดวัตถุทีละวัตถุดังนี้

for (var i=0; i<objects.length; i++) {
  // each added object requires a separate WebGL draw call
  scene.add(createNewObject(objects[i]));
}
renderer.render(scene, camera);

แต่วิธีข้างต้นจำเป็นต้องมีการเรียกแยกกันสำหรับแต่ละรายการ หากต้องการวาดวัตถุหลายชิ้นพร้อมกัน คุณสามารถรวมวัตถุเข้าด้วยกันเป็นรูปทรงเรขาคณิตเดียวและหลบหนีไปได้ด้วยการวาดวัตถุครั้งเดียว โดยทำดังนี้

var geo = new THREE.Geometry();
for (var i=0; i<objects.length; i++) {
  // bundle the objects into a single geometry
  // so that they can be drawn with a single draw call
  addObjectToGeometry(geo, objects[i]);
}
// GOOD! Only one object to add to the scene!
scene.add(new THREE.Mesh(geo, material));
renderer.render(scene, camera);

เอาล่ะ ตอนนี้คุณมีแนวคิดเบื้องต้นแล้ว กลับมาเขียนการสาธิตและเริ่มสร้างภาพเคลื่อนไหวจากตัวอักษรนับล้านๆ ตัวกัน

การตั้งค่าเรขาคณิตและพื้นผิว

ขั้นตอนแรก เราจะสร้างพื้นผิวที่มีบิตแมปแบบตัวอักษรอยู่ ฉันใช้ Canvas แบบ 2 มิติ พื้นผิวที่ได้มีตัวอักษรทั้งหมดที่ฉันต้องการวาด ขั้นตอนถัดไปคือการสร้างบัฟเฟอร์ที่มีพิกัดพื้นผิวของสไปรท์ชีต วิธีนี้เป็นวิธีที่ง่ายและไม่ซับซ้อนในการตั้งค่าตัวอักษร แต่ก็ทำให้สิ้นเปลืองพลังงานเล็กน้อยเพราะวิธีนี้ใช้ทศนิยม 2 จุดต่อจุดยอดมุมเป็นจุดยอดพิกัดของพื้นผิว วิธีที่สั้นลงสำหรับการฝึกกับผู้อ่านคือการรวมดัชนีตัวอักษรและดัชนีมุมเป็นค่าหนึ่ง แล้วแปลงกลับไปเป็นพิกัดพื้นผิวในตัวปรับแสงเงาจุดยอด

วิธีสร้างพื้นผิวตัวอักษรโดยใช้ Canvas 2D มีดังนี้

var fontSize = 16;

// The square letter texture will have 16 * 16 = 256 letters, enough for all 8-bit characters.
var lettersPerSide = 16;

var c = document.createElement('canvas');
c.width = c.height = fontSize*lettersPerSide;
var ctx = c.getContext('2d');
ctx.font = fontSize+'px Monospace';

// This is a magic number for aligning the letters on rows. YMMV.
var yOffset = -0.25;

// Draw all the letters to the canvas.
for (var i=0,y=0; y<lettersPerSide; y++) {
  for (var x=0; x<lettersPerSide; x++,i++) {
    var ch = String.fromCharCode(i);
    ctx.fillText(ch, x*fontSize, yOffset*fontSize+(y+1)*fontSize);
  }
}

// Create a texture from the letter canvas.
var tex = new THREE.Texture(c);
// Tell Three.js not to flip the texture.
tex.flipY = false;
// And tell Three.js that it needs to update the texture.
tex.needsUpdate = true;

ผมอัปโหลดอาร์เรย์สามเหลี่ยมไปยัง GPU ด้วย จุดยอดเหล่านี้ใช้โดยตัวปรับแสงเงาจุดยอดเพื่อวางตัวอักษรไว้บนหน้าจอ จุดยอดจะกำหนดเป็นตำแหน่งตัวอักษรในข้อความ เพื่อที่ว่าหากคุณแสดงผลอาร์เรย์สามเหลี่ยมตามที่เป็นอยู่ คุณจะได้รับการแสดงผลเลย์เอาต์พื้นฐานของข้อความ

การสร้างเรขาคณิตสำหรับหนังสือ:

var geo = new THREE.Geometry();

var i=0, x=0, line=0;
for (i=0; i<BOOK.length; i++) {
  var code = BOOK.charCodeAt(i); // This is the character code for the current letter.
  if (code > lettersPerSide * lettersPerSide) {
    code = 0; // Clamp character codes to letter map size.
  }
  var cx = code % lettersPerSide; // Cx is the x-index of the letter in the map.
  var cy = Math.floor(code / lettersPerSide); // Cy is the y-index of the letter in the map.

  // Add letter vertices to the geometry.
  var v,t;
  geo.vertices.push(
    new THREE.Vector3( x*1.1+0.05, line*1.1+0.05, 0 ),
    new THREE.Vector3( x*1.1+1.05, line*1.1+0.05, 0 ),
    new THREE.Vector3( x*1.1+1.05, line*1.1+1.05, 0 ),
    new THREE.Vector3( x*1.1+0.05, line*1.1+1.05, 0 )
  );
  // Create faces for the letter.
  var face = new THREE.Face3(i*4+0, i*4+1, i*4+2);
  geo.faces.push(face);
  face = new THREE.Face3(i*4+0, i*4+2, i*4+3);
  geo.faces.push(face);

  // Compute texture coordinates for the letters.
  var tx = cx/lettersPerSide, 
      ty = cy/lettersPerSide,
      off = 1/lettersPerSide;
  var sz = lettersPerSide*fontSize;
  geo.faceVertexUvs[0].push([
    new THREE.Vector2( tx, ty+off ),
    new THREE.Vector2( tx+off, ty+off ),
    new THREE.Vector2( tx+off, ty )
  ]);
  geo.faceVertexUvs[0].push([
    new THREE.Vector2( tx, ty+off ),
    new THREE.Vector2( tx+off, ty ),
    new THREE.Vector2( tx, ty )
  ]);

  // On newline, move to the line below and move the cursor to the start of the line.
  // Otherwise move the cursor to the right.
  if (code == 10) {
    line--;
    x=0;
  } else {
    x++;
  }
}

ตัวปรับแสงเงา Vertex สำหรับทำให้ตัวอักษรเคลื่อนไหว

เมื่อใช้โปรแกรมเฉดสี Vertex แบบเรียบๆ ฉันจะเห็นมุมมองข้อความแบบราบเรียบ ไม่มีอะไรหวือหวา ทำงานได้อย่างดี แต่ถ้าต้องการทำให้เคลื่อนไหวได้ ฉันต้องทำภาพเคลื่อนไหวใน JavaScript และ JavaScript ก็ทำงานช้าสักหน่อยสำหรับการทำให้จุดยอด 6 ล้านจุดที่เกี่ยวข้องเคลื่อนไหว โดยเฉพาะอย่างยิ่งในกรณีที่คุณต้องการทำในทุกเฟรม อาจจะมีวิธีที่เร็วกว่านี้ก็ได้

ทำไมเราต้องสร้างภาพเคลื่อนไหวตามกระบวนการ ซึ่งหมายความว่าเราคำนวณตำแหน่งและการหมุนทั้งหมดของเราใน Vertex Shades ตอนนี้ฉันไม่จำเป็นต้องเรียกใช้ JavaScript เพื่ออัปเดตตำแหน่งของจุดยอด ตัวปรับแสงเงาจุดรวมทำงานเร็วมากและฉันได้อัตราเฟรมที่ราบรื่นแม้จะมีรูปสามเหลี่ยม 1 ล้านรูปที่เคลื่อนไหวแยกกันในแต่ละเฟรม ในการแก้ไขแต่ละรูปสามเหลี่ยม ฉันปัดพิกัดของจุดยอดมุมลงเพื่อให้ทั้ง 4 จุดของรูปสี่เหลี่ยมจตุรัสทั้ง 4 ชิ้นเป็นพิกัดที่ไม่ซ้ำกัน 1 พิกัด ตอนนี้ผมสามารถใช้พิกัดนี้เพื่อตั้งค่าพารามิเตอร์ภาพเคลื่อนไหวของตัวอักษรที่เป็นปัญหา

หากต้องการปัดเศษพิกัดลง พิกัดจากตัวอักษร 2 ตัวที่ต่างกันจะไม่สามารถทับซ้อนกัน วิธีที่ง่ายที่สุดคือการใช้สี่เหลี่ยมจตุรัสที่มีตัวอักษรเป็นรูปสี่เหลี่ยมจตุรัสที่มีออฟเซ็ตเล็กๆ คั่นตัวอักษรจากตัวอักษรที่อยู่ด้านขวาและติดกับเส้นเหนือตัวอักษร ตัวอย่างเช่น ใช้ความกว้างและความสูง 0.5 สำหรับตัวอักษรและจัดเรียงตัวอักษรในพิกัดที่เป็นจำนวนเต็ม ในตอนนี้เมื่อคุณปัดเศษพิกัดของจุดยอดของตัวอักษรใดๆ ลง คุณจะได้พิกัดด้านซ้ายล่างของตัวอักษร

ปัดเศษพิกัดจุดยอดมุมลงเพื่อหามุมซ้ายบนของตัวอักษร
การปัดเศษพิกัดจุดยอดมุมลงเพื่อดูมุมซ้ายบนของตัวอักษร

เพื่อให้เข้าใจถึงตัวปรับแสงเงาของจุดยอดมุมแบบเคลื่อนไหว ผมจะพูดถึงตัวปรับเฉดสีสำหรับจุดสุดท้ายง่ายๆ ก่อน ซึ่งเป็นสิ่งที่ปกติจะเกิดขึ้นเมื่อคุณวาดโมเดล 3 มิติไปยังหน้าจอ ระบบจะแปลงจุดยอดของโมเดลด้วยเมทริกซ์การแปลง 2 คู่เพื่อฉายภาพจุดยอดมุม 3 มิติแต่ละจุดลงบนหน้าจอ 2 มิติ เมื่อใดก็ตามที่รูปสามเหลี่ยมกำหนดโดยจุดยอด 3 จุดเหล่านี้เข้าสู่วิวพอร์ต ตัวปรับแสงเงาจะประมวลผลพิกเซลที่ครอบคลุมพิกเซลเพื่อใส่สีจุดเหล่านั้น อย่างไรก็ตาม นี่คือตัวปรับเฉดสีเวอร์เท็กซ์แบบง่าย

varying float vUv;

void main() {
  // modelViewMatrix, position and projectionMatrix are magical
  // attributes that Three.js defines for us.

  // Transform current vertex by the modelViewMatrix
  // (bundled model world position & camera world position matrix).
  vec4 mvPosition = modelViewMatrix * position;

  // Project camera-space vertex to screen coordinates
  // using the camera's projection matrix.
  vec4 p = projectionMatrix * mvPosition;

  // uv is another magical attribute from Three.js.
  // We're passing it to the fragment shader unchanged.
  vUv = uv;

  gl_Position = p;
}

และตอนนี้ ตัวปรับเฉดสี Vertex แบบเคลื่อนไหว โดยพื้นฐานแล้ว เครื่องมือนี้จะทำงานเช่นเดียวกับเครื่องมือปรับแสงเงาจุดยอดแบบธรรมดา แต่แค่บิดเล็กน้อย แทนที่จะแปลงจุดยอดแต่ละจุดโดยใช้เพียงเมทริกซ์การแปลง ก็สามารถใช้การแปลงแบบเคลื่อนไหวที่ขึ้นอยู่กับเวลาได้เช่นกัน เพื่อให้ตัวอักษรแต่ละตัวเคลื่อนไหวแตกต่างกันเล็กน้อย ตัวปรับแสงเงาของจุดยอดภาพเคลื่อนไหวจะแก้ไขภาพเคลื่อนไหวตามพิกัดของตัวอักษรด้วย มันจะดูซับซ้อนกว่าโปรแกรมสร้างเฉดสีเวอร์เท็กซ์แบบธรรมดา เพราะว่ามันซับซ้อนกว่าด้วย

uniform float uTime;
uniform float uEffectAmount;

varying float vZ;
varying vec2 vUv;

// rotateAngleAxisMatrix returns the mat3 rotation matrix
// for given angle and axis.
mat3 rotateAngleAxisMatrix(float angle, vec3 axis) {
  float c = cos(angle);
  float s = sin(angle);
  float t = 1.0 - c;
  axis = normalize(axis);
  float x = axis.x, y = axis.y, z = axis.z;
  return mat3(
    t*x*x + c,    t*x*y + s*z,  t*x*z - s*y,
    t*x*y - s*z,  t*y*y + c,    t*y*z + s*x,
    t*x*z + s*y,  t*y*z - s*x,  t*z*z + c
  );
}

// rotateAngleAxis rotates a vec3 over the given axis by the given angle and
// returns the rotated vector.
vec3 rotateAngleAxis(float angle, vec3 axis, vec3 v) {
  return rotateAngleAxisMatrix(angle, axis) * v;
}

void main() {
  // Compute the index of the letter (assuming 80-character max line length).
  float idx = floor(position.y/1.1)*80.0 + floor(position.x/1.1);

  // Round down the vertex coords to find the bottom-left corner point of the letter.
  vec3 corner = vec3(floor(position.x/1.1)*1.1, floor(position.y/1.1)*1.1, 0.0);

  // Find the midpoint of the letter.
  vec3 mid = corner + vec3(0.5, 0.5, 0.0);

  // Rotate the letter around its midpoint by an angle and axis dependent on
  // the letter's index and the current time.
  vec3 rpos = rotateAngleAxis(idx+uTime,
    vec3(mod(idx,16.0), -8.0+mod(idx,15.0), 1.0), position - mid) + mid;

  // uEffectAmount controls the amount of animation applied to the letter.
  // uEffectAmount ranges from 0.0 to 1.0.
  float effectAmount = uEffectAmount;

  vec4 fpos = vec4( mix(position,rpos,effectAmount), 1.0 );
  fpos.x += -35.0;

  // Apply spinning motion to individual letters.
  fpos.z += ((sin(idx+uTime*2.0)))*4.2*effectAmount;
  fpos.y += ((cos(idx+uTime*2.0)))*4.2*effectAmount;

  vec4 mvPosition = modelViewMatrix * fpos;

  // Apply wavy motion to the entire text.
  mvPosition.y += 10.0*sin(uTime*0.5+mvPosition.x/25.0)*effectAmount;
  mvPosition.x -= 10.0*cos(uTime*0.5+mvPosition.y/25.0)*effectAmount;

  vec4 p = projectionMatrix * mvPosition;

  // Pass texture coordinates and the vertex z-coordinate to the fragment shader.
  vUv = uv;
  vZ = p.z;

  // Send the final vertex position to WebGL.
  gl_Position = p;
}

ในการใช้เครื่องปรับเฉดสีเวอร์เท็กซ์ ฉันใช้ THREE.ShaderMaterial ซึ่งเป็นประเภทวัสดุที่ให้คุณใช้ตัวปรับเฉดสีที่กำหนดเองและระบุเครื่องแบบให้กับตัวปรับได้ นี่คือวิธีที่ฉันใช้ THREE.ShaderMaterial ในการสาธิต:

// First, set up uniforms for the shader.
var uniforms = {

  // map contains the letter map texture.
  map: { type: "t", value: 1, texture: tex },

  // uTime is the urrent time.
  uTime: { type: "f", value: 1.0 },

  // uEffectAmount controls the amount of animation applied to the letters.
  uEffectAmount: { type: "f", value: 0.0 }
};

// Next, set up the THREE.ShaderMaterial.
var shaderMaterial = new THREE.ShaderMaterial({
  uniforms: uniforms,

  // I have my shaders inside HTML elements like
  // <script id="vertex" type="text/x-glsl-vert">... shaderSource ... <script>

  // The below gets the contents of the vertex shader script element.
  vertexShader: document.querySelector('#vertex').textContent,

  // The fragment shader is a bit special as well, drawing a rotating
  // rainbow gradient.
  fragmentShader: document.querySelector('#fragment').textContent
});

// I set depthTest to false so that the letters don't occlude each other.
shaderMaterial.depthTest = false;

ฉันจะอัปเดตเครื่องแบบตัวปรับแสงเงาในเฟรมภาพเคลื่อนไหวทุกเฟรม ดังนี้

// I'm controlling the uniforms through a proxy control object.
// The reason I'm using a proxy control object is to
// have different value ranges for the controls and the uniforms.
var controller = {
  effectAmount: 0
};

// I'm using <a href="http://code.google.com/p/dat-gui/">DAT.GUI</a> to do a quick & easy GUI for the demo.
var gui = new dat.GUI();
gui.add(controller, 'effectAmount', 0, 100);

var animate = function(t) {
  uniforms.uTime.value += 0.05;
  uniforms.uEffectAmount.value = controller.effectAmount/100;
  bookModel.position.y += 0.03;

  renderer.render(scene, camera);
  requestAnimationFrame(animate, renderer.domElement);
};
animate(Date.now());

คุณก็จะเห็นภาพเคลื่อนไหว แบบให้เฉดสี เมนูดูค่อนข้างซับซ้อน แต่สิ่งเดียวจริงๆ ที่ทำได้คือการย้ายตัวอักษรไปรอบๆ ในรูปแบบที่ขึ้นอยู่กับเวลาปัจจุบันและดัชนีของแต่ละตัวอักษร หากประสิทธิภาพไม่ใช่เรื่องที่ต้องกังวล เป็นไปได้ว่าตรรกะนี้ทำงานอยู่ใน JavaScript อย่างไรก็ตาม JavaScript ไม่ได้เป็นโซลูชันที่ใช้ได้ผลจริงเมื่อมีวัตถุที่เคลื่อนไหวได้หลายหมื่นชิ้น

ข้อกังวลที่เหลืออยู่

ปัญหาหนึ่งในขณะนี้คือ JavaScript ไม่ทราบเกี่ยวกับตำแหน่งของอนุภาค หากคุณต้องการทราบตำแหน่งของอนุภาค คุณสามารถทำซ้ำตรรกะตัวปรับแสงเงาจุดยอดใน JavaScript และคำนวณตำแหน่งจุดยอดมุมใหม่โดยใช้โปรแกรมทำงานบนเว็บทุกครั้งที่คุณต้องการตำแหน่ง วิธีนี้จะช่วยให้เธรดการแสดงผลของคุณไม่ต้องรอการคำนวณและยังสร้างภาพเคลื่อนไหวต่อไปในอัตราเฟรมที่ราบรื่น

สำหรับภาพเคลื่อนไหวที่ควบคุมได้มากขึ้น คุณสามารถใช้ฟังก์ชันการแสดงผลเป็นพื้นผิวเพื่อสร้างภาพเคลื่อนไหวระหว่างตำแหน่ง 2 ชุดที่ JavaScript ให้ไว้ ก่อนอื่น แสดงผลตำแหน่งปัจจุบันเป็นพื้นผิว จากนั้นเคลื่อนไหวไปยังตำแหน่งที่กำหนดในพื้นผิวแยกต่างหากที่ JavaScript จัดเตรียมไว้ให้ ข้อดีก็คือคุณสามารถอัปเดตตำแหน่งที่ระบุโดย JavaScript เพียงส่วนเล็กๆ ต่อเฟรมได้และยังคงทำให้ตัวอักษรทุกตัวเคลื่อนไหวได้อย่างต่อเนื่องโดยที่ตัวปรับแสงเงาของจุดยอดมุมจะทวีตำแหน่งต่างๆ

อีกข้อกังวลหนึ่งคืออักขระ 256 ตัวนั้นน้อยเกินไปที่จะใช้ข้อความที่ไม่ใช่ ASCII หากดันขนาดแผนที่พื้นผิวเป็น 4096x4096 ขณะที่ลดขนาดตัวอักษรเป็น 8 พิกเซล คุณสามารถใส่อักขระ UCS-2 ทั้งชุดลงในแผนที่พื้นผิวได้ อย่างไรก็ตาม ขนาดแบบอักษร 8px อ่านได้ยาก หากต้องการปรับขนาดแบบอักษรให้ใหญ่ขึ้น คุณสามารถใช้พื้นผิวหลายแบบสำหรับแบบอักษรได้ โปรดดูตัวอย่างจากการสาธิตแผนที่สไปรท์นี้ อีกสิ่งหนึ่งที่จะช่วยได้คือการสร้างเฉพาะตัวอักษรที่ใช้ในข้อความของคุณ

สรุป

ในบทความนี้ เราจะแนะนำคุณตลอดขั้นตอนการใช้งานการสาธิตภาพเคลื่อนไหวที่อิงตามตัวปรับแสงเงาเวอร์เท็กซ์โดยใช้ Three.js เดโมดังกล่าวทำให้ตัวอักษรนับล้านเคลื่อนไหวแบบเรียลไทม์บน MacBook Air รุ่นปี 2010 การใช้วิธีดังกล่าวจะรวมหนังสือทั้งเล่มเป็นวัตถุรูปทรงเรขาคณิตเพียงชิ้นเดียวเพื่อให้การวาดภาพมีประสิทธิภาพ ตัวอักษรแต่ละตัวสามารถทำโดยการหาว่าจุดยอดเป็นของตัวอักษรใดและสร้างภาพเคลื่อนไหวให้กับจุดยอดตามดัชนีตัวอักษรในข้อความของหนังสือ

รายการอ้างอิง