Bu sıcak yaz günlerinde boş durmamak amacıyla oluşturulmuş bir dokümanla karşınızdayım yeniden.Eveet neler yapıyorduk en son,hmm ilk iki makalede delphi ile oyun programlama konusunu anlatmaya çalışmışım,umarım faydalı olmuştur. Bu yazımda 3. boyuta el atacağım. Burada anlatılan algoritma evrensel olsa da yine de temel düzeyde Delphi bilmeniz lazım (yoğun talep olursa Visual C++ ya da VB versiyonunu da hazırlarım)

Bugün 3 boyutlu programlamaya giriş yapacağız. Öncelikle amacımızı belirlememiz lazım.

Bu yazıyı kimler okusun?: 3 boyutlu oyunlar yapmak isteyen herkese yönelik bir giriş dokümanı hazırlamaya çalıştım. Ayrıca bu dökümanda anlatılmak istenen biraz da Hammer'da iki harita yapıp Doom 3 ayarında oyun yapacağım diyen zihniyete karşı bilimsel ve mantıklı bir yaklaşım oluşturmaktır. Yani bir amaç da ilerde bir yerde tıkanmadan daha işin başında neyle uğraştığınızı görmenizdir.

Neden 3 boyut?: 99'un başlarında 2d oyunlar piyasadan yavaş yavaş çekilmeye başlarken yerlerini 3 boyutlu kardeşlerine bırakmaya başladılar. O dönem özellikle Half-Life, Q2, Q3 gibi oyunlar ard arda çıkarak bu geçişi hızlandırdı,günümüzde ise 3 boyut kullanmayan ticari oyuna rastlamak hemen hemen imkansız,2d'nin en başarılı olduğu alan olan platform oyunlarında bile 3. boyut etkisini hissettirmeye başladı(örn: Duke Nuke'm Manhattan Project). Bu yüzden geleceğe yönelik oyun tasarımları artık 3. boyutu standart olarak kabul etmek zorunda kalıyor.(Bu tabiki 2d tamamen işe yaramaz demek değildir,eğer tek başınıza oyun yapıyorsanız 2d'yi tercih etmek daha mantıklı olacaktır.)

Bilgi Düzeyi: Bu site dahil forumlarda 3 boyutlu programlama hakkında söylenen pek çok şey vardır,oldukça zor olduğu,yüksek matematik bilmeden yapılamayacağı,tek başına yapılamayacağı vs. vs... Bunların bir kısmı doğru bir kısmı da yanlıştır.Mesela 3 boyutlu programlama yapabilmek için yüksek matematiğe ihtiyaç yoktur ama iyi bir geometri bilgisi gerekir. Ayrıca trigonometri bilmeden burada anlatılanları anlamanız,ya da ayrıca directx veya openGL kasmanız da pek mümkün değildir.8. sınıfın altındaki kardeşlerimiz veya bugüne kadar "trigonometri neymiş lan" diyenler bu yüzden ya şimdi matematik çalışacaklar ya da başka limanlara açılacaklardır.malesef..

3d nasıl oluyor?: 3d programlamada çevremizdeki gibi 3 boyutlu bir uzay sözkonusudur, bu boşluğa 3 boyut koordinatları kullanılarak cisimler yerleştirilir,her nokta x,y ve z diye tabir edilen üç koordinata sahiptir. Genel anlamda x ve y cismin karşıdan görünüşünü z ise derinliğini temsil eder. şu halde düzlemde bir noktayı tanımlayabilmek için en az 3 değere ihtiyacımız var demektir.Bu mantıkla:

3 boyutlu çizim yapabilmemiz için en temel gereksinimimiz 3 değeri tutabilecek bir değişken yapısı yani bir type ya da c'cilerin deyimiyle struct'tur.

type point = record
x,y,z:Real;
end;

ekteki örnek projede bir küp ve bir üçgenoid (4 tane eşkenar üçgenden oluşan cisim,ismi ben uydurdum,300 sene oldu geometri görmeyeli,esas adını hatırlayamadım,cismi görünce tanırsınız nasıl olsa.) çizeceğiz.şimdi onların da type'ını tanımlayalım.

type cube=record
n:array[1..8] of point;
end;

type Ucgenoid=record
n:array[1..4] of point;
end;


