Giriş
Bu makaledeki amacım, ekranda sabit bir kare hızında bir milyon animasyonlu harf çizmektir. Bu görev, modern GPU'larla oldukça kolay bir şekilde yapılabilir. Her harf iki dokulu üçgenden oluşur. Dolayısıyla, kare başına yalnızca iki milyon üçgenden bahsediyoruz.
Geleneksel JavaScript animasyon geçmişine sahipseniz tüm bunlar çılgınlık gibi gelebilir. Her karede iki milyon üçgenin güncellenmesi, günümüzde JavaScript ile yapmak isteyeceğiniz bir şey değildir. Neyse ki modern GPU'ların muhteşem gücünden yararlanmamızı sağlayan WebGL'ye sahibiz. Modern bir GPU ve biraz gölgelendirici sihiriyle iki milyon animasyonlu üçgenle çalışmak oldukça kolaydır.
Verimli WebGL kodu yazma
Etkili WebGL kodu yazmak için belirli bir zihniyete sahip olmanız gerekir. WebGL'yi kullanarak çizim yapmanın genel yolu, her nesne için üniformalarınızı, arabelleklerinizi ve gölgelendiricilerinizi ayarlamak ve ardından nesneyi çizme çağrısı yapmaktır. Bu çizim yöntemi, az sayıda nesne çizerken işe yarar. Çok sayıda nesne çizmek için WebGL durum değişikliklerinin miktarını en aza indirmeniz gerekir. Başlangıçta, nesneler arasında gölgelendiricileri değiştirmek zorunda kalmamak için tüm nesneleri birbiri ardına aynı gölgelendiriciyi kullanarak çizin. Parçacıklar gibi basit nesneler için birkaç nesneyi tek bir arabelleğe toplayabilir ve JavaScript'i kullanarak düzenleyebilirsiniz. Bu sayede, her bir parçacık için gölgelendirici üniformalarını değiştirmek yerine yalnızca köşelik arabelleğini yeniden yüklemeniz gerekir.
Ancak gerçekten hızlı bir şekilde işlem yapmak için hesaplamanızın büyük bir kısmını gölgelendiricilere göndermeniz gerekir. Burada bunu yapmaya çalışıyorum. Gölgelendiricileri kullanarak bir milyon harfi canlandırın.
Makalenin kodunda, WebGL kodu yazarken karşılaşılan tüm sıkıcı kalıpları soyutlayan Three.js kitaplığı kullanılmaktadır. WebGL durum ayarlaması ve hata işleme için yüzlerce satır kod yazmak yerine Three.js ile yalnızca birkaç satır kod yazmanız yeterlidir. Three.js'den WebGL gölgelendirici sistemine erişmek de kolaydır.
Tek bir çizim çağrısı kullanarak birden fazla nesne çizme
Aşağıda, tek bir çizim çağrısı kullanarak birden fazla nesneyi nasıl çizebileceğinize dair küçük bir sözde kod örneği verilmiştir. Geleneksel yöntem, her seferinde bir nesneyi şu şekilde çizmektir:
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);
Ancak yukarıdaki yöntem, her nesne için ayrı bir çizim çağrısı gerektirir. Birden fazla nesneyi aynı anda çizmek için nesneleri tek bir geometriye gruplandırabilir ve tek bir çizim çağrısıyla çizebilirsiniz:
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);
Temel fikri anladığınıza göre demo yazmaya ve bu milyonlarca harfi animasyona dönüştürmeye başlayalım.
Geometriyi ve dokuları ayarlama
İlk adım olarak, üzerinde harf bitmap'leri bulunan bir doku oluşturacağım. Bunun için 2D tuvali kullanıyorum. Elde edilen dokuda, çizmek istediğim tüm harfler bulunur. Bir sonraki adım, harf sprite e-tablosunun doku koordinatlarını içeren bir arabellek oluşturmaktır. Bu, harfleri ayarlamak için kolay ve basit bir yöntem olsa da doku koordinatları için her köşe başına iki kayan nokta kullandığından biraz israflıdır. Okuyucuya alıştırma olarak bırakılan daha kısa bir yol, harf dizini ve köşe dizini değerlerini tek bir sayıya paketleyip bunu köşe verici gölgelendiricide tekrar doku koordinatlarına dönüştürmektir.
Canvas 2D'yi kullanarak harf dokusunu şu şekilde oluşturuyorum:
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;
Üçgen dizisini de GPU'ya yüklerim. Bu köşe noktaları, köşe noktası gölgelendirici tarafından harfleri ekrana yerleştirmek için kullanılır. Köşeler, metindeki harf konumlarına ayarlanır. Böylece, üçgen dizisini olduğu gibi oluşturmanız durumunda metnin temel bir düzen oluşturmasını sağlarsınız.
Albümün geometrisini oluşturma:
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++;
}
}
Harfleri animasyonlu hale getirmek için kullanılan köşe gölgelendirici
Basit bir köşe gölgelendirici kullanarak metnin düz bir görünümünü elde ettim. Karmaşık bir çözüme gerek yok. İyi çalışıyor ancak animasyon yapmak istersem animasyonu JavaScript'te yapmam gerekiyor. Ayrıca JavaScript, özellikle her karede yapmak isterseniz ilgili altı milyon köşeyi animasyonlu hale getirmek için biraz yavaştır. Belki daha hızlı bir yol vardır.
Evet, işlemsel animasyon yapabiliriz. Bu, tüm konum ve dönme hesaplamalarımızı köşe düğümü gölgelendiricisinde yaptığımız anlamına gelir. Artık köşe noktalarının konumlarını güncellemek için JavaScript çalıştırmam gerekmiyor. Köşe noktası gölgelendirici çok hızlı çalışıyor ve her karede bir milyon üçgen ayrı ayrı animasyonlu olsa bile akıcı bir kare hızı elde ediyorum. Üçgenleri tek tek ele almak için köşe koordinatlarını aşağı yuvarladım. Böylece, bir harf dörtgeninin dört noktası da tek bir benzersiz koordinatla eşlendi. Artık söz konusu harfin animasyon parametrelerini ayarlamak için bu koordinatı kullanabilirim.
Koordinatların başarılı bir şekilde aşağı yuvarlanabilmesi için iki farklı harften gelen koordinatlar çakışamaz. Bunu yapmanın en kolay yolu, harfleri sağ tarafındaki harften ve üstündeki satırdan ayıran küçük bir ofset içeren kare harf dörtlüleri kullanmaktır. Örneğin, harfler için 0,5 genişlik ve yükseklik kullanabilir ve harfleri tam sayı koordinatlarına hizalayabilirsiniz. Artık herhangi bir harf köşesinin koordinatını aşağı yuvarladığınızda harfin sol alt köşesinin koordinatını elde edersiniz.
Animasyonlu köşe verisi gölgelendiriciyi daha iyi anlamak için önce sıradan bir köşe verisi gölgelendiriciyi inceleyeceğim. Bu durum, ekrana 3D model çizdiğinizde genellikle yaşanır. Modelin köşe noktaları, her 3D köşe noktasını 2D ekrana yansıtmak için birkaç dönüşüm matrisi tarafından dönüştürülür. Bu köşelerden üçü tarafından tanımlanan bir üçgen görüntü alanının içine düştüğünde, kapladığı pikseller renklendirilmek üzere parçacık gölgelendirici tarafından işlenir. Basit bir köşe düğümü gölgelendiriciyi aşağıda bulabilirsiniz:
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;
}
Şimdi de animasyonlu köşe verisi gölgelendiricisine geçelim. Temel olarak, basit köşe verisi gölgelendiricisiyle aynı şeyi yapar ancak küçük bir fark vardır. Her bir köşeyi yalnızca dönüşüm matrisleriyle dönüştürmek yerine, zamana bağlı animasyonlu bir dönüşüm de uygular. Animasyonlu köşe düğümü gölgelendirici, her harfin animasyonunu biraz farklı hale getirmek için animasyonu harfin koordinatlarına göre de değiştirir. Basit bir köşe düğümü gölgelendiricisinden çok daha karmaşık görünecektir. Bunun nedeni, daha karmaşık olmasıdır.
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;
}
Köşe düğümü gölgelendiriciyi kullanmak için özel gölgelendiriciler kullanmanıza ve bunlar için üniformalar belirtmenize olanak tanıyan bir malzeme türü olan THREE.ShaderMaterial
kullanıyorum. Demoda THREE.ShaderMaterial'ı şu şekilde kullanıyorum:
// 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;
Her animasyon karesindeki gölgelendirici üniformalarını güncelliyorum:
// 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());
İşte gölgelendirici tabanlı animasyon. Oldukça karmaşık görünse de tek yaptığı, harfleri mevcut saate ve her harfin dizine bağlı olacak şekilde hareket ettirmektir. Performans önemli değilse bu mantığı JavaScript'te çalıştırabilirsiniz. Ancak on binlerce animasyonlu nesneyle karşılaştığınızda JavaScript artık uygun bir çözüm olmaktan çıkar.
Kalan endişeler
Şu anda JavaScript, parçacık konumlarını bilmiyor. Parçacıklarınızın nerede olduğunu gerçekten bilmeniz gerekiyorsa JavaScript'te köşe gölgelendirici mantığını kopyalayıp konumlara her ihtiyacınız olduğunda bir web işleyici kullanarak köşe konumlarını yeniden hesaplayabilirsiniz. Böylece oluşturma iş parçacığınızın matematik işlemlerini beklemesi gerekmez ve pürüzsüz bir kare hızında animasyon yapmaya devam edebilirsiniz.
Daha kontrol edilebilir bir animasyon için JavaScript tarafından sağlanan iki konum grubu arasında animasyon oluşturmak üzere dokuya oluşturma işlevini kullanabilirsiniz. Öncelikle mevcut konumları bir dokuya oluşturun, ardından JavaScript tarafından sağlanan ayrı bir dokuda tanımlanan konumlara doğru animasyon oluşturun. Bunun avantajı, JavaScript tarafından sağlanan konumların küçük bir kısmını kare başına güncelleyebilmeniz ve yine de konumları tweening yapan köşe düğümü gölgelendiricisiyle her karede tüm harfleri animasyonlu olarak göstermeye devam edebilmenizdir.
Bir diğer sorun da, ASCII olmayan metinler için 256 karakterin çok az olmasıdır. Yazı tipi boyutunu 8 piksel azaltırken doku haritası boyutunu 4096x4096'ya çıkarırsanız UCS-2 karakter kümesinin tamamını doku haritasına sığdırabilirsiniz. Ancak 8 piksel yazı tipi boyutu çok okunaklı değil. Daha büyük yazı tipi boyutları için yazı tipinizle birden fazla doku kullanabilirsiniz. Örnek için bu sprite atlası demosuna göz atın. Yardımcı olabilecek bir diğer şey de yalnızca metninizde kullanılan harfleri oluşturmaktır.
Özet
Bu makalede, Three.js'i kullanarak bir köşe gölgelendirici tabanlı animasyon demosu uygulamanız için size yol gösterdik. Demoda, 2010 MacBook Air'de bir milyon harf gerçek zamanlı olarak canlandırılıyor. Uygulama, verimli çizim için kitabın tamamını tek bir geometri nesnesine sardı. Harfler, hangi köşe noktalarının hangi harfe ait olduğunu tespit edip köşe noktalarını kitap metnindeki harfin dizine göre animasyonlu hale getirerek tek tek canlandırıldı.