1. 程式人生 > >通過OSG實現對模型的日照模擬

通過OSG實現對模型的日照模擬

目錄

  • 1. 載入模型
  • 2. 光照
    • 1) 環境反射
    • 2) 漫反射
    • 3) 日照方向
      • (1) 太陽高度角和太陽方位角
      • (2) 計算過程
    • 4) 改進實現
  • 3. 陰影
  • 4. 太陽高度角與太陽方位角的計算
    • 1) 太陽高度角計算公式
    • 2) 太陽方位角計算公式
    • 3) 太陽赤緯計算公式
    • 4) 時角計算公式
    • 5) 真太陽時
  • 5. 參考文獻

1. 載入模型

通過OpenSceneGraph載入一個傾斜攝影的場景模型資料:

#include <iostream>
#include <Windows.h>

#include <osgViewer/Viewer>
#include <osgDB/ReadFile>

using namespace std;

int main()
{
    string osgPath = "D:/Data/Dayanta_OSGB/Data/MultiFoderReader.osgb";
    osg::Node * node = osgDB::readNodeFile(osgPath);
    osgViewer::Viewer viewer;
    viewer.setSceneData(node);
    viewer.setUpViewInWindow(100, 100, 800, 600);
    return viewer.run();
}

執行結果顯示的場景如下:

想要對模型進行日照模擬,就需要用到光照和陰影技術。注意此時模型上的部分陰影是紋理上自帶的。

2. 光照

osgViewer的預設場景中是有燈光的,調整上述的場景的視角,某些地方是全黑的,而且場景效果偏暗。這裡需要設定自己需要的環境反射和漫反射。

1) 環境反射

環境反射是針對環境光而言的,在環境反射中,環境光照射物體是各方面均勻、強度相等的,因此環境光不用設定位置和方向,只需要指定顏色。

2) 漫反射

漫反射是針對平行光和點光源光而言的。太陽光照就是平行光,由於太陽距離地球很遠,陽光到達地球的時可以認為是平行的。平行光可以用一個方向和一個顏色來定義。當然,對於像燈泡那樣的點光源光,還需要指定光源的位置。

3) 日照方向

(1) 太陽高度角和太陽方位角

對於太陽光照來說,其方向並不是隨便設定的。這裡需要引入太陽高度角和太陽方位角兩個概念,通過這兩個角度,可以確定日照的方向。

太陽高度角指的就是太陽光的入射方向和地平面之間的夾角;而太陽方位角略微複雜點,指的是太陽光線在地平面上的投影與當地子午線的夾角,可近似地看作是豎立在地面上的直線在陽光下的陰影與正南方向的夾角。其中方位角以正南方向為0,由南向東向北為負,有南向西向北為正。例如太陽在正東方,則其方位角為-90度;在正東北方時,方位角為-135度;在正西方時,方位角是90度,在正西北方為135度;當然在正北方時方位角可以表示為正負180度。

(2) 計算過程

根據上述定義,對於空間某一點的日照光線,可以有如下示意圖。

令太陽光線長度L1=1,有如下推算過程:

α是太陽高度角,則日照方向Z長度L3=sin(α);
L1在地平面(XY)平面的長度L2 = cos(α);
β是太陽方位角,則日照方向X長度L5 = L2cos(β);
同時日照方向Y長度L4 = L2
sin(β)。

因此,對於太陽高度角α和太陽方位角β,日照光線的單位向量n(x,y,z)為:

X = cos(α)cos(β);
Y = cos(α)
sin(β);
Z = sin(α);

4) 改進實現

在OSG中是通過設定光照節點加入到場景節點中來實現光照的。這裡把太陽高度角設定成45度,太陽方位角度設定成315度。通過上述轉換,得到光照方向。有一點要注意的是osg::Light沒有顯式的設定平行光的介面,請教大牛才知道只需要在setPosition()函式中設定w分量為0就可以了。關於這一點我也確實有點不理解。

#include <iostream>
#include <Windows.h>

#include <osgViewer/Viewer>
#include <osgDB/ReadFile>
#include <osg/Light>

using namespace std;
using namespace osg;