gördüğünüz gibi bir küpün 8 köşesi olduğu için onu 8 pointli tanımladık,mantık oldukça basit.tabi üçgenoid(ulan gıcık oldum ya, neydi adı bu şeklin)in de 4 tane köşesi var.

Şimdi kolaydan zora gittiğimiz için gelelim bir kübün yaratılmasına,bunun için CreateCube diye bir fonksiyon hazırladım.Bu fonksiyon parametre olarak 4 değer alıyor,ilk üçü bu kübün merkez noktasının x,y,z koordinatları,4.sü de kübün bir kenarının uzunluğunu belirtiyor,yani kübü oluşturmak için herşey elimizde.evet işte CreateCube:

function TForm1.CreateCube(centerx,centery,centerz,side:real):Cube;
var t:Integer;
begin
with
result do begin
//x: eğer noktalar tekse,merkezin solunda kalıyorlar
//demektir,bu da xlerinin centerdan kenar'ın yarısı
//kadar eksik olacağını gösterir,çiftler için tam tersi //geçerlidir.Bu genel bir kural değildir,sırf kod biraz
//kısalsın diye benim getirdiğim bir düzenleme...

for
t:=1 to 8 do
if
odd(t) then
n[t].x :=centerx-side/2
else

n[t].x :=centerx+side/2;

//y:1'den 4'e kadar olan noktalar merkezin üstünde kalıyor
//o yüzden y'lerini artıralım
for t:=1 to 4 do
n[t].y :=centery+side/2;
//5..8 arası noktalar da merkezin altında
for
t:=5 to 8 do
n[t].y :=centery-side/2;

//z:Malesef kodu kısaltıcak bir algoritma kalmadı
//bu yüzden 8 noktanın da z koordinatlarını tek tek yazıyoruz.

n[1].z :=centerz+side/2; n[2].z :=centerz+side/2;
n[7].z :=centerz+side/2; n[8].z :=centerz+side/2;
n[3].z :=centerz-side/2; n[4].z :=centerz-side/2;
n[5].z :=centerz-side/2; n[6].z :=centerz-side/2;
end
;
end
;

 

Yukarıdaki resim küpün noktalarının yerleşimini göstermektedir,gördüğünüz gibi 1234 kübün tavanını 5678 tabanını oluşturmakta.
Bu fonksiyonda esas iş sadece merkezin koordinatlarını ve kenar boyunu bilerek tek tek köşelerin koordinatlarını oluşturmak.

Evet böylece küpümüz oluştu,"ee benden bu kadar" deyip sıvışmadan önce,halletmemiz gereken ufak bir problem kaldı,bu küpü ekrana nasıl çizdireceğiz? Şu an için küpün varlığından sadece bellek haberdar,oysa bunu görsel anlama taşıyabilmek için çizmemiz lazım. 3 boyutlu programlamanın esas can alıcı noktasına geldik,bu yüzden bayanlar baylar dikkatimizi bu yöne verelim,bu işi sağlayacak algoritma öyle olmalı ki, bize 3 koordinatla verilen noktaları 2 boyutlu ekranımıza gerekli perspektif açısıyla çizsin,yani kübe karşıdan baktığımızda 3 boyutlu olduğunu algılayalım.Hmm bunu nasıl yapacağız,cevap biraz can sıkıcı olsa da aslında birazcık kafa yorulduğunda oldukça basitleşen bir konu : Trigonometri ...
Şimdi basit bir örnek ele alalım, dikdörtgen karton bir levhaya baktığımızı düşünelim,Aşağıdaki resimler tamamen kendi çizimim olduğu için berbat görünmektedir kabul,ancak konu hakkında genel bir fikir verecektir.eee öyle umarım yani.. (her iki resimde de gözlemci ve levhaya kuşbakışı olarak bakıyoruz.)

