收藏 分享(赏)

光照贴图.doc

上传人:tkhy51908 文档编号:7195936 上传时间:2019-05-09 格式:DOC 页数:26 大小:1.03MB
下载 相关 举报
光照贴图.doc_第1页
第1页 / 共26页
光照贴图.doc_第2页
第2页 / 共26页
光照贴图.doc_第3页
第3页 / 共26页
光照贴图.doc_第4页
第4页 / 共26页
光照贴图.doc_第5页
第5页 / 共26页
点击查看更多>>
资源描述

1、简介这篇文档阐述了在游戏中或者图形应用程序中创建光照贴图的过程.目的这篇文档的目的是解释创建光照图的过程。描述了光照图 (light map lumels)是怎样计算的以及最终像素的颜色是怎样决定的。当然这篇文档并没有涉及到最新的可能会被新一代的图形芯片应用的逐像素技术。假定条件假定读者对与 3D 游戏编程和 3D 图形精髓有很深的认识,尤其是光照,材质,3D 几何,多边形,面, 纹理,纹理坐标等。同时,这篇文档也仅仅解释了光照图的赋值或者说产生的过程,并没有解释光照图纹理坐标映射是怎样计算的。如果你的网格没有光照图纹理映射(UV),我们就不能够使用一个简单的方法去测试光照图的产生.点击这里可

2、以阅读更多与此相关的文章。如果你在继续读这篇之前已经急切得想知道基于光照技术的光照图的效果,点击这里去查看demo, 可以下载一个交互式的 demo.基本光照你可能已经看过一些游戏非常接近真实的氛围(我的意思是,仅仅看上去接近). 其中的原因就是她们使用了光照. 如果游戏没有使用它,那么看上去就非常的平淡. 正式因为光照使得玩家一次次的陶醉与游戏。看看下面的不同之处,带有光照的世界.右手白色的菱形物体代表的是一个电光源.没有光照的世界这个结果的差别是很明显的。已经看到了这个结果了,现在我们来看看实际中的一些不同类型的关照。(主要是指静态世界)1. 顶点光照 对于每一个顶点,基于光照计算一个颜色

3、值 颜色值会在三角形顶点间插值 三角行/面不能太大,否则看上去会很失真。 多边形必须被镶嵌(tessellated)到一个合适的水平,这样看上去才会比较好。 如果顶点的数量很多,计算所花费的时间也会相应变长,因为要对每一个顶点计算 不会导致加载纹理 所有的计算都是实时的。 阴影是不正确的 可以得到相当惊人的光照效果。2. 逐像素光照(实时)这篇文章没有涉及到当前实际中的逐像素光照技术。比如,这个效果可以在当前的图形显卡,比如 NVIDIA GeForce 3/4, ATI Radeon 8500/9700 以及以后的显卡得到实现。 对于每一个将要绘制的像素点,按照光照模型来计算颜色 不会导致引

4、擎大量得加载操作 不太适合实时游戏中使用 可以得到正确的阴影(碰撞检测也是可以实现的 ) 可以得到最佳的实时光照效果,但是太慢而不能在实时游戏中使用。3. 逐像素光照(光照图) 可以实现真实光照的效果 动态光照需要很多大量得工作 可以结合顶点光照来实现实时动态光照 昂贵的光照计算是预处理的 运行时,所有的计算都已经被硬件完成了.所以非常快. 视觉效果得质量取决于光照图的大小。 是我们能用最小的花费能得到的最接近逐像素光照效果的方法 对于每一个三角形,不同的纹理图首先被应用,然后光照图通常再和它经行乘运算。使用光照图逐像素光照在这篇文档的剩余部分我们将讨论基于光照的光照图。光照图是另外一种类型的

5、纹理图. 光照图和 diffuse 图唯一的区别就是,diffuse 图保存的是平常的颜色值,而光照图保存的是多边形上得光照结果. 其它都是一样的。 光照图纹理被映射到三角形/多边形使用唯一的纹理坐标 光照图的加载和 diffuse 图的加载是一样的 Diffuse 图上的每一个像素通常引用一个纹理(texel),而光照图是应用一个亮度(lumel)。在我们进入光照图计算之前,我们先看看 2d 图是怎样被映射到 3d 的三角面上的。上面左手边上的图是一个 NxN 维的 2d 的纹理右手边上的是一个 3d 多边形.纹理坐标是和每一个顶点相关的。可以发现,上面得纹理以1:1 映射到多边形.下面的图

