手把手教你架構3D引擎高階篇系列一

海洋_發表於2018-09-09

最近一段時間事情比較多,從本篇部落格開始,我手把手教大家如何開發一款類似商業引擎Unity的開發,我們在這裡要閱讀學習一些編寫引擎的知識,編寫引擎之前,我們需要安裝Visual Studio VS2017,Windows作業系統是64位的,程式語言是C++,我們的引擎會使用比如Lua,C#作為引擎的指令碼,這也是方便後期引擎邏輯的擴充套件。我們在架構一款引擎之前要查閱一些技術相關的資料,其實我們開發遊戲也是一樣的,比如做一些演算法,我們也要查閱相關的資料。在這裡也是教大家遇到問題如何解決問題。學習查閱資料也是因人而異的,下面我會根據自己的體會跟大家交流寫高階引擎的一些方法或者說心得。

引擎程式設計必備知識

學習引擎和編寫引擎是完全不一樣的,學習引擎只要會用就可以,不需要關心整個引擎是如何是實現的,比如我們使用Unity引擎,不需要掌握引擎的所有模組,我們只關心我們專案中寫邏輯用到的模組就可以;還有優化,也是在引擎已有的基礎上修修補補;而編寫引擎需要去架構引擎以及引擎的各個模組編寫,使用哪些圖形庫?記憶體管理要如何做?渲染要實現哪些演算法?指令碼選用什麼語言等等,換句話說引擎的方方面面我們都要認真考慮,工作量還是相當大的。編寫引擎需要具備哪些知識?在編寫引擎之前,希望讀者已經掌握了線性代數,資料結構,高等數學,常用圖形學演算法比如Bezier,BSpine等等。另外,在架構設計方面,常用的設計模式也是需要掌握的,OpenGL或者DirectX API圖形庫等。如果上面列舉的知識點,讀者還有缺失的,利用業餘時間補一下,以上列舉的都是最基礎的,在編寫引擎之前我們也需要查閱一些資料豐富我們的知識庫,查閱哪些資料呢?這個是根據我自己的需要列的提綱,這個也是因人而異,自己感覺在哪方面比較缺失就可以查閱哪方面的資料,這個也是告訴讀者一個學習的方法,本篇部落格圍繞著以下幾個方面去查閱相關的技術點。
這裡寫圖片描述

Rendering

引擎最重要的是渲染,引擎渲染是評價引擎的一個重要指標,渲染離不開Shader程式設計,關於Shader程式設計的學習,讀者如果基礎比較薄弱,在這裡給讀者推薦一本Shader程式設計基礎書籍《CG教程——可程式設計實時圖形權威指南》這本書講述的都是Shader基礎程式設計,掌握了Shader基礎程式設計後,需要深入學習者可以看《GPU程式設計精粹》系列書籍,現在很多大公司都有圖形學高階工程師這個職位,學好了Shader程式設計也可以作為謀生的手段。。。。。
學習引擎離不開OpenGL和DirectX API圖形庫,這個是3D引擎底層架構的基礎,每個引擎都有自己的座標系是左手還是右手 與OpenGL或者DirectX相關的,OpenGL是跨平臺的圖形API,而DirectX是針對Windows作業系統的,我的部落格中就有關於OpenGL和D3D12的介紹,D3D12正在寫一個系列文章,感興趣的讀者可以學習,下面我們就要查閱學習一些資料了。
在Shder程式設計中,我們會涉及到一些Shader程式碼的優化,我以前部落格中也介紹過,在這裡建議大家學習這篇文件:http://www.humus.name/Articles/Persson_LowLevelThinking.pdf
如下圖所示:
這裡寫圖片描述
一個簡單算式的不同寫法,效率也是不一樣的,通過Shader程式的優化,我們可以看到編寫引擎不是那麼容易的事情。

我們再看看後處理渲染,看一個引擎是否強大,後處理渲染佔很大的比重,我們在開發引擎時也要參考後處理實現技術,我在翻閱時看到了國外一篇介紹後處理渲染的,連結地址:
http://www.geforce.com/whats-new/guides/rise-of-the-tomb-raider-graphics-and-performance-guide