1. resimde gözlemci levhaya 90 derece açıyla bakıyor ,böyle bir durumda cisim gerçek yüzey boyutuyla gözüne yansır,yani 10mtlik cismi 10mt görür,
2.resimde ise levha 60° sola kaymıştır. bu durumda 10 mtlik levha boyu artık hipotenüstür,görme alanına giren boyut da dik kenarlardan biridir, bu boyut("k" dersek kendisine) trigonometri yardımıyla hemen bulunur:
k=cos(30)*hipotenüs.
k=(kkök(3)/2)*10
k=5*kkök(3) tür,yani çizmemiz gereken uzunluk (aslında hala 10 metre olan levha boyu için) 5*kkök(3) metredir.
Aynı şekilde ilk durumu ele alırsak çizmemiz gereken uzaklık:
k=cos(0)*hip
k=1*10=10 mt. olmaktadır.
İşte trigonometrinin sürekli mevzu konusu olması da bu inanılmaz faydalı özelliğinden kaynaklanır.
Notlar:
kkök: karekök demektir.
cos(30)=kkök(3)/2'dir.
cos değerleri elimizdeki üçgeni kullanarak k/hip değerleri kullanılarak hesaplanır,
cos değerleri zaten bilindiği için ilişkinin tersi kullanılarak,hip bilindiğinde k'nın bulunmasını sağlar.

Peki bu olayı her kenar için tek tek hesaplayacak kodu nasıl hazırlayacağız:Önce bilgisayarda sinüs ve cosinüs değerleri radyan olarak hesaplandığı için radyanı açıya çevirecek bir düzeltmeye ihtiyacımız var,buna fixer adını verdim.Şöyle ekliyoruz:

const fixer=180/pi;

mesela sin(30) 'u hesaplatmak istediğimizde sin(30*fixer) kodunu yazıyoruz.
pekala bakış açımız ne olacak ? üç tip bakış açımız var dikey,yatay ve derinlik açısı,hesaplama coşmasın diye derinliği devre dışı bırakıyoruz şimdilik,şu halde bir dikey bir de yatay açımız var.dikey açıyı başınızı aşağı yukarı hareket ettirdiğinizde değişen açı olarak düşünebilirsiniz,yatay açı da kafayı sağa sola çevirdiğimizde değişen açı olsun.Tanımlıyoruz:

hAngle,vAngle:real;

Aşağıdaki iki fonksiyon, üç koordinatı verilen herhangi bir noktanın X ve Y noktası olarak görünümünü geri döndürmekte
yani 3 boyutlu bir dünyayı 2 boyutlu bir düzleme projekte edebilmekte. Aslında aşağıdaki iki temel fonksiyon bütün 3d'nin temeli sayılabilir. anlamıyorsanız kendinizi yormayın 1.si biraz daha açık olmakla birlikte alt tabanda yatan matematik oldukça karmaşık.
Temel olarak bilmeniz gereken bunların ne yaptığı,başta eklenen 200 değeri tamamen keyfi,görüntü form'un ortasında oluşsun diye ekledim.

function perX(x,y,z: real): integer;
begin
result:=200+round(x*cos(hAngle*fixer)+z*sin(hAngle*fixer));
end;

function perY(x,y,z: real): integer;
begin
result:=200+round((-y*cos(vAngle*fixer)+(-x*sin(hAngle*fixer)+z*cos(hAngle*fixer))*sin(vAngle*fixer)));
end;

Evet keyifle finale doğru yaklaşıyoruz,şimdi çevirici fonksiyonlarımız çalışıyor mu diye kontrol etmek ve projeyi tamamlamak için render etmeliyiz.Bu yüzden RenderCube'u oluşturdum:

procedure TForm1.RenderCube(mCube:Cube);
var i:Integer;
begin
with
mcube do
begin

canvas.Pen.Color :=clGreen; //yeşil çizelim!
//tavanı çizmeye 1'den başladık,2-4-3 ve 1'e dönüş sırasıyla gittik anlamdığınız
// kısımlar için en üstteki resim 1'e bakın.moveTo ve LineTo'yu
// açıklamama gerek yok sanırım.

canvas.MoveTo(xyztox(n[1].x,n[1].y,n[1].z),xyztoy(n[1].x,n[1].y,n[1].z));
canvas.LineTo(xyztox(n[2].x,n[2].y,n[2].z),xyztoy(n[2].x,n[2].y,n[2].z));
canvas.LineTo(xyztox(n[4].x,n[4].y,n[4].z),xyztoy(n[4].x,n[4].y,n[4].z));
canvas.LineTo(xyztox(n[3].x,n[3].y,n[3].z),xyztoy(n[3].x,n[3].y,n[3].z));
canvas.LineTo(xyztox(n[1].x,n[1].y,n[1].z),xyztoy(n[1].x,n[1].y,n[1].z));
//taban aynı mantıkla 5-6-7-8-5 şeklinde çiziliyor.

