2009年4月22日 星期三

Scene Graph Traveler

這一篇,就當做給我自己的記錄吧。

Scene Graph Traveler是我自己取的物件名字,是一個獲准在Scene Graph的樹狀結構中逛大街的物件。之所以會設計這樣的物件,主要是要從一個最近冒出來的需求,來調整整個引擎在Scene Graph中的架構。

原本,我在引擎中有一個Frustum Culler物件,負責將Scene Graph中所有可被Frustum見到的節點蒐集起來,然後一一交給Renderer做繪製。Culler會以遞迴呼叫的方式,跑一遍Scene Graph的樹狀結構,檢查與節點的碰撞關係,如果節點已經被Culler剔除了,那麼節點的Sub-Tree就可以不必再計算。

Scene Graph的遞迴方式,就類似以下的Code

er_code CNode::OnGetVisibleSet(CCuller* culler)
{
    if (node in frustum)
    {
        culler->Insert(this);
     }
     else return ER_OK;

    if (m_ChildList.size()==0) return ER_OK;
    er_code er=ER_OK;
    ChildList::iterator iter=m_ChildList.begin();
    while (iter!=m_ChildList.end())
    {
        er=(*iter)->OnGetVisibleSet(culler);
        if (er) return er; 
        ++iter;
    }
    return ER_OK;
}

 

現在,需求來了。

我要用滑鼠指標點選場景裡面的一個物件,所以我將滑鼠的座標,轉換成場景空間中的一條射線。我叫它做Picker。

Picker搜尋物件的方式跟Culler很類似,也是需要跑一遍Scene Graph中的樹狀結構,檢查射線與節點的碰撞關係,如果射線與節點有碰撞,就繼續搜尋Sub-Tree,否則就可以不必再計算。

簡單的做法呢,就是Scene Graph中,再增加一個讓Picker遞迴的函式。不過這麼一來,我就讓Picker與Scene Graph綁在了一起。

所以這絕對不是好設計。

Culler與Picker在Scene Graph中的行為幾乎是相同的,所差別只在判斷是否要繼續搜尋Sub-Tree的方式。至於在樹狀結構中遞迴的方式則是完全相同。所以我將這行為抽離出來,變成了Scene Graph Traveler。

class CSceneTraveler
{
public:
    enum TravelResult
    {
        Travel_InterruptAll=0,
        Travel_Interrupt,
        Travel_SubTree,
    };
public:
    CSceneTraveler() {};
    virtual ~CSceneTraveler() {};
    virtual TravelResult TravelTo(CNode* ) =0;
};

這是一個抽象物件,需要在Scene Graph中逛大街的物件,都可以繼承它來取得在Scene Graph中遞迴的許可。所以,Culler以及Picker就從這個類別繼承出來,並且各自實作所需要的TravelTo函式。

而Scene Graph Node本身的函式,則是做了這樣的修改

CSceneTraveler::TravelResult CNode::VisitBy(CSceneTraveler* rpTraveler)
{
    if (!rpTraveler) return CSceneTraveler::Travel_InterruptAll;
    CSceneTraveler::TravelResult res=rpTraveler->TravelTo(this);
    if (res!=CSceneTraveler::Travel_SubTree) return res;  // don't go sub-tree
    if (m_ChildList.size()==0) return CSceneTraveler::Travel_Interrupt;
    ChildList::iterator iter=m_ChildList.begin();
    while (iter!=m_ChildList.end())
    {
        res=(*iter)->VisitBy(rpTraveler);
        if (res=CSceneTraveler::Travel_InterruptAll) return res;
        ++iter;
    }
    return res;
}

 

其實會這樣修改設計,還有一個原因。我的Scene Graph以及Culler是放在繪圖核心的模組裡,而Picker是在遊戲層的物件,為了保持物件之間關係的乾淨清爽,所以我並沒有直接用簡單的方法,把Picker綁在Scene Graph裡,而是將這個Traveler抽離了出來。

未來還會不會有其他的物件可以繼承這個相同的行為,我也不知道。

2009年4月5日 星期日

DirectX Shader 材質

最近拿起了3ds max,玩一玩裡頭的DirectX Shader 材質。發現它其實還挺好用的。

首先先將材質換成DirectX Shader

max_1

然後材質的參數就會換成這樣的介面

max_2

第一個,DirectX Shader的欄位,可以讓我們指定使用的Fx檔案,第二個Parameters的欄位,裡面所有的參數,都是從Fx檔案裡定義的。只要照著標準的規則寫,參數便能夠列在這上面,讓製作人員去調整。同時在做模型輸出的時候,也同樣可以取得這些設定的值。

至於這些個參數是怎麼定義的,可以打開default.fx來看。