看看老外實現的效果:
這裡寫圖片描述

非常強大,我們也要朝著這個目標去努力。

遊戲天空盒雲層的渲染在引擎中也是非常重要的,尤其在軍事模擬專案裡面,比如我們說的飛行模擬,關於雲層的渲染,Unity提供了很多的外掛,如果我們需要在自己的引擎中實現,網上也有這方面的介紹,因為我們使用的是C++程式設計,所以查詢資料還是需要與C++相關的,雲層的渲染,從效率角度出發,同樣可以使用多執行緒,雲渲染效果如下所示:
這裡寫圖片描述
是不是很絢麗?參考網址:
https://software.intel.com/en-us/articles/dynamic-volumetric-cloud-rendering-for-games-on-multi-core-platforms

文章的末尾還提供了原始碼,可以直接移植到引擎中。。。。。

另外,我還發現過一個在2002年實現的雲層渲染,其實我們引擎的開發技術很多都是以前的經典演算法現在還是適用的,這個是在2002年發表的《Real-Time Cloud Rendering for Games 》論文實現的雲層渲染:
這裡寫圖片描述

這裡寫圖片描述

參考網址:http://vterrain.org/Atmosphere/Clouds/index.html
對應的Demo實現程式碼都有,可以參考。。。。。。。下面再介紹渲染中的裁剪實現方式。

關於裁剪的渲染,在此也查閱了相關的文件,我們採用如下的裁剪系統:
這裡寫圖片描述
每個剔除步驟左側的箭頭表示輸入資料,右側的箭頭輸出資料,顏色與相應GPU緩衝區的顏色匹配。使用該種方案,平均剔除率高2.3倍,幀時間快1.6倍。
接下來再看看我們的渲染系統,如下圖所示:
這裡寫圖片描述

參考網址:https://eidosmontreal.com/en/news/deferred-next-gen-culling-and-rendering-for-dawn-engine

Mipmap這個大家都聽說過,那我們如何處理帶有Alpha通道的材質的MipMap呢?這個問題估計大家都沒想過,屬於引擎細節問題,如果我們不進行帶有Alpha通道的MipMap處理會出現下面的情況發生:
這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

以上三幅圖是從近到遠,顯然這個不是我們想要的,遠處就看不到樹的葉子了,我們需要的是遠近都是一樣的,否則這種效果很難看的,我們需要的效果是遠處的應該是下圖所示的:
這裡寫圖片描述

解決方案可以採用下面處理Alpha Mipmaps的程式碼,如下所示:

// Output first mipmap.
context.compress(image, compressionOptions, outputOptions);

// Estimate original coverage.
const float coverage = image.alphaTestCoverage(A_r);

// Build mipmaps and scale alpha to preserve original coverage.
while (image.buildNextMipmap(nvtt::MipmapFilter_Kaiser))
{
    image.scaleAlphaToCoverage(coverage, A_r);
    context.compress(tmpImage, compressionOptions, outputOptions);
}

參考網址:http://the-witness.net/news/2010/09/computing-alpha-mipmaps/

另外還有一篇文章介紹相關技術,參考網址:https://cdn.steamstatic.com.8686c.com/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf

大地形的渲染是做任何引擎都無法迴避的,我們可以參考網上相關的論文:
http://www.humus.name/Articles/Persson_CreatingVastGameWorlds.pdf

另外我在部落格中也提到了大地形相關的技術,感興趣的可以看看我以前寫的部落格。上面是關於渲染的資料查閱,下面再看看LOD的。

LOD

LOD 這個是最常用的技術了,網上很多資料介紹的,前幾天我寫了一篇文章介紹大密度建築場景載入的解決方案,裡面提到了AutoLod自動製作LOD模型的博文。當然它是作為Unity的外掛實現的,而我們需要用C++實現,思路是可以借鑑的,關於LOD,我查閱了一篇文章在這方面寫的挺好的,推薦給大家。
這裡寫圖片描述
利用該方法實現的LOD對三角形的簡化,該外掛是對UE4的,也是用C++實現的,可以參考:
https://shaderbits.com/blog/octahedral-impostors/

