翻译 Light Mapping - Theory and Implementation
看到这篇文章不错,所以就翻译了下来,翻译过程中肯定有很多错误,请大家批评指正,而且匆匆翻译完后也没有认真核对,大家如果看到有什么地方不妥的,请以原文为准
http://www.flipcode.com/archives/Light_Mapping_Theory_and_Implementation.shtml#derivation
由于原文中有大量图片,而我用word直接粘贴不行,所以我就直接把word文档附在这里/Files/bravesailor/光照贴图.doc
【下文中都没有贴图片,麻烦】
简介
这篇文档阐述了在游戏中或者图形应用程序中创建光照贴图的过程.
目的
这篇文档的目的是解释创建光照图的过程。描述了光照图 (light map lumels)是怎样计算的以及最终像素的颜色是怎样决定的。
当然这篇文档并没有涉及到最新的可能会被新一代的图形芯片应用的逐像素技术。
假定条件
假定读者对与3D游戏编程和3D图形精髓有很深的认识,尤其是光照,材质,3D几何,多边形,面,纹理,纹理坐标等。
同时,这篇文档也仅仅解释了光照图的赋值或者说产生的过程,并没有解释光照图纹理坐标映射是怎样计算的。如果你的网格没有光照图纹理映射(UV),我们就不能够使用一个简单的方法去测试光照图的产生.
点击这里可以阅读更多与此相关的文章。
如果你在继续读这篇之前已经急切得想知道基于光照技术的光照图的效果,点击这里去查看demo, 可以下载一个交互式的demo.
基本光照
你可能已经看过一些游戏非常接近真实的氛围(我的意思是,仅仅看上去接近). 其中的原因就是她们使用了光照. 如果游戏没有使用它,那么看上去就非常的平淡. 正式因为光照使得玩家一次次的陶醉与游戏。看看下面的不同之处,
带有光照的世界.右手白色的菱形物体代表的是一个电光源.
没有光照的世界
这个结果的差别是很明显的。
已经看到了这个结果了,现在我们来看看实际中的一些不同类型的关照。(主要是指静态世界)
1. 顶点光照
l 对于每一个顶点,基于光照计算一个颜色值
l 颜色值会在三角形顶点间插值
l 三角行/面不能太大,否则看上去会很失真。
l 多边形必须被镶嵌(tessellated)到一个合适的水平,这样看上去才会比较好。
l 如果顶点的数量很多,计算所花费的时间也会相应变长,因为要对每一个顶点计算
l 不会导致加载纹理
l 所有的计算都是实时的。
l 阴影是不正确的
l 可以得到相当惊人的光照效果。
2. 逐像素光照(实时)
这篇文章没有涉及到当前实际中的逐像素光照技术。比如,这个效果可以在当前的图形显卡,比如NVIDIA GeForce 3/4, ATI Radeon 8500/9700以及以后的显卡得到实现。
l 对于每一个将要绘制的像素点,按照光照模型来计算颜色
l 不会导致引擎大量得加载操作
l 不太适合实时游戏中使用
l 可以得到正确的阴影(碰撞检测也是可以实现的)
l 可以得到最佳的实时光照效果,但是太慢而不能在实时游戏中使用。
3. 逐像素光照(光照图)
l 可以实现真实光照的效果
l 动态光照需要很多大量得工作
l 可以结合顶点光照来实现实时动态光照
l 昂贵的光照计算是预处理的
l 运行时,所有的计算都已经被硬件完成了.所以非常快.
l 视觉效果得质量取决于光照图的大小。
l 是我们能用最小的花费能得到的最接近逐像素光照效果的方法
l 对于每一个三角形,不同的纹理图首先被应用,然后光照图通常再和它经行乘运算。
使用光照图逐像素光照
在这篇文档的剩余部分我们将讨论基于光照的光照图。光照图是另外一种类型的纹理图. 光照图和diffuse图唯一的区别就是,diffuse图保存的是平常的颜色值,而光照图保存的是多边形上得光照结果. 其它都是一样的。
l 光照图纹理被映射到三角形/多边形使用唯一的纹理坐标
l 光照图的加载和diffuse图的加载是一样的
l Diffuse图上的每一个像素通常引用一个纹理(texel),而光照图是应用一个亮度(lumel)。
在我们进入光照图计算之前,我们先看看2d图是怎样被映射到3d的三角面上的。
上面左手边上的图是一个NxN维的2d的纹理
右手边上的是一个3d多边形.纹理坐标是和每一个顶点相关的。可以发现,上面得纹理以1:1映射到多边形.
下面的图显示了一个2D图被映射或者粘贴到多边形上。这个多边形只用了部分贴图,所以映射的比例不是1:1。我们可以发现,多边形只覆盖了全图得部分区域。
仔细观察上图,然后我们讨论光照图. Diffuse图可以共享多边形也可以不共享。比如,一个diffuse图上的像素可以属于”n”个多边形。但是,对于一个光照图,一个像素点仅仅属于一个多边形。
光照图中的每一个像素都有一个世界中的相应的位置, 必须很好的理解这个概念。因为每一个三角形的顶点在世界中都有一个位置。三角形所拥有的每一个光照图像素在世界中都有一个位置.
有一个像素中心得概念. 当我们提及一个像素的时候,意味着像素中心. 像素不是一个点,而是一个盒子.
基于上面的标准.当我们想要得到任何一个像素得UV坐标的时候,通过一下公式计算
X=(w+0.5)/Width
Y=(h+0.5)/height
上面的公式中,w和h是当前向我们将要计算UV的像素点的偏移值。
Width=光照图的宽度
Height=光照图得高度
就像下图所展示的那样。
现在我将对上面所说得进行阐述和解释。首先我们来看下图
在上图中
l 三角形通过3个粗的黑边/线来界定
l 三角形的三个顶点是(0,0,0),(0,100,0)和(100,0,0)
l 因为三个顶点得z坐标相同,所以我们可以安全的在计算中忽略掉z
l 在三角形内的每一个盒子表示了唯一的一个(光照图中的)像素。
切记一个像素点是一个盒子不是一个点。
绿色的盒子意味着像素位于三角形内并且光照图的这个像素点属于这个三角形。
粉红色的盒子意味着像素中心落在三角形之外,光照图上的这个像素点不属于这个三角形。
这个三角形包含15个像素点
l 我们现在要做的就是通过观察三角形和像素来确定每一个像素的正确的理论上的世界位置
看图上的最后一行, 有5个像素点并且这个边得长度是x轴向上的100个单元
l 就是说每个像素是20个单元
l 同时,这条边上的各个位置只是x有变化,y和z值保持不变
l 第一个像素的x值应该是20
l 第一个像素的位置应该是(20.0, 0.0, 0.0)
l 第二个像素得位置应该是(40.0, 0.0, 0.0),第三个应该是(60.0, 0.0, 0.0)…..
同理可以得到其它像素的位置
再次提醒下,上面的结果是不正确的.只是为了使大家明白像素的”世界位置”的概念。
从上面的图中,我们可以得知三角形拥有的(光照图)像素越多,像素与像素之间的世界位置越小,输出越平滑。
如果你现在还没有明白像素中心得概念以及像素的世界位置,那么你最好把文档从开始到这里再看一遍.如果你不是很清楚,千万不要继续看下面的。
下面的图显示了使用光照图的结果
一个简单的基于光照技术的光照图
现在我们开始实际的光照映射。完整的计算光照图的过程被分为3个部分。它们是:
1. 计算光照图的纹理坐标
2. 计算每个像素的世界位置以及normal
3. 计算每个像素得最终颜色
a. 计算光照图的纹理坐标
这是最开始以及最终的过程.它包括把每一个多边形指定到光照图上的特定位置。仅仅这个问题就是一个非常长得话题。我将准备跳过这个步骤直接进行下一个步骤。
然而如果你想知道关于这个的文章,我将会提供一些链接在本文得结束。
同时这也是最重要的步骤,因为它决定了使用纹理空间的效率。当然这个也可以通过编辑工具来自动处理比如3ds max或者maya。
如果你想使用一个快速得方法来产生一个世界来测试光照图.可以按照下面的步骤:
l 创建一个空的非常大的立方体.(空的,因为碰撞检测是不需要的)
l 手动设定diffuse和光照图的纹理坐标
更好的做法是,所有的面都使用一个diffuse图而使用6个不同得光照图。
可以以1:1的比例来映射光照图到立方体的多边形
l 创建一个或者两个灯在立方体的顶部
l 使用这个设置来测试所产生的光照图
l 在下一个步骤中,测试所产生的阴影.可以在立方体的底面的中心创建一个盒子并且添加碰撞检测
b. 计算每个像素的世界位置以及normal
这个是光照图的预处理过程. 光照图中的每一个像素都将映射到世界中的一个位置.这正是我们需要计算的。假设世界几何体以及光照图的大小没有改变,这个数据是静态的.这个就只需要计算一次并且被重复使用。
考虑像下面这样得三角形. 为什么我们在下面的三角形中对于顶点我们只使用2d坐标,这个问题将会在后面给予解释。图一
上图我们已知的是:
a. 我们知道三角形的端点
b. 我们知道3个顶点的纹理坐标(光照图)
我们需要计算的是:给定一个纹理坐标值(在一个2d三角形之内),计算出在2d三角形上的位置。我们必须为 每一个三角形所对应的光照图上的每一个像素点都做上述的计算.(记住,光照图上的一个像素点属于并且只能属于一个三角形或者是多边形)
注意:在下面得几个图中,我将使用特定的方程式,图标以及从Chris Hecker’s article on Perspective Texture Mapping一文中引用一些东西。可以参考上面这篇文章来了解更多的相关知识。
上面图中得三角形p0 p1 p2。每一个顶点都有一个屏幕空间(2D空间)与它关联,另外,有一个任意的参数c,这个可以是高洛德着色的颜色.或者透视纹理映射的1/z, u/z,v/z. 因为c是任意得参数,我们可以在2维的三角形的表面线性的插值。我们可以构造p3,p4这样就有
我们知道y4 = y0, x3=x0
(这个方程式是从Chris Hecker’s article on Perspective Texture Mapping.我这里所做的改变就是对这个引用进行了详细的阐述.)这个引用公式非常简单的只包括了一些简单得替换以及变量的重新排序
上面2个方程告诉我们c是怎样随着x和y得变化而改变的。 给定位置(x,y)我们可以计算出那个位置得c值。 对于光照图而言,恰恰相反, 我们知道纹理坐标(比如,c)我们需要反推出位置信息. 我们将使用下面的公式.这个是直接从上面得2个方程中得到的。
denominator = (v0-v2)(u1-u2) - (v1-v2)(u0-u2)
dp dx (x1-x2)(v0-v2) - (x0-x2)(v1-v2)--- = --- = --------------------------------------du du denominator
dp dx (x1-x2)(u0-u2) - (x0-x2)(u1-u2)--- = --- = --------------------------------------dv dv -denominator
dq dy (y1-y2)(v0-v2) - (y0-y2)(v1-v2)--- = --- = --------------------------------------du du denominator
dq dy (y1-y2)(u0-u2) - (y0-y2)(u1-u2)--- = --- = --------------------------------------dv dv -denominator
现在可以得到uv位置(相对于第一个顶点的光照图的纹理坐标)
duv.x = uv->x - u0duv.y = uv->y - v0uv是个指针,指向纹理坐标,通过这个纹理坐标可以计算出世界位置。u0和v0是第一个顶点的光照图纹理坐标。
假定pos是指向最有一个位置
Equation 3
pos->x = (x0) + (dpdu * duv.x) + (dpdv * duv.y) pos->y = (y0) + (dqdu * duv.x) + (dqdv * duv.y)
我们有2d的位置对应与三角形和uv坐标
现在我们将通过给定的uv来计算出相应得位置信息
Equation 3
pos->x = (x0) + (dpdu * duv.x) + (dpdv * duv.y) pos->y = (y0) + (dqdu * duv.x) + (dqdv * duv.y)
现在我们得到了与uv相对应得三角形的2d位置。我们拿图一当做一个例子,我们假定与位置相对应的uv坐标在{0.5,1.0}之间,事实上纹理坐标在{0.5,1.0}之间正好是落在三角形之内的。
得到下面得值
dxdu = dpdu = 100dxdv = dpdv = 0dydu = dqdu = 0dydv = dqdv = 200
duv.x = (0.5 - 0) = 0.5duv.y = (1.0 - 0) = 1.0所以位置是Pos->x = 150Pos->y = 300现在我们得到了2d的位置,但是最终我们需要3d位置去做一些3d运算.那该怎么做呢?我们知道一个平面可以用Ax+By+Cz+D=0来表示。每一个多边形/三角形都有一个与之相关联的平面方程。同时,我们也可以把三角形/多边形沿着它的2个主要得轴投影,比如.我们投影一个3d的三角形到2d。通过忽略它得一个轴来实现。这个必须要明白,因为纹理2d的而三角形是在3d空间之内的。
那到底应该忽略哪个轴呢?我们怎样选择要忽略得轴?假设给定了平面得法线,选择最接近平面normal的2个轴向。比如a,b,c(代表的是x,y,z轴的值)是平面得法线,找出它们中绝对值最大的那个,然后忽略这个轴。
如果平面法线是(0,1,0),那么,我们将会选择xz轴而忽略y轴。如果平面法线是(0,-1,0),那么我们依然是选择xz轴。
现在再回到图一如果所给定的三角形得平面法线是(0,1,0),那么图一中顶点的值(Xn, Yn)实际上代表的是(Xn,Zn). 使用(Xn, Yn)主要是为了保持连续性。
记住,三角形仍然是3d的,只是我们通过忽略一个轴把它转换成2d的
观看下方程3。 我们得到了2d纹理坐标的世界位置. 我们必须把它还原成3d. 所以,使用平面方程, 依赖于我们在哪个轴向上对三角形进行了投影,我们必须使用正确得方程。
比如:平面normal是(0.123, 0.824, 0.34)
根据上面所提到的,我们忽略y轴。所以我们得到2d的位置,这将是x和z的分量。我们需要计算出y的分量。
平面方程是Ax+By+Cz+D=0.
By = -(Ax+Cz+D)
y = -(Ax+Cz+D) / B.这样我们就得到了y分量。同理,我们也可以计算不同投影下的其它分量。
首先让我们看看lumel结构。
struct Lumel
{D3DXVECTOR3 Position ; // lumel position in the world.
D3DXVECTOR3 Normal ; // lumel normal (used for
// calculating N.L)
DWORD Color ; // final color.
BYTE r, g, b, a; // the red, green, blue and alpha
// component.
int LegalPosition ; // is this lumel legal.
DWORD polygonIndex ; // index of the polygon that it
// belongs to.
} ;这个结构被用在每一个光照图的每一个像素。如果一个光照图是64x64大小. 则所需的内存大小为:
这个结构的大小是40 bytes
总大小就是 (64*64*40) bytes = 163840Bytes=160Kb
这只是个测试用例.
Lumel结构中的LegalPosition成员,保存了特定的像素是否属于一个多边形的信息。
用来显示光照图的顶点的结构应该类似与下面这样.
struct LMVertex
{D3DXVECTOR3 Position ; // vertex position.
D3DXVECTOR3 Normal ; // vertex normal
DWORD Color ; // vertex color.
D3DXVECTOR2 t0 ; // diffuse texture co-ordinates.
D3DXVECTOR2 t1 ; // light map texture co-ordinates.
} ;
用来显示光照图的多边形的结构应该类似下面这样.
struct LMPolygon
{LMVertex *vertices ; // array of vertices.
WORD *indices ; // array of indices.
DWORD VertexCount, // No. of vertices in the array
FaceCount ; // No. of faces to draw in this
// polygon.
DWORD DiffuseTextureIndex ; // the index in to the diffuse
// texture array
DWORD LMTextureIndex ; // the index in to the light-map
// texture array
} ;
下面是为每一个像素点计算世界位置的一个伪代码,这个函数称作BuildLumelInfo.
BuildLumelInfo(){// this function has to be called for each light map texture
for(0 to lightmap height)
{for(0 to lightmap width)
{w = current width during the iteration (for loop)
h = current height during the iteration (for loop)
U = (w+0.5) / width
V = (h+0.5) / height
UV.x = UUV.y = V
if (LumelGetWorldPosition(/*UV, this light map texture*/)) SUCCEEDED
then// Mark this lumel as LEGAL.
else
{// Mark this lumel as illegal � in the sense that no triangle uses this pixel / lumel.
} }
}}
LumelGetWorldPosition( UV, light map texture ){for( number of polygons sharing this light map texture )
{
// do the "Bounding Box" lightmap texture co-ordinate rejection // test.
if( /*UV co-ordinates of the light map do not fall inside the
polygon‘s MAXIMUM and MINIMUM UV co-ordinates*/ )
then//try next polygon ;
// code for the above explanation
if(uv->x < poly->minUV.x) continue ;
if(uv->y < poly->minUV.y) continue ;
if(uv->x > poly->maxUV.x) continue ;
if(uv->y > poly->maxUV.y) continue ;
for( /* number of faces in this polygon */ )
{/*Get the three vertices that make up this face.
Check if light map UV co-ordinates actually fall inside the polygon’s
UV co-ordinates. This routine is similar to routines like �PointInPolygon�
or �PointInTriangle�.
If YES, then call GetWorldPos to get the actual world position in 3D
for this given light map UV co-ordinate. If NO, then this is not a legal pixel, i.e. this pixel does not belong to THIS polygon.*/ } }}
GetWorldPos(UV uv){// get uv position relative to uv0
duv.x = uv->x - uv0->x ;
duv.y = uv->y - uv0->y ;
// retrieve the components of the two major axis.
// i.e. here we are converting from 3D triangle to 2D.
switch(PlaneProjection)
{case PLANE_XZ :
// collect X and Z components
break ;
case PLANE_XY :
// collect X and Y components
break ;
case PLANE_YZ :
// collect Y and Z components
break ;
}
// Calculate the gradients from the equations derived above.
// See Equation 3 above.
Now calculate gradients.i.e. dp/du, dp/dv, dq/du, dq/dv, etc.
// In the following line, I have used a, b, instead of X, Y or Z.
// This is because, depending on the polygon�s plane we
// choose either XY or YZ or XZ components. Hence, a and b map to
// either XY or YZ or XZ components
pos->a = (a0) + (dpdu * duv.x) + (dpdv * duv.y)pos->b = (b0) + (dqdu * duv.x) + (dqdv * duv.y)
// get the world pos in 3D
// calculate the remaining single co-ordinate based on the polygon�s
// plane.
switch(PlaneProjection)
{case PLANE_XZ :
// We would have got X and Z as the 2D components.
// calculate the Y component.
y = -(Ax+Cz+D) / B.break ;
case PLANE_XY :
// We would have got X and Y as the 2D components.
// calculate the Z component.
z = -(Ax+By+D) / C.break ;
case PLANE_YZ :
// We would have got Y and Z as the 2D components.
// calculate the X component.
x = -(By+Cz+D) / A.break ;
}}
填充lumel信息的函数如下:记住,假设几何体,光照图纹理坐标以及光照图的大小都是不变的。这个函数就可以只被调用一次然后就可以重复利用了.这样,如果你改变了一个光的属性,不需要再一次重新构建整个数据. 你所要做得就是调用函数BuildLightMaps. BuildLumelInfoForAllLightmaps()应该在BuildLightMaps()之前调用。
BuildLumelInfoForAllLightmaps(){// Do initialization here.
for (number of light maps)
{// If memory for Lumels not allocated, then,
// Allocate memory to hold the lumel info for this particular
// light map.
BuildLumelInfo(this_light_map) ; // Calculates the
// world position for all the lumels.
}}
c.计算每个像素的最终颜色
这是计算光照图的最后一个步骤.这里我们将会实际填充光照图上每一个像素上的值.下面给出计算每个像素颜色的伪代码
BuildThisLightMap(){for(0 to lightmap height)
{for(0 to lightmap width)
{lumel = current lumel ;
if(lumel is not legal)
thentry next lumel.
for( number of lights )
{// cos theta = N.L
dir = lightPosition - lumel->Positiondot = D3DXVec3Dot(&lumel->Normal, &dir) ;
// if light is facing away from the lumel, then ignore
// the effect of this light on this lumel.
if( dot < 0.0 )
try next light ;
distance = distance between lumel and light source.
if(distance > light range)
try next light ;
// Check Collision of ray from light source to lumel.
if( collision occurred )
then {// lumel is in shadow.
continue ;
}
// GetColorFromLightSource.
// Write color info to lumel.
} } }}
可以发现,伪代码解释的相当完美.这是基本得光照计算。在这里不想花费太多的时间. 让我们看看下面为所有的光照图计算颜色的伪代码
BuildLightMaps(){for (number of light maps)
{BuildThisLightMap () ; // does all the lighting calculations.
BlurThisMap() ; // blur�s the light map.
FillAllIllegalPixelsForThisLightMap() ; // fills all the illegal
// lumels with the closest
// color � to prevent bleeding when
// bi-linear filtering is used.
WriteThisLightMapToFile() ; // finally write the lightmap colors
// to file.
// I write it in a 24-bit BMP format.
}}
我们仔细看看这两个函数BlurThisMap 和 FillAllIllegalPixelsForThisLightMap是干什么的.
不管我们做了什么,如果你还没有打开任何的过滤,则像素将会在最终的渲染图片中可见。这个是相当不真实的而且使得玩家很烦。所以我们必须使得像素变得更加得平滑。
可以使用任何你想使用的过滤器来达到平滑. 这里我们使用box过滤器。这就是我所使用得代码
BlurThisMap(){for(0 to height)
{for(0 to width)
{w = current width during the iteration (for loop)
h = current height during the iteration (for loop)
current_pixel = GetCurrentPixel(w,h)
// Get neighboring 8 pixels for current_pixel, ignoring the
// illegal pixels.
sum_color = Add color from the neighboring legal pixels.
// calculate the average.
final_color = sum_color / no. of neighboring legal pixels.
SetCurrentPixelColor(w, h, final_color) ; } }}
实际上如果在游戏中已经打开了双线性过滤, 则像素点将会被减少. 同时也使得map更加平滑了.如果没有使用任何模糊操作而得到的结果相当满意,那么我们可以忽略这个模糊的过程。
在使用双线性过滤的时候还有另外一个问题. 那就是混合得问题。
当在游戏中打开双线性过滤. 这意味着, 特定得像素点不属于任何多边形.
通常, 任何有效的像素点的颜色应该是0, 因为没有为这个像素点计算任何的颜色。
所以,当渲染的时候,任何时候当一个像素点被选中,我们应该使用像素点周围的颜色平均值.这就是我们所说得混合。
如果正在使用双线性或者三次线性过滤,我们无论如何都会摆脱掉有效的像素。
实际上,我们是不能去掉它们的. 我们所能做的就是用一个有效像素点周围的像素点得颜色混合值来填充这个像素。
这样混合的问题就解决了. 这也是函数FillAllIllegalPixelsForThisLightMap所做的。
解决混合问题的另外一种方法就是不用填充有效像素的值,而是把所有有效像素的颜色值设置成环境颜色. 虽然这将会降低显示结果,但同时这也是很便宜的。虽然它并不是一个正确的方法. 也许可以考虑下用这种方法来产生实时光照图。
看看下面的图:
基于双线性过来的混合打开
双线性过滤打开了,但是没有混合
显示光照图:
我们已经花费了很多时间计算了光照图,现在应该是显示这个图得时候了,下面是在Directx 8.1下面的代码:
// set the appropriate values for the texture stages.// here I�m assuming that the device supports two or more texture stages.
SETTEXTURESTAGE(device8, 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1) ;SETTEXTURESTAGE(device8, 0, D3DTSS_COLORARG1, D3DTA_TEXTURE) ;
// multiply
SETTEXTURESTAGE(device8, 1, D3DTSS_COLOROP, D3DTOP_MODULATE) ;
SETTEXTURESTAGE(device8, 1, D3DTSS_COLORARG2, D3DTA_TEXTURE) ;SETTEXTURESTAGE(device8, 1, D3DTSS_COLORARG1, D3DTA_CURRENT) ;
// set the appropriate vertex shader.
SETVERTEXSHADER(device8, FVF_MYVERTEXSHADER) ;
SETTRANSFORM(device8, D3DTS_WORLD) ; // set the world matrix
// set the texture for the first stage.SETTEXTURE(device8, 0, diffuseTexture) ;
// set the texture for the second stage.
SETTEXTURE(device8, 1, lightMapTexture) ;
DrawPrimitive() ; // draw the polygons
结束。。。
就像我上面所提的,你可以仔细阅读Chris Hecker的文章, 弄清楚我所使用的公式。
更多关于光照图方面的讲述:
· A flat scene without light maps or any lighting.
· Scene rendered with simple vertex lighting.
· A light map that has been generated for the scene.
· Scene rendered with a medium resolution light map.
· Scene rendered with a high resolution light map.
· Scene rendered with light map only, i.e. no diffuse texture.
学了这个你能做什么
· 用这个来点亮我们得关卡
· 实现动态光影这将是很cool的
· 使用我们所学到的光映射技术,可以用辐射度光照来实现它
结论:
随着更加强大的图形显卡的到来, 使用光照图来实现光照几乎是越来越少了. 这个文章只是提供一些基本的但是有用的关于创建光照图的方法和过程。
浙公网安备 33010602011771号