Model Yükleme

Assimp ile çalışma ve gerçek yükleme kodunu yazma zamanı geldi. Bu bölümün amacı, bir modeli bütünüyle temsil eden yeni bir sınıf oluşturmaktır: birden fazla mesh içerebilen, muhtemelen birden fazla dokuya sahip bir model. Ahshap balkonu, kuleleri ve hatta bir havuzu olan bir ev, tek bir model olarak yüklenebilir. Modeli Assimp aracılığıyla yükleyecek ve bir önceki bölümdearrow-up-right tanımladığımız Mesh nesnelerine çevireceğiz.

Lafı fazla uzatmadan, Model sınıfının yapısını sunuyoruz:

class Model 
{
    public:
        Model(char *path)
        {
            loadModel(path);
        }
        void Draw(Shader &shader);	
    private:
        // model verileri
        vector<Mesh> meshes;
        string directory;

        void loadModel(string path);
        void processNode(aiNode *node, const aiScene *scene);
        Mesh processMesh(aiMesh *mesh, const aiScene *scene);
        vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, 
                                             string typeName);
};

Model sınıfı, Mesh nesnelerinden oluşan bir vector içerir ve constructor'ında bir dosya yolu alarak modeli anında yükler. Private fonksiyonların her biri Assimp'in içe aktarma sürecinin farklı bir aşamasını işler; kısa süre içinde bunları ele alacağız. Ayrıca dosya yolunun dizinini sakladığımıza dikkat edin; dokuları yüklerken buna ihtiyacımız olacak.

Draw fonksiyonu özel bir fonksiyon değildir; temelde her mesh üzerinde döngü kurarak ilgili Draw fonksiyonunu çağırır:


OpenGL'e 3D Model İçe Aktarmak

Bir modeli içe aktarıp kendi yapımıza çevirmek için önce Assimp'in uygun başlıklarını eklememiz gerekiyor:

Constructor'ın doğrudan çağırdığı loadModel işlevi içinde, modeli Assimp'in scene nesnesi adı verilen bir yapıya yükleme için Assimp'i kullanıyoruz. Model yükleme serisinin ilk bölümündenarrow-up-right hatırlayabileceğiniz gibi bu, Assimp veri arayüzünün kök nesnesidir. Scene nesnesine sahip olduğumuzda yüklenen modele ait tüm verilere erişebiliriz.

Assimp'in harika yanı, farklı dosya formatlarını yüklemenin tüm teknik ayrıntılarını güzel bir şekilde soyutlaması ve bunu tek bir satırla yapmasıdır:

Assimp'in namespace'inden bir Importer nesnesi tanımlıyor, ardından ReadFile fonksiyonunu çağırıyoruz. Fonksiyon, ikinci argüman olarak bir dosya yolu ve birkaç post-processing seçeneği bekler. aiProcess_Triangulate ayarıyla modelin (tamamen) üçgenlerden oluşmadığı durumlarda Assimp'e önce tüm model ilkel şekillerini üçgene dönüştürmesini söylüyoruz. aiProcess_FlipUVs ise işleme sırasında y ekseninde doku koordinatlarını çeviriyor (Dokulararrow-up-right bölümünden hatırlayabileceğiniz gibi OpenGL'deki görüntülerin büyük çoğunluğu y ekseninde tersine çevrilmiştir). Diğer bazı kullanışlı seçenekler:

  • aiProcess_GenNormals: Model normal vektörler içermiyorsa her vertex için normal vektörler oluşturur.

  • aiProcess_SplitLargeMeshes: Büyük mesh'leri daha küçük alt mesh'lere böler; rendering'inizde izin verilen maksimum vertex sayısı varsa ve yalnızca daha küçük mesh'leri işleyebiliyorsa kullanışlıdır.

  • aiProcess_OptimizeMeshes: Optimizasyon için çizim çağrılarını azaltarak birkaç mesh'i daha büyük bir mesh'e birleştirmeye çalışır.