6、显示了一个 2D 图被映射或者粘贴到多边形上。这个多边形只用了部分贴图,所以映射的比例不是 1:1。我们可以发现,多边形只覆盖了全图得部分区域。仔细观察上图,然后我们讨论光照图. Diffuse 图可以共享多边形也可以不共享。比如,一个diffuse 图上的像素可以属于”n”个多边形。但是,对于一个光照图,一个像素点仅仅属于一个多边形。光照图中的每一个像素都有一个世界中的相应的位置, 必须很好的理解这个概念。因为每一个三角形的顶点在世界中都有一个位置。三角形所拥有的每一个光照图像素在世界中都有一个位置. 有一个像素中心得概念. 当我们提及一个像素的时候,意味着像素中心. 像素不是一个点,而是一

7、个盒子. 基于上面的标准.当我们想要得到任何一个像素得 UV 坐标的时候,通过一下公式计算X=(w+0.5)/WidthY=(h+0.5)/height上面的公式中,w 和 h 是当前向我们将要计算 UV 的像素点的偏移值。Width=光照图的宽度Height=光照图得高度就像下图所展示的那样。现在我将对上面所说得进行阐述和解释。首先我们来看下图在上图中 三角形通过 3 个粗的黑边/线来界定 三角形的三个顶点是(0,0,0),(0,100,0)和(100,0,0) 因为三个顶点得 z 坐标相同,所以我们可以安全的在计算中忽略掉 z 在三角形内的每一个盒子表示了唯一的一个(光照图中的) 像素。切

8、记一个像素点是一个盒子不是一个点。绿色的盒子意味着像素位于三角形内并且光照图的这个像素点属于这个三角形。粉红色的盒子意味着像素中心落在三角形之外,光照图上的这个像素点不属于这个三角形。这个三角形包含 15 个像素点 我们现在要做的就是通过观察三角形和像素来确定每一个像素的正确的理论上的世界位置看图上的最后一行, 有 5 个像素点并且这个边得长度是 x 轴向上的 100 个单元 就是说每个像素是 20 个单元 同时,这条边上的各个位置只是 x 有变化,y 和 z 值保持不变 第一个像素的 x 值应该是 20 第一个像素的位置应该是(20.0, 0.0, 0.0) 第二个像素得位置应该是(40.0

9、, 0.0, 0.0), 第三个应该是 (60.0, 0.0, 0.0)同理可以得到其它像素的位置再次提醒下,上面的结果是不正确的.只是为了使大家明白像素的”世界位置”的概念。从上面的图中,我们可以得知三角形拥有的(光照图) 像素越多 ,像素与像素之间的世界位置越小,输出越平滑。如果你现在还没有明白像素中心得概念以及像素的世界位置,那么你最好把文档从开始到这里再看一遍.如果你不是很清楚,千万不要继续看下面的。下面的图显示了使用光照图的结果一个简单的基于光照技术的光照图现在我们开始实际的光照映射。完整的计算光照图的过程被分为 3 个部分。它们是:1. 计算光照图的纹理坐标2. 计算每个像素的世界

10、位置以及 normal3. 计算每个像素得最终颜色a. 计算光照图的纹理坐标这是最开始以及最终的过程.它包括把每一个多边形指定到光照图上的特定位置。仅仅这个问题就是一个非常长得话题。我将准备跳过这个步骤直接进行下一个步骤。然而如果你想知道关于这个的文章,我将会提供一些链接在本文得结束。同时这也是最重要的步骤,因为它决定了使用纹理空间的效率。当然这个也可以通过编辑工具来自动处理比如 3ds max 或者 maya。如果你想使用一个快速得方法来产生一个世界来测试光照图.可以按照下面的步骤: 创建一个空的非常大的立方体.(空的,因为碰撞检测是不需要的) 手动设定 diffuse 和光照图的纹理坐标更

11、好的做法是,所有的面都使用一个 diffuse 图而使用 6 个不同得光照图。可以以 1:1 的比例来映射光照图到立方体的多边形 创建一个或者两个灯在立方体的顶部 使用这个设置来测试所产生的光照图 在下一个步骤中,测试所产生的阴影.可以在立方体的底面的中心创建一个盒子并且添加碰撞检测b. 计算每个像素的世界位置以及 normal这个是光照图的预处理过程. 光照图中的每一个像素都将映射到世界中的一个位置.这正是我们需要计算的。假设世界几何体以及光照图的大小没有改变,这个数据是静态的.这个就只需要计算一次并且被重复使用。考虑像下面这样得三角形. 为什么我们在下面的三角形中对于顶点我们只使用 2d

