"bir yazılımcının not defteri.."

3 Ocak 2012 Salı

WeBGL - 2 / Animasyon Temelleri

Herkese Merhaba yeniden.

Bugün çok da detayına girmeden ama işimizi de görecek kadar animasyon temellerinden söz edeceğiz. Bunlar aslında 3D ile uğraşan arkadaşların malum çok iyi bildikleri bir konu. İçinizde flash ya da silverlight gibi konularla bir miktar uğraşmış olanlarınız var ise, onlardaki temel mantık da yine aynıdır. Nedir bu mantık? Animasyon, belli periyotlarla peşi sıra render edilen (çizilen) karelerinin (hızlı bir şekilde) gösterilmesidir. Örneğin bir cismin ekranda sağa doğru ilerlediği bir animasyon aslında her bir karede (frame) küçük bir miktar daha sağda göründüğü resimlerin (frame) ard-arda gösterilmesinden ibarettir.

Burada iki kare arasındaki mini bekleme süresi (zaman periyodu) animasyonun temel noktalasıdır. Bu zaman ne kadar kısa olursa framelerin (karelerin) sıklığı o kadar artacaktır. Örneğin bir şeklin 3 saniye içinde 10 metre sağ tarafa doğru ilerlemesini anime edeceksek ve saniyede 10 frame ile bunu yaparsak toplamda animasyonumuz 3 saniyede 10x3 = 30 kare olur. Ama aynı animasyonu saniyede 60 kare ile yaparsak yine 3 saniyede 60x3 = 180 kare  olacaktır. Farkı nedir? Karelerin sıklığının artması kareler arasında daha yumuşak bir hareket geçişi sağlayacağından görüntü daha akıcı ve keskin olur. Ve tam tersi durumda da görüntü de "karelenme" yani duraksama olur. Evet her iki animasyonda da şeklimiz 3 saniye içinde 10 metre sağa kayacaktır. Ama ikinci animasyonun görüntü kalitesi daha gerçekçi olacaktır.

Buradaki ölçü birimi FPS değeridir. FPS bir saniye içindeki frame sayısıdır. FPS arttıkça görüntü akışkanlığı ve keskinliği de artar. Elbette tüm bunlar fps değerini (atıyorum) 500 e çıkarabileceğiniz anlamına gelmiyor :) Her bir karenin ayrı bir render işlemi anlamına geldiğini düşünürsek ne kadar çok fps o kadar çok sistem yüküdür. Ayrıca insan gözü  saniyede en fazla en fazla  30 - 35 frame i algılayabiliyor. Tabi bunu bir miktar daha artırmak oyunlardaki konforlu artıracaktır. Ama sonuçta FPS değeri pratikte hiçbir zaman 60 ı geçmez.

Bir saniye 1.000 mili saniye olduğunua göre saniyede 60 frame ile oynayan yani FPS değeri 60 olan bir animasyonun iki frame arasındaki bekleme süresi :  1.000 / 60 = 16 dır. Olayı zaman olarak ele aldığımızda bekleme süresinin kısalığı daha çok frame demektir.

Şimdi tekrar WebGL e dönelim. Sahnede içinde bulundğumuz andaki görüntüyü render etmek için  renderer.render(scene, camera);  gibi bir komut kullanmıştık hatırlayın. Peki olayı bir animasyona çevirmek için ne yapmalıyız? Her aşamasında sahnedeki objelerin özelliklerinin değiştiği bir render döngüsü kullanmalıyız. Ve eğer bu hareketliliğin sürekli olmasını istiyorsak - ki oyunlardaki hareketler süreklilik arz edecektir -  biz aksini belirtmediğimiz sürece bu sürekli yinelenen bir döngü olacak demektir.

Burada kastettiğim şey bir sonsuz döngü değil. Sonsuz döngüler kolayca browser i kilitler ve başka bir işlem yapılmasına da izin vermez. Ayrıca yuklarıda bahsettiğim her bir döngü adımında belli bir mini bekleme süresi de olmalı. Dolayısı ile bu iş için özyinelemeli yani  "Recursive Fonksiyonlar" kullanılır. Zamanında  C Programlama dili ile uğraşmış olanlarınız bu Recursive kelimesini daha önce duymuşlardır. Basit tanım olarak recursive fonksyon kendi kendini çağıran fonksyondur. WebGL i JavaScript ile kodladığımıza göre bu olayı da JavaScript syntax ı ile yapacağız demektir ve Javascript de tam da istediğimiz gibi hem özyineleme yapan,, hem de bunu bizim belirttiğimiz  belli bir zaman diliminde gerçekleştirebilen iki sihirli komut mevcuttur :  setTimeout ve setInterval. Hemen birer örnek ile bu ikisini kısaca izah edelim :

  <html>

      <head>
          <script>
              function recursive_start() {
                  document.write("fonksyon yinelemesi...<br />");
                  setTimeout(recursive_start, 500); 
              }
          </script>   
      </head>

      <body onload=recursive_start()>
      </body>

  </html>