// light direction (view space)
float3 lightDir : Direction < 
    string UIName = "Light Direction";
    string Object = "TargetLight";
    > = {-0.577, -0.577, 0.577};
// material reflectivity
float4 k_a  <
    string UIName = "Ambient";
    > = float4( 0.47f, 0.47f, 0.47f, 1.0f );    // ambient
float4 k_d  <
    string UIName = "Diffuse";
    > = float4( 0.47f, 0.47f, 0.47f, 1.0f );    // diffuse
float4 k_s  <
    string UIName = "Specular";
    > = float4( 1.0f, 1.0f, 1.0f, 1.0f );    // specular
int n<
    string UIName = "Specular Power";
    string UIType = "IntSpinner";
    float UIMin = 0.0f;
    float UIMax = 50.0f;   
    >  = 15;

這個程式的語法,稱做 DirectX Standard Annotations and  Semantics,角括號裡面的字串以及其它變數的宣告,在Effect Fx中是沒有作用的,但是,在max中,就定義了Parameters的參數介面。

除了在DirectX SDK中有文件說明,max的網站上也可以下載到相關的文件(不過版本很舊了)。

用DirectX Shader材質有兩個好處,一是這個Effect Fx的檔案我們可以直接在遊戲程式中使用。第二是,這個材質能夠所見即所得的顯示出來材質效果。

所以,我很認真的在考慮,也許就直接把Effect Fx檔案拿來當做引擎中的材質使用好了。

2009年3月19日 星期四

Intelligent Mistakes

Intelligent Mistakes: How to Incorporate Stupidity Into Your AI Code

by Mick West

===================================================

每天看Gamasutra,偶爾會看到一些好東西。

這個主題叫做「有智慧的錯誤」。

人都是會出錯的,所謂「人有失手,馬有亂蹄」,但是電腦AI不會出錯,只有計算夠不夠完整的問題。

但是這樣的遊戲,玩起來就死板板的,也喪失了一些趣味。

作者舉了他自己做的撞球遊戲當做例子,撞球遊戲的AI不難,就是簡單物理碰撞計算而已,對電腦來說,出桿可以非常非常的精準,但是「人」就不行了,要不就打歪,要不就力道不對,總之是不可能精準的像電腦一樣。

所以,這樣的遊戲玩起來,就是跟一個精準得要命的電腦對手在玩。不但每顆球都打得很「千」,連不小心放的「嗆斯」都沒有。

這就很無趣了。

AI is “too good”.

所以,要做一些精心設計的錯誤,讓玩家覺得他佔到便宜,好比說,偶爾故意放個「嗆斯」、偶爾「突槌」一下、偶爾不小心把母球打進洞....

這樣就有趣多了,人生本來就是充滿了不確定啊....

2009年3月12日 星期四

Remote Debug

遇過這樣的狀況嗎?

『我們的程式,在自己的電腦上執行,一切正常,但是交到別人手上執行,卻常常出問題,更不要說是交給老闆測試了,老闆的電腦特別奇怪,好好的程式交給老闆就一定會出事。』

這種時候,就是放61點天賦大絕的時候了。

這個大絕叫做『遠端除錯』,Remote Debug。

概念是這樣,我們在近端--也就是我們自己的電腦--用VC或是其他的編譯工具進行除錯,程式則是在遠端電腦(也就是老闆的電腦)上執行,Trace/Debug的做法,與我們在自己的電腦上除錯是相同的,所不同的是,我們控制的是老闆電腦上的程式。

用這種方式除錯,遠端電腦上是不需要安裝任何的編譯工具的,只需要兩種東西。

一個是程式的執行檔和相關的資料檔案,另外一個就是稱做Remote Debug Server或是Remote Debug Monitor的程式。這個Remote Debug Monitor是一個很小的程式,通常放在VC或是編譯工具的安裝光碟中,請拿出來安裝在遠端電腦上。

然後,在遠端電腦先將remote debug server跑起來,等待近端的連線。

接著我們在近端電腦打開程式專案,做一些設定--每一種編譯工具的設定方式都不同,甚至不同版本的VC++都有不同的設置方式--之後,開始debug run。這時候,遠端電腦上的執行檔就會被跑起來,而所有的debug output訊息,會顯示在近端的編譯工具裡。同時,也可以設定break point,去trace 遠端電腦上的程式流程。

這樣,我們就能夠很快的發現bug,解決bug,也就不會在老闆面前滅團了。

2009年2月6日 星期五

邪惡的巨集

巨集(Macro)是邪惡的。

它很好用,可以簡化我們的程式碼,節省很多Coding的時間。

但是它暗地裡在破壞我們程式碼的物件架構,在物件封裝中到處鑽孔。