12、坐标,这个问题将会在后面给予解释。图一上图我们已知的是:a. 我们知道三角形的端点b. 我们知道 3 个顶点的纹理坐标(光照图)我们需要计算的是:给定一个纹理坐标值 (在一个 2d 三角形之内),计算出在 2d 三角形上的位置。我们必须为 每一个三角形所对应的光照图上的每一个像素点都做上述的计算.( 记住,光照图上的一个像素点属于并且只能属于一个三角形或者是多边形)注意:在下面得几个图中,我将使用特定的方程式,图标以及从 Chris Heckers article on Perspective Texture Mapping 一文中引用一些东西。可以参考上面这篇文章来了解更多的相关知识。上面图

13、中得三角形 p0 p1 p2。每一个顶点都有一个屏幕空间(2D 空间) 与它关联,另外,有一个任意的参数 c,这个可以是高洛德着色的颜色.或者透视纹理映射的 1/z, u/z,v/z. 因为 c 是任意得参数,我们可以在 2 维的三角形的表面线性的插值。我们可以构造 p3,p4 这样就有2421421ycycxx我们知道 y4 = y0, x3=x0(这个方程式是从 Chris Heckers article on Perspective Texture Mapping.我这里所做的改变就是对这个引用进行了详细的阐述.)这个引用公式非常简单的只包括了一些简单得替换以及变量的重新排序上面 2 个

14、方程告诉我们 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 -denomin

15、atordq 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 3pos-x = (x0) + (dpdu

16、* duv.x) + (dpdv * duv.y) pos-y = (y0) + (dqdu * duv.x) + (dqdv * duv.y)我们有 2d 的位置对应与三角形和 uv 坐标现在我们将通过给定的 uv 来计算出相应得位置信息Equation 3pos-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.

17、5,1.0之间正好是落在三角形之内的。得到下面得值dxdu = dpdu = 100dxdv = dpdv = 0dydu = dqdu = 0dydv = dqdv = 200duv.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 个主要得轴投影,比如.我们

18、投影一个 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

19、,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.这样我

20、们就得到了 y 分量。同理,我们也可以计算不同投影下的其它分量。首先让我们看看 lumel 结构。struct LumelD3DXVECTOR3 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 lu

21、mel legal.DWORD polygonIndex ; / index of the polygon that it / belongs to. ;这个结构被用在每一个光照图的每一个像素。如果一个光照图是 64x64 大小. 则所需的内存大小为: 这个结构的大小是 40 bytes总大小就是 (64*64*40) bytes = 163840Bytes=160Kb这只是个测试用例. Lumel 结构中的 LegalPosition 成员,保存了特定的像素是否属于一个多边形的信息。用来显示光照图的顶点的结构应该类似与下面这样.struct LMVertexD3DXVECTOR3 Posit

22、ion ; / vertex position.D3DXVECTOR3 Normal ; / vertex normalDWORD Color ; / vertex color.D3DXVECTOR2 t0 ; / diffuse texture co-ordinates.D3DXVECTOR2 t1 ; / light map texture co-ordinates. ;用来显示光照图的多边形的结构应该类似下面这样.struct LMPolygonLMVertex *vertices ; / array of vertices.WORD *indices ; / array of indi

23、ces.DWORD VertexCount, / No. of vertices in the arrayFaceCount ; / No. of faces to draw in this/ polygon.DWORD DiffuseTextureIndex ; / the index in to the diffuse/ texture arrayDWORD LMTextureIndex ; / the index in to the light-map/ texture array ;下面是为每一个像素点计算世界位置的一个伪代码,这个函数称作 BuildLumelInfo.BuildLu

