1. 程式人生 > >【DirectX11】第九篇 光照模型——高光

【DirectX11】第九篇 光照模型——高光

      本系列文章主要翻譯和參考自《Real-Time 3D Rendering with DirectX and HLSL》一書(感謝原書作者),同時會加上一點個人理解和拓展,文章中如有錯誤,歡迎指正。
      這裡是書中的程式碼和資源。

      本文所有的環境和工具使用都基於之前的文章,如有不明白的地方請先參考本系列之前的幾篇文章。

本文索引:

關於燈光

      在現實世界中,沒有光我們將看不見任何東西,你所看見的物體或者是反射了光源的光或者是本身就能自發光。在計算機渲染的過程中,你將模擬燈光與物體的互動,並以此增加3D物體表面的細節。但是燈光的相互影響是一個非常複雜的過程,在目前的技術中並不能達到在一個可互動的幀率範圍內進行這樣大量的重複計算。因此,一般會採用一種近似演算法,用一種描述燈光與3D模型如何互動的燈光模型來為你的感官增加更多可感受的細節。這篇文章中將介紹一些基礎的光照模型。

Specular Highlights: 高光

      當你模擬漫反射時,你只要能夠提供出粗糙、亞光的表面感就可以了。這對大部分情況都是適用的,並且這提供了高光其他部分的基礎照明。但有些時候你也需要製作一個閃光的表面模擬,例如,拋光金屬和大理石地板。這部分要講解的就是如何去實現這種高光效果。

(1) Phong 馮氏(光照模型)

      有很多種方法可以用來模擬高光反射。我們第一個要研究的高光模型就是馮氏反射模型,這個模型以其發明者的名字名字,就是來自猶他大學(University of Utah)的Bui Tuong Phong。
      跟漫反射不同,高光的計算需要根據觀察者(相機)的位置和角度來計算。你可以在現實世界中觀察到,當你改變觀察高光物體的位置和角度時,其光斑位置也會發生變化。馮氏模型闡述了高光的計算依賴於觀察者的觀察方向和反射光向量之間的夾角。公式如下:
這裡寫圖片描述


      這裡的R是反射光向量,V是觀察方向,而指數S是高光的大小。越小的高光其指數值越大。反射光用下面的公式計算:
      設頂點的單位法向量為N,有公式:
            R + L = (2N • L)N (這裡再次提醒一下,L的方向是由頂點指向光源的)
      由這個可以推出:
這裡寫圖片描述
      該公式中N代表表面法線,L代表光線向量。

      下面的程式碼實現了一個馮氏模型(關於include檔案Common.fxh如何引用請參考上一篇文章):
程式碼段Listing 6.5 Phong.fx