Burada ne oluyor? Özetle : head tagı içinde bir fonksyon var ve bu fonsyon sayfa yüklendiği anda bir kez çağrılıyor. İlk bakışta tek bir kez çalışacağını düşünebileceğimiz bu küçük kodu html dosyası haline getirip browser da görüntülersek ekrana her yarım saniye de bir,, yani saniyede iki kez "fonksyon yinelemesi..." yazdığını görürüz. Bunun nedeni setTimeout komutu ile fonksyonun kendi kendini her yarım sanitede bir çağırmasıdır. setTimeout iki parametre alır, ilki çağrılacak olan kodun ismi, ikincisi ise milisaniye cinsinden bekleme süresidir. Yani setTimeout kendisine 2. parametrede verilen süre kadar bekledikten sonra ilk parametredeki komutu çalıştırmaktadır. Ve bu durum tam da animasyon için ihtiyaç duyduğumuz yapıyı bize sağlamaktadır. Yani belli periyotlarla yinelenen render fonksyonu. Birazdan detaylı örnek yapacağız. Ancak hazır konu açılmışken ikinci benzer komutumuz olan setInterval i de küçük bir örnekle hatırlayalım :

    <html>

        <head>
            <script>
                function recursive_start() {
                    document.write("fonksyon yinelemesi...<br />");                                    }
            </script>   
        </head>

        <body onload="setInterval(recursive_start,500)">
        </body>

    </html>

Evet, ilk örnek ile tamamen aynı çıktığı veren bu kodun çalışma mantığı da setTiemout a çok yakındır. setInterval de diyoruz ki: "bundan sonra şu fonksyonu şu sıklıkta sürekli çağır. Ben işime devam edeceğim. Sen olayı sürdür bi yandan."  Dolayısı ile fonksyonun içine ekstra birşey yazmıyoruz. Oldukça kullanışlı...

Evet bu kadar JavaScript muhabbeti yeter, artık WebGL e dönelim,, yani artık biraz iş konuşalım :)) Şimdi yukarıda anlattığım JavaScript komutu ile bir "özyinelemeli" (gavurca tabirle: recursive)  fonksyon yazacağım. Ve bu fonksyonun yineleme sıklığını kendim belirleyeceğim. Animasyona basit bir örnek teşkil edecek olan uygulamamızda sadece dönen bir dikdörtgen prizma olacak. Dilerseniz önce kodun tamamını vereyim sonra üzerine konuşalım :

    <html>

    <head>
        <script src="js/Three.js"></script>       
        <script language=javascript>       
            function anim() {
                mesh.rotation.z += 0.03;          
                renderer.render(scene, camera);            
                setTimeout(anim, 16);       
            }
        </script>
    </head>


    <body>

        <script>    
                // global javasacript değişkenler
                var renderer, scene, camera, mesh;

                // renderer  
                renderer = new THREE.WebGLRenderer();
                renderer.setSize(window.innerWidth, window.innerHeight);
                document.body.appendChild(renderer.domElement);

                // camera
                camera = new THREE.PerspectiveCamera(
                                       45,
                                       window.innerWidth /
                                       window.innerHeight, 1, 1000);
                camera.position.x = 0;
                camera.position.y = -400;
                camera.position.z = 400;        
                camera.rotation.x = 45 * (Math.PI / 180);

                // scene
                scene = new THREE.Scene();

                // geometry        
                var duzlem = new THREE.CubeGeometry(200, 100, 100);
     
                // plane mesh
                mesh = new THREE.Mesh(duzlem, new THREE.MeshNormalMaterial());
           
                // sahneye ekle         
                scene.add(mesh);

                // animasyon fonsyonuna git ve render döngüsü başlat
                anim();      
              
        </script>
    </body>

    </html>

Bu küçük uygulamanın çıktısı da şöyledir :
   
            

