Home > 圖像編程 | 遊戲編程 > 完成 Win32上的OpenGL/Cg

完成 Win32上的OpenGL/Cg

星期六日都坐在家中做 OpenGL/Cg 的移植,比想像中遇到更多問題,但終於在三月最後一個晚上完成。這裡做一個簡單的回顧,希望大家遇到類似的問題時可以減少脫髮。

編譯期選擇圖像 API

之前曾經寫過一篇《跨平台的圖像渲染引擎》,提及使用 abstract factory pattern 或 conditional compilation (macro) 來為圖像 API 抽像化。後者的優點是效能較高 (不需使用 virtual function call),但缺點是代碼難看。為了改善後者的缺點,我想到了使用 inheritance 去避免在 class 的定義裡寫 macros。舉一個例子就會明白。

首先,利用 #include 後可以是一個 macro 的功能 (Visual C++ 裡只可以是一個 macro identifier,而 gcc 可以 expand macro),把平台相關的 header 和 implementation 檔放在同一個地方:

// GraphicsConfig.h
#ifdef MIL_GRAPHICS_D3D9
#	include "dx9/d3d9.h"
#	include "dx9/d3dx9.h"
#	define MIL_GRAPHICS_DEVICE_H			"D3d9/D3d9Device.h"
#	define MIL_GRAPHICS_DEVICE_INC		"D3d9/D3d9Device.inc"
#	define MIL_GRAPHICS_TEXTURE_H		"D3d9/D3d9Texture.h"
#	define MIL_GRAPHICS_TEXTURE_INC		"D3d9/D3d9Texture.inc"
// ...
#endif // MIL_GRAPHICS_D3D9

#ifdef MIL_GRAPHICS_GL
#	include "GL/gl.h"
#	include "Cg/cg.h"
#	include "Cg/cgGL.h"
#	define MIL_GRAPHICS_DEVICE_H			"Gl/GlDevice.h"
#	define MIL_GRAPHICS_DEVICE_INC		"Gl/GlDevice.inc"
#	define MIL_GRAPHICS_TEXTURE_H		"Gl/GlTexture.h"
#	define MIL_GRAPHICS_TEXTURE_INC		"Gl/GlTexture.inc"
// ...
#endif // MIL_GRAPHICS_GL

之後,每個類別都建立一個平台無關的基底類別(我以 Generic prefix 命名) 。這些類別包含不同平台都共通的特性。

#include "GraphicsConfig.h"

class MIL_API GenericDevice
{
protected:
	GenericDevice();
	~GenericDevice();

public:
	void SetWorld(const Math::Matrix44& world);
	void SetView(const Math::Matrix44& view);
	void SetProjection(const Math::Matrix44& projection);
	void SetMaterial(Material* material);

protected:
	Math::Matrix44 mWorld;
	Math::Matrix44 mView;
	Math::Matrix44 mProjection;
	Material *mMaterial;
};

#include MIL_GRAPHICS_DEVICE_H

留意最後的一行包含了平台的 header,而該 header 裡定義了個別平台的類別。但每個平台的類別名稱都是一樣的,例如 Direct3D9 裡的 Device 和 OpenGL 的 Device 類別都命名做 Device,但編譯時只會選擇一個版本。

// D3d9Device.h
class MIL_API Device : public GenericDevice
{
private:
	Device();
	~Device();

public:
	bool IsOpen() const;
	void Open(int window);
	void Close();

	void Clear(const Math::Color& color);
	void BeginScene();
	void EndScene();
	void Present();

	void DrawGeometry(Geometry* geometry);
	void DrawLine(const Math::Vector3& p1, const Math::Vector3& p2, const Math::Color& color);

	IDirect3D9 *GetDirect3D() const;
	IDirect3DDevice9 *GetDirect3DDevice() const;

	static Device& Instance();

private:
	IDirect3D9 *mDirect3D;
	IDirect3DDevice9 *mDirect3DDevice;
};

Implementation (.cpp) 檔案的做法也是一樣,Generic 的 .cpp 檔包含平台的 .inc 檔。

此外,Generic 基底類別的 Constructor 是設定為 protected 的,換句話說,使用者只能使用個別平台的類別和其基底類別的 public 介面。

這個解決方案的代碼比使用 #ifdef 方式寫的好看,也更容易加入新的平台。當要加入平台時,只需更改一個檔案裡 (GraphicsConfig.h) 設定和建立平台的檔案,而不需要為現有的每個類別加 macro。

