KARADENİZ TEKNİK ÜNİVERSİTESİ Bilgisayar Mühendisliği Bölümü Bilgisayar Grafikleri Laboratuarı PÜRÜZLÜ YÜZEY ÜRETİMİ 1. Giriş Cisimlerin yüzey görüntülerindeki parlaklık değişimi iki nedene dayanır. Birincisi yüzey dokusu (texture), ikincisi ise yüzeydeki pürüzlülüktür. Yüzey pürüzlülüğü, yüzey görüntüsü doku elemanı kabul edilerek izdüşüm tekniğiyle gerçek anlamda üretilemez. Çünkü doku elemanı olarak kullanılan resim dosyası sabittir. Yüzeyin pürüzlü görünümü ise o yüzeyi aydınlatan ışık kaynağının konumuna bağlıdır ve ışık kaynağı konum değiştirdikçe pürüzlü görünüm de değişmektedir. Yani yüzeyin herhangi bir noktasının çukur/tümsek görünmesi o noktanın ışık kaynağı tarafından aydınlatılıp/aydınlatılmadığına bağlıdır. Pürüzlü yüzey üretme yöntemi (“Bump Mapping”) ilk olarak 1978 yılında Jim Blinn tarafından kullanıldı. Bu yönteme göre klasik doku kaplamada kullanılan resim dosyasındaki renk değişimlerinden faydalanılarak o resim dosyasının kaplanacağı yüzeyin normali değiştirilir. Herhangi bir yüzeyin ışık kaynağının konumuna göre rengi belirlenirken normale bağlı hesaplama yapıldığından kullanılan doku sayesinde değişen normalle birlikte yüzey üzerinde tümsekler/çukurlar varmış gibi bir görüntü ortaya çıkar. Örneğin taşlardan örülmüş bir duvar dokusu kullanılsın. Bu doku düzlemsel bir yüzey üzerine bump mapping yöntemine göre kaplandığında gerçekte yüzey düzlemsel olduğu halde pürüzlü görünecektir. Bu deneyde bump mapping yöntemine göre pürüzlü yüzeyler üretilecektir. Programlama dili olarak Visual C++ 2003, grafik programlama API’si olarak da DirectX kullanılacaktır. Visual C++’da DirectX kodu yazabilmek için Microsoft’un sitesinden DirectX SDK indirilip kurulur. “C:\Program Files\Microsoft DirectX SDK” altındaki include ve lib klasörleri Visual C++’ın “C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK” altındaki include ve lib klasörlerine kopyalanır. DirectX tek başına pürüzlü yüzey üretmek için yeterli değildir. Çünkü DirectX’te herhangi bir poligon boyanırken ışık kaynağına bağlı olarak sadece köşe noktaları (vertex) için renk değeri hesaplanır. Poligonun içinde kalan noktaların rengi interpolasyonla bulunur. Dolayısıyla DirectX’te boyama poligon mertebesinde yapılır. Halbuki bump mapping yönteminde doku kaplamada kullanılacak resim dosyasındaki renk değişimi hesaplanırken piksel mertebesinde çalışılmaktadır. Ekran kartlarında 2 temel işlemci vardır: • Vertex İşlemci • Fragment (Pixel) İşlemci Vertex işlemci adından da anlaşılacağı üzere vertexler üzerinde, dünya koordinatlarından ekran koordinatlarına izdüşüm, backface culling, clipping, z-buffering ve doku koordinatı hesabı yapar. Fragment işlemci de teker teker piksellerin son renk değerini hesaplar. DirectX, OpenGL gibi API’ler ekran kartlarının sadece vertex işlemcisini programlayabilir. Ekran kartlarının her iki işlemsini programlayabilmek için geliştirilmiş dillere genel olarak “shading language” denir. Bu dillerin vertex işlemciyi programlayan kodlarına “vertex shader”; fragment işlmeciyi programlayan kodlarına da “fragment (pixel) shader” denir. DirectX’in shading dili HLSL (High Level Shading Language), OpengL’in ise GLSL’dir. HLSL’de yazılan kod D3DXCreateEffectFromFile komutuyla DirectX ortamında yüklenir ve çalıştırılır (benzeri GLSL ve OpenGL için yapılır). Dolayısıyla bump mapping için hem DirectX hem de HLSL kodu yazılacaktır. Öncelikle HLSL dili hakkında genel bilgiler verilecek ardından HLSL’de bump mapping programının nasıl yazılacağı anlatılacaktır. 2. HLSL Dili Hakkında Genel Bilgiler HLSL dili syntax yapısı olarak C++ diline oldukça benzemektedir. HLSL dilinde yazılan kodlara “effect” kodu denir ve “.fx” uzantılı dosyaya kaydedilir. İlk olarak Şekil-1‘deki gibi küpü mavi renge boyayacak “color.fx” adlı HLSL vertex shader kodu incelenecektir. Şekil-1: Küp Örneği color.fx uniform extern float4x4 gWVP; struct OutputVS { float4 posH : POSITION0; float4 color : COLOR0; }; OutputVS ColorVS(float3 posL : POSITION0, float4 c : COLOR0) { OutputVS outVS = (OutputVS)0; outVS.posH = mul(float4(posL, 1.0f), gWVP); outVS.color = c; return outVS; } technique ColorTech { pass P0 { vertexShader = compile vs_2_0 ColorVS(); } } uniform extern float4x4 gWVP; Burada uniform biz C++ kodunda değişiklik yapmadıkça değişkenin sabit olduğunu gösterir. extern ifadesi C++ kodunun bu değişkene dışardan erişebildiğini gösterir. float4x4 ifadesi de 4X4 boyutunda matris olduğunu gösterir. Burada tanımlanan “gWVP” adlı değişken world, view ve projection matrislerinin bir araya getirilmiş halidir. Yani dünya koordinatlarından ekran koordinatlarına izdüşüm yapar. Bu matris C++ kodunda hesaplandığından extern olarak tanımlandı. 2 struct OutputVS { float4 posH : POSITION0; float4 color : COLOR0; }; Burada tanımlanan structure ColorVS adlı vertex shaderın döndürdüğü değerdir. Structure’ın 2 değişkene sahip olduğu görülür: float4 posH : POSITION0; / / vertex koordinatlarını gösterir. float4 color : COLOR0; / / vertexin rengini gösterir. Burada :POSITION0 ve :COLOR0 şeklindeki ifadelere “semantic” denir. Dikkat edilirse her iki değişken de float4 türündendir. Yalnız biri koordinat diğeri ise renk bilgisi tutmaktadır. Ekran kartında koordinatlar ve renkler farklı registerlarda tutulurlar. Dolayısıyla :POSITION0 ve :COLOR0 semanticleri değişkenin hangi registerda tutulacağını gösterir. OutputVS ColorVS(float3 posL : POSITION0, float4 c : COLOR0){ . . . } vertex shaderın ismidir. Dikkat edilirse vertexlerin koordinatlarını ve rengini parametre olarak almakta ve OutputVS türünden değer döndürmektedir. Bu örnekte ColorVS adlı vertex shader küpe ait üçgenlerin içini boyayacaktır. Bunun için üçgenlerin koordinatlarına ve rengine ihtiyaç duymaktadır. ColorVS OutputVS outVS = (OutputVS)0; outVS.posH = mul(float4(posL, 1.0f), gWVP); outVS.color = c; return outVS; Yukarıdaki kodda öncelikle outVS adlı struct değişken OutputVS türünden tanımlanmaktadır. Daha sonra outVS nin posH ve color değişkenleri setlenmektedir. posH setlenirken posL değişkeniyle dünya koordinatlarına sahip üçgenler alınıp gWVP matrisiyle çarpılarak ekran koordinatlarına izdüşüm yapılmaktadır. technique ColorTech { pass P0 { vertexShader = compile vs_2_0 ColorVS(); } } technique ve pass ekran kartının özelliklerine bağlı olarak vertex shaderın türünü belirlemede kullanılır. Örneğin burada compile vs_2_0 ile ekran kartına vertex shader 2.0 desteği ile derlenmesi söylenmiştir. Ekran kartını yeteneklerin bağlı olarak 3.0 hatta 4.0 olabilir. Örneğin GeForce 7 serisi 3.0 ’ı, GeForce 8 serisi 4.0 ’ı desteklemektedir. Kodda küpün koordinatları ve rengi C++ kodundan alınmaktadır. Dolayısıyla HLSL’de yazılan vertex shader küpün koordinatları ve renginde bir değişiklik yapmamaktadır. Eğer küpün rengi vertex shaderda değiştirilmek istenirse örneğin rengi kırmızı olsun denirse kod şu şekilde değiştirebilir: OutputVS ColorVS(float3 posL : POSITION0) { OutputVS outVS; outVS.posH = mul(float4(posL, 1.0f), gWVP); outVS.color = float4(1, 0, 0, 1); //(R,G,B,A):Reg,Gren,Blue,Alpha return outVS; } 3 Programın Visual C++ ortamında yüklenmesi şu şekildedir: IDirect3DDevice9* ID3DXEffect* gd3dDevice; mFX; D3DXCreateEffectFromFile(gd3dDevice,"color.fx",0,0,D3DXSHADER_DEBUG,0,&mFX,&errors) D3DXCreateEffectFromFile komutunda gd3dDevice DirectX device’ını temsil eder. DirectX’te kod yazmak için öncelikle bir device tanımlamak şarttır. "color.fx" effect kodunu adını gösterir. mFX de effect’e pointerdır. Nasıl DirectX için device gerekiyorsa HLSL için de mFX tanımlamak şarttır. Yüklenen HLSL effectinin koşulması şu şekildedir: mhTech = mFX->GetTechniqueByName("ColorTech"); mhWVP = mFX->GetParameterByName(0, "gWVP"); gd3dDevice-Clear(0,0,D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,0xffeeeeee,1.0f, 0); gd3dDevice->BeginScene(); gd3dDevice->SetStreamSource(0, mVB, 0, sizeof(VertexCol)); gd3dDevice->SetIndices(mIB); gd3dDevice->SetVertexDeclaration(VertexCol::Decl); mFX->SetTechnique(mhTech); mFX->SetMatrix(mhWVP, &(mView*mProj)); unsigned int numPasses = 0; mFX->Begin(&numPasses, 0); for(unsigned int i = 0; i < numPasses; ++i) { mFX->BeginPass(i); gd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 8, 0, 12); mFX->EndPass(); } mFX->End(); gd3dDevice->EndScene(); DirectX’te çizim gd3dDevice->BeginScene() ile gd3dDevice->EndScene() komutları arasında; HLSL’de ise mFX->Begin() ile mFX->End() komutları arasında olur. for()döngüsü içinde mFX->BeginPass(i)ile effect dosyasındaki bütün pass lar işlenir. Bizim kodumuzda tek pass var. Dolayısıyla for() döngüsü bir kez dönecektir. mFX->BeginPass(i) ile mFX->EndPass() arasında DrawIndexedPrimitive komutuyla küpe ait üçgenlerin çizimi yapılmaktadır. mhTech = GetTechniqueByName("ColorTech") ile effect dosyasından technique ismi alınır. Daha sonra mFX->SetTechnique(mhTech) ile technique setlenir. Benzeri şekilde "gWVP" parametresi mhWVP = mFX->GetParameterByName(0, "gWVP") ile alınır ve mFX>SetMatrix(mhWVP, &(mView*mProj)) ile setlenir. Kodda geçen aşağıdaki 3 satır küp için gerekli index ve vertex buffer setlemeleri içindir. gd3dDevice->SetStreamSource(0, mVB, 0, sizeof(VertexCol)); gd3dDevice->SetIndices(mIB); gd3dDevice->SetVertexDeclaration(VertexCol::Decl); 4 Bu vertex shader örneği adından da anlaşılacağı gibi ekran kartının sadece vertex işlemcisini programlayan bir örnek oldu. HLSL’nin asıl avantajı pixel işlemciyi de programlayabilmesidir. Bunun için yazılan koda pixel shader denir. Genelde pixel shader tek başına yazılmaz, vertex shaderla birlikte yazılır. Vertex shaderın çıktısı olan koordinatlar pixel shadera girdi olur ve pixel shader da bu koordinatlar için son renk değerini hesaplar. Dolayısıyla pixel shaderın vertex shadera göre avantajını gösteren en iyi örnek boyama örneğidir. Burada yazılacak olan pixel shaderla bir çaydanlık (teapot) Phong yöntemine göre boyanacaktır. Yani her bir pixel için Phong’a göre ambient, diffuse ve specular bileşenler hesaplanacaktır. Pixel shaderın, vertex shadera göre avantajını göstermek açısından önce tek vertex shader sonra da hem vertex hem de pixel shader birlikte yazılacak (çünkü pixel shader tek başına yazılmıyordu) ve bu kodların çıktıları karşılaştırılacaktır. Programı yazmadan önce Phong boyama yönteminden bahsedelim. Phong’a göre herhangi bir pixelin rengi onun ambient, diffuse ve specular bileşenlerinin toplamıdır. Bu bileşenler hesaplanırken bakış noktasının, ışık kaynağının konumu ve boyanacak cismin yüzey normali kullanılır. Ambient bileşen bütün pixeller için başlangıçta belirlenmiş default bir renktir. Genelde beyazın tonları seçilir. Işık kaynağı tarafından direkt aydınlatılmayan nesneler siyah görünmezler çünkü dolaylı olarak yani başka nesnelerden yansıyan ışınlar sayesinde kısmen de olsa aydınlatılırlar. Bu özelliği boyamada hesaba katmak için ambient bileşen kullanılır. Diffuse bileşen hesaplanırken ışık kaynağının konumu ve yüzey normali gereklidir. Diffuse bileşen ışık kaynağına olan vektör ile yüzey normali arasındaki açıdır. Her iki vektör de birim vektör olduğu durum için açı, bu vektörlerin skaler çarpımıyla bulunur. HLSL’de skaler çarpım θ=dot(L,N) komutuyla yapılır. Şekil-2’de diffuse bileşen gösterilmiştir: Şekil-2: Diffuse Bileşen Specular bileşen hesaplanırken bakış noktasının, ışık kaynağının konumu ve yüzey normali gereklidir. Specular bileşen ışık kaynağına olan L vektörünün yüzey normali N ’den aynasal yansıması R vektörü ile bakış noktasına olan C vektörü arasındaki açıdır. Yani β=dot(R,C) ile hesaplanır. Burada R vektörü R = L - (2L*N)N ile hesaplanır. Specular bileşen Şekil-3’te gösterilmiştir. Şekil-3: Specular Bileşen 5 Phong boyama yapan vertex shader aşağıdaki gibidir: uniform extern float4x4 gWorld; uniform extern float4x4 gWorldInverseTranspose; uniform extern float4x4 gWVP; uniform uniform uniform uniform uniform uniform uniform uniform uniform extern extern extern extern extern extern extern extern extern float4 float4 float4 float4 float4 float4 float float3 float3 gAmbientMtrl; gAmbientLight; gDiffuseMtrl; gDiffuseLight; gSpecularMtrl; gSpecularLight; gSpecularPower; gLightVecW; gEyePosW; struct OutputVS { float4 posH : POSITION0; float4 color : COLOR0; }; OutputVS AmbientDiffuseSpecVS(float3 posL : POSITION0, float3 normalL : NORMAL0) { OutputVS outVS = (OutputVS)0; float3 normalW = mul(float4(normalL, 0.0f), gWorldInverseTranspose).xyz; normalW = normalize(normalW); float3 posW = mul(float4(posL, 1.0f), gWorld).xyz; float3 toEye = normalize(gEyePosW - posW); float3 r = reflect(-gLightVecW, normalW); float t = pow(max(dot(r, toEye), 0.0f), gSpecularPower); float s = max(dot(gLightVecW, normalW), 0.0f); float3 spec = t*(gSpecularMtrl*gSpecularLight).rgb; float3 diffuse = s*(gDiffuseMtrl*gDiffuseLight).rgb; float3 ambient = gAmbientMtrl*gAmbientLight; outVS.color.rgb = ambient + diffuse + spec; outVS.color.a = gDiffuseMtrl.a; outVS.posH = mul(float4(posL, 1.0f), gWVP); return outVS; } technique AmbientDiffuseSpecTech { pass P0 { vertexShader = compile vs_2_0 AmbientDiffuseSpecVS(); } } 6 Programın başında tanımlanan ilk 3 değişken koordinat sistemleri asındaki dönüşüm işlemini yapmaktadır. Sonra gelen değişkenlerin değerleri şöyledir: mLightVecW mDiffuseMtrl mDiffuseLight mAmbientMtrl mAmbientLight mSpecularMtrl mSpecularLight mSpecularPower = = = = = = = = AmbientDiffuseSpecVS D3DXVECTOR3(0.0, 0.0f, -1.0f); D3DXCOLOR(0.0f, 0.0f, 1.0f, 1.0f); D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); D3DXCOLOR(0.0f, 0.0f, 1.0f, 1.0f); D3DXCOLOR(0.4f, 0.4f, 0.4f, 1.0f); D3DXCOLOR(0.8f, 0.8f, 0.8f, 1.0f); D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); 8.0f; vertex shaderınını başında gerekli koordinat dönüşümleri yapıldıktan sonra: float3 toEye = normalize(gEyePosW - posW) ile bakış noktasına olan vektör hesaplanır float3 r = reflect(-gLightVecW, normalW) ile normalden yansıyan vektör hesaplanır float t = pow(max(dot(r, toEye), 0.0f), gSpecularPower) ile specular bileşen hesaplanır. Burada gSpecularPower specular highlightın yarıçapını belirler. Küçük gSpecularPower değerleri için yarıçap büyük; büyük gSpecularPower değerleri için de küçüktür. float s = max(dot(gLightVecW, normalW), 0.0f) ile diffuse bileşen hesaplanır. float3 spec = t*(gSpecularMtrl*gSpecularLight).rgb; float3 diffuse = s*(gDiffuseMtrl*gDiffuseLight).rgb; float3 ambient = gAmbientMtrl*gAmbientLight; Yukarıdaki 3 satır ile son ambient, diffuse ve specular renk değerleri hesaplanır ve outVS.color.rgb = ambient + diffuse + spec; outVS.color.a = gDiffuseMtrl.a; ile bu değerler toplanıp, alpha (parlaklık) değeri de eklenerek çaydanlığın son renk bulunur. Programın ekran görüntüsü aşağıda verilmiştir. Dikkat edilirse henüz hala vertexler üzerinde işlem yapıldığından görüntüdeki vertexler seçilebilmektedir. Bu programın pixel shader versiyonununda pixel mertebesinde hesap yapılacağından çok daha yumuşak (smooth) bir görüntü elde edilecektir. Yani vertexler görünmeyecektir. 7 Bu örenğin pixel shader versiyonunda da benzeri bir kod yazılacaktır. Daha önce de bahsedildiği gibi pixel shader kodu tek başına yazılmayacak, vertex shaderla birlikte yazılacaktır. Buna vertex shader kodu koordinatlarla ilgili işlemleri yapacak; pixel shader da renk hesabı yapacaktır. Dolayısıyla ambient, diffuse ve specular bileşenler pixel shaderda hesaplanıp son renk değeri bulunacaktır. Pixel shader ve ekran görüntüsü aşağıdaki gibidir. struct OutputVS { float4 posH : POSITION0; float3 normalW : TEXCOORD0; float3 posW : TEXCOORD1; }; OutputVS AmbientDiffuseSpecVS(float3 posL : POSITION0, float3 normalL : NORMAL0) { OutputVS outVS = (OutputVS)0; outVS.normalW = mul(float4(normalL, 0.0f), gWorldInverseTranspose).xyz; outVS.normalW = normalize(outVS.normalW); outVS.posW = mul(float4(posL, 1.0f), gWorld).xyz; outVS.posH = mul(float4(posL, 1.0f), gWVP); return outVS; } float4 AmbientDiffuseSpecPS(float3 posW : TEXCOORD1, float3 normalW : TEXCOORD0) : COLOR { BURADA ANBIENT, DIFFUSE, SPECULAR, RENK HESAPLANIR VE COLOR OLARAK DÖNDÜRÜLÜR } technique AmbientDiffuseSpecTech { pass P0 { vertexShader = compile vs_2_0 AmbientDiffuseSpecVS(); pixelShader = compile ps_2_0 AmbientDiffuseSpecPS(); } } 8 3. Bump Mapping Bump mapping yöntemi daha önce de bahsettiğimiz gibi doku olarak kullanılan resim dosyasındaki renk değişimlerine bağlı olarak bu dokuyu üzerine kaplayacağımız yüzeyin normalini pixel pixel değiştirerek tümsekler/çukurlar varmış izlenimi verir. Çünkü yüzey normali değiştiği için Phong yöntemiyle hesaplanan diffuse ve specular bileşen de değişecektir. Burada iki türlü doku söz konusudur. Birincisi doku kaplamada kullanılacak resim dosyası; ikincisi de bu resim dosyasından elde edilecek normal map dosyası. Her iki dosya da resim dosyası (örneğin .bmp) formatındadır. Yalnız birincisi kaplanacak doku ikincisi ise yüzey normalini değiştirecek dokudur. Şekil-4’te örnek bir doku ve bu dokudan elde dilmiş normal map dokusu verilmiştir. Şekil-4: Solda doku, sağda da bu dokudan elde edilen normal map Normal map dokusuna dikkat edilirse R,G,B (Red, Gren, Blue) renklerinden oluştuğu görülür. Yatayda (x-ekseni) değişim varsa Red, düşeyde (y-ekseni) değişim varsa Gren, değişim yoksa da Blue renge boyanmıştır. Herhangi bir h(xj,yi) pikseli için değişim olup olmadığı aşağıdaki ifadelerle hesaplanır: δh h( x j + Δx, y i ) − h( x j − Δx, yi ) = δx 2Δx δh h( x j , y i + Δx) − h( x j , y i − Δx) = 2Δy δy 9 Bump Mapping pixel shader kodu aşağıda verilmiştir: float4 NormalMapPS(float3 toEyeT : TEXCOORD0, float3 lightDirT : TEXCOORD1, float2 tex0 : TEXCOORD2) : COLOR { toEyeT = normalize(toEyeT); lightDirT = normalize(lightDirT); float3 lightVecT = -lightDirT; float3 normalT = tex2D(NormalMapS, tex0); normalT = 2.0f*normalT - 1.0f; normalT = normalize(normalT); float3 r = reflect(-lightVecT, normalT); float t = pow(max(dot(r, toEyeT), 0.0f), gMtrl.specPower); float s = max(dot(lightVecT, normalT), 0.0f); if(s <= 0.0f) t = 0.0f; float3 spec = t*(gMtrl.spec*gLight.spec).rgb; float3 diffuse = s*(gMtrl.diffuse*gLight.diffuse).rgb; float3 ambient = gMtrl.ambient*gLight.ambient; float4 texColor = tex2D(TexS, tex0); float3 color = (ambient + diffuse)*texColor.rgb + spec; return float4(color, gMtrl.diffuse.a*texColor.a); } float3 normalT = tex2D(NormalMapS, tex0) ile normal map dokusundan normal alınmaktadır. Dokudan alınan normal değerleri [0,1] arasına değişmektedir. [-1,1] aralığına ötelemek için normalT = 2.0f*normalT - 1.0f yapılmıştır. Elde edilen normal değeri ile diffuse ve specular bileşenler hesaplanmıştır. Şekil-5’te bump mapping ile üretilmiş bir görüntü verilmiştir: 10 Şekil-5: Bump mapping ile üretilmiş bir görüntü 4. Deneye Hazırlık • Örnek programları http://ceng.ktu.edu.tr/~cakir/graflab.html adresinden indirip çalıştırınız. • Programlar üzerinde değişiklikler yaparak sonuçlarını gözlemleyiniz. Örneğin specular bileşen olarak Jim Bliin’in farklı bir yaklaşımı vardır. Burada R yansıma vektörü yerine H= • N +C half vektörü kullanılır. Vertex/pixel shader kodlarını buna değiştiriniz. 2 Bump Mapping yönteminin Environment bump mapping, Reflection Bump Mapping gibi türevleri vardır. Bu yöntemler hakkında araştırma yapınız. 11