所以,前兩天,我將程式碼裡頭的

#include "windowsx.h"

給拔掉。

這個標頭檔,定義了很多巨集,有些是簡化windows訊息程式碼,有些是簡化windows API。平常是很好用的啦,但是,好死不死的,我有個樹狀結構的物件,裡頭有幾個函式-- GetFirstChild, GetNextChild, ... 就跟windows API的巨集衝突到了。

好端端擺在物件中的GetFirstChild函式,被windows API的GetFirstChild巨集取代掉,所以編譯器瞬間開始混亂,看不懂我的程式碼在幹什麼,連錯誤回報也是亂報一通不知所云。

花了時間除錯,拔掉了這些個讓人偷懶的巨集。

所以這巨集真不是好東西。

裹著糖衣的毒藥。

Code Complete的作者在書裡也有說,巨集,能不用就不用,務必謹慎使用。

是啊,別偷懶了,該拔掉的就拔掉吧。

==================================================

其實今天一早,還遇見了另一個類似的問題。

我在數學函式庫裡,定義了一個靜態常數PI (就是圓周率啦),還特別放在一個Math物件中,用Math::PI這樣的語法來存取,避免這種常數定義衝突的狀況。

class Math
{
...
static const float PI;
};

不過,又來了,好死不死,Max SDK裡面也定義了一個PI,而且是直接#define,

#define PI  ((float)3.1415926535)

結果我怎麼辦? 我只能先引入自己數學函式庫的標頭檔,然後再引入Max SDK的標頭檔,順序絕對不能換,否則編譯器就會開始錯亂。

2009年1月23日 星期五

又多了個小外甥

今天才寫,有點晚了。

一月十五日,老妹生了第二胎,是個3240克的健康胖小子。

本來,還想著要他跟姐姐一樣過完年才出來,不過這小子的確跟姐姐一樣,也是大約37週就把媽媽的營養吃完,準備出來。

也好,兩人的生日都是正月十五,姐姐是農曆,弟弟是國曆。

看起來黑黑的,似乎又是個小黑皮。

這個胖小子,也是妹妹的夫家期盼了很久的男丁,上頭已經有三個姐姐了。

昨天老妹家裡在幫小娃娃取名字,剛好在MSN上撞見,我就跟著參考了意見。

「宗」: 不要,太菜市場了。

「宸」: 網路上查了字典,這個字好。辰是龍,屋頂下的龍,表示這個字是帝王的住所,也可以引申為帝王的意思。

「菘」: 看起來不錯,仔細一查,哇咧,原來這個植物就是「白菜」。算了,還是不要這個字吧。

「瑋」、「瑜」、「瑄」: 都不錯,都是「美玉」。不過就是有點像女生的名字,轉念一想,周瑜也用這個名字啊,還好啦。

最後選了「宸瑋」兩個字。很不錯吧,帝王的美玉。

小娃娃才出生一個星期,會不會對他期望太高了? 哈....

老妹照了幾張照片,不過都有點糊,還是小小張的看就好,放大看會更糊。

照片 001

照片 002

2009年1月13日 星期二

Finding Oriented Bounding Box

前一陣子發現計算OBB的演算法,研究了幾天,本以為大致都了解了,但深入去思考推導之後,卻才發現,原來我是錯得離譜,Totally Wrong!! (翻譯叫做 "一整個錯!!")

於是又從頭開始,一步一步從數學的角度來看。

觀念還是一樣,把美術模型的每一個頂點,都當做是取樣點,然後用Least Square Fit的方法,找出OBB需要的中心點以及三個軸向。

OBB的中心點取得,根據的是偏微分取極值的方法,三個軸向呢,則是用Principal Axis的概念,從Eigen vector來取得。

底下是兩個參考資料。

http://mathworld.wolfram.com/LeastSquaresFitting.html

http://www.geometrictools.com/Documentation/LeastSquaresFitting.pdf

這個推導,背後的數學有點小複雜,不過,大概都是些矩陣向量的運算概念。

最後呢,得到一個取得OBB的步驟。

1. 先建立一個可以解出Eigen value, Eigen vector的函式庫(或者去抄一個)

2. 把所有頂點座標加起來,找出平均值,就是OBB的中心。

3. 對於每一個頂點,計算與中心點在 x,y,z 三個方向的誤差值 X,Y,Z ,取得兩兩相乘的值,XX, XY, XZ, YY, YZ, ZZ,把每個頂點所算出來的這六個值,全部加起來平均。然後塞到一個3x3的矩陣內。

4. 把這個矩陣丟給Eigen System求解,拿回三個Eigen vector。

5. 用這三個Eigen vector得到的座標系統,轉換原來的頂點,找出在這個座標系統中的Box長寬高。