Şimdi kodlar üzerinde biraz konuşalım ve neler yaptığımızı açıklayalım. Geçen makalemizde renderer in çıktı genişliğini de 500 pixel olarak sınırlamıştık: renderer.setSize(500, 300); Ve bu çıktıyı da bir div elementinin içine yönlendirmiştik:  $('#container').append(renderer.domElement);  Bu örneğimizde ise ekranın tamamını kullanacağız. Ekranın genişlik ve yüksekliğinin tamamını dinamik olarak alıp renderer objesine kullanmasını söyledik. Ve de doğrudan body elementine yani sayfanın ana gövdesine yönlendirdik. Geçen sefer küre çizmiştik bu sefer ki şeklimiz hareket ettirmek üzere bir dikdörtgen prizma oldu, yani :

  // geometry        
  var duzlem = new THREE.CubeGeometry(200, 100, 100);

Aslında 6 parametre alan CubeGeometry komutunun ilk 3 parametresi tahmin edeceğiniz gibi x, y ve z eksenlerindeki 3 uzunluktur. Eğer bir küp çizmek istese idik üç parametreyi de aynı büyüklükte verirdik.

Konumuz bugün matertaller değil, o yüzden onu basitçe renklendirecek bir basit materyal uyguladık.

Kameraya birkaç parametre verdiğimiz gözünüze çarpmıştır. Burada yapmaya çalıştığımız sadece şekle yukarı çaprazdan aşağı doğru bakmayı sağlamak idi. Bunun için hem kameranın pozisyonunu değiştirdik hem de onu 45 derece aşağı doğru bakmasını sağladık. Burada dikkatinizi çekmek istediğim bir satır var :


 // kamera
 camera.rotation.x = 45 * (Math.PI / 180); 

Rotation ile belli bir eksende döndürmek içindi hatırlayın. Peki cameranın aşağı doğru bakması için neden sadece 45 yazmadıkta yukarıdaki gibi bir formül yazdık? Çünkü açı belirtmek için radyan birimi kullanmalıyız ve 45 derecelik açıyı radyan cinsinden ifade etmek için böyle bir çeviri yaptık. Bu formülü bütün rotasyon işlemlerinde açı belirtmek için kullanabilirsiniz.

Kemera ile ilgili olarak söylenebilecek en önemli şey,, ilk yaratıldığında ve hiçbir değer verilmediğinde, bakış yönü default olarak -Z ekranına doğru olduğu; ama Z eksenindeki pozisyon değerinin sıfır olduğu için tabiri caizse ekran camına yapışık bir konumda olduğudur. Dolaıyısı ile ekran camına yapışık diğer objeler bu şekilde görüntülenemeyecektir. Görüntü almak istiyor isek kamerayı biraz kendimize doğru çekmeliyiz bu yüzden onu biraz +Z eksenine kaydırırız ki objeler ile arasında mesafe olsun. Ya da,, objeleri -Z ekseninde ileri iterseniz bu da işe yarar. Lakin küçük basit uygulamalarda ilk yol daha pratik.

Dönüşler yani rotation işlemleri ilk başta insana biraz karışık gelebiliyor. Aslında tüm yapmamız gereken o eksen hizasında cismin tam ortasından bir görünmez çubuğun geçtiğini hayal etmek. Yani cismi o eksende bir şişe geçirdiğinizi hayal edin. Sonra nasıl bir dönüş yapabilir diye fafanızda bir çizin. Olay netleşecektir. Dolayısı ile kameranın aşağı doğru bakması için kameraya yanlamasına bir çubuk geçmesi gerekir, yani cismi delen şiş X ekseninde olacak ve aşağı bakması için +X  ekseninde dönecektir.

Gelelim animasyon konusunda (e nihayet) : Dikkat ederseniz en son komut olarak "anim" isimli fonksyonu çağırdık. Normalde biz bu fonksyonu tek bir kez çağırdık. Ama fonksyon içindeki  setTimeout(anim, 16);  satırı her 16 milisaniyede bir tekrar tekrar kendisini çağıracak şekilde ayarlı olduğundan yineleme başladı. Yinelenen anim fonksyonu içinde de döndürme ve render etme işlemleri var. Yani? Saniyede yaklaşık 60 kez küçük bir miktar daha dönüş yapacak ve her seferinde render edilecek ve kullanıcı bu döngüyü ekrandan animasyon olarak izleyecek demektir. Temel mantık aşağı yukarı böyle. Tabi setInterVal komutunu da kullanabilirdik. Ama o da bir benzeri olduğundan ayrı örnek yapmaya gerek yok.

