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 章,“实时计算动态辐照环境贴图”,该章节可在线获得。