Assimp harika bir post-processing seçenekleri seti sunar ve tümünü buradaarrow-up-right bulabilirsiniz. Bir modeli Assimp aracılığıyla yüklemek (gördüğünüz gibi) şaşırtıcı derecede kolaydır. Zor kısım, döndürülen scene nesnesini kullanarak yüklenen verileri bir Mesh nesneleri dizisine çevirmektir.

Tam loadModel fonksiyonu şuradan listelenmiştir:

Modeli yükledikten sonra scene'nin ve kök node'unun null olup olmadığını kontrol ediyoruz; ayrıca verilerin eksik olup olmadığını gösteren bir flag'i de kontrol ediyoruz. Bu hata koşullarından biri karşılanırsa importer'ın GetErrorString fonksiyonundan alınan hata mesajını yazdırıp çıkıyoruz. Bıyık hata yoksa dosya yolunun dizinini elde ediyoruz.

Hiçbir şey yanlış gitmezse sahnenin tüm node'larını işlemek istiyoruz. İlk node'u (kök node) özyinelemeli processNode fonksiyonuna geçiriyoruz. Her node muhtemelen bir dizi çocuk içerdiğinden, önce söz konusu node'u işlemek, sonra tüm node çocuklarını işlemek ve böyle devam etmek istiyoruz.

Assimp yapısından hatırlayabileceğiniz gibi her node, belirli bir mesh'e işaret eden bir dizi mesh indeksi içerir. Amacımız bu indeksleri alıp ilgili mesh'leri işlemek, ardından her node'un çocuk node'ları için aynı işlemi tekrarlamaktır. processNode işlevinin içeriği aşağıda gösterilmektedir:

Node'un mesh indekslerini tek tek kontrol ediyor ve sahnenin mMeshes dizisini indeksleyerek ilgili mesh'i alıyoruz. Döndürülen mesh, Mesh nesnesi üreten processMesh fonksiyonuna geçirilir; sonuç meshes vektörüne eklenir.

Tüm mesh'ler işlendikten sonra node'un çocuklarını yineliyoruz ve her biri için processNode özyinelemeli olarak çağrılır; hiç çocuğu kalmayan node'a ulaşıldığında özyineleme durur.

circle-info

Dikkatli bir okuyucu, node'lardan herhangi birini işlemeyi unutup sahnenin tüm mesh'lerini doğrudan indeksler üzerindeki tüm bu karmaşık çalışma olmadan döngüye alabileceğimizi fark etmiş olabilir. Bunu yapma nedenimiz, bu şekilde node'ları kullanmanın başlangıç fikrinin mesh'ler arasında bir üst-alt ilişkisi tanımlamasıdır. Bu ilişkileri özyinelemeli olarak yineleyerek, belirli mesh'leri diğer mesh'lerin ebeveyni olarak tanımlayabiliriz.

Bu tür bir sistemin örnek kullanım durumu, bir araba mesh'ini ötelemek ve tüm çocuklarının (motor mesh, direksiyon mesh'i ve lastik mesh'leri gibi) birlikte ötelenmesini sağlamak istediğinizde ortaya çıkar; böyle bir sistem üst-alt ilişkileri kullanılarak kolaylıkla oluşturulabilir.

Şu an böyle bir sistem kullanmıyoruz; ancak mesh verileriniz üzerinde daha fazla kontrol istediğinizde genel olarak bu yaklaşımda kalmak önerilir. Bu node benzeri ilişkiler sonuçta modelleri oluşturan sanatçılar tarafından tanımlanır.


Assimp'ten Mesh'e

Bir aiMesh nesnesini kendi mesh nesnemize çevirmek çok zor değildir. Yapmamız gereken tek şey, mesh'in ilgili özelliklerinin her birine erişmek ve bunları kendi nesnemizde saklamaktır. processMesh fonksiyonunun genel yapısı şöyle olur:

Bir mesh'i işlemek 3 aşamalıdır: tüm vertex verilerini al, indeksleri al, ilgili materyal verilerini al. İşlenen veriler 3 vektörde saklanır; bunlardan bir Mesh oluşturulup çağrıcıya döndürülür.