Peki gözünüze hiç bir sorun çarptı mı? Mesela tarayıcınızda başka bir taba geçtiğinizde, hatta tarayıcınızı görev çubuğuna indirdiğinizde arkada bu animasyon işlemlerinin aynen sürmeye devam ettiğini farkettiniz mi?  Bu CPU , GPU ve RAM in gereksiz kullanımıdır. Tamam ekranda bir tane küpü çevirmek windowslarınızı göçertmez (belkide göçertir :)) ama daha ileri ve karmaşık uygulamalarda işlem yükü arttıkça ekrana bakmadığınız durumlarda durmasını, işlem yapılMAMAsını isteyebilirsiniz. Bunun için bworser tabının aktif olup olmamasını kontrol eden kodlar yazabiliriz. Ama tam da bu işler için yani animasyon için bize hazır bir kod THREE.JS içinde gelmektedir : RequestAnimationFrame.js Bu küçük sihirli kod animasyon işlemlerimizde bize ciddi faydalar sağlar. Örneğin browserların olay döngüleri ile daha etkileşimli olmasından dolayı animasyonlarımız daha akıcı olur. Ve en önemlisi de kullanıcı farklı bir tab a geçtiğinde ya da browser i görev çubuğuna indirgediğinde işlemi durdurmaktadır. Bu da kaynakların verimli kullanılması asçısından oldukça önemlidir. Hele ki çalıştığı platform bir mobil platform ise...

RequestAnimationFrame.js in animasyonda nasıl kullanılacağını göstermeden önce size içinde ne olduğunu göstermek istiyorum :

 /**
 * Provides requestAnimationFrame in a cross browser way.
 * http://paulirish.com/2011/requestanimationframe-for-smart-animating/
 */

if ( !window.requestAnimationFrame ) {

 window.requestAnimationFrame = ( function() {

  return window.webkitRequestAnimationFrame ||
  window.mozRequestAnimationFrame ||
  window.oRequestAnimationFrame ||
  window.msRequestAnimationFrame ||
  function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) {

   window.setTimeout( callback, 1000 / 60 );

  };

 } )();

}

Açıklama satırında da gördüğünüz gibi Paul Irish tarafından üretilen bu küçük basit ama marifetli kod un yeni versiyonlarını da kendisinin sitesinden takip edebilirsiniz. Ama zaten THREE.JS nin yeni versiyonlarında da RequestAnimationFrame.js in son güncel versiyonu yer alacaktır.

Şimdi bu makalenin asıl konusu olan RequestAnimationFrame.js kullanarak bir örnek animasyon yapalım,, aynı küp örneğine benzer birşey yapıcam, tek farkı animasyon işlemlerini RequestAnimationFrame.js e bırakıcam,

    <script src="js/Three.js"></script>
    <script src="js/RequestAnimationFrame.js"></script>

    <script>
    
        // global değişkenler
        var renderer, scene, camera, plane;
       
        function anim() {
            plane.rotation.z += 0.03;
            renderer.render(scene, camera);        
            // animasyonu başlat
            requestAnimationFrame(function () { anim(); });              
        }



        window.onload = function () {
            renderer = new THREE.WebGLRenderer();
            renderer.setSize(window.innerWidth, window.innerHeight);
            document.body.appendChild(renderer.domElement);

            // camera
            camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000);

            camera.position.x = 0;
            camera.position.y = -400;
            camera.position.z = 400;
            camera.rotation.x = 45 * (Math.PI / 180);

            // scene
            scene = new THREE.Scene();

            //geometry        
            var duzlem = new THREE.CubeGeometry(200, 100, 100);
        
            // plane mesh
            plane = new THREE.Mesh(duzlem, new THREE.MeshNormalMaterial());        

            //plane.overdraw = true;   //bu olmadan da çalıştı
            scene.add(plane);

            //animasyon fonksyonuna git
            anim();        
        };    

    </script>

Gördüğünüz gibi tek farklılık "anim()" fonksyonunun içinde. setTimeout u çıkarıp yerine requestAnimationFrame(function () { anim(); });  yazdık. Ve gerisini sihirli kod bloğumuz hallediyor. Bunu denerken söz konusu js dosyanızı projenize dahil etmeyi unutmayın.