比較靜態方式 binding 和 abstract factory pattern,除了 static/dynamic 的驅別外,必須注意靜態 binding 中每個平台的介面是完全獨立的,而 abstract factory pattern 有一個共用的基底類介面。例如,如果我更改 Direct3D9 版本的 Device 中的某個函式的簽名,這個程序庫是可以如常編譯出 Direct3D9 和 OpenGL 版本,但是使用這個函式的程式就不能編譯出兩個版本。使用 abstract factory pattern 不會有這個問題,只要修改基底類別的介面,所有繼承的類別也要同時修改才能通過編譯。由於編譯函式庫時不會出錯,使用較靜態方式 binding 在撰寫和修改介面必須要格外小心。在我的項目中,有一個好處是 Swig 的 interface 檔可以幫我檢查兩個版本是否合符 script 的要求。

Cg Toolkit

Nvidia 的 Cg Toolkit 是一套能編譯多種格式的 shader 源代碼到多個目的平台的工具和函式庫。它還提供 Win32 (D3D8/D3D9, OpenGL),Linux (OpenGL) 和 OSX (OpenGL) 的二進位版本,甚至 PS3 也支持。我計劃在 OpenGL (多個平台) 中使用 Cg Toolkit。

Cg Runtime 的功能包括編譯 Cg 源代碼和 CgFX 檔案,和把它的結果設定至 Direct3D 或 OpenGL。CgFX 其本上和 HLSL Effect (.fx) 的格式一樣,只是 CgFX 可支持跨平台的 Profile,也可以設置 OpenGL 的 Render states。我直接拿了 Cg Composer 的 Blinn.cgfx 和 blinn_bump_reflect.cgfx 用來測試。

Cg Toolkit 的文檔相當精簡,User Guide 只簡單介紹一些概念及一些 API 的呼叫次序等,而每個函式的 API Reference 只有一句描述,大部份例子寫著 “to be written”。

初始化 Cg Runtime、載入及編譯 CgFX 檔案都是很簡單的。惡夢就在 vertex attribute binding 開始……

Vertex Binding 的慘痛經驗

要做 vertex attribute binding 時,便遇到第一個問題。Cg Runtime 只提供了 cgGLEnableClientState() 和 cgGLSetParameterPointer(),即是說,必須每次 Draw-call 都要從 client (main memory) 把 vertex data 複製至 server (video memory),亦即是說在Cg的 API 上不支持 Vertex Buffer Object (VBO)。

先不計較效率吧,把功能先實現起來再說。竟然發現利用 cgGetEffectParameterBySemantic() 拿不到 varing parameter!! (例如 POSITION)。

「不是吧。」心裡想。那麼找找 Cg Toolkit 裡的 example 看看它怎樣做。竟然 (第二次竟然) 沒有用過 cgGLSetParameterPointer() 這個 API。它是用 glVertexPointer() 和 glNormalPointer() 去設定,而不是用 semantics。但是用這種 API 不能設定例如 binormal、tangent 等 vertex attributes,OpenGL 有一個 generic 的 glVertexAttribPointerARB() extension,但我不知道如何把 Cg 的 parameter 轉為它需要的 location 參數 (可能是 Cg 的 Resource Index 吧? Reference 沒有提示)。

最後,還是回去使用 cgGLSetParameterPointer(),經過不斷的 trial-and-error 之後,得出以下的代碼:

static char *semantics[] = {
	"POSITION",
	"NORMAL",
	"TANGENT0",
	"BINORMAL0",
	"COLOR",
	"TEXCOORD0",
	"TEXCOORD1",
	"TEXCOORD2",
	"TEXCOORD3",
	"TEXCOORD4",
	"TEXCOORD5",
	"TEXCOORD6",
	"TEXCOORD7",
};

CGpass pass = cgGetFirstPass(mMaterial->mTechnique);
while (pass) {
	cgSetPassState(pass);

	CGstateassignment vertexProgramStateAssignment = cgGetNamedStateAssignment(pass, "VertexProgram");
	MIL_ASSERT(vertexProgramStateAssignment);

	CGprogram vertexProgram = cgGetProgramStateAssignmentValue(vertexProgramStateAssignment);
	MIL_ASSERT(vertexProgram);

	for (int i = 0; i < geometry->mVertexDeclaration.elementCount; i++) {
		const char *semantic = semantics[geometry->mVertexDeclaration.elements[i].usage];
		for (CGparameter param = cgGetFirstLeafParameter(vertexProgram, CG_PROGRAM); param; param = cgGetNextLeafParameter(param))
			if (strcmp(cgGetParameterSemantic(param), semantic) == 0) {
				cgGLEnableClientState(param);
				cgGLSetParameterPointer(
					param, 
					geometry->mVertexDeclaration.elements[i].fsize, 
					geometry->mVertexDeclaration.elements[i].type, 
					geometry->mVertexDeclaration.stride,
					((char *)geometry->mVertexBuffer) + geometry->mVertexDeclaration.elements[i].offset);
				break;
			}
	}

	glDrawElements(geometry->mPrimitiveType, geometry->mPrimitiveCount, geometry->mIndexType, geometry->mIndexBuffer);

	cgResetPassState(pass);
	pass = cgGetNextPass(pass);
}