Vertex verilerini almak oldukça basittir: Her döngü yinelemesinin ardından vertices dizisine eklediğimiz bir Vertex struct'ı tanımlıyoruz. Mesh (yani mesh->mNumVertices aracılığıyla alınan) içinde ne kadar vertex varsa o kadar döngü kuruyoruz. Yineleme içinde bu struct'ı tüm ilgili verilerle doldurmak istiyoruz. Vertex konumları için şöyle yapılır:

Assimp'in verilerini geçici bir vec3'e aktardığımıza dikkat edin. Bu gereklidir çünkü Assimp, vektör, matris, string vb. için kendi veri türlerini korur ve glm'nin veri türlerine iyi dönüşüm yapmazlar.

circle-info

Assimp, vertex konum dizisini mVertices olarak adlandırmaktadır — en sezgisel isim sayılmaz.

Normal'lerin düzeni tamamen aynıdır:

Doku koordinatları kabaca aynıdır; ancak Assimp, vertex başına 8 farklı doku koordinatına izin verir. 8'ini kullanmayacağız; yalnızca ilk doku koordinatları seti ile ilgileniyoruz. Ayrıca mesh'in gerçekten doku koordinatları içerip içermediğini kontrol etmek isteyeceğiz:

vertex struct'i gerekli attribute'larla doldu. Yineleme sonunda bunu vertices vektörüne ekliyoruz. Bu işlem mesh'in her vertex'i için tekrarlanır.


İndeksler

Assimp'in arayüzü, her mesh'i bir yüzler dizisi olarak tanımlar; her yüz bizim durumumuzda (yani aiProcess_Triangulate seçeneği nedeniyle) her zaman üçgen olan bir ilkeli temsil eder. Bir yüz, çizilmesi gereken vertex indekslerini hangi sırada içerir. Dolayısıyla tüm yüzleri yineleyip yüzün tüm indekslerini indices vektörüne saklarsak hazır oluruz:

Dış döngü bittikten sonra glDrawElements aracılığıyla mesh'ü çizmek için eksiksiz bir vertex ve indeks veri kümesine sahip oluruz. Tartışmayı tamamlamak için materyalü de işleyeceğiz.


Materyal

Node'lara benzer şekilde, bir mesh yalnızca bir materyal nesnesine indeks içerir. Bir mesh'in materyalini almak için scene'in mMaterials dizisini indekslemeliyiz. Mesh'in materyal indeksi, mMaterialIndex özelliğinde ayarlanmıştır:

Scene'in mMaterials dizisinden aiMaterial nesnesini alıyoruz. Ardından mesh'in diffuse ve/veya specular dokularını yüklemek istiyoruz. loadMaterialTextures yardımcı fonksiyonu dokuları materyalden alır, yükler ve başlatır; Texture struct'larından oluşan bir vektör döndürür.

loadMaterialTextures fonksiyonu, verilen doku türünün tüm doku konumları üzerinde yineleme yapar, dokunun dosya konumunu alır, ardından dokuyu yükler ve oluşturur, bilgileri bir Vertex struct'ında saklar. Şuna benzer görünür:

GetTextureCount fonksiyonu aracılığıyla materyalde saklanan doku miktarını kontrol ediyoruz. GetTexture fonksiyonu aracılığıyla dokunun dosya konumlarının her birini alıyoruz; bu sonucu bir aiString'e saklar. Ardından, bizim için bir doku yükleyen (stb_image.h ile) ve dokunun ID'sini döndüren TextureFromFile helper fonksiyonunu kullanıyoruz.

circle-info

Model dosyalarındaki doku dosyası yollarının modelin gerçek nesnesinin lokal yollarında olduğunu varsayıyoruz; yani modelin konumuyla aynı dizinde. Daha sonra (yani loadModel fonksiyonunda) aldığımız dizin dizesini doku konumu dizesiyle birleştirerek tam doku yolunu elde edebiliriz.

İnternette bulunan bazı modeller, dokularının yerleri için mutlak yollar kullanır ki bu her makinede çalışmaz. Bu durumda muhtemelen dosyayı manuel olarak düzenlemek ve dokular için yerel yollar kullanmak isteyeceksiniz (mümkünse).


Optimizasyon