LOD演算法網上也有很多,這裡就不囉嗦了。。。。。。

Water

評價引擎是否牛逼的另一個指標是水的渲染,每個引擎也是必備的,網上很多介紹水,實話實說渲染的都不真實,看看我給讀者推薦的渲染水的實現效果:
這裡寫圖片描述

這裡寫圖片描述

通過上面這兩幅是不是給你一種以假亂真的效果,實現水渲染離不開Shader的編寫,Shader最基本的組成就是頂點著色器和可程式設計著色器。下面把頂點著色器程式碼和片段著色器程式碼給讀者展示一下:
先看頂點著色器程式碼:

varying vec3  Normal;
varying vec3  EyeDir;


varying vec2 texNormal0Coord;
varying vec2 texColorCoord;
varying vec2 texFlowCoord;

uniform float osg_FrameTime;
uniform mat4 osg_ViewMatrixInverse;
varying float myTime;
void main(void)
{

    gl_Position    = ftransform();
    Normal         = normalize(gl_NormalMatrix * gl_Normal);

    vec4 pos       = gl_ModelViewMatrix * gl_Vertex;
    EyeDir         = vec3(osg_ViewMatrixInverse*vec4(pos.xyz,0));;

    texNormal0Coord   = gl_MultiTexCoord1.xy;
    texColorCoord = gl_MultiTexCoord3.xy;
    texFlowCoord = gl_MultiTexCoord2.xy;

    myTime = 0.01 * osg_FrameTime;
}

片段著色器程式碼比較複雜,涉及到多張貼圖:

uniform sampler2D normalMap;

uniform sampler2D colorMap;
uniform sampler2D flowMap;
uniform samplerCube cubeMap;

varying vec3  Normal;
varying vec3  EyeDir;

varying vec2 texNormal0Coord;
varying vec2 texColorCoord;
varying vec2 texFlowCoord;

varying float myTime;