#include "include\\Common.fxh"
/*************** Resources ***************/ cbuffer CBufferPerFrame { float4 AmbientColor : AMBIENT < string UIName = "Ambient Light"; string UIWidget = "Color"; > = {1.0f, 1.0f, 1.0f, 0.0f}; float4 LightColor : COLOR < string Object = "LightColor0"; string UIName = "Light Color"; string UIWidget = "Color"; > = {1.0f, 1.0f, 1.0f, 1.0f}; float3 LightDirection : DIRECTION < string Object = "DirectionalLight0"; string UIName = "Light Direction"; string Space = "World"; > = {0.0f, -1.0f, -1.0f}; float3 CameraPosition : CAMERAPOSITION<string UIWidget="None";>; } cbuffer CBufferPerObject { float4x4 WorldViewProjection : WORLDVIEWPROJECTION <string UIWidget="None";>; float4x4 World : WORLD <string UIWidget="None";>; float4 SpecularColor : SPECULAR < string UIName = "Specular Color"; String UIWidget = "Color"; > = {1.0f, 1.0f, 1.0f, 1.0f}; float SpecularPower : SPECULARPOWER < string UIName = "Specular Power"; string UIWidget = "Slider"; float UIMin = 1.0; float UIMax = 255.0; float UIStep = 1.0; > = {25.0f}; } Texture2D ColorTexture < string ResourceName = "default_color.dds"; string UIName = "Color Texture"; string ResourceType = "2D"; >; SamplerState ColorSampler { Filter = MIN_MAG_MIP_LINEAR; AddressU = WRAP; AddressV = WRAP; }; RasterizerState DisableCulling { CullMode = NONE; }; /*************** Data Structures ***************/ struct VS_INPUT { float4 ObjectPosition : POSITION; float2 TextureCoordinate : TEXCOORD; float3 Normal : NORMAL; }; struct VS_OUTPUT { float4 Position : SV_Position; float3 Normal : NORMAL; float2 TextureCoordinate : TEXCOORD0; float3 LightDirection : TEXCOORD1; float3 ViewDirection : TEXCOORD2; }; /*************** Vertex Shader ***************/ VS_OUTPUT vertex_shader(VS_INPUT IN) { VS_OUTPUT OUT = (VS_OUTPUT)0; OUT.Position = mul(IN.ObjectPosition, WorldViewProjection); OUT.TextureCoordinate = get_corrected_texture_coordinate(IN.TextureCoordinate); OUT.Normal = normalize(mul(float4(IN.Normal, 0), World).xyz); OUT.LightDirection = normalize(-LightDirection); float3 worldPosition = mul(IN.ObjectPosition, World).xyz; OUT.ViewDirection = normalize(CameraPosition - worldPosition); return OUT; } /*************** Pixel Shader ***************/ float4 pixel_shader(VS_OUTPUT IN) : SV_Target { float4 OUT = (float4)0; float3 normal = normalize(IN.Normal); float3 lightDirection = normalize(IN.LightDirection); float3 viewDirection = normalize(IN.ViewDirection); float n_dot_1 = dot(lightDirection, normal); float4 color = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate); float3 ambient = AmbientColor.rgb * AmbientColor.a * color.rgb; float3 diffuse = (float3)0; float3 specular = (float3)0; if(n_dot_1 > 0) { diffuse = LightColor.rgb * LightColor.a * saturate(n_dot_1) * color.rgb; //R = 2 * (N.L) * N - L float3 reflectionVector = normalize(2 * n_dot_1 * normal - lightDirection); //specular = R.V^n with gloss map in color texture's alpha channel specular = SpecularColor.rgb * SpecularColor.a * min(pow(saturate(dot(reflectionVector, viewDirection)), SpecularPower), color.w); OUT.rgb = ambient + diffuse + specular; OUT.a = 1.0f; return OUT; } OUT.rgb = ambient + diffuse; OUT.a = color.a; return OUT; } /*************** Techniques ***************/ technique10 main10 { pass p0 { SetVertexShader(CompileShader(vs_4_0, vertex_shader())); SetGeometryShader(NULL); SetPixelShader(CompileShader(ps_4_0, pixel_shader())); SetRasterizerState(DisableCulling); } }

(2) Phong Preamble : 馮氏光照模型變數準備

      對比上一篇文章中的漫反射效果,CBufferPerFrame緩衝區中只增加了一個成員:CameraPosition。這個常量儲存了攝像機的位置也間接決定了視線方向。當你為這個常量關聯了CAMERAPOSITION語義時,NVIDIA FX Composer會自動將Render面板中的攝像機位置繫結到這個常量上。
      CBufferPerObject緩衝區中增加了兩個成員SpecularColor和SpecularPower。SpecularColor和環境光、平行光有同樣的函式體;他定義了高光的顏色和強度。這個多出來的部分可以讓你獨立調整高光而不用依賴於平行光。SpecularPower是指馮氏高光模型中的指數部分,用來調整高光的強度。
      VS_OUTPUT結構體中也多出一個ViewDirection向量,這個向量用來將計算好的視線方向傳遞給光柵階段進行處理。

(3) Phong Vertex Shader : 馮氏模型頂點著色器

      在頂點著色器中,計算出了視線方向。但計算之前必須先統一座標系,因此需要藉助world矩陣將其轉換到世界座標系下。

(4) Phong Pixel Shader : 馮氏模型畫素著色器

      畫素著色器中新加入了對高光部分的計算。這部分光只有在光線面向模型表面的時候才會有,所以必須先判斷n_dot_1>0時才執行。
      此外,畫素著色器中比較複雜的是計算高光的那段程式碼,先將這段程式碼列在下面:
程式碼段Listing 6.6 Phong.fx中計算高光的程式碼

//specular = R.V^n with gloss map in color texture's alpha channel
specular = SpecularColor.rgb * SpecularColor.a * min(pow(saturate(dot(reflectionVector, viewDirection)), SpecularPower), color.w);

      這段程式碼是根據馮氏光照模型的公式編寫的。使用pow函式,其底數為反射光向量和視線向量的點乘,指數部分為SpeculiarPower常量。saturate()函式限制了計算結果在0.0到1.0之間,也就限制了角度必須在0~90度之間。
      註釋中也說明了高光的計算還依賴於光澤貼圖(gloss map),這個貼圖儲存在紋理貼圖的alpha通道中。光澤貼圖,或者說高光貼圖(specular map)要麼儲存在一張單獨的紋理中(這張紋理只有一個通道),要麼是像本文中的例子,存在材質貼圖的某一部分,例如alpha通道中。高光貼圖根據貼圖製作者的意圖改變了高光的計算結果。本文所使用的地球表面紋理中,海洋部分由於是水面應該是有高光反射的,而陸地部分應該是沒有高光反射的。下面這張圖展示了高光貼圖的內容,可以看出陸地部分是黑色的,其值為0,這部分畫素最後計算出來的高光部分的值應該也是0,因此只保留了漫反射部分的光照,而水面部分則會加入完全的高光效果。
這裡寫圖片描述

(5) Phong Output : 馮氏光照模型效果輸出

      下面的圖片中,左圖展示了帶有高光通道的貼圖的渲染效果,右圖的紋理中則不帶有高光通道,注意觀察他們在陸地部分的反射光效果區別:
這裡寫圖片描述

(6) Blinn-Phong —— 改進的馮氏光照模型

      1977年,Jim Blinn提出了簡化版的馮氏模型,基於Half-Vector對光照模型的計算進行修改,Half-Vector是入射光與視線向量的和向量的一半(其實也等於入射光向量與視線向量的中間向量,因為在計算Half-Vector前要對這兩個向量先進行單位化),計算公式如下:
這裡寫圖片描述
      從該公式可以看出,計算出的H(Half-Vector)是視線向量和入射光線向量想加後的單位向量。Blinn-Phong光照模型由表面法線向量和Half-Vector計算出(與Phong模型的反射光和視線向量不同),這樣增大了公式中的底數,計算公式如下:
這裡寫圖片描述

(7) Blinn-Phong Pixel Shader : Blinn-Phong光照模型畫素著色器

      由於Blinn-Phong光照模型是在Phong光照模型的基礎上修改的,他們大部分程式碼都是相似的,因此,只在此列出不同的部分,以下是畫素著色器程式碼:

程式碼段 Listing 6.7 BlinnPhong.fx中的畫素著色器

float4 pixel_shader(VS_OUTPUT IN) : SV_Target
{
    float4 OUT = (float4)0;

    float3 normal = normalize(IN.Normal);
    float3 lightDirection = normalize(IN.LightDirection);
    float3 viewDirection = normalize(IN.ViewDirection);
    float n_dot_1 = dot(lightDirection, normal);

    float4 color = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
    float3 ambient = AmbientColor.rgb * AmbientColor.a * color.rgb;

    float3 diffuse = (float3)0;
    float3 specular = (float3)0;

    if(n_dot_1 > 0)
    {
        diffuse = LightColor.rgb * LightColor.a * saturate(n_dot_1) * color.rgb;

        float3 halfVector = normalize(lightDirection + viewDirection);

        //specular = N.H^s with gloss map in color texture's alpha channel
        specular = SpecularColor.rgb * SpecularColor.a * min(pow(saturate(dot(normal, halfVector)), SpecularPower), color.w);

        OUT.rgb = ambient + diffuse + specular;
        OUT.a = 1.0f;

        return OUT;
    }

    OUT.rgb = ambient + diffuse + specular;
    OUT.a = 1.0f;

    return OUT;
}

      Blinn-Phong光照模型和Phong光照模型的顯示效果幾乎是一樣的,但由於他的底數加大了,所以需要調整指數,也就是高光強度部分才能和Phong光照模型效果一樣。如下圖所示,左圖為Blinn-Phong光照模型渲染效果,右圖為Phong光照模型渲染效果,兩個material的光照強度值設定的都為25:
這裡寫圖片描述
      可以看出Blinn-Phong光照模型的光斑在同樣的光照強度引數下明顯要比Phong光照模型的大些。

(8) Blinn-Phong with Intrinsics : Blinn-Phong光照模型實現中使用行內函數

      關於行內函數(Intrinsics)的解釋清查閱參考資料【1】中的instrinsics部分。

關於Intrinsics函式的原文引用說明
      行內函數有可能會被誤會成我們通常想的那樣,主要是這個單詞我翻譯不正確(intrinsics).這樣的函式可以被C和C++的程式所呼叫.看上去和別的函式沒有很多的區別,最多也就名字比較古怪.但是其實當這個程式碼在被編譯器編譯的時候,它會被轉化為有序的低階指令.這些指令就是NEON的指令了.所以這樣就辦到了在高階語言層次使用低階語言了.主要是很簡單的可以使用.最為主要的就是程式設計師不用去接觸彙編了,可以減小優化的難度.當然我可以說這樣的優化效率沒有使用匯編的來的高.
      對於上面的這種技術其實就是ARM公司本身給你做好了一些函式,你就直接呼叫這些函式,這些函式在編譯的時候就可以直接轉化成NEON的彙編指令.為了支援這些內聯的函式所以必須要包含標頭檔案arm_neon.h.

      文章中重要提到的是C裡面的Intrinsics函式,但HLSL中的Intrinsics函式原理和他應該是類似的。

      HLSL中提供了lit()函式,用來幫助計算Lambertian模型的漫反射部分和Blinn-Phong模型的高光部分。一個提升效能的很好的辦法就是儘量在能使用Intrinsics函式或者說內建函式的時候使用它們,因為如上文所說這型別的函式在硬體中進行過優化。因此,我們將用lit()函式重寫過的Blinn-Phong模型畫素著色器列在下面:

程式碼段Listing 6.8 重寫的Blinn-Phong光照模型畫素著色器

float4 pixel_shader(VS_OUTPUT IN) : SV_Target
{
    float4 OUT = (float4)0;

    float3 normal = normalize(IN.Normal);
    float3 lightDirection = normalize(IN.LightDirection);
    float3 viewDirection = normalize(IN.ViewDirection);
    float n_dot_1 = dot(normal, lightDirection);
    float3 halfVector = normalize(lightDirection + viewDirection);
    float n_dot_h = dot(normal, halfVector);

    float4 color = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
    float4 lightCoefficients = lit(n_dot_1, n_dot_h, SpecularPower);

    float3 ambient = get_vector_color_contribution(AmbientColor, color.rgb);
    float3 diffuse = get_vector_color_contribution(LightColor, lightCoefficients.y * color.rgb);
    float3 specular = get_scalar_color_contribution(SpecularColor, min(lightCoefficients.z, color.w));

    OUT.rgb = ambient + diffuse + specular;
    OUT.a = 1.0f;

    return OUT;
}

      以上程式碼的改寫和畫素著色器有很多不同,為計算每種燈光單獨封裝了函式,所封裝的get_vector_color_contribution和get_scalar_color_contribution函式在第七篇文章中提到的Common.fxh檔案中已經寫好了。並且在這個著色器中移除了條件判斷句。新增加了lit()的呼叫,這個函式接受將n_dot_1和n_dot_h、高光強度作為輸入引數,能實現和之前計算漫反射和blinn-phong高光模型計算出的結果一樣的效果,返回值為float4,x和w的輸出值總是1,y值代表的是這個畫素上漫反射光照的計算結果,z值代表這個畫素上blinn-phong光照計算結果。

(9) Blinn-Phong vs. Phong : 兩種高光模型計算的對比

      Blinn-Phong 和 Phong兩種光照模型的實現效果相似,只是需要調節高光強度的大小不一樣,還有應該就是渲染效率的問題。由於Blinn-Phong 中涉及到平方根的運算(這個運算來源於對half-vector向量的normalize計算)。但是,之後我們使用了lit()函式,這個行內函數使得Blinn-Phong 比Phong模型的計算又稍微提升了一些,並且減少了程式碼量。因此,在後面需要使用到帶有漫反射和高光的模型時都將使用這個函式。

總結

      這篇文章中主要講解了兩種高光模型的演算法和shader實現。需要注意的是,本文中主要介紹了統一模型上混合了高光和漫反射的情況,但現實中也有很多模型是單一的,即比如玻璃杯和紫砂壺這類的物品他們的表面材質大部分是統一的。這種情況比較簡單,只要注意對高光貼圖的使用即可。

參考連結