Cg 程式設計/Unity/多光源

本教程介紹了基於影像的光照,特別是漫反射(輻射度)環境貼圖及其在立方體貼圖中的實現。(Unity 的 光探針 可能以類似的方式工作,但使用動態渲染的立方體貼圖。)
本教程基於 “反射表面”部分。如果您還沒有閱讀該教程,現在是一個非常好的時間閱讀它。
考慮左側影像中雕塑的光照。自然光線透過窗戶照射進來。部分光線在到達雕塑之前,會從地板、牆壁和參觀者身上反射回來。此外,還有一些人工光源,它們的光線也會直接和間接地照射到雕塑上。需要多少個方向光和點光才能令人信服地模擬這種複雜的光照環境?至少要超過幾個(可能超過十幾個),因此光照計算的效能極具挑戰。
基於影像的光照解決了這個問題。對於由環境貼圖(例如立方體貼圖)描述的靜態光照環境,基於影像的光照允許我們透過立方體貼圖中的單個紋理查詢,計算任意數量的光源的光照(有關立方體貼圖的描述,請參見 “反射表面”部分)。它是如何工作的?
在本節中,我們將重點關注漫反射照明。假設立方體貼圖的每個紋素(即畫素)都充當方向光源。(請記住,立方體貼圖通常被認為是無限大的,因此只有方向很重要,而位置無關。)對於給定的表面法線方向,可以透過 “漫反射”部分 中描述的方法計算出最終的光照。它基本上是表面法線向量N和指向光源的向量L之間的餘弦。
由於紋素是光源,因此L只是從立方體中心到立方體貼圖中紋素中心的指向。一個具有 32×32 個紋素/面的小型立方體貼圖已經具有 32×32×6 = 6144 個紋素。新增成千上萬個光源的照明在即時情況下將無法實現。但是,對於靜態立方體貼圖,我們可以預先計算所有可能的表面法線向量N的漫反射照明,並將它們儲存在查詢表中。當用特定表面法線向量照亮表面上的一個點時,我們就可以在該預計算的查詢表中查詢特定表面法線向量N的漫反射照明。
因此,對於特定的表面法線向量N,我們將新增(即整合)立方體貼圖中所有紋素的漫反射照明。我們將此表面法線向量的最終漫反射照明儲存在第二個立方體貼圖(“漫反射輻射度環境貼圖”或簡稱“漫反射環境貼圖”)中。這個第二個立方體貼圖將充當查詢表,其中每個方向(即表面法線向量)都對映到一個顏色(即潛在的成千上萬個光源的漫反射照明)。因此,片段著色器非常簡單(它可以使用 “反射表面”部分 中的頂點著色器)。
float4 frag(vertexOutput input) : COLOR
{
return texCUBE(_Cube, input.normalDir);
}
它只是使用光柵化表面點的表面法線向量查詢預計算的漫反射照明。但是,漫反射環境貼圖的預計算要複雜一些,下一節將對此進行描述。
本節提供了一些 C# 程式碼來演示漫反射(輻射度)環境貼圖的立方體貼圖計算。為了在 Unity 中使用它,在專案視窗中選擇建立 > C# 指令碼,並將其命名為“ComputeDiffuseEnvironmentMap”。然後在 Unity 的文字編輯器中開啟該指令碼,將 C# 程式碼複製到其中,並將指令碼附加到具有以下著色器的材質的遊戲物件上。當為著色器屬性_OriginalCube(在著色器使用者介面中標記為環境貼圖)指定新的可讀立方體貼圖(尺寸足夠小)時,該指令碼將使用相應的漫反射環境貼圖更新著色器屬性_Cube(即使用者介面中的漫反射環境貼圖)。請注意,立方體貼圖必須是“可讀的”,即在建立立方體貼圖時,您必須在檢查器中選中可讀。另外請注意,您應該使用面部尺寸為 32×32 或更小的小型立方體貼圖,因為對於較大的立方體貼圖,計算時間往往很長。因此,在 Unity 中建立立方體貼圖時,請確保選擇足夠小的尺寸。
該指令碼只包含幾個函式:Awake() 初始化變數;Update()負責與使用者和材質進行通訊(即讀取和寫入著色器屬性);computeFilteredCubemap() 執行計算漫反射環境貼圖的實際工作;getDirection() 是一個用於computeFilteredCubemap() 的小型實用程式函式,用於計算立方體貼圖中每個紋素的關聯方向。請注意,computeFilteredCubemap() 不僅整合了漫反射照明,而且還透過將沿接縫的相鄰紋素設定為相同的平均顏色來避免立方體貼圖的面之間出現不連續的接縫。
確保將 C# 指令碼檔案命名為“ComputeDiffuseEnvironmentMap”。
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 = GetComponent<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)
{
GetComponent<Renderer>().sharedMaterial.SetTexture("_Cube", null);
originalCubeMap = null;
filteredCubeMap = null;
return;
}
}
else if (originalTexture == originalCubeMap
&& filteredCubeMap != null
&& GetComponent<Renderer>().sharedMaterial.GetTexture("_Cube") == null)
{
GetComponent<Renderer>().sharedMaterial.SetTexture("_Cube",
filteredCubeMap); // set the computed
// diffuse environment map in the shader
}
else if (originalTexture != originalCubeMap
|| filteredCubeMap
!= GetComponent<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
!= GetComponent<Renderer>().sharedMaterial.GetTexture("_Cube"))
{
if (GetComponent<Renderer>().sharedMaterial.GetTexture("_Cube")
!= null)
{
DestroyImmediate(
GetComponent<Renderer>().sharedMaterial.GetTexture(
"_Cube")); // clean up
}
}
if (filteredCubeMap != null)
{
DestroyImmediate(filteredCubeMap); // clean up
}
originalCubeMap = originalTexture;
filteredCubeMap = computeFilteredCubeMap();
//computes the diffuse environment map
GetComponent<Renderer>().sharedMaterial.SetTexture("_Cube",
filteredCubeMap); // set the computed
// diffuse environment map in the shader
return;
}
else
{
originalCubeMap = null;
filteredCubeMap = null;
GetComponent<Renderer>().sharedMaterial.SetTexture("_Cube", null);
GetComponent<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 "Cg shader with image-based diffuse lighting" {
Properties {
_OriginalCube ("Environment Map", Cube) = "" {}
_Cube ("Diffuse Environment Map", Cube) = "" {}
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// User-specified uniforms
uniform samplerCUBE _Cube;
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float3 normalDir : TEXCOORD0;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrixInverse = unity_WorldToObject;
// multiplication with unity_Scale.w is unnecessary
// because we normalize transformed vectors
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.pos = UnityObjectToClipPos(input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
return texCUBE(_Cube, input.normalDir);
}
ENDCG
}
}
}
上面的著色器和指令碼足以計算大量靜態方向光源的漫反射照明。但是,“鏡面高光”部分 中討論的鏡面照明怎麼辦,即
首先,我們需要改寫這個公式,使其只依賴於光源方向 **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 所著的《GPU Gems 2》中的第 10 章“動態輻照環境貼圖的即時計算”,該書由 Matt Pharr(編輯)於 2005 年由 Addison-Wesley 出版,並可以 線上獲取。
- Unity 的內建動態基於影像的照明方法,你應該閱讀 Unity 的光探頭文件。