第十一章 場景管理 Part4(11.5 To 11.7)
5 人贊了文章
5.光源
引擎中一共介紹4種光源,包括天光、方向光、點光源、聚光燈。它們的關係如下:
圖11.22 光源繼承圖
class VSGRAPHIC_API VSCuller : public VSMemObject{ FORCEINLINE unsigned int GetLightNum()const; FORCEINLINE VSLight * GetLight(unsigned int i)const; FORCEINLINE void ClearLight(); void InsertLight(VSLight * pLight); bool HasLight(VSLight * pLight); VSArray<VSLight *> m_LightSet;}
VSCuller裡面的m_LightSet存儲可見光源信息,裡面函數根據名字大家就知道含義,實現的代碼就不貼出來了,很簡單。
class VSGRAPHIC_API VSLight : public VSNodeComponent{ enum //LightType { LT_POINT, LT_SPOT, LT_DIRECTION, LT_SKY, LT_MAX }; public: //光源層級 virtual void UpdateAll(double dAppTime); //返回LightType virtual unsigned int GetLightType()const = 0; //這個光源是否影響這個Geometry virtual bool IsRelative(VSGeometry * pGeometry); //對光源裁剪,光源是否可見 virtual bool Cullby(VSCuller & Culler); //得到光源的光照範圍 virtual void GetLightRange() = 0; //更新光源位置信息 virtual void UpdateTransform(double dAppTime);};
VSLight是純虛基類,大部分函數都要子類去實現。基類的IsRelative只是簡單判斷了一下Mesh是否接受光照,子類可以繼承實現不同光源對Mesh是否有影響。同樣下面的Cullby也是簡單添加進Culler的光照列表,子類要通過判斷光源的Bounds是否可見,才決定是否加入Culler;和其他節點一樣更新之後要把m_bEnable設置成false。每個光源還是都要重寫GetLightRange計算新的照射範圍,一但Transform信息改變那麼就要重新計算GetLightRange。
bool VSLight::IsRelative(VSGeometry * pGeometry){ if(pGeometry->GetMeshNode()->m_bLighted) { return true; } else { return false; } return true;}bool VSLight::Cullby(VSCuller & Culler){ m_bEnable = true; Culler.InsertLight(this); return true;}void VSLight::UpdateAll(double dAppTime){ VSNode::UpdateAll(dAppTime); m_bEnable = false;}void VSLight::UpdateTransform(double dAppTime){ if (m_bIsChanged) { GetLightRange(); } VSNodeComponent::UpdateTransform(dAppTime);}
(1)間接光
下面的VSIndirectLight表示間接光,也是一個虛基類,引擎沒有實現Light Map,而實時模擬的間接光照也沒有實現,根據上面類圖,只有一個SkyLight(天光)繼承VSIndirectLight,這裡用天光來模擬間接光,其他的光源都是局部光照,後面渲染的章節會詳細講到。
class VSGRAPHIC_API VSIndirectLight : public VSLight{ public: virtual bool Cullby(VSCuller & Culler); virtual void SetLocalScale(const VSVector3 & fScale); virtual void SetLocalRotate(const VSMatrix3X3 & Rotate); virtual void SetLocalTransform(const VSTransform & LocalTransform); virtual void SetLocalMat(const VSMatrix3X3W VSMat); FORCEINLINE void SetRange(VSVector3 & Range) { m_Range.x = Range.x > 1.0f ? Range.x : m_Range.x; m_Range.y = Range.y > 1.0f ? Range.y : m_Range.y; m_Range.z = Range.z > 1.0f ? Range.z : m_Range.z; GetLightRange(); } protected: //照射範圍 VSVector3 m_Range; //計算Bounds virtual void GetLightRange(); //Bounds VSAABB3 m_WorldRenderBV;};
我給間接光也加入了照射範圍,這個範圍是一個矩形。GetLightRange是計算間接光的Bounds(m_WorldRenderBV),每次設置都會重新計算Bounds。為了避免照射範圍過小,加入了範圍的限制。
VSIndirectLight::VSIndirectLight(){ m_bInheritScale = false; m_bInheritRotate = false; m_Range = VSVector3(999999.0f, 999999.0f, 999999.0f);}void VSIndirectLight::SetLocalScale(const VSVector3 & fScale){}void VSIndirectLight::SetLocalRotate(const VSMatrix3X3 & Rotate){}void VSIndirectLight::SetLocalTransform(const VSTransform & LocalTransform){ VSVector3 Tranlation = LocalTransform.GetTranslate(); SetLocalTranslate(Tranlation);}void VSIndirectLight::SetLocalMat(const VSMatrix3X3W VSMat){ VSVector3 Tranlation = VSMat.GetTranslation(); SetLocalTranslate(Tranlation);}
間接光比較特殊,只有位置信息對它起作用,當它掛接到其他節點下,不繼承旋轉和縮放,你設置旋轉和縮放是沒有用的。
void VSIndirectLight::GetLightRange(){ VSVector3 Pos = GetWorldTranslate(); m_WorldRenderBV = VSAABB3(Pos, m_Range.x, m_Range.y, m_Range.z);}
計算GetLightRange很簡單,一旦位置信息改變就要更新Bounds。
bool VSIndirectLight::Cullby(VSCuller & Culler){ unsigned int uiVSF = Culler.IsVisible(m_WorldRenderBV, true); if (uiVSF == VSCuller::VSF_ALL || uiVSF == VSCuller::VSF_PARTIAL) { m_bEnable = true; Culler.InsertLight(this); } return true;}
這裡是對光源可見性裁剪,m_bEnable = true表示光源是有效的。
bool VSIndirectLight::IsRelative(VSGeometry * pGeometry){ if (!VSLight::IsRelative(pGeometry)) { return false; } VSAABB3 GeometryAABB = pGeometry->GetWorldAABB(); if (GeometryAABB.RelationWith(m_WorldRenderBV) == VSNOINTERSECT) { return false; } return true;}
IsRelative看看節點是否受到這個光影響。兩個Bounds要至少相交才可以。
class VSGRAPHIC_API VSSkyLight : public VSIndirectLight { VSColorRGBA m_UpColor; VSColorRGBA m_DownColor; virtual unsigned int GetLightType()const { return LT_SKY; } };
終於見到一個間接光的實體,VSSkyLight和環境光差不多,只不過它不是純色的,分為上下兩個顏色,計算上也和不同環境光疊加有些不同,後面渲染的時候會詳細介紹。
(2)局部光
局部光的基類是VSLocalLight,這個類裡面有很多內容,大部分都和陰影有關,我只列出了和本節相關的,那麼它裡面只有m_Diffuse和m_Specular,光源本質是沒有單獨的高光顏色一說,正常做高光計算都是用的光發出的顏色(m_Diffuse),不過遊戲有些不一樣,單獨弄出來個高光顏色有時候可以做一些特殊效果。
class VSGRAPHIC_API VSLocalLight : public VSLight { VSColorRGBA m_Diffuse; VSColorRGBA m_Specular; };
接下里就是大家常見的方向光、點光源和聚光燈,這裡代碼也去掉大部分和本節無關的東西。
class VSGRAPHIC_API VSDirectionLight : public VSLocalLight { virtual unsigned int GetLightType()const{return LT_DIRECTION;} virtual void GetLightRange(); VSAABB3 m_WorldRenderBV; virtual bool Cullby(VSCuller & Culler); virtual bool IsRelative(VSGeometry * pGeometry); }; class VSGRAPHIC_API VSPointLight : public VSLocalLight { FORCEINLINE void SetRange(VSREAL Range) { m_Range = Range; GetLightRange(); } virtual unsigned int GetLightType()const{return LT_POINT;} virtual bool Cullby(VSCuller & Culler); virtual bool IsRelative(VSGeometry * pGeometry); virtual void GetLightRange(); VSSphere3 m_WorldRenderBV; VSREAL m_Range; }; class VSGRAPHIC_API VSSpotLight : public VSLocalLight { FORCEINLINE void Set(VSREAL Range, VSREAL Falloff, VSREAL Theta, VSREAL Phi) { m_Range = Range; m_Falloff = Falloff; m_Theta = Theta; m_Phi = Phi; GetLightRange(); } virtual unsigned int GetLightType()const{return LT_SPOT;} virtual bool Cullby(VSCuller & Culler); virtual bool IsRelative(VSGeometry * pGeometry); virtual void GetLightRange(); VSAABB3 m_WorldRenderBV; VSREAL m_Range; VSREAL m_Falloff; VSREAL m_Theta; VSREAL m_Phi; };
我一下子把三個光源都列了出來,它們基本都大同小異,點光源設置照射範圍就要重新計算GetLightRange(),聚光燈設置照射範圍和夾角也要重新計算GetLightRange()。點光源Bounds是球體,聚光燈是AABB,可能大家不理解的是方向光也加入了Bounds,這個不是給傳統方向光照使用的,傳統的所有的物體都要受到影響,這個是給LightFunction使用的,至於這個是什麼東西,渲染章節我會揭曉的。
至於IsRelative、Cullby,三種光源都差不多,只有方向光判斷了是否是LightFunction。這裡我只給出點光源和聚光燈GetLightRange的代碼並講解,而方向光的在LightFunction時候介紹。
void VSPointLight::GetLightRange(){ VSVector3 Point3 = GetWorldTranslate(); m_WorldRenderBV = VSSphere3(Point3, m_Range);}
點光源Bounds是個球體計算比較簡單,代碼一幕瞭然。
void VSSpotLight::GetLightRange(){ VSVector3 Dir, Up, Right; GetWorldDir(Dir, Up, Right); VSVector3 Point3 = GetWorldTranslate(); VSREAL R = TAN(m_Phi * 0.5f) * m_Range; VSOBB3 Obb(Dir, Up, Right, m_Range * 0.5f, R, R, Point3 + Dir * m_Range * 0.5f); m_WorldRenderBV = Obb.GetAABB();}
聚光燈實際是一個椎體形狀,椎體和平面位置關係判斷比較複雜,這樣和相機平面判斷複雜度也上升,所以我還是把椎體轉成OBB,同樣OBB判斷複雜度也不低,最後從OBB轉成AABB。
圖11.23 聚光燈三角錐到OBB到AABB
6.相機和光源的更新管理
相機和光源是引擎中最重要的兩個節點,他們都是繼承於NodeComponent,而且他們都有朝向和位置,也要去更新,不過它們都屬性和場景物體並不一樣,它們各自有各自的功能,所以不能用場景管理的方法管理它們。
相機最終目的是要把在相機體內的物體最後渲染到一張目標上(RenderTarget),而光源最終目的是照亮它範圍內的物體,而且相機和光源都可以掛接在任意一個節點上,跟隨移動。它們的位置更新可以和物體類似,直接隨著場景節點更新。由於他們的特殊性,不得不把他們單獨調離出來,在整個場景更新的時候,就要把所有的相機和光源都收集起來。
class VSGRAPHIC_API VSSpatial :public VSObject{protected: VSArray<VSLight *> m_pAllLight; VSArray<VSCamera *> m_pAllCamera;}
每個節點都會動態的保存當前自己和子節點的光源和相機,每幀遍歷的時候都會搜集一次。
void VSNode::UpdateNodeAll(double dAppTime){ …………………………………………………………….. for (unsigned int i = 0; i < m_pChild.GetNum(); i++) { if (m_pChild[i]) m_pChild[i]->UpdateNodeAll(dAppTime); } UpdateLightState(dAppTime); UpdateCameraState(dAppTime); …………………………………………………………….}void VSNode::UpdateLightState(double dAppTime){ if(m_pAllLight.GetNum() > 0) m_pAllLight.Clear(); for(unsigned int i = 0 ; i < m_pChild.GetNum() ; i++) { if(m_pChild[i]) { if(m_pChild[i]->m_pAllLight.GetNum() > 0) m_pAllLight.AddElement(m_pChild[i]->m_pAllLight,0,m_pChild[i]->m_pAllLight.GetNum() - 1); } }}void VSNode::UpdateCameraState(double dAppTime){ if(m_pAllCamera.GetNum() > 0) m_pAllCamera.Clear(); for(unsigned int i = 0 ; i < m_pChild.GetNum() ; i++) { if(m_pChild[i]) { if(m_pChild[i]->m_pAllCamera.GetNum() > 0) m_pAllCamera.AddElement(m_pChild[i]->m_pAllCamera,0,m_pChild[i]->m_pAllCamera.GetNum() - 1); } }}
而相機和光源的就是這次遞歸的終結者。
void VSCamera::UpdateCameraState(double dAppTime){ VSNode::UpdateCameraState(dAppTime); m_pAllCamera.AddElement(this);}void VSLight::UpdateLightState(double dAppTime){ VSNode::UpdateLightState(dAppTime); m_pAllLight.AddElement(this);}
最後每個Scene會搜集到這個Scene中所有的相機和光源信息。
void VSScene::CollectUpdateInfo(){ if(m_pAllCamera.GetNum() > 0) m_pAllCamera.Clear(); if(m_pAllLight.GetNum() > 0) m_pAllLight.Clear(); if (m_bIsBuild == false) { for (unsigned int i = 0; i < m_ObjectNodes.GetNum(); i++) { if (m_ObjectNodes[i]) { if (m_ObjectNodes[i]->m_pAllLight.GetNum() > 0) m_pAllLight.AddElement(m_ObjectNodes[i]->m_pAllLight, 0, m_ObjectNodes[i]->m_pAllLight.GetNum() - 1); } } for (unsigned int i = 0; i < m_ObjectNodes.GetNum(); i++) { if (m_ObjectNodes[i]) { if (m_ObjectNodes[i]->m_pAllCamera.GetNum() > 0) m_pAllCamera.AddElement(m_ObjectNodes[i]->m_pAllCamera, 0, m_ObjectNodes[i]->m_pAllCamera.GetNum() - 1); } } } else { if (m_pStaticRoot) { if (m_pStaticRoot->m_pAllCamera.GetNum() > 0) m_pAllCamera.AddElement(m_pStaticRoot->m_pAllCamera, 0, m_pStaticRoot->m_pAllCamera.GetNum() - 1); } for (unsigned int i = 0; i < m_pDynamic.GetNum(); i++) { if (m_pDynamic[i]) { if (m_pDynamic[i]->m_pAllCamera.GetNum() > 0) m_pAllCamera.AddElement(m_pDynamic[i]->m_pAllCamera, 0, m_pDynamic[i]->m_pAllCamera.GetNum() - 1); } } if (m_pStaticRoot) { if (m_pStaticRoot->m_pAllLight.GetNum() > 0) m_pAllLight.AddElement(m_pStaticRoot->m_pAllLight, 0, m_pStaticRoot->m_pAllLight.GetNum() - 1); } for (unsigned int i = 0; i < m_pDynamic.GetNum(); i++) { if (m_pDynamic[i]) { if (m_pDynamic[i]->m_pAllLight.GetNum() > 0) m_pAllLight.AddElement(m_pDynamic[i]->m_pAllLight, 0, m_pDynamic[i]->m_pAllLight.GetNum() - 1); } } }}
至於搜集起來怎麼用,我會到渲染的時候介紹給大家。
7.番外篇——淺談Prez、軟硬體遮擋剔除*
不過相機裁剪還相對比較粗略,它只是簡單的判斷Bounds是否在相機體內,並不能完全的把不可見物體排除掉,所以像素級別的裁剪演算法也出來了——遮擋剔除。一種使用軟體光柵化的方法,另一種是用硬體的方法。
軟體的遮擋剔除其實和硬體的遮擋剔除原理一樣的,只不過它要模擬整個光柵化過程。基本方法:判斷物體的所有像素是否都沒有通過深度測試,如果都沒有通過,則這個物體完全不可見。兩種方法在真正處理上相同點是:渲染的都不會是模型網格,而是粗略的Bounds。軟體方法用真正的網格肯定得不償失,硬體方法其實也是;他們不同在於:軟體演算法為了加快速度,不得不用低解析度來模擬渲染過程,而且投影在模擬渲染平面上的Bounds是不規則的形狀,必須通過它在2D平面的AABB才能加快每個像素的判斷過程(不過不知道現在有沒有比這個快的演算法);硬體方法通常要延後一幀來判斷,它取得的GPU返回的信息實際上是上一幀執行測試的信息,這樣避免過多的GPU的等待,畢竟現在GPU和CPU之間共享數據還是存在瓶頸的,因為所有的信息都是延後一幀的,並且連續,對於人類的眼睛其實根本就觀察不出來。
其實用不用遮擋剔除,它們之間都存在著一種博弈,博弈的地方在於,通過Bounds的粗略剪裁後,沒有剪裁的物體渲染花費的時候和遮擋剔除消耗的時間究竟誰大,還有不同的遮擋剔除之間誰的效果最好,其實這個很難下定論。有可能你有個好的CPU,而一塊爛GPU,那麼軟體的遮擋剔除很可能效果比硬體遮擋剔除效果要好;還有一種可能是你CPU很爛,但GPU很強,那麼你用硬體的遮擋剔除比軟體的遮擋剔除要好,還有一種可能是不用遮擋剔除最好。這些不僅僅和硬體有緊密關係,而且和場景的複雜度、當前視角、渲染的方法都也有很大關係。
還有一種現在都普遍認可的方法就是EarlyZ,或者叫PreZ。這種方法是渲染原始模型信息,用最簡單的Shader,但顏色信息不輸出,只輸出深度信息。有了深度信息後面再渲染一次物體,很多不可見的像素就會被排除門外。其實這一過程也並不加快所有機器的運行速度,雖然增加這一過程,對像素著色的壓力減小,但有些不好的顯卡對每次DrawCall特別敏感,這些DrawCall帶來的開銷,有可能還不如把它畫上去。
不過現在對於PC端的顯卡來說,PreZ帶來的消耗基本微乎其微,有些高級的PC端顯卡,硬體的遮擋剔除帶來的消耗也慢慢降低,不過我相信的是,軟體光柵化的遮擋剔除如果沒有好的演算法出現必定會被淘汰,隨著GPU的能力的逐漸增強,CPU和GPU之間數據交換加快,硬體帶來的優勢越來越明顯。
為了說明整個過程我畫了下面的圖,這裡並不是所有的過程都需要,根據不同的場景和硬體,目前來看還是要有取捨的,這個圖只是為了讓大家明白每個過程處於什麼位置。
圖11.23 剪裁的整個流程
1.相機裁剪就不多說了,要麼八叉樹、四叉樹、二叉樹、Portal,現在也沒什麼太好的方法,過濾後的物體集合是A,一旦開起了硬體遮擋剔除,在相機裁剪階段就要讀出這個物體是否有像素可見,得出的集合是B,這個過程在相機裁剪中最好一起做。
2.非透明物體從前向後排序這個過程對於很弱的CPU,而且不是什麼高級渲染,最好不要用。這個過程要算出距離,然後再排序,速度可想而知,最後弄不好還不如正常渲染。但對於強大的CPU,非常高級的渲染,尤其對像素填充率很高的延遲渲染,那麼排序還是有必要的,這樣後面的物體的某些像素就有可能被前面的物體給遮擋住。
3.軟體光柵化這個過程還是要評估一下,再決定是不是要用。如果演算法足夠快,也未嘗不可,總之要評估。
4.PreZ這個過程就像我上面說的,顯卡不咋地的,還是不要用了,瓶頸在像素填充率上而不是在DrawCall上,那麼想也不想,PreZ想直接上。
5.硬體的遮擋剔除是在渲染之後,其實在PreZ之後就可以了,如果沒有PreZ那麼就只能用在渲染之後,唯一等待就是要ZBuffer,有了ZBuffer就可以知道把這些粗略的Bounds畫上去,這些物體像素是否都沒通過深度測試,不過一般開不起來PreZ的機器,硬體遮擋剔除估計也很難力挽狂瀾,所以他們基本是難兄難弟。這個過程要注意,這裡渲染的Bounds是第1階段集合A的物體的Bounds而不是集合B的。這也就是第1階段為什麼要兩個集合都存在的原因,正常渲染流程用B,硬體遮擋剔除用A,如果有疑問自己好好想一想,就當作課後習題了。
推薦閱讀:
※2D精靈Batch系統設計
※Korok字體系統設計
※靈活的2D粒子系統設計
※從零開始手敲次世代遊戲引擎(五十四)
※第1章 引擎的紛爭 (感謝大食堂對本章的校驗)
TAG:遊戲引擎 |