//新增燈光節點
void AddLight(osg::ref_ptr<osg::Group> group)
{
    double solarAltitude = 45.0;
    double solarAzimuth = 315.0;

    //開啟光照
    osg::ref_ptr<osg::StateSet> stateset = new osg::StateSet();
    stateset = group->getOrCreateStateSet();
    stateset->setMode(GL_LIGHTING, osg::StateAttribute::ON);                // 啟用光照
    stateset->setMode(GL_LIGHT0, osg::StateAttribute::ON);              // 啟用指定光源

    //建立一個Light物件
    osg::ref_ptr<osg::Light> light = new osg::Light();
    light->setLightNum(0);

    //設定方向:平行光
    osg::Vec3 arrayvector(0.0f, 0.0f, -1.0f);
    double fAltitude = osg::DegreesToRadians(solarAltitude);                //光源高度角
    double fAzimuth = osg::DegreesToRadians(solarAzimuth);          //光源方位角
    arrayvector[0] = cos(fAltitude)*cos(fAzimuth);
    arrayvector[1] = cos(fAltitude)*sin(fAzimuth);
    arrayvector[2] = sin(fAltitude);
    light->setDirection(arrayvector);

    //平行光位置任意,但是w分量要為0
    osg::Vec4 lightpos(arrayvector[0], arrayvector[1], arrayvector[2], 0.0f);
    light->setPosition(lightpos);   

    //設定環境光的顏色
    light->setAmbient(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));

    //設定散射光顏色
    light->setDiffuse(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));     

    //    //設定恆衰減指數
    //    light->setConstantAttenuation(1.0f);
    //    //設定線形衰減指數
    //    light->setLinearAttenuation(0.0f);
    //    //設定二次方衰減指數
    //    light->setQuadraticAttenuation(0.0f);

    //建立光源
    osg::ref_ptr<osg::LightSource> lightSource = new osg::LightSource();
    lightSource->setLight(light);

    group->addChild(lightSource);
}


int main()
{
    //根節點
    osg::ref_ptr<osg::Group> root = new osg::Group; 
    root->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE);        //預設去掉光照

    //
    string osgPath = "C:/Data/baoli/Production_3/Data/MultiFoderReader.osgb";
    osg::Node * node = osgDB::readNodeFile(osgPath);
    root->addChild(node);

    //
    AddLight(root);

    //
    osgViewer::Viewer viewer;
    viewer.setSceneData(root);
    viewer.setUpViewInWindow(100, 100, 800, 600);
    return viewer.run();
}

最終執行結果是模型整體有了亮度,但是由於紋理的效果,光照的明暗效果的效果沒有顯現出來。但是如果是白模,將會看到很明顯的明暗效果。

3. 陰影

在OSG中已經實現了生成陰影的元件osgShadow。其具體呼叫方式也比較簡單,首先將節點和燈光加入到ShadowedScene物件,然後標明投射者和被投射者,最後選擇一種陰影渲染演算法應用到場景就可以了。

注意這裡的陰影渲染演算法應該選用ShadowMap,因為我這裡的投射者和被投射者都是同一個物體,很多例子裡面用的ShadowTexture演算法是不支援自投影的。

#include <iostream>
#include <Windows.h>

#include <osgViewer/Viewer>
#include <osgDB/ReadFile>
#include <osg/Light>

#include <osgShadow/ShadowedScene>
#include <osgShadow/ShadowMap>

using namespace std;
using namespace osg;

//新增燈光節點
void AddLight(osg::ref_ptr<osg::Group> group)
{
    double solarAltitude = 45.0;
    double solarAzimuth = 315.0;

    //開啟光照
    osg::ref_ptr<osg::StateSet> stateset = new osg::StateSet();
    stateset = group->getOrCreateStateSet();
    stateset->setMode(GL_LIGHTING, osg::StateAttribute::ON);                // 啟用光照
    stateset->setMode(GL_LIGHT0, osg::StateAttribute::ON);              // 啟用指定光源

    //建立一個Light物件
    osg::ref_ptr<osg::Light> light = new osg::Light();
    light->setLightNum(0);

    //設定方向:平行光
    osg::Vec3 arrayvector(0.0f, 0.0f, -1.0f);
    double fAltitude = osg::DegreesToRadians(solarAltitude);                //光源高度角
    double fAzimuth = osg::DegreesToRadians(solarAzimuth);          //光源方位角
    arrayvector[0] = cos(fAltitude)*cos(fAzimuth);
    arrayvector[1] = cos(fAltitude)*sin(fAzimuth);
    arrayvector[2] = sin(fAltitude);
    light->setDirection(arrayvector);

    //平行光位置任意,但是w分量要為0
    osg::Vec4 lightpos(arrayvector[0], arrayvector[1], arrayvector[2], 0.0f);
    light->setPosition(lightpos);   

    //設定環境光的顏色
    light->setAmbient(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));

    //設定散射光顏色
    light->setDiffuse(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));     

    //    //設定恆衰減指數
    //    light->setConstantAttenuation(1.0f);
    //    //設定線形衰減指數
    //    light->setLinearAttenuation(0.0f);
    //    //設定二次方衰減指數
    //    light->setQuadraticAttenuation(0.0f);

    //建立光源
    osg::ref_ptr<osg::LightSource> lightSource = new osg::LightSource();
    lightSource->setLight(light);

    group->addChild(lightSource);
}


