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 的光探头文档。