void main (void)
{

        // texScale determines the amount of tiles generated.
    float texScale = 35.0;
    // texScale2 determines the repeat of the water texture (the normalmap) itself
    float texScale2 = 10.0; 
    float myangle;
    float transp;
    vec3 myNormal;

    vec2 mytexFlowCoord = texFlowCoord * texScale;
    // ff is the factor that blends the tiles.
      vec2 ff =  abs(2*(frac(mytexFlowCoord)) - 1) -0.5;      
    // take a third power, to make the area with more or less equal contribution
    // of more tile bigger
      ff = 0.5-4*ff*ff*ff;
      // ffscale is a scaling factor that compensates for the effect that
      // adding normal vectors together tends to get them closer to the average normal
      // which is a visible effect. For more or less random waves, this factor
      // compensates for it 
      vec2 ffscale = sqrt(ff*ff + (1-ff)*(1-ff));
    vec2 Tcoord = texNormal0Coord  * texScale2;

    // offset makes the water move
        vec2 offset = vec2(myTime,0);

    // I scale the texFlowCoord and floor the value to create the tiling
    // This could have be replace by an extremely lo-res texture lookup
    // using NEAREST pixel.
    vec3 sample = texture2D( flowMap, floor(mytexFlowCoord)/ texScale).rgb;

    // flowdir is supposed to go from -1 to 1 and the line below
    // used to be sample.xy * 2.0 - 1.0, but saves a multiply by
    // moving this factor two to the sample.b
    vec2 flowdir = sample.xy -0.5;    

    // sample.b is used for the inverse length of the wave
    // could be premultiplied in sample.xy, but this is easier for editing flowtexture
    flowdir *= sample.b;

    // build the rotation matrix that scales and rotates the complete tile
    mat2 rotmat = mat2(flowdir.x, -flowdir.y, flowdir.y ,flowdir.x);

    // this is the normal for tile A
    vec2 NormalT0 = texture2D(normalMap, rotmat * Tcoord - offset).rg;

    // for the next tile (B) I shift by half the tile size in the x-direction
    sample = texture2D( flowMap, floor((mytexFlowCoord + vec2(0.5,0)))/ texScale ).rgb;
    flowdir = sample.b * (sample.xy - 0.5);
    rotmat = mat2(flowdir.x, -flowdir.y, flowdir.y ,flowdir.x);
      // and the normal for tile B...
      // multiply the offset by some number close to 1 to give it a different speed
      // The result is that after blending the water starts to animate and look
      // realistic, instead of just sliding in some direction.
      // This is also why I took the third power of ff above, so the area where the
      // water animates is as big as possible
      // adding a small arbitrary constant isn't really needed, but helps to show
      // a bit less tiling in the beginning of the program. After a few seconds, the
      // tiling cannot be seen anymore so this constant could be removed.
      // For the quick demo I leave them in. In a simulation that keeps running for
      // some time, you could just as well remove these small constant offsets
      vec2 NormalT1 = texture2D(normalMap, rotmat * Tcoord - offset*1.06+0.62).rg ; 

      // blend them together using the ff factor
      // use ff.x because this tile is shifted in the x-direction 
      vec2 NormalTAB = ff.x * NormalT0 + (1-ff.x) * NormalT1;

    // the scaling of NormalTab and NormalTCD is moved to a single scale of
    // NormalT later in the program, which is mathematically identical to
      // NormalTAB = (NormalTAB - 0.5) / ffscale.x + 0.5;

      // tile C is shifted in the y-direction 
    sample = texture2D( flowMap, floor((mytexFlowCoord + vec2(0.0,0.5)))/ texScale ).rgb;
    flowdir = sample.b * (sample.xy - 0.5);
    rotmat = mat2(flowdir.x, -flowdir.y, flowdir.y ,flowdir.x);       
    NormalT0 = texture2D(normalMap, rotmat * Tcoord - offset*1.33+0.27).rg;

      // tile D is shifted in both x- and y-direction
      sample = texture2D( flowMap, floor((mytexFlowCoord + vec2(0.5,0.5)))/ texScale ).rgb;
    flowdir = sample.b * (sample.xy - 0.5);
    rotmat = mat2(flowdir.x, -flowdir.y, flowdir.y ,flowdir.x);
      NormalT1 = texture2D(normalMap, rotmat * Tcoord - offset*1.24).rg ;

      vec2 NormalTCD = ff.x * NormalT0 + (1-ff.x) * NormalT1;
      // NormalTCD = (NormalTCD - 0.5) / ffscale.x + 0.5;

    // now blend the two values together
      vec2 NormalT = ff.y * NormalTAB + (1-ff.y) * NormalTCD;

      // this line below used to be here for scaling the result
      //NormalT = (NormalT - 0.5) / ffscale.y + 0.5;

      // below the new, direct scaling of NormalT
    NormalT = (NormalT - 0.5) / (ffscale.y * ffscale.x);
    // scaling by 0.3 is arbritrary, and could be done by just
    // changing the values in the normal map
    // without this factor, the waves look very strong
    NormalT *= 0.3; 
     // to make the water more transparent 
    transp = texture2D( flowMap, texFlowCoord ).a;
    // and scale the normals with the transparency
    NormalT *= transp*transp;

    // assume normal of plane is 0,0,1 and produce the normalized sum of adding NormalT to it
    myNormal = vec3(NormalT,sqrt(1-NormalT.x*NormalT.x - NormalT.y*NormalT.y));

    vec3 reflectDir = reflect(EyeDir, myNormal);
    vec3 envColor = vec3 (textureCube(cubeMap, -reflectDir)); 

    // very ugly version of fresnel effect
    // but it gives a nice transparent water, but not too transparent
    myangle = dot(myNormal,normalize(EyeDir));
    myangle = 0.95-0.6*myangle*myangle;

    // blend in the color of the plane below the water  

    // add in a little distortion of the colormap for the effect of a refracted
    // view of the image below the surface. 
    // (this isn't really tested, just a last minute addition
    // and perhaps should be coded differently

    // the correct way, would be to use the refract routine, use the alpha channel for depth of 
    // the water (and make the water disappear when depth = 0), add some watercolor to the colormap
    // depending on the depth, and use the calculated refractdir and the depth to find the right
    // pixel in the colormap.... who knows, something for the next version
    vec3 base = texture2D(colorMap,texColorCoord + myNormal/texScale2*0.03*transp).rgb;
    gl_FragColor = vec4 (mix(base,envColor,myangle*transp),1.0 );

        // note that smaller waves appear to move slower than bigger waves
        // one could use the tiles and give each tile a different speed if that
        // is what you want 
}

