這一系列依賴於最小規模的用於着色器和渲染工具的lwjgl-basics API. 代碼已經被移植到 LibGDX. 這些概念是足夠通用的, 它們能被應用於Love2D, GLSL Sandbox, iOS
, 或者其餘支持 GLSL
的平臺.php
本文聚焦於 3D
光照和法線貼圖技術, 以及咱們如何把它們應用到 2D
遊戲中, 示範下圖所示, 左邊是紋理貼圖, 右邊實時應用了光照:html
一旦你理解了光照的概念, 把它應用於任何設置都是很是直截了當的. 這裏是一個 Java4K
示例中的法線貼圖的例子, 例如, 經過軟件渲染:java
效果跟這個 YouTube流行視頻 和這個 Love2D示例 中展現的同樣, 你還能夠在 [GLSL] Using Normal Maps to Illuminate a 2D Texture (LibGDX) 看到效果, 其中包括一個可執行的示例.ios
正如咱們在以前的教程中討論過的, 一個 GLSL
向量是一個浮點數的容器, 一般保存諸如位置(x,y,z)
之類的值. 在數學中,向量意味着至關多的內容,以及用於表示長度(即大小)和方向. 若是你對向量很陌生而且想要學習關於它們更多一些的知識, 查看下面這些連接:git
爲了計算光照, 咱們須要使用網格的"法線
". 一個表面法線是一個垂直於切線平面的向量. 簡單來講, 它是一個向量, 垂直於給定頂點處的網格. 下面咱們會看到一個網格, 每一個頂點都有一條法線.github
每一個向量都指向外面, 遵循着網格的彎曲形狀. 下面是另外一個例子, 此次是一個簡單的 2D
邊沿視圖:算法
法線貼圖
(Normal Mapping
)是一個遊戲編程技巧, 它容許咱們渲染相同數目的多邊形(例如低解析度的網格模型), 可是在計算光照時使用高解析度網格模型的法線. 這爲咱們帶來更好的感覺, 關於深度, 真實性和光滑度.編程
(圖像來自於這個出色的博客文章Making Worlds 3 - That's no Moon...)app
高面數網格模型或者說精雕模型的法線被編碼到一個紋理貼圖(即法線圖)中, 當咱們渲染低面數網格模型時會從片斷着色器中對它進行取樣. 結果以下:less
譯者注: 左側是4百萬個三角形的高模, 中間是500個三角形的低模, 右側是在500個三角形的低模上使用法線貼圖後的效果
咱們的表面法線是單位向量, 一般位於範圍 -1.0
到 1.0
之間. 咱們能夠經過把法線範圍轉換爲 0.0
到 1.0
之間來把法線向量(x, y, z)
存儲到一個 RGB
紋理貼圖中. 下面是僞碼:
Color.rgb = Normal.xyz / 2.0 + 0.5;
例如, 一個法線 (-1, 0, 1)
會被做爲 RGB
編碼爲 (0, 0.5, 1)
. x
軸(左/右)被保存到紅色通道, y
軸(上/下)被保存到綠色通道, z
軸(前/後)被保存到藍色通道. 最終的法線圖(normal map
)看起來就是下面這個樣子:
典型地, 咱們使用程序來生成法線圖, 而不是手動繪製.
理解法線圖, 把每一個通道獨立出來查看會更清楚:
看着,綠色通道,咱們看到更亮的部分(值更接近於 1.0
) 定義了法線指向上方的區域,而更暗的區域(值更接近爲 0.0
) 定義了法線指向下方的區域. 大多數的法線圖會是藍色,由於Z
軸(藍色通道)一般指向咱們(即值爲 1.0
).
在咱們遊戲的片斷着色器中, 咱們能夠把法線解碼, 經過執行跟以前編碼時相反的操做, 把顏色值展開爲範圍 -1.0
到 1.0
之間:
//sample the normal map NormalMap = texture2D(NormalMapTex, TexCoord); //convert to range -1.0 to 1.0 Normal.xyz = NormalMap.rgb * 2.0 - 1.0;
注意: 要記住不一樣的引擎和軟件會使用不一樣的座標系, 綠色通道可能須要翻轉.
在計算機圖形學中, 咱們有大量的算法,能夠結合起來打造 3D
對象的不一樣渲染效果. 在這篇文章咱們將專一於 Lambert
着色,沒有任何反射(諸如"光澤"或"發光"). 其餘的技術,像Phong
, Cook-Torrance
, 和 Oren–Nayar
, 能夠用來產生不一樣的視覺效果(粗糙表面、 有光澤的表面等等)。
咱們整個光照模型看起來像這樣:
N = normalize(Normal.xyz) L = normalize(LightDir.xyz) Diffuse = LightColor * max(dot(N, L), 0.0) Ambient = AmbientColor * AmbientIntensity Attenuation = 1.0 / (ConstantAtt + (LinearAtt * Distance) + (QuadraticAtt * Distance * Distance)) Intensity = Ambient + Diffuse * Attenuation FinalColor = DiffuseColor.rgb * Intensity.rgb
說實話,你不須要從數學角度理解爲何這個能夠起做用,但若是你有興趣, 能夠閱讀更多有關"N dot L
"的內容, 在這裏GLSL Tutorial – Directional Lights per Vertex I和這裏Lambertian reflectance.
一些關鍵的術語:
Normal-法線
: 從法線圖中解碼獲得的法線向量 XYZ.LightDir-光線方向
: 從物體表面到光源位置的向量, 咱們將會簡單解釋.Diffuse Color-漫射顏色
: 紋理貼圖的 RGB 顏色, 沒有光.Diffuse-漫射
: 跟Lambertian
反射相乘的光線顏色, 這是咱們光照等式的主要部分.Ambient-環境光
: 處於陰影中的顏色和強度, 例如, 一個戶外場景會有一個更亮的環境光強度, 比起一個暗淡燈光下的戶內場景.Attenuation-衰減
: 這是光線的隨距離而下降
, 例如, 當咱們遠離點光源時強度/亮度
的損失. 有多種方法來計算衰減--對於咱們的目標而言, 咱們將會使用常量-線性-二次方
衰減. 這裏用3
個係數來計算衰減, 咱們能夠改變它們來影響光線衰減的視覺效果.Intensity-強度
: 咱們陰影算法的強度--離1.0
越近意味着有光, 離0.0
越近意味着沒有光.下面的圖有助於你對咱們的光照模型有個直觀的理解:
正如你所見, 感受它是至關模塊化的, 咱們能夠拿走那些不須要的部分, 就像衰減(attenuation
) 或光線顏色(light colors
).
如今, 讓咱們把它們應用到 GLSL
模型上. 注意咱們只處理 2D
, 在 3D
中還有一些額外的考慮在這篇教程沒有覆蓋到(譯者注:就是空間變換, 在 3D
場景下, 法線圖中的法線所在的空間爲正切空間, 光線所在的空間爲世界空間, 須要統一到同一個空間計算纔有意義). 咱們將把模型分解爲多個單獨部分, 每個都創建在下面的基礎上.
你能夠在這裏看到Java代碼示例. 它是相對直截了當的, 並不會介紹過多的在在前面的課程中尚未討論過的內容. 咱們將使用如下兩種紋理貼圖︰
咱們的示例根據鼠標位置(歸一化到分辨率)調整 LightPos.xy
, 根據鼠標滾輪(點擊則重置光線的 Z
值)調整 LightPos.z
(深度). 在特定的座標系中, 就像 LibGDX
, 你可能須要翻轉 Y
值.
注意, 咱們的例子使用了以下這些常量, 你能夠調整它們來得到不一樣的視覺效果:
public static final float DEFAULT_LIGHT_Z = 0.075f; ... //Light RGB and intensity (alpha) public static final Vector4f LIGHT_COLOR = new Vector4f(1f, 0.8f, 0.6f, 1f); //Ambient RGB and intensity (alpha) public static final Vector4f AMBIENT_COLOR = new Vector4f(0.6f, 0.6f, 1f, 0.2f); //Attenuation coefficients for light falloff public static final Vector3f FALLOFF = new Vector3f(.4f, 3f, 20f);
下面是咱們的渲染代碼, 就像 教程4 同樣, 咱們會在渲染時使用多重紋理:
... //update light position, normalized to screen resolution float x = Mouse.getX() / (float)Display.getWidth(); float y = Mouse.getY() / (float)Display.getHeight(); LIGHT_POS.x = x; LIGHT_POS.y = y; //send a Vector4f to GLSL shader.setUniformf("LightPos", LIGHT_POS); //bind normal map to texture unit 1 glActiveTexture(GL_TEXTURE1); rockNormals.bind(); //bind diffuse color to texture unit 0 glActiveTexture(GL_TEXTURE0); rock.bind(); //draw the texture unit 0 with our shader effect applied batch.draw(rock, 50, 50);
陰影貼圖的結果:
下面對光線使用了更低的 Z
值:
這裏是咱們完整的片斷着色器
//attributes from vertex shader varying vec4 vColor; varying vec2 vTexCoord; //our texture samplers uniform sampler2D u_texture; //diffuse map uniform sampler2D u_normals; //normal map //values used for shading algorithm... uniform vec2 Resolution; //resolution of screen uniform vec3 LightPos; //light position, normalized uniform vec4 LightColor; //light RGBA -- alpha is intensity uniform vec4 AmbientColor; //ambient RGBA -- alpha is intensity uniform vec3 Falloff; //attenuation coefficients void main() { //RGBA of our diffuse color vec4 DiffuseColor = texture2D(u_texture, vTexCoord); //RGB of our normal map vec3 NormalMap = texture2D(u_normals, vTexCoord).rgb; //The delta position of light vec3 LightDir = vec3(LightPos.xy - (gl_FragCoord.xy / Resolution.xy), LightPos.z); //Correct for aspect ratio LightDir.x *= Resolution.x / Resolution.y; //Determine distance (used for attenuation) BEFORE we normalize our LightDir float D = length(LightDir); //normalize our vectors vec3 N = normalize(NormalMap * 2.0 - 1.0); vec3 L = normalize(LightDir); //Pre-multiply light color with intensity //Then perform "N dot L" to determine our diffuse term vec3 Diffuse = (LightColor.rgb * LightColor.a) * max(dot(N, L), 0.0); //pre-multiply ambient color with intensity vec3 Ambient = AmbientColor.rgb * AmbientColor.a; //calculate attenuation float Attenuation = 1.0 / ( Falloff.x + (Falloff.y*D) + (Falloff.z*D*D) ); //the calculation which brings it all together vec3 Intensity = Ambient + Diffuse * Attenuation; vec3 FinalColor = DiffuseColor.rgb * Intensity; gl_FragColor = vColor * vec4(FinalColor, DiffuseColor.a); }
如今, 把它分解. 首先, 咱們從兩個紋理貼圖中取樣:
//RGBA of our diffuse color vec4 DiffuseColor = texture2D(u_texture, vTexCoord); //RGB of our normal map vec3 NormalMap = texture2D(u_normals, vTexCoord).rgb;
接着, 咱們須要從當前的片斷(譯者注:即像素)肯定光線向量, 而且糾正它的縱橫比例(aspect ratio
). 而後在歸一化(normalize
)以前肯定 LightDir
向量的值(長度):
//Delta pos vec3 LightDir = vec3(LightPos.xy - (gl_FragCoord.xy / Resolution.xy), LightPos.z); //Correct for aspect ratio LightDir.x *= Resolution.x / Resolution.y; //determine magnitude float D = length(LightDir);
在咱們的光照模型中, 咱們須要從 NormalMap.rgb
中解碼 Normal.xyz
, 而且歸一化咱們的向量:
vec3 N = normalize(NormalMap * 2.0 - 1.0); vec3 L = normalize(LightDir);
下一步是計算 Diffuse
(漫射) 項. 爲了這個, 咱們須要使用 LightColor
. 在咱們的例子中, 咱們將會把光線顏色(RGB
)和強度(alpha
)相乘: LightColor.rgb * LightColor.a
. 所以, 全部這些看起來以下:
//Pre-multiply light color with intensity //Then perform "N dot L" to determine our diffuse term vec3 Diffuse = (LightColor.rgb * LightColor.a) * max(dot(N, L), 0.0);
接着, 咱們預相乘(pre-multiply)環境顏色(ambient color
)和強度:
vec3 Ambient = AmbientColor.rgb * AmbientColor.a;
下一步是用咱們的 LightDir
的值(前面計算好的)來肯定衰減
(Attenuation
). 統一變量降低係數
(Falloff
) 定義了咱們的常量, 線性和2次方的衰減係數:
float Attenuation = 1.0 / ( Falloff.x + (Falloff.y*D) + (Falloff.z*D*D) );
接着, 計算光強度
(Intensity
)和最終顏色
(FinalColor
), 而且把它們傳遞給 gl_FragColor
. 注意, 咱們機智地保留了 DiffuseColor
的 alpha
值:
vec3 Intensity = Ambient + Diffuse * Attenuation; vec3 FinalColor = DiffuseColor.rgb * Intensity; gl_FragColor = vColor * vec4(FinalColor, DiffuseColor.a);
LightDir
和 attenuation
依賴於分辨率. 這意味着更改分辨率會影響咱們的光的衰減. 根據你的遊戲,不一樣的實現上分辨率無關多是必需的.Y
座標系和你所採用的法線圖生成程序(例如 CrazyBump
)之間的差別. 一些程序容許你導出一個翻轉了Y
軸的法線圖. 下面的圖片展現了這個問題:實現多光源, 咱們只要簡單地調整一下算法, 以下:
vec3 Sum = vec3(0.0); for (... each light ...) { ... calculate light using our illumination model ... Sum += FinalColor; } gl_FragColor = vec4(Sum, DiffuseColor.a);
注意, 這樣會在你的着色器中引入更多分支(譯者注:也就是這個循環), 它會致使性能下降.
這有時被稱爲"N 照明
"(N lighting
), 由於咱們的系統僅支持一個固定數目 N
的光源. 若是你計劃包括大量的光源, 你可能想要調查多個繪製調用(例如 additive blending
), 或延遲渲染Deferred shading.
在某個時間點, 你可能會問本身:"爲何我不直接作一個3D
遊戲?". 比起試着把這些概念應用到 2D
精靈來講, 這是個正當的問題而且可能會帶來更好的性能和更少的開發時間.
這裏有各類從一張圖片生成法線圖的方法. 用於轉換2D
圖像爲法線圖的經常使用程序和濾鏡包括以下:
注意, 不少程序都會產生鋸齒和錯誤, 閱讀這篇文章How NOT To Make Normal Maps From Photos Or Images來得到更多細節.
你也能使用 3D
建模軟件, 如 Blender 或 ZBrush 來精心雕琢出高質量的法線圖.
一個工做流的想法是, 生成一個低面數,很是粗糙的 3D
對象在你的藝術資源中. 而後你可使用這個 Blender Template: Normal Map Pass 把你的對象渲染爲一個 2D
正切空間內的法線圖. 而後你就能在 PhotoShop
中打開這個法線圖而且處理這個漫射(diffuse)顏色圖了.
下面是一個 Blender
模板的樣子:
在建立個人 WebGL
的 法線圖像素藝術演示時, 有一堆我不得不考慮的事項. 你能夠從這裏查看源碼和細節.
效果以下圖:
在這個示例中, 我想讓衰減做爲一個風格元素變得可見. 典型的作法帶來很是平滑的衰減, 它和塊狀像素藝術風格衝突. 相反, 我使用 cel shading
的光線, 給它一個階梯狀的衰減. 經過片斷着色器中的 if-else
語句實現了簡單的卡通着色.
下一步的考慮是, 咱們但願光線的邊緣像素的比例隨着精靈(sprites)的像素變化. 實現這個目標的一個方法是經過光照着色器把咱們的場景繪製到一個 FBO
中, 而後用一個默認的着色器以一個較大的尺寸把它渲染到屏幕上. 在咱們的塊狀像素藝術中這種照明方式影響整個"紋素
"(texels
).