Peki buraya kadar herşey güzel hoş da,, ben saniyede kaç frame oynatıldığını yada browser tabı pasif duruma geçriğinde render işleminin durduğunu görmek istersem? THREE.JS kütüphanesinin içinde bunun için de bir hazır araç yok mudur? Olmasa idi bu soruyu sormazdım değil mi ? :) Aslında animasyon konumuz yüzeysel bir anlatım ile bu kadar. Ancak FPS durumunu gösteren küçük bir yardımcı aracımızı da tanıtalım: Stats.js Bu güzel araç uygulama ekranına küçük grafiksel bir gösterge yerleştirir ve sürekli FPS değerini rakamsal ve de grafiksel olarak gösterir. Web deki çoğu örnekte ekranın sol üst köşesinde bir grafik görürsünüz. İşte o Stats.js yardımcı aracıdır. Uygulaması da gayet basit :

varolan kodların aynını kullanıcam, sadece eklediğim satırları bold yapıcam. Zaten anlarsınız. Öncelikle tabi THREE.JS içindeki stats.js dosyasını projemize ekliyoruz. Kodların son hali ise şu şekilde :

    <script src="js/Three.js"></script>
    <script src="js/RequestAnimationFrame.js"></script>
    <script src="js/stats.js"></script>


    <script>
    
        // global değişkenler
        var renderer, scene, camera, plane, stats;

        // animasyon fonksyonu
        function anim() {
            plane.rotation.z += 0.03;
            renderer.render(scene, camera);
            // durumu güncelle     
            stats.update();  
            // animasyonu başlat
            requestAnimationFrame(function () { anim(); });              
        }



        window.onload = function () {
            renderer = new THREE.WebGLRenderer();
            renderer.setSize(window.innerWidth, window.innerHeight);
            document.body.appendChild(renderer.domElement);

            // camera
            camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000);
            camera.position.x = 0;
            camera.position.y = -400;
            camera.position.z = 400;
            camera.rotation.x = 45 * (Math.PI / 180);

            // scene
            scene = new THREE.Scene();

            //geometry        
            var duzlem = new THREE.CubeGeometry(200, 100, 100);
         
            // plane mesh
            plane = new THREE.Mesh(duzlem, new THREE.MeshNormalMaterial());

            //plane.overdraw = true;   //bu olmadan da çalıştı
            scene.add(plane);

            // status grafik nesnemizi yaratalım
            stats = new Stats();
            stats.domElement.style.position = 'absolute';
            stats.domElement.style.top = '0px';
            document.body.appendChild(stats.domElement);     

            //animasyon fonksyonuna git
            anim();
        };    

    </script>

Eğer herşey yolunda gitti ise artık FPS durumunu göteren grafik de ekranın sol üst köşesinde belirecektir. Benim dönen şeklin üstünde de görünüyor olmalı.

Basit de olsa animasyon konusuna bir giriş yapmaya çalıştık ve bir iki yardımcı aracı inceledik. Bir sonraki WebGL makalemizde görüşmek üzere hoşçakalın.


2 yorum:

Mr.E dedi ki...

Öncelikle WebGL konusunda bilgilendirmeler ve dersler için teşekkürler. WebGL çok canlı grafikler tasarlanmasına ortam sağlıyor. Html5 ile beraber de çok çok gelişeceğe benziyor. Ben biraz openGl biliyorum ve c,c++ dilleri ile opengl de birşeyler yapmaya çalışıyorum. Peki webGl deki tasarımla opengl deki aynı mıdır? javascript bilmiyorum ama opengl de yaptığım gibi webgl de de görsel efektler ve grafikler tasarlamak istiyorum. Javascript öğrenmem zamanımı çok alır mı? Opengl ile webgl kodlamaları arasında çok farklar var mı?

Gökhan ZER dedi ki...

yakında Google Native Client ile ilgili bir yazı yayınlayacağım burada. O yazıda tüm bu söylediklerine çok köklü bir cevap olacağını umuyorum.

WebGL, OpenGL nin biraz küçültülmüş ve özellikleri azaltılmış hali biliyorsun. OpenGL bilen için WebGL basit kaçması lazım. JavaScript ile kodlansa bile yinede temeli bildiğin için JS fazla bir sorun teşkil etmeyecektir, heleki C++ bilen biri için.

Oyun konusu ile de ilgileniyorsan webGL nin bir üst teknolojisi olan Native Client ı da öneririm. Yazımda bahsedicem :)

WebGL ile oyun yapmak için hazır bir kaç webGL gameengine kütüphanesi var. ama onlarda çok başarılı sayılmaz. ve evet çok zamanını alır bence.. hele ki tek başına isen; ancak bu durum hızla değişebilir de; çünkü Unity, UDK, Blender gibi 3D ortamlar Native Client a destek vermeye hazırlanıyor.

Yorum için teşekkürler.