詳情檢視:https://www.rug.nl/society-business/centre-for-information-technology/research/hpcv/publications/watershader/

其實上面的水渲染實現,已經滿足了大部分水面渲染的要求,但是我們還要不滿足,繼續看看有沒有其他水的渲染方式,比如我們說的海戰水渲染,或者說虛擬模擬渲染,實現這樣的水怎麼實現的呢?圖片的展示是最好的方式,下面再給讀者展示幾幅:
這裡寫圖片描述

這裡寫圖片描述

詳情檢視水渲染:https://cgzoo.files.wordpress.com/2012/04/water-technology-of-uncharted-gdc-2012.pdf

我們再看一種夏日風情的水渲染效果:
這裡寫圖片描述

詳情檢視網址:https://www.gamedev.net/articles/programming/graphics/rendering-water-as-a-post-process-effect-r2642

關於水渲染資料就給讀者介紹到這裡,我這裡還有一些,都是國外的,正常是無法訪問的,大家懂的。這裡就不給大家列舉了,以上關於水渲染的參考足夠了,我們能把這些技術掌握就很牛了。下面再去查閱另一個引擎中比較重要的模組——燈光。

Lighting

關於燈光的渲染,這裡也不多贅述了,我這裡查閱了兩篇相關的文件,供學習參考:
這裡寫圖片描述

每個引擎關於燈光的實現很多,兩篇相關的文章:
https://eheitzresearch.wordpress.com/415-2/

https://seblagarde.wordpress.com/2012/09/29/image-based-lighting-approaches-and-parallax-corrected-cubemap/#more-444

PBR

PBR是近幾年比較流行的渲染技術,一方面是因為移動端硬體提升了,PBR演算法可以在移動端執行了,我查閱了一下相關的資料,網上很多的,下面的供參考:
這裡寫圖片描述

網址:http://www.makinggames.biz/news/building-your-own-engine-part-2-into-the-guts-of-the-rendering-engine,6313.html

Particle

網上有個開源的Particle Universe,針對Ogre開源的,我以前寫的部落格中有介紹,讀者可以去查閱一下。
這裡寫圖片描述
另外,我查閱了兩篇文章關於Particle的介紹:
https://www.gdcvault.com/play/1020176/Scripting-Particles-Getting-Native-Speed

http://www.simppa.fi/blog/the-new-particle/

Postprocess

Postprocess中文名字是後處理渲染,針對的是整個場景的渲染,我們先看一下DOF介紹:
http://encelo.netsons.org/2008/04/15/depth-of-field-reloaded/
對應的部落格都有原始碼可供下載學習:
http://encelo.netsons.org/programming/opengl/

另外SSAO大家也要關注一下,文件地址:http://aras-p.info/blog/2009/09/17/strided-blur-and-other-tips-for-ssao/

再看看Gaussian blur的介紹文件:http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/

還有一篇是Neural Network Ambient Occlusion的介紹:http://theorangeduck.com/page/neural-network-ambient-occlusion

我這裡並沒有把後處理的所有演算法都查閱一遍,如果你對其他的比如Bloom,Blur等不瞭解可以自行查閱,我是按照自己在寫引擎前查閱的相關資料,每個人的情況是不同的,這裡是給大家提供了一種查閱文獻的方法供參考。

Shadow

