GLSL 程式設計/Unity/多光源

本教程介紹了基於影像的照明,特別是漫反射(輻照度)環境對映及其在立方體貼圖中的實現。
本教程基於“反射表面”部分。如果您還沒有閱讀本教程,現在是一個非常好的時間閱讀它。
考慮左側影像中雕塑的照明。有自然光從窗戶照進來。一些光線在到達雕塑之前會從地板、牆壁和參觀者身上反射回來。此外,還有人工光源,它們的光線也直接和間接地照射到雕塑上。需要多少個方向光源和點光源才能令人信服地模擬這種複雜的照明環境?至少要超過幾個(可能要超過十幾個),因此照明計算的效能具有挑戰性。
基於影像的照明解決了這個問題。對於由環境貼圖(例如立方體貼圖)描述的靜態照明環境,基於影像的照明允許我們透過在立方體貼圖中進行一次紋理查詢來計算任意數量的光源的照明(有關立方體貼圖的描述,請參見“反射表面”部分)。它是如何工作的?
在本節中,我們將重點介紹漫反射照明。假設立方體貼圖的每個紋素(即畫素)都充當一個方向光源。(請記住,立方體貼圖通常被認為是無限大的,因此只有方向重要,而位置無關。)可以使用“漫反射”部分中描述的方法來計算給定表面法線方向的最終照明。它基本上是表面法線向量N和指向光源的向量L之間的餘弦
由於紋素是光源,L僅僅是立方體中心到立方體貼圖中紋素中心的方位。一個小的立方體貼圖,每個面有 32×32 個紋素,已經擁有 32×32×6 = 6144 個紋素。透過數千個光源進行照明將無法即時完成。但是,對於靜態立方體貼圖,我們可以預先計算所有可能的表面法線向量N的漫反射照明並將其儲存在一個查詢表中。當用特定表面法線向量照明表面上的一個點時,我們可以從該預計算的查詢表中查詢特定表面法線向量N的漫反射照明。
因此,對於特定表面法線向量N,我們新增(即積分)立方體貼圖中所有紋素的漫反射照明。我們將此表面法線向量的最終漫反射照明儲存在第二個立方體貼圖(“漫反射輻照度環境貼圖”或簡稱“漫反射環境貼圖”)中。這個第二個立方體貼圖將充當一個查詢表,其中每個方位(即表面法線向量)都對映到一個顏色(即可能由數千個光源產生的漫反射照明)。因此,片段著色器非常簡單(這個片段著色器可以使用“反射表面”部分中的頂點著色器)
#ifdef FRAGMENT
void main()
{
gl_FragColor = textureCube(_Cube, normalDirection);
}
#endif
它只是一個使用柵格化表面點上的表面法線向量查詢預計算漫反射照明的操作。但是,漫反射環境貼圖的預計算稍微複雜一些,將在下一節中介紹。
本節提供一些 JavaScript 程式碼來說明漫反射(輻照度)環境貼圖的立方體貼圖計算。為了在 Unity 中使用它,請在專案檢視中選擇建立 > JavaScript。然後在 Unity 的文字編輯器中開啟指令碼,將 JavaScript 程式碼複製到其中,並將指令碼附加到具有以下著色器材料的遊戲物件上。當為著色器屬性_OriginalCube(在著色器使用者介面中標記為環境貼圖)指定了一個新的尺寸足夠小的立方體貼圖時,該指令碼將使用相應的漫反射環境貼圖更新著色器屬性_Cube(即使用者介面中的漫反射環境貼圖)。請注意,該指令碼只接受面尺寸為 32×32 或更小的立方體貼圖,因為對於更大的立方體貼圖,計算時間往往很長。因此,在 Unity 中建立立方體貼圖時,請確保選擇一個尺寸足夠小的立方體貼圖。
該指令碼只包含幾個函式:Awake() 初始化變數;Update()負責與使用者和材料進行通訊(即讀取和寫入著色器屬性);computeFilteredCubemap()執行計算漫反射環境貼圖的實際工作;getDirection()是一個用於computeFilteredCubemap()的小實用程式函式,用於計算立方體貼圖中每個紋素相關的方向。請注意,computeFilteredCubemap()不僅會整合漫反射照明,還會透過將沿接縫的相鄰紋素設定為相同的平均顏色來避免立方體貼圖面之間的不連續接縫。
@script ExecuteInEditMode()
private var originalCubemap : Cubemap; // a reference to the
// environment map specified in the shader by the user
private var filteredCubemap : Cubemap; // the diffuse irradiance
// environment map computed by this script
function Update()
{
var originalTexture : Texture =
renderer.sharedMaterial.GetTexture("_OriginalCube");
// get the user-specified environment map
if (originalTexture == null)
// did the user specify "None" for the environment map?
{
if (originalCubemap != null)
{
originalCubemap = null;
filteredCubemap = null;
renderer.sharedMaterial.SetTexture("_Cube", null);
}
return;
}
else if (originalTexture == originalCubemap
&& filteredCubemap != null
&& null == renderer.sharedMaterial.GetTexture("_Cube"))
{
renderer.sharedMaterial.SetTexture("_Cube", filteredCubemap);
// set the computed diffuse environment map in the shader
}
else if (originalTexture != originalCubemap
|| filteredCubemap
!= renderer.sharedMaterial.GetTexture("_Cube"))
// has the user specified a cube map that is different of
// what we had processed previously?
{
if (EditorUtility.DisplayDialog("Processing of Environment Map",
"Do you want to process the cube map of face size "
+ originalTexture.width + "x" + originalTexture.width
+ "? (This will take some time.)",
"OK", "Cancel"))
// does the user really want to process this cube map?
{
originalCubemap = originalTexture;
if (filteredCubemap
!= renderer.sharedMaterial.GetTexture("_Cube"))
{
if (null != renderer.sharedMaterial.GetTexture("_Cube"))
{
DestroyImmediate(renderer.sharedMaterial.GetTexture(
"_Cube")); // clean up
}
}
if (null != filteredCubemap)
{
DestroyImmediate(filteredCubemap); // clean up
}
computeFilteredCubemap();
// compute the diffuse environment map
renderer.sharedMaterial.SetTexture("_Cube", filteredCubemap);
// set the computed diffuse environment map in the shader
}
else // no cancel the processing and reset everything
{
originalCubemap = null;
filteredCubemap = null;
renderer.sharedMaterial.SetTexture("_OriginalCube", null);
renderer.sharedMaterial.SetTexture("_Cube", null);
}
}
return;
}
function computeFilteredCubemap()
// This function computes a diffuse environment map in
// "filteredCubemap" of the same dimensions as "originalCubemap"
// by integrating -- for each texel of "filteredCubemap" --
// the diffuse illumination from all texels of "originalCubemap"
// for the surface normal vector corresponding to the direction
// of each texel of "filteredCubemap".
{
filteredCubemap = Cubemap(originalCubemap.width,
originalCubemap.format, true);
// create the diffuse environment cube map
var filteredSize : int = filteredCubemap.width;
var originalSize : int = originalCubemap.width;
// compute all texels of the diffuse environment
// cube map by iterating over all of them
for (var filteredFace : int = 0; filteredFace < 6; filteredFace++)
{
for (var filteredI : int = 0; filteredI < filteredSize; filteredI++)
{
for (var filteredJ : int = 0; filteredJ < filteredSize; filteredJ++)
{
var filteredDirection : Vector3 =
getDirection(filteredFace,
filteredI, filteredJ, filteredSize).normalized;
var totalWeight : float = 0.0;
var originalDirection : Vector3;
var originalFaceDirection : Vector3;
var weight : float;
var filteredColor : Color = Color(0.0, 0.0, 0.0);
// sum (i.e. integrate) the diffuse illumination
// by all texels in the original environment map
for (var originalFace : int = 0; originalFace < 6; originalFace++)
{
originalFaceDirection = getDirection(originalFace,
1, 1, 3).normalized; // the normal vector of the face
for (var originalI : int = 0; originalI < originalSize; originalI++)
{
for (var originalJ : int = 0; originalJ < originalSize; originalJ++)
{
originalDirection = getDirection(originalFace,
originalI, originalJ, originalSize);
// direction to the texel, i.e. light source
weight = 1.0 / originalDirection.sqrMagnitude;
// take smaller size of more distant texels
// into account
originalDirection = originalDirection.normalized;
weight = weight
* Vector3.Dot(originalFaceDirection,
originalDirection); // take tilt of texels
// compared to face into account
weight = weight * Mathf.Max(0.0,
Vector3.Dot(filteredDirection,
originalDirection));
// directional filter for diffuse illumination
totalWeight = totalWeight + weight;
// instead of analytically normalization,
// we just normalize to the potentially
// maximum illumination
filteredColor = filteredColor +
weight * originalCubemap.GetPixel(originalFace,
originalI, originalJ);
// add the illumination by this texel
}
}
}
filteredCubemap.SetPixel(filteredFace, filteredI,
filteredJ, filteredColor / totalWeight);
// store the diffuse illumination of this texel
}
}
}
// Avoid seams between cube faces:
// average edge texels to the same color on both sides of the seam
// (except corner texels, see below)
var maxI : int = filteredCubemap.width - 1;
var average : Color;
for (var i : int = 1; i < maxI; i++)
{
average = (filteredCubemap.GetPixel(0, i, 0)
+ filteredCubemap.GetPixel(2, maxI, maxI - i)) / 2.0;
filteredCubemap.SetPixel(0, i, 0, average);
filteredCubemap.SetPixel(2, maxI, maxI - i, average);
average = (filteredCubemap.GetPixel(0, 0, i)
+ filteredCubemap.GetPixel(4, maxI, i)) / 2.0;
filteredCubemap.SetPixel(0, 0, i, average);
filteredCubemap.SetPixel(4, maxI, i, average);
average = (filteredCubemap.GetPixel(0, i, maxI)
+ filteredCubemap.GetPixel(3, maxI, i)) / 2.0;
filteredCubemap.SetPixel(0, i, maxI, average);
filteredCubemap.SetPixel(3, maxI, i, average);
average = (filteredCubemap.GetPixel(0, maxI, i)
+ filteredCubemap.GetPixel(5, 0, i)) / 2.0;
filteredCubemap.SetPixel(0, maxI, i, average);
filteredCubemap.SetPixel(5, 0, i, average);
average = (filteredCubemap.GetPixel(1, i, 0)
+ filteredCubemap.GetPixel(2, 0, i)) / 2.0;
filteredCubemap.SetPixel(1, i, 0, average);
filteredCubemap.SetPixel(2, 0, i, average);
average = (filteredCubemap.GetPixel(1, 0, i)
+ filteredCubemap.GetPixel(5, maxI, i)) / 2.0;
filteredCubemap.SetPixel(1, 0, i, average);
filteredCubemap.SetPixel(5, maxI, i, average);
average = (filteredCubemap.GetPixel(1, i, maxI)
+ filteredCubemap.GetPixel(3, 0, maxI - i)) / 2.0;
filteredCubemap.SetPixel(1, i, maxI, average);
filteredCubemap.SetPixel(3, 0, maxI - i, average);
average = (filteredCubemap.GetPixel(1, maxI, i)
+ filteredCubemap.GetPixel(4, 0, i)) / 2.0;
filteredCubemap.SetPixel(1, maxI, i, average);
filteredCubemap.SetPixel(4, 0, i, average);
average = (filteredCubemap.GetPixel(2, i, 0)
+ filteredCubemap.GetPixel(5, maxI - i, 0)) / 2.0;
filteredCubemap.SetPixel(2, i, 0, average);
filteredCubemap.SetPixel(5, maxI - i, 0, average);
average = (filteredCubemap.GetPixel(2, i, maxI)
+ filteredCubemap.GetPixel(4, i, 0)) / 2.0;
filteredCubemap.SetPixel(2, i, maxI, average);
filteredCubemap.SetPixel(4, i, 0, average);
average = (filteredCubemap.GetPixel(3, i, 0)
+ filteredCubemap.GetPixel(4, i, maxI)) / 2.0;
filteredCubemap.SetPixel(3, i, 0, average);
filteredCubemap.SetPixel(4, i, maxI, average);
average = (filteredCubemap.GetPixel(3, i, maxI)
+ filteredCubemap.GetPixel(5, maxI - i, maxI)) / 2.0;
filteredCubemap.SetPixel(3, i, maxI, average);
filteredCubemap.SetPixel(5, maxI - i, maxI, average);
}
// Avoid seams between cube faces: average corner texels
// to the same color on all three faces meeting in one corner
average = (filteredCubemap.GetPixel(0, 0, 0)
+ filteredCubemap.GetPixel(2, maxI, maxI)
+ filteredCubemap.GetPixel(4, maxI, 0)) / 3.0;
filteredCubemap.SetPixel(0, 0, 0, average);
filteredCubemap.SetPixel(2, maxI, maxI, average);
filteredCubemap.SetPixel(4, maxI, 0, average);
average = (filteredCubemap.GetPixel(0, maxI, 0)
+ filteredCubemap.GetPixel(2, maxI, 0)
+ filteredCubemap.GetPixel(5, 0, 0)) / 3.0;
filteredCubemap.SetPixel(0, maxI, 0, average);
filteredCubemap.SetPixel(2, maxI, 0, average);
filteredCubemap.SetPixel(5, 0, 0, average);
average = (filteredCubemap.GetPixel(0, 0, maxI)
+ filteredCubemap.GetPixel(3, maxI, 0)
+ filteredCubemap.GetPixel(4, maxI, maxI)) / 3.0;
filteredCubemap.SetPixel(0, 0, maxI, average);
filteredCubemap.SetPixel(3, maxI, 0, average);
filteredCubemap.SetPixel(4, maxI, maxI, average);
average = (filteredCubemap.GetPixel(0, maxI, maxI)
+ filteredCubemap.GetPixel(3, maxI, maxI)
+ filteredCubemap.GetPixel(5, 0, maxI)) / 3.0;
filteredCubemap.SetPixel(0, maxI, maxI, average);
filteredCubemap.SetPixel(3, maxI, maxI, average);
filteredCubemap.SetPixel(5, 0, maxI, average);
average = (filteredCubemap.GetPixel(1, 0, 0)
+ filteredCubemap.GetPixel(2, 0, 0)
+ filteredCubemap.GetPixel(5, maxI, 0)) / 3.0;
filteredCubemap.SetPixel(1, 0, 0, average);
filteredCubemap.SetPixel(2, 0, 0, average);
filteredCubemap.SetPixel(5, maxI, 0, average);
average = (filteredCubemap.GetPixel(1, maxI, 0)
+ filteredCubemap.GetPixel(2, 0, maxI)
+ filteredCubemap.GetPixel(4, 0, 0)) / 3.0;
filteredCubemap.SetPixel(1, maxI, 0, average);
filteredCubemap.SetPixel(2, 0, maxI, average);
filteredCubemap.SetPixel(4, 0, 0, average);
average = (filteredCubemap.GetPixel(1, 0, maxI)
+ filteredCubemap.GetPixel(3, 0, maxI)
+ filteredCubemap.GetPixel(5, maxI, maxI)) / 3.0;
filteredCubemap.SetPixel(1, 0, maxI, average);
filteredCubemap.SetPixel(3, 0, maxI, average);
filteredCubemap.SetPixel(5, maxI, maxI, average);
average = (filteredCubemap.GetPixel(1, maxI, maxI)
+ filteredCubemap.GetPixel(3, 0, 0)
+ filteredCubemap.GetPixel(4, 0, maxI)) / 3.0;
filteredCubemap.SetPixel(1, maxI, maxI, average);
filteredCubemap.SetPixel(3, 0, 0, average);
filteredCubemap.SetPixel(4, 0, maxI, average);
filteredCubemap.Apply();
// apply all the texture.SetPixel(...) commands
}
function getDirection(face : int, i : int, j : int, size : int)
: Vector3
// This function computes the direction that is
// associated with a texel of a cube map
{
var direction : Vector3;
if (face == 0)
{
direction = Vector3(0.5,
-((j + 0.5) / size - 0.5), -((i + 0.5) / size - 0.5));
}
else if (face == 1)
{
direction = Vector3(-0.5,
-((j + 0.5) / size - 0.5), ((i + 0.5) / size - 0.5));
}
else if (face == 2)
{
direction = Vector3(((i + 0.5) / size - 0.5),
0.5, ((j + 0.5) / size - 0.5));
}
else if (face == 3)
{
direction = Vector3(((i + 0.5) / size - 0.5),
-0.5, -((j + 0.5) / size - 0.5));
}
else if (face == 4)
{
direction = Vector3(((i + 0.5) / size - 0.5),
-((j + 0.5) / size - 0.5), 0.5);
}
else if (face == 5)
{
direction = Vector3(-((i + 0.5) / size - 0.5),
-((j + 0.5) / size - 0.5), -0.5);
}
return direction;
}
作為上述 JavaScript 程式碼的替代方案,您也可以使用以下 C# 程式碼。
using UnityEngine;
using UnityEditor;
using System.Collections;
[ExecuteInEditMode]
public class ComputeDiffuseEnvironmentMap : MonoBehaviour
{
public Cubemap originalCubeMap;
// environment map specified in the shader by the user
//[System.Serializable]
// avoid being deleted by the garbage collector,
// and thus leaking
private Cubemap filteredCubeMap;
// the computed diffuse irradience environment map
private void Update()
{
Cubemap originalTexture = null;
try
{
originalTexture = renderer.sharedMaterial.GetTexture(
"_OriginalCube") as Cubemap;
}
catch (System.Exception)
{
Debug.LogError("'_OriginalCube' not found on shader. "
+ "Are you using the wrong shader?");
return;
}
if (originalTexture == null)
// did the user set "none" for the map?
{
if (originalCubeMap != null)
{
renderer.sharedMaterial.SetTexture("_Cube", null);
originalCubeMap = null;
filteredCubeMap = null;
return;
}
}
else if (originalTexture == originalCubeMap
&& filteredCubeMap != null
&& renderer.sharedMaterial.GetTexture("_Cube") == null)
{
renderer.sharedMaterial.SetTexture("_Cube",
filteredCubeMap); // set the computed
// diffuse environment map in the shader
}
else if (originalTexture != originalCubeMap
|| filteredCubeMap
!= renderer.sharedMaterial.GetTexture("_Cube"))
{
if (EditorUtility.DisplayDialog(
"Processing of Environment Map",
"Do you want to process the cube map of face size "
+ originalTexture.width + "x" + originalTexture.width
+ "? (This will take some time.)",
"OK", "Cancel"))
{
if (filteredCubeMap
!= renderer.sharedMaterial.GetTexture("_Cube"))
{
if (renderer.sharedMaterial.GetTexture("_Cube")
!= null)
{
DestroyImmediate(
renderer.sharedMaterial.GetTexture(
"_Cube")); // clean up
}
}
if (filteredCubeMap != null)
{
DestroyImmediate(filteredCubeMap); // clean up
}
originalCubeMap = originalTexture;
filteredCubeMap = computeFilteredCubeMap();
//computes the diffuse environment map
renderer.sharedMaterial.SetTexture("_Cube",
filteredCubeMap); // set the computed
// diffuse environment map in the shader
return;
}
else
{
originalCubeMap = null;
filteredCubeMap = null;
renderer.sharedMaterial.SetTexture("_Cube", null);
renderer.sharedMaterial.SetTexture(
"_OriginalCube", null);
}
}
}
// This function computes a diffuse environment map in
// "filteredCubemap" of the same dimensions as "originalCubemap"
// by integrating -- for each texel of "filteredCubemap" --
// the diffuse illumination from all texels of "originalCubemap"
// for the surface normal vector corresponding to the direction
// of each texel of "filteredCubemap".
private Cubemap computeFilteredCubeMap()
{
Cubemap filteredCubeMap = new Cubemap(originalCubeMap.width,
originalCubeMap.format, true);
int filteredSize = filteredCubeMap.width;
int originalSize = originalCubeMap.width;
// Compute all texels of the diffuse environment cube map
// by itterating over all of them
for (int filteredFace = 0; filteredFace < 6; filteredFace++)
// the six sides of the cube
{
for (int filteredI = 0; filteredI < filteredSize; filteredI++)
{
for (int filteredJ = 0; filteredJ < filteredSize; filteredJ++)
{
Vector3 filteredDirection =
getDirection(filteredFace,
filteredI, filteredJ, filteredSize).normalized;
float totalWeight = 0.0f;
Vector3 originalDirection;
Vector3 originalFaceDirection;
float weight;
Color filteredColor = new Color(0.0f, 0.0f, 0.0f);
// sum (i.e. integrate) the diffuse illumination
// by all texels in the original environment map
for (int originalFace = 0; originalFace < 6; originalFace++)
{
originalFaceDirection = getDirection(
originalFace, 1, 1, 3).normalized;
//the normal vector of the face
for (int originalI = 0; originalI < originalSize; originalI++)
{
for (int originalJ = 0; originalJ < originalSize; originalJ++)
{
originalDirection = getDirection(
originalFace, originalI,
originalJ, originalSize);
// direction to the texel
// (i.e. light source)
weight = 1.0f
/ originalDirection.sqrMagnitude;
// take smaller size of more
// distant texels into account
originalDirection =
originalDirection.normalized;
weight = weight * Vector3.Dot(
originalFaceDirection,
originalDirection);
// take tilt of texel compared
// to face into account
weight = weight * Mathf.Max(0.0f,
Vector3.Dot(filteredDirection,
originalDirection));
// directional filter
// for diffuse illumination
totalWeight = totalWeight + weight;
// instead of analytically
// normalization, we just normalize
// to the potential max illumination
filteredColor = filteredColor + weight
* originalCubeMap.GetPixel(
(CubemapFace)originalFace,
originalI, originalJ); // add the
// illumination by this texel
}
}
}
filteredCubeMap.SetPixel(
(CubemapFace)filteredFace, filteredI,
filteredJ, filteredColor / totalWeight);
// store the diffuse illumination of this texel
}
}
}
// Avoid seams between cube faces: average edge texels
// to the same color on each side of the seam
int maxI = filteredCubeMap.width - 1;
for (int i = 0; i < maxI; i++)
{
setFaceAverage(ref filteredCubeMap,
0, i, 0, 2, maxI, maxI - i);
setFaceAverage(ref filteredCubeMap,
0, 0, i, 4, maxI, i);
setFaceAverage(ref filteredCubeMap,
0, i, maxI, 3, maxI, i);
setFaceAverage(ref filteredCubeMap,
0, maxI, i, 5, 0, i);
setFaceAverage(ref filteredCubeMap,
1, i, 0, 2, 0, i);
setFaceAverage(ref filteredCubeMap,
1, 0, i, 5, maxI, i);
setFaceAverage(ref filteredCubeMap,
1, i, maxI, 3, 0, maxI - i);
setFaceAverage(ref filteredCubeMap,
1, maxI, i, 4, 0, i);
setFaceAverage(ref filteredCubeMap,
2, i, 0, 5, maxI - i, 0);
setFaceAverage(ref filteredCubeMap,
2, i, maxI, 4, i, 0);
setFaceAverage(ref filteredCubeMap,
3, i, 0, 4, i, maxI);
setFaceAverage(ref filteredCubeMap,
3, i, maxI, 5, maxI - i, maxI);
}
// Avoid seams between cube faces:
// average corner texels to the same color
// on all three faces meeting in one corner
setCornerAverage(ref filteredCubeMap,
0, 0, 0, 2, maxI, maxI, 4, maxI, 0);
setCornerAverage(ref filteredCubeMap,
0, maxI, 0, 2, maxI, 0, 5, 0, 0);
setCornerAverage(ref filteredCubeMap,
0, 0, maxI, 3, maxI, 0, 4, maxI, maxI);
setCornerAverage(ref filteredCubeMap,
0, maxI, maxI, 3, maxI, maxI, 5, 0, maxI);
setCornerAverage(ref filteredCubeMap,
1, 0, 0, 2, 0, 0, 5, maxI, 0);
setCornerAverage(ref filteredCubeMap,
1, maxI, 0, 2, 0, maxI, 4, 0, 0);
setCornerAverage(ref filteredCubeMap,
1, 0, maxI, 3, 0, maxI, 5, maxI, maxI);
setCornerAverage(ref filteredCubeMap,
1, maxI, maxI, 3, 0, 0, 4, 0, maxI);
filteredCubeMap.Apply(); //apply all SetPixel(..) commands
return filteredCubeMap;
}
private void setFaceAverage(ref Cubemap filteredCubeMap,
int a, int b, int c, int d, int e, int f)
{
Color average =
(filteredCubeMap.GetPixel((CubemapFace)a, b, c)
+ filteredCubeMap.GetPixel((CubemapFace)d, e, f)) / 2.0f;
filteredCubeMap.SetPixel((CubemapFace)a, b, c, average);
filteredCubeMap.SetPixel((CubemapFace)d, e, f, average);
}
private void setCornerAverage(ref Cubemap filteredCubeMap,
int a, int b, int c, int d, int e, int f, int g, int h, int i)
{
Color average =
(filteredCubeMap.GetPixel((CubemapFace)a, b, c)
+ filteredCubeMap.GetPixel((CubemapFace)d, e, f)
+ filteredCubeMap.GetPixel((CubemapFace)g, h, i)) / 3.0f;
filteredCubeMap.SetPixel((CubemapFace)a, b, c, average);
filteredCubeMap.SetPixel((CubemapFace)d, e, f, average);
filteredCubeMap.SetPixel((CubemapFace)g, h, i, average);
}
private Vector3 getDirection(int face, int i, int j, int size)
{
switch (face)
{
case 0:
return new Vector3(0.5f,
-((j + 0.5f) / size - 0.5f),
-((i + 0.5f) / size - 0.5f));
case 1:
return new Vector3(-0.5f,
-((j + 0.5f) / size - 0.5f),
((i + 0.5f) / size - 0.5f));
case 2:
return new Vector3(((i + 0.5f) / size - 0.5f),
0.5f, ((j + 0.5f) / size - 0.5f));
case 3:
return new Vector3(((i + 0.5f) / size - 0.5f),
-0.5f, -((j + 0.5f) / size - 0.5f));
case 4:
return new Vector3(((i + 0.5f) / size - 0.5f),
-((j + 0.5f) / size - 0.5f), 0.5f);
case 5:
return new Vector3(-((i + 0.5f) / size - 0.5f),
-((j + 0.5f) / size - 0.5f), -0.5f);
default:
return Vector3.zero;
}
}
}
正如承諾的那樣,實際的著色器程式碼非常短;頂點著色器是“反射表面”部分中頂點著色器的簡化版本
Shader "GLSL shader with image-based diffuse lighting" {
Properties {
_OriginalCube ("Environment Map", Cube) = "" {}
_Cube ("Diffuse Environment Map", Cube) = "" {}
}
SubShader {
Pass {
GLSLPROGRAM
// Uniform specified by the user or by a script
uniform samplerCube _Cube; // the diffuse environment map
// The following built-in uniforms
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform mat4 _World2Object; // inverse model matrix
// Varyings
varying vec3 normalDirection;
#ifdef VERTEX
void main()
{
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
normalDirection = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor = textureCube(_Cube, normalDirection);
}
#endif
ENDGLSL
}
}
}
上面的著色器和指令碼足以計算大量靜態方向光源的漫反射照明。但是,“鏡面高光”部分中討論的鏡面照明呢,即
首先,我們需要重寫這個方程,使其只依賴於光源方向 **L** 和反射檢視向量 **R**
有了這個方程,我們可以計算一個查詢表(即立方體貼圖),它包含了任何反射檢視向量 **R** 針對多個光源的鏡面光照。為了使用這樣的查詢表查詢鏡面光照,我們只需要計算反射檢視向量,並在立方體貼圖中進行紋理查詢。事實上,這正是“反射表面” 部分的著色器程式碼所做的。因此,我們實際上只需要計算查詢表。
事實證明,上面提供的 JavaScript 程式碼可以很容易地改編來計算這樣的查詢表。我們所要做的就是更改以下程式碼行:
weight = weight * Mathf.Max(0.0,
Vector3.Dot(filteredDirection, originalDirection));
// directional filter for diffuse illumination
更改為:
weight = weight * Mathf.Pow(Mathf.Max(0.0,
Vector3.Dot(filteredDirection, originalDirection)), 50.0);
// directional filter for specular illumination
其中 50.0 應該被替換為 的變數。這允許我們計算任何特定光澤度的查詢表。(如果在著色器中使用 textureCubeLod 指令顯式地指定了 mipmap 級別,則相同的立方體貼圖可用於光澤度的不同值;但是,這種技術超出了本教程的範圍。)
總結
[edit | edit source]恭喜你,你已經完成了這個相當高階的教程!我們已經瞭解了:
- 基於影像的渲染是什麼。
- 如何計算和使用立方體貼圖來實現漫射環境貼圖。
- 如何將程式碼改編為鏡面反射。
進一步閱讀
[edit | edit source]如果你想了解更多關於:
- 立方體貼圖,你應該閱讀“反射表面” 部分。
- (動態) 漫射環境貼圖,你可以閱讀 Gary King 在 Matt Pharr(編輯)於 2005 年由 Addison-Wesley 出版的小冊子“GPU Gems 2” 中的第 10 章,“即時計算動態輻照環境貼圖”,該章節可線上獲得。