canvas.MoveTo(xyztox(n[5].x,n[5].y,n[5].z),xyztoy(n[5].x,n[5].y,n[5].z));
canvas.LineTo(xyztox(n[6].x,n[6].y,n[6].z),xyztoy(n[6].x,n[6].y,n[6].z));
canvas.LineTo(xyztox(n[8].x,n[8].y,n[8].z),xyztoy(n[8].x,n[8].y,n[8].z));
canvas.LineTo(xyztox(n[7].x,n[7].y,n[7].z),xyztoy(n[7].x,n[7].y,n[7].z));
canvas.LineTo(xyztox(n[5].x,n[5].y,n[5].z),xyztoy(n[5].x,n[5].y,n[5].z));

//yanyüzler 1-7 arası , 3-5 arası , 2-8 arası , 4-6 arası şekline
// (4 tane yan çizgi olduğu için)

canvas.MoveTo(xyztox(n[1].x,n[1].y,n[1].z),xyztoy(n[1].x,n[1].y,n[1].z));
canvas.LineTo(xyztox(n[7].x,n[7].y,n[7].z),xyztoy(n[7].x,n[7].y,n[7].z));

canvas.MoveTo(xyztox(n[3].x,n[3].y,n[3].z),xyztoy(n[3].x,n[3].y,n[3].z));
canvas.LineTo(xyztox(n[5].x,n[5].y,n[5].z),xyztoy(n[5].x,n[5].y,n[5].z));

canvas.MoveTo(xyztox(n[2].x,n[2].y,n[2].z),xyztoy(n[2].x,n[2].y,n[2].z));
canvas.LineTo(xyztox(n[8].x,n[8].y,n[8].z),xyztoy(n[8].x,n[8].y,n[8].z));

canvas.MoveTo(xyztox(n[4].x,n[4].y,n[4].z),xyztoy(n[4].x,n[4].y,n[4].z));
canvas.LineTo(xyztox(n[6].x,n[6].y,n[6].z),xyztoy(n[6].x,n[6].y,n[6].z));

canvas.Font.Color :=clred;
// en son köşelere adlarını da yazalım.
for
i:=1 to 8 do
canvas.TextOut(xyztox(n[i].x,n[i].y,n[i].z),xyztoy(n[i].x, n[i].y,n[i].z),'x'+inttostr(i));

end
;
end
;

Son olarak form'da bulunan iki adet scrollbar'a açıların değişimini sağlayan kodları ekleyelim:Scrollbarların min değeri 0 max değeri 360 olacak tam bir daire dönümünü tamamlasın diye.

procedure TForm1.ScrollBar1Change(Sender: TObject);
begin
vangle:=scrollbar1.Position ;
canvas.Brush.Color :=clWhite;
canvas.FillRect(rect(0,0,600,600));
renderCube(iste);
end;

procedure TForm1.ScrollBar2Change(Sender: TObject);
begin
hangle:=scrollbar2.Position ;
canvas.Brush.Color :=clWhite;
canvas.FillRect(rect(0,0,600,600));
renderCube(iste);
end;

Evet işlem tamam!,nasıl çalıştığını görmek için örnek projeyi çalıştırmanız gerekmekte. Neler yaptığımıza şöyle bir göz atarsak directX ya da OpenGL kullanmadan tamamen Windows GDI'ı kullanarak ve temel matematiksel algoritmalar yardımıyla 3d çizim yaptık. Bu girişle artık 3d programlamayı daha iyi anlayacağınızı ümit ediyorum,çünkü openGL ya da DirectX'de de mantık aynı.
Bu noktadan sonra isterseniz daha karmaşık cisimler çizdirebilirsiniz,biraz daha trigonometri ile küre veya silindir çizdirilebilir mesela,ya da StrechDIBits vs gibi API'ler kullanıp bu polygonları dokuyla kaplayamaya kasabilirsiniz.5-6 tane daha fonksiyon ekleyebilir,dll olarak derleyip CemilX ya da AhmetGL gibi bir isimle piyasaya sürebilirsiniz,size kalmış...
Bir dahaki yazıda görüşmek üzere..

Levent Baykan Bayar 08/2004