陰影包括實時陰影和LightMap靜態的,說到陰影我建議還是需要學習一下影象處理,我也查閱了相關文獻:http://www.gamasutra.com/blogs/JoshKlint/20160518/272888/Banding_and_Dithering.php

關於陰影演算法有PSSM,SDSM,CSM等,其中PSSM,CSM在我以前的部落格中有介紹,這裡就不多說了,可以看看SDSM的渲染
這裡寫圖片描述

參考文件:https://software.intel.com/sites/default/files/m/d/4/1/d/8/sampleDistributionShadowMaps_SIGGRAPH2010_notes.pdf

為了深入探討Shadow的實現,在這裡給讀者推薦幾篇好的文獻:
http://publications.lib.chalmers.se/records/fulltext/198979/198979.pdf

http://www.cse.chalmers.se/~uffe/Fast%20Precomputed%20Ambient%20Occlusion%20for%20Proximity%20Shadows-jgt.pdf

https://docs.microsoft.com/zh-cn/windows/desktop/DxTechArts/common-techniques-to-improve-shadow-depth-maps

Vegetation

UE4在這方面做的非常好,看一下UE4渲染效果:
這裡寫圖片描述

參考文件:https://polycount.com/discussion/comment/2258343#Comment_2258343

Animation

角色的骨骼動畫我們選擇的是FBX檔案,但是一些基礎知識我們還是需要掌握的,IK反向動力學,參考網址:
http://theorangeduck.com/page/simple-two-joint

Scripting

Lua程式設計,除了官方提供的基礎文件,我這裡再給讀者推薦一份學習的文件資料:
https://eliasdaler.wordpress.com/2016/01/07/using-lua-with-c-in-practice-part4/
這個系列文件還是不錯的,學習起來比較容易

AI

AI是與行為樹相關的,我查閱了一下文件:http://www.gamasutra.com/blogs/ChrisSimpson/20140717/221339/Behavior_trees_for_AI_How_they_work.php

除了以上列出的技術點,我還研究了一下國外遊戲渲染的效果。

Studies

除了瀏覽上面的技術外,我還重點研究了一下關於圖形學,遊戲中的每張畫面都是每幀遊戲執行的影象,看看下面這幅圖:
這裡寫圖片描述
對遊戲中畫面我們進行分析看出,其實一個好的遊戲畫面是由多種元素組成的,比如上圖所示的,有Probe,這個在Unity引擎中也有,Fog,SSAO,再加上Diffuse,Specular, Normal等貼圖就可以把上面的場景復原出來,我們以前做遊戲時,看到某個遊戲場景吸引人,主程跟主美一起分析它是如何實現的。再分析一張遊戲圖片:
這裡寫圖片描述
上面這副影象我們對它做了進一步細緻詳細的分析。是不是很有意思?

這裡寫圖片描述

在此給讀者推薦幾個相關的技術網站:
http://www.adriancourreges.com/blog/2016/09/09/doom-2016-graphics-study/

http://www.adriancourreges.com/blog/2017/12/15/mgs-v-graphics-study/

http://www.adriancourreges.com/blog/2015/11/02/gta-v-graphics-study/

http://www.adriancourreges.com/blog/2015/06/23/supreme-commander-graphics-study/

http://www.adriancourreges.com/blog/2015/03/10/deus-ex-human-revolution-graphics-study/

學習老外的東西,可以做到取長補短。。。。。。。

總結

以上,是我在架構高階引擎之前查閱的相關資料,我查閱的這些資料都是老外寫的,這裡再補充一點,英語閱讀,想寫引擎的讀者英語水平也不能太差,畢竟現在市面上流行的引擎都是國外編寫的,包括註釋在內都是英文,當然,相關技術國內也有人網上介紹,國內的大家自行檢視就好了。如果沒有一定的基礎這些文獻掌握起來還是有一定難度的,資料的閱讀一方面幫我們擴充演算法思路,另一方面也幫助我們完善引擎的技術。當然,除了我上面給大家列舉的,我們還要閱讀一些相關的書籍,這個要根據個人情況而定。

相關文章