由於不能使用 cgGetEffectParameterBySemantic(),而必須從每個 pass 裡的 vertex program 找一個 parameter 的 semantic (沒有 cgGetParameterBySemantic()) 合符 vertex declaration 的 usage。簡單說明自設的 mVertexDeclaration,它和 Direct3D9 的 Vertex Declaration 相似,記錄了一個 vertex buffer 的每個 vertex attribute 的意義及格式,和 Direct3D 不同的地方是它暫時不能輸入 multiple stream。

想要快一點? 對每個 effect 的每個 pass 記錄 semantic -> parameter 的 mapping 吧。

Texture Binding 的慘痛經歷

我拿到的 Blinn.cgfx 有以下的 texture 部份:

texture ColorTexture : DIFFUSE  <
    string ResourceName = "default_color.dds";
    string UIName =  "Diffuse Texture";
    string ResourceType = "2D";
>;

sampler2D ColorSampler = sampler_state {
    Texture = ;
    MinFilter = LinearMipMapLinear;
    MagFilter = Linear;
    WrapS = Repeat;
    WrapT = Repeat;
};  

希望和 Direct3D 的 Effect Framework 一樣,就會寫出以下代碼:

cgGLSetTextureParameter(cgGetEffectParameterBySemantic("DIFFUSE"), texture);
// Cg API Refernece
// cgGLSetTextureParameter - sets the value of a texture parameter 
// Parameters
// param: The texture parameter that will be set. 
// texobj: An OpenGL texture object name to which the parameter will be set. 

Error: Invalid parameter

看 API reference 基本是無意義的。幾經查找,發現不能設定 Texture parameter,必須設定 Sampler parameter。就是說 cgGLSetTextureParameter() 的第一個參數是設定 sampler 的 parameter。天啊,那為甚麼叫SetTextureParameter!!

我不想把 API 用 sampler parameter 去設定 texture,這和一般的理解不同。於是又再閱讀那百多個 API 的內容,看看那一個有這樣的功能。

沒有這樣的功能。

最後,只好用一大堆 API 堆砌出這個功能來:

for (CGparameter param = cgGetFirstEffectParameter(mEffect); param; param = cgGetNextParameter(param)) 
	if (cgGetParameterClass(param) == CG_PARAMETERCLASS_SAMPLER) 
		if (CGstateassignment sa = cgGetNamedSamplerStateAssignment(param, "Texture")) 
			if (cgGetTextureStateAssignmentValue(sa) == (CGparameter)parameter) {
				cgGLSetTextureParameter(param, texture);
				cgSetSamplerState(param);
			}
		}
	}
}

想快一點? 記著每個 effect 的 texture -> samplers mapping 吧。

影像函式庫 SOIL

在 Direct3D 中有 D3DX 去讀取影像檔案至 texture。OpenGL 本身沒有這樣的功能,於是便在網上尋寶,找到了一個 OpenGL 專用又 light-weight 的函式庫叫 SOIL。它能讀取 png, jpg, dds, bmp, tga,並寫入幾個格式。它又不需要 external dependency (如 libpng, libjpeg 等),整個函式庫的 source code 只有 230KB,而且還是 public domain。嵌入它是這幾天最輕鬆的工作。

讀取 texture:

GLuint texture = SOIL_load_OGL_texture(filename, SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, SOIL_FLAG_DDS_LOAD_DIRECT | SOIL_FLAG_MIPMAPS);

如果沒有 mip-map 它可自動替你生成。還有很多選項功能。

讀取 cube map:

GLUint cubeTexture = SOIL_load_OGL_single_cubemap(filename, SOIL_DDS_CUBEMAP_FACE_ORDER, SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, SOIL_FLAG_DDS_LOAD_DIRECT | SOIL_FLAG_MIPMAPS)

你還可以給它 6 張 2D texture 去組成 cube map。它也支持從 memory 去 decode texture。

結語

有時候在想,編程是否等於花大量精力去解決一些低階的技術性問題。明天希望在 Linux 下編譯 OpenGL 版本,那麼四月就可以開始做新的東西了。

Comments:0

Comment Form
Remember personal info

Trackbacks:0

Trackback URL for this entry
http://miloyip.seezone.net/wp-trackback.php?p=46
Listed below are links to weblogs that reference
完成 Win32上的OpenGL/Cg from Milo的遊戲開發

Home > 圖像編程 | 遊戲編程 > 完成 Win32上的OpenGL/Cg

Search
Feeds
Meta

Return to page top