本教程介绍了基于贴图的光照,特别是漫射(辐射)环境映射以及用立方体贴图的实现。(Unity的灯光探测器大概是以相似的方式工作,除了它有动态渲染的立方体贴图。)
本教程基于章节“反射表面”。如果你没有阅读过这章,这是一个好机会来阅读它。
多光源的漫射光照
考虑一下上图中雕塑上的光照。有自然光从窗户照射进来。一些光会在到达雕塑之前从地板、墙壁和参观者反弹回来。另外,还有一些人造光源,它们的光束同样会直接或间接地照射到雕塑上。需要多少方向光源和点光源来模拟这种复杂光照环境才会令人信服?至少要多于少数(很有可能要多于一打),并且光源计算的性能就是一个挑战。
这个问题通过基于图像的照明来解决。对于由环境贴图描述的静态光照环境,比如一个立方体贴图,基于图像的光照允许在立方体贴图中通过任意数量单个纹理查找的光源来计算光照(参考章节“反射表面”中对于立方体贴图的描述)。那么它是如何工作的呢?
在本章中我们把焦点放在漫射光照上面。假设立方体贴图的每个纹素(也就是像素)作为一个方向光源,(记住立方体贴图通常被假设为无限大,这样只跟方向有关而跟位置无关。)对于指定表面法向量最终的光照可以如章节“漫反射”中描述的一样被计算出来。它是表面法向量N和指向光源的向量L之间的余弦值:
既然纹素就是光源,那么L就是从立方体中心指向立方体贴图中纹素中心的方向。一个32×32纹素每面的小型立方体贴图有着32×32×6 = 6144的纹素。通过成千上万的光源添加的光照并不会实时工作。但是,对于一个静态的立方体贴图,我们可以对所有可能的表面法向量N提前计算漫射光照,然后把它们存储在一张查找表中。当用指定的表面法向量点亮表面上的一个点,我们就能在那张预计算的查询表中对于指定表面法向量N查找漫射光照。
于是,对于指定的表面法向量N,我们通过立方体纹理的所有纹素添加(也叫整合)漫射光照。我们在第二张立方体贴图(“漫辐射环境贴图”或叫“漫射环境贴图”)中为这个表面法向量存储最终的漫射光照。第二张立方体贴图作为一张查询表,在上面每个方向(也就是表面法向量)会映射成一个颜色(即通过潜在的成千上万光源的漫射光照)。片元着色器因此就很简单了(这个可以使用章节“反射表面”中的顶点着色器):
float4 frag(vertexOutput input) : COLOR
{
return texCUBE(_Cube, input.normalDir);
}
这只是使用光栅化表面点的表面法向量进行预计算漫射照明的查找。但是,漫射环境贴图的预计算有时会如下面描述的一样更复杂一点。
漫射环境贴图的计算
本节会用一些JavaScript 代码来说明漫射(辐射)环境映射的立方体映射的计算。为了在Unity中使用它,在Project Window中选择Create > JavaScript。然后在Unity的文本编辑器中打开这个脚本,把JavaScript代码拷贝过去,然后把这个脚本附着在有以下着色器的材质上去。当一张新的足够小尺寸的立方体贴图由着色器属性_OriginalCube
指定(在着色器用户接口中用Environment Map来标记),脚本将会使用相应的漫射环境贴图来更新着色器属性_Cube
(即在用户接口中的Diffuse Environment Map)。注意这个立方体贴图必须是“可读的”,也就是当创建立方体贴图时你必须在Inspector中确保勾选了Readable。同样要注意的是你必须使用表面尺寸为32×32或更小的小型立方体贴图,因为对于更大的立方体贴图来说计算时间也会趋向更长。于是,当在Unity中创建一个立方体贴图,请确保选择一个足够小的尺寸。
这个脚本只包含少数几个函数:Awake()
初始化变量;Update()
负责使用者和材质之间的交互(即读写着色器属性);computeFilteredCubemap()
做计算漫射环境映射的实际工作;以及getDirection()
是一个小型实用的函数,它为computeFilteredCubemap()
计算立方体贴图中跟每个纹素相关的方向。注意computeFilteredCubemap()
不只是整合了漫射光照,它还通过设置延接缝处的邻近纹素为相同的平均颜色来避免不连续的接缝。
这里只给出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 = 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 = _World2Object;
// 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 = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
return texCUBE(_Cube, input.normalDir);
}
ENDCG
}
}
}
镜面(光泽)反射的一些改变
以上的着色器和脚本足够可以通过大量静态方向光源来计算漫射光照。但是,对于章节“镜面高光”中讨论的镜面光照应该怎么办,也就是:
First, we have to rewrite this equation such that it depends only on the direction to the light source L and the reflected view vector
首先,我们必须重写这个等式,这样它只是取决于指向光源的方向L和反射观察向量:
用这个等式我们可以通过任意的反射观察向量来计算一张包含着镜面光照的查询表。为了用这张表来查询镜面光照,我们只需要计算 反射观察向量以及在立方体贴图中执行纹理查找。实际上,这就是章节“反射表面”中着色器代码所做的事。于是,我们实际上只需要计算这张查询表。
结果证明以上的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
应该被变量替换。这个允许我们对任意指定的光亮计算查询表。(如果在着色器中mipmap等级通过textureCubeLod指令被显示指定,相同的立方体贴图可以被用作不同值的光亮;但是,这个技术已经不在本章的讨论范围了。)
总结
恭喜,你已经达到了一个相当高级的教程的结尾!我们看到了:
- 什么是基于图像的渲染。
- 如何计算并使用一张纹理贴图来实现漫射环境贴图。
- 如何为镜面反射改编代码。