24、melInfo()/ this function has to be called for each light map texturefor(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) / widthV = (h+0.5) / heightUV.x = UUV.y = Vif (LumelGetWorldPosition(/*U

25、V, 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

26、co-ordinate rejection / test.if( /*UV co-ordinates of the light map do not fall inside the polygons MAXIMUM and MINIMUM UV co-ordinates*/ ) then/try next polygon ;/ code for the above explanationif(uv-x minUV.x) continue ;if(uv-y minUV.y) continue ;if(uv-x poly-maxUV.x) continue ;if(uv-y poly-maxUV.

27、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 polygonsUV co-ordinates. This routine is similar to routines like PointInPolygonor PointInTriangle.If YES, then call GetWorldPos to get

28、 the actual world position in 3Dfor this given light map UV co-ordinate.If NO, then this is not a legal pixel, i.e. this pixel does notbelong to THIS polygon.*/GetWorldPos(UV uv)/ get uv position relative to uv0duv.x = uv-x - uv0-x ;duv.y = uv-y - uv0-y ;/ retrieve the components of the two major ax

29、is./ i.e. here we are converting from 3D triangle to 2D.switch(PlaneProjection)case PLANE_XZ :/ collect X and Z componentsbreak ;case PLANE_XY :/ collect X and Y componentsbreak ;case PLANE_YZ :/ collect Y and Z componentsbreak ;/ Calculate the gradients from the equations derived above. / See Equat

30、ion 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 polygons plane we/ choose either XY or YZ or XZ components. Hence, a and b map to / either XY or YZ or XZ componentspos-a = (a

31、0) + (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 polygons / plane.switch(PlaneProjection)case PLANE_XZ :/ We would have got X and Z as the 2D components./ calculate the Y component.y =

32、 -(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 信息的函数如下:记住 ,假设几何体,光照图纹理坐标以及光照图的大

33、小都是不变的。这个函数就可以只被调用一次然后就可以重复利用了.这样,如果你改变了一个光的属性,不需要再一次重新构建整个数据. 你所要做得就是调用函数 BuildLightMaps. BuildLumelInfoForAllLightmaps()应该在 BuildLightMaps()之前调用。BuildLumelInfoForAllLightmaps()/ Do initialization here.for (number of light maps)/ If memory for Lumels not allocated, then, / Allocate memory to hold t

34、he 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

35、not legal)thentry next lumel.for( number of lights )/ cos theta = N.Ldir = lightPosition - lumel-Positiondot = D3DXVec3Dot(/ if light is facing away from the lumel, then ignore/ the effect of this light on this lumel.if( dot light range)try next light ; / Check Collision of ray from light source to

36、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() ; /

37、 blurs 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.我们仔细看看这两个函数 Blur

38、ThisMap 和 FillAllIllegalPixelsForThisLightMap 是干什么的.不管我们做了什么,如果你还没有打开任何的过滤,则像素将会在最终的渲染图片中可见。这个是相当不真实的而且使得玩家很烦。所以我们必须使得像素变得更加得平滑。可以使用任何你想使用的过滤器来达到平滑. 这里我们使用 box 过滤器。这就是我所使用得代码BlurThisMap()for(0 to height)for(0 to width)w = current width during the iteration (for loop)h = current height during the ite

39、ration (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_

40、color) ;实际上如果在游戏中已经打开了双线性过滤, 则像素点将会被减少. 同时也使得 map 更加平滑了.如果没有使用任何模糊操作而得到的结果相当满意,那么我们可以忽略这个模糊的过程。在使用双线性过滤的时候还有另外一个问题. 那就是混合得问题。当在游戏中打开双线性过滤. 这意味着, 特定得像素点不属于任何多边形.通常, 任何有效的像素点的颜色应该是 0, 因为没有为这个像素点计算任何的颜色。所以,当渲染的时候,任何时候当一个像素点被选中,我们应该使用像素点周围的颜色平均值.这就是我们所说得混合。如果正在使用双线性或者三次线性过滤,我们无论如何都会摆脱掉有效的像素。实际上,我们是不能去掉它

41、们的. 我们所能做的就是用一个有效像素点周围的像素点得颜色混合值来填充这个像素。这样混合的问题就解决了. 这也是函数 FillAllIllegalPixelsForThisLightMap 所做的。解决混合问题的另外一种方法就是不用填充有效像素的值,而是把所有有效像素的颜色值设置成环境颜色. 虽然这将会降低显示结果,但同时这也是很便宜的。虽然它并不是一个正确的方法. 也许可以考虑下用这种方法来产生实时光照图。看看下面的图:基于双线性过来的混合打开双线性过滤打开了,但是没有混合显示光照图:我们已经花费了很多时间计算了光照图,现在应该是显示这个图得时候了,下面是在Directx 8.1 下面的代码

42、:/ set the appropriate values for the texture stages./ here Im assuming that the device supports two or more texture stages. SETTEXTURESTAGE(device8, 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1) ;SETTEXTURESTAGE(device8, 0, D3DTSS_COLORARG1, D3DTA_TEXTURE) ;/ multiplySETTEXTURESTAGE(device8, 1, D3DTSS_COLO

43、ROP, 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 te

44、xture 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. S

45、cene 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 的 使用我们所学到的光映射技术,可以用辐射度光照来实现它结论:随着更加强大的图形显卡的到来, 使用光照图来实现光照几乎是越来越少了. 这个文章只是提供一些基本的但是有用的关于创建光照图的方法和过程。

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 企业管理 > 管理学资料

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报