int main()
{
    //根節點
    osg::ref_ptr<osg::Group> root = new osg::Group; 
    root->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE);        //預設去掉光照

    //標識陰影接收物件                                                                                                                      
    const int ReceivesShadowTraversalMask = 0x1;
    //標識陰影投影物件
    const int CastsShadowTraversalMask = 0x2;

    //陰影節點
    osg::ref_ptr<osgShadow::ShadowedScene> shadowedScene = new osgShadow::ShadowedScene();
    shadowedScene->setReceivesShadowTraversalMask(ReceivesShadowTraversalMask);
    shadowedScene->setCastsShadowTraversalMask(CastsShadowTraversalMask);   
    root->addChild(shadowedScene);
    
    //場景節點
    string osgPath = "C:/Data/baoli/Production_3/Data/MultiFoderReader.osgb";
    osg::Node * node = osgDB::readNodeFile(osgPath);
    shadowedScene->addChild(node);
    
    //設定投射者
    node->setNodeMask(CastsShadowTraversalMask);               //只需要設定投射體,那麼預設情況下所有的物體都是被投物體

    //ShadowMap陰影演算法
    osg::ref_ptr<osgShadow::ShadowMap> sm = new osgShadow::ShadowMap;
    shadowedScene->setShadowTechnique(sm.get());

    //
    AddLight(shadowedScene);

    //
    osgViewer::Viewer viewer;
    viewer.setSceneData(root);
    viewer.setUpViewInWindow(100, 100, 800, 600);
    return viewer.run();
}

最後的實現效果如下,可以看到很明顯的陰影效果:

4. 太陽高度角與太陽方位角的計算

到這裡光照和陰影的效果就已經完全實現了,但是我這裡模擬的是太陽日照的效果,那麼一個新的問題又產生了。前面說根據太陽高度角與太陽方位角計算光照的方向。那麼太陽高度角與太陽方位角又是怎麼計算出來的呢?這裡推薦一篇寫的不錯的文章:太陽高度角方位角計算。可惜這篇文章的圖片已失效,我這裡就把四個計算公式再貼一下:

1) 太陽高度角計算公式

2) 太陽方位角計算公式

3) 太陽赤緯計算公式

4) 時角計算公式

5) 真太陽時

那篇文章中其他的公式都很清晰,但是關於真太陽時的描述其實我覺得沒有講清楚,看的是一頭霧水。後來我也在網上查閱一些資料,令人好笑的是這個真太陽時關聯的最多的卻是算命算生辰八字。那我就通過這個一步步來講這個真太陽時是怎麼來的。

我們知道,古代是通過日晷等方式來計時的,例如午時就是影子最短的時候。但是由於日照到達地球的差異,烏魯木齊和北京的午時肯定不是同一時刻。古代的人沒有那個技術條件,將各地的時間統一起來,都是各地用各自的地方時來計時。所以算生辰八字和計算日照一樣,都需要當地最精確的太陽光照造成的時間,這個時間就是真太陽時。

但是我們現在都是有行政時間的,無論在北京或者烏魯木齊,用的都是東經120度的中國北京時間。而在世界上是分24個時區的,每15度就是一個時區。那麼可以算算北京時間12點整在烏魯木齊的真太陽時是多少。

經查閱烏魯木齊的經度大約為87.68,那麼時差為(87.68- 120.0)/15.0=-2.154667,也就是負2小時9分鐘16.8秒,因此可算得烏魯木齊的地方時就是9時50分43.2秒。那麼這個算出來的地方時是不是就是真太陽時呢?

其實也不是的。這個時間其實是平太陽時。平太陽時假設地球繞太陽是標準的圓形,一年中每天都是均勻的。但是地球繞日執行的軌道是橢圓的,則地球相對於太陽的自轉並不是均勻的,每天並不都是24小時,有時候少有時候多。這個時間差異就是真太陽時差。

在查閱真太陽時差的時候發現資料真的挺少,而且各有說法。有的說真太陽時差每年都不一樣,是根據天文資訊計算出來的,每年都會發布一次;而在維基百科上面給出了每天的真太陽時差的模擬計算公式;更多的是給了一張表,按照表的日期取值就行了[什麼是真太陽時]。我這裡只能採信第三種,例如5月29日的真太陽時差是+2分22秒,那麼將上面計算的平太陽時加上這個時差,為9時53分5.2秒。即5月29日北京時間烏魯木齊的真太陽時為9時53分5.2秒。

5. 參考文獻

  1. Shadows
  2. 太陽高度角方位角計算
  3. 什麼是真太陽時
  4. (轉載)關於太陽(衛星)天頂角,太陽高度角,太陽方位角的整理
  5. DEM-地貌暈渲圖的生成原理
  6. OSG 學習第四天:光照