Henüz tamamen bitmedi; yapmak istediğimiz büyük (ancak tamamen gerekli olmayan) bir optimizasyon var. Çoğu sahne, çeşitli mesh'lerde birkaç dokusunu yeniden kullanır. Birden fazla mesh üzerinde granit dokusu kullanan bir ev hayal edin. Bu doku zemine, tavanlara, merdivene, belki bir masaya ve belki de yakınlardaki küçük bir kuyuya da uygulanabilir. Dokuları yüklemek ucuz bir işlem değildir ve mevcut implementasyonumuzda tam olarak aynı doku daha önce birkaç kez yüklenmiş olsa bile her mesh için yeni bir doku yüklenir ve oluşturulur. Bu, model yükleme implementasyonunuzun darboğazı haline gelebilir.

Bu nedenle yüklenen tüm dokuları global olarak saklayıp model koduna küçük bir düzeltme yapıyoruz. Bir doku yüklemek istediğimizde önce daha önce yüklenip yüklenmediğini kontrol ederiz; yüklendiyse yükleme rutinini atlayarak işlem gücu tasarrufu sağlarız. Karşılaştırma yapabilmek için yollarını da saklamamiz gerekir:

Ardından yüklenen tüm dokuları model sınıf dosyasının başında private değişken olarak tanımlanan başka bir vektörde saklıyoruz:

loadMaterialTextures fonksiyonunda, doku yolunu mevcut doku yolunun textures_loaded vektöründeki tüm dokularla karşılaştırmak istiyoruz. Eşleşirse, bu doku yolunun daha önce yüklendiğini biliyoruz ve mesh'in dokusu olarak bulunan doku struct'ını kullanıp doku yükleme/oluşturma bölümünü atlıyoruz. Güncellenmiş fonksiyon şöyle gösterilir:

circle-exclamation

Model sınıfının tam kaynak kodunu buradanarrow-up-right bulabilirsiniz.


Artık Konteynerler Yok!

Haydi implementasyonumuzu gerçek bir sanatçı tarafından yaratılmış bir modeli içe aktararak deneyelim. Berk Gedik'in bu muhteşem Hayatta Kalma Gitar Sırt Çantasınıarrow-up-right yükleyeceğiz. Model, bir .obj dosyası olarak ve ilgili .mtl dosyasıyla birlikte dışa aktarılmıştır. Düzeltilmiş modeli buradanarrow-up-right indirebilirsiniz. Tüm dokuların ve model dosyalarının aynı dizinde bulunması gerektiğini unutmayın.

circle-info

Sırt çantasının değiştirilmiş versiyonu, yerel göreli doku yolları kullanır ve albedo ve metallic dokularını sırasıyla diffuse ve specular olarak yeniden adlandırdı.

Şimdi bir Model nesnesi tanımlayın ve model dosyasının konumunu iletin. Model otomatik yüklenecek ve hata yoksa render döngüsünde Draw fonksiyonuyla çizilecektir. Artık buffer tahsisi, attribute pointer veya draw command yok — yalnızca tek satır. Fragment shader yalnızca diffuse dokuyu çıkarıyorsa sonuç şuna benzer:

Model diffuse doku

Tam kaynak kodu buradanarrow-up-right bulabilirsiniz. Modeli yüklemeden önce stb_image.h'ye dokuları dikey olarak çevirmesini söylediğinizden emin olun; aksi takdirde dokular çok karmaşık görünecektir.

Işıklandırmaarrow-up-right bölümlerinden öğrendiğimiz gibi render denklemine nokta ışıklar ekleyerek ve specular map'lerle birlikte harika sonuçlar elde edebiliriz:

Model ışıklandırma

Bunu şimdiye kadar kullandığımız konteyner sahnelerinden çok daha etkileyici bulmak gerekir. Assimp ile internette bulduğunuz pek çok modeli yükleyebilirsiniz; ancak bazı modeller hâlâ sorunlu olabilir: çalışmayan doku yolları ya da Assimp'in desteklemediği formatlar bu sorunların başında gelir.


İlave Okuma


Orijinal kaynak: LearnOpenGL – Model-Loading/Modelarrow-up-right Türkçe çeviri: OpenGL Öğrenin projesi

Last updated