问题
没有正确地光照,场景会缺乏真实感。在某些情况中,如果光照不正确3D效果会完全消失。
例如,一个有着不透明颜色的球,如果没有光照,球的所有像素将会是相同的颜色,在屏幕上看起来像一个平面的盘子。当光照正确时,球上面对光照的部分比其他部分的颜色更亮,使球看起来是一个真实的3D对象。
解决方案
在计算机图形学中,所有的3D对象都是由三角形组成的。你想使三角形对应入射光可以正确地被照亮。图6-1显示了一条从左向右的单向光,它影响到一个矩形的六个不同位置,每个矩形都是由两个三角形组成的。
图6-1 根据入射光的三角形光照情况
简单地定义光源的位置和对象的位置不足以让显卡在对象上添加正确地光照。对3D对象的每个三角形来说,你需要添加一些信息,是显卡可以计算照射到表面的光照强度。
这可以通过指定每个顶点的法线向量做到,法线在图6-1中用三角形顶点上的线段表示。一旦为每个顶点定义了正确的法线,BasicEffect就能使用正确的光照绘制对象。
工作原理
在图6-1中,绘制了一些矩形(每个矩形都是由两个三角形组成的),它们的颜色表示入射光强度。矩形越垂直于入射光方向,接受到的光越多。最后一个矩形垂直于光线方向被完全照亮。而第一个矩形平行于光线,所以不接受光照。
定义法线
那么显卡是如何知道相关信息的呢?在三角形的每个顶点上,你将定义垂直于三角形的方向。这个方向叫做法线。法线方向在图6-1中表示为伸出三角形顶点的线段。因为垂直于平面的方向是唯一的,所以矩形上所有顶点具有相同的法线方向。
法线方向让显卡可以计算三角形上获得的光线。如教程6-5所述,你可以将法线投影到光线方向上,如图6-2所示。光线方向用图片底部的长箭头表示,从左指向右。 图6-2中的旋转的黑色条代表图6-1中的矩形。法线在光线方向上的投影用光线箭头上的粗黑色块表示,黑色块越大,三角形获得的光越多。
图6-2 将法线投影到光线方向上
图中左边的三角形的法线垂直于光线方向,所以投影为0,三角形不受光照。右边的三角形的法线平行于光线方向,投影最大,被完全照亮。
给定光线和法线,显卡可以很容易地计算出投影长度。这就是显卡正确计算光照的方法。
将光照施加到场景中
显卡会计算每个顶点上的光强,然后用这个值乘以像素的初始颜色。
在顶点中添加法线数据
前面的段落中解释了除了包含3D位置和颜色,每个顶点中还需存储法线方向。
XNA拥有一个预定义的顶点格式可以在每个顶点中保存法线: VertexPositionNormalTexture结构。这个格式让你可以为每个顶点保存3D位置,法线方向和纹理坐标。可参加教程5-2学习带纹理的三角形,参加教程5-12学习如何自定义顶点格式。
下面的方法创建了一个拥有6个顶点的数组,定义了两个三角形形成一个矩形。这个矩形平躺在地面上,所以所有Y坐标都是0。因此,法线的方向为(0,1,0) Up方向,因为这就是垂直于矩形的方向。顶点格式还要有纹理坐标,这样显卡才知道如何从图像采样颜色(见教程5-2)。
private void InitVertices() { vertices = new VertexPositionNormalTexture[6]; int i = 0; vertices[i++] = new VertexPositionNormalTexture(new Vector3(-1, 0, 1), new Vector3(0, 1, 0), new Vector2(1,1)); vertices[i++] = new VertexPositionNormalTexture(new Vector3(-1, 0, -1), new Vector3(0, 1, 0), new Vector2(0,1)); vertices[i++] = new VertexPositionNormalTexture(new Vector3(1, 0, -1), new Vector3(0, 1, 0), new Vector2(0,0)); vertices[i++] = new VertexPositionNormalTexture(new Vector3(1, 0, -1), new Vector3(0, 1, 0), new Vector2(0,0)); vertices[i++] = new VertexPositionNormalTexture(new Vector3(1, 0, 1), new Vector3(0, 1, 0), new Vector2(1,0)); vertices[i++] = new VertexPositionNormalTexture(new Vector3(-1, 0, 1), new Vector3(0, 1, 0), new Vector2(1,1)); myVertexDeclaration = new VertexDeclaration(device, VertexPositionNormalTexture.VertexElements); }
最后一行代码确保VertexDeclaration (见教程5-1)只被创建一次,因为它不需要改变。
技巧:如果三角形是平铺在地面上的,很容易计算法线。可参加教程5-7学习如何为一个复杂的对象自动计算法线。
正法线和负法线
一个三角形其实有两个垂直方向。如果如前面的代码三角形是在地面上的,你可以定义一个指向上方或下方的法线。那么应该使用哪一个?这很重要,因为选择了错误的方向会导致错误的光照,通常会不受光照。
原则是,你需要选择指向对象外部的那个法线。
通常,三角形的一个面可以看做一个3D物体的“里面”,而另一个面就是“外面”。在这种情况下,你想选择指向物体外部的法线。
设置BasicEffect参数
定义好顶点后就可以绘制三角形了。本章的第二部分解释了如何编写HLSL effects,但第一个教程只使用BasicEffect。BasicEffect是一个预定义的effect,可以用来绘制使用基本光照效果的物体。确保有一个BasicEffect变量,因为每帧都创建一个新BasicEffect对象太耗费资源。在类顶部添加这个变量:
BasicEffect BasicEffect basicEffect;
在LoadContent方法中实例化basicEffect :
basicEffect = new BasicEffect(device, null);
下面的设置将从一个单向光绘制3D场景的光照,例如像太阳:
basicEffect.World = Matrix.Identity; basicEffect.View = fpsCam.ViewMatrix; basicEffect.Projection = fpsCam.ProjectionMatrix; basicEffect.Texture = myTexture; basicEffect.TextureEnabled = true; basicEffect.LightingEnabled = true; basicEffect.AmbientLightColor = new Vector3(0.1f, 0.1f, 0.1f); basicEffect.PreferPerPixelLighting = true; basicEffect.DirectionalLight0.Direction = new Vector3(1, -1, 0); basicEffect.DirectionalLight0.DiffuseColor = Color.White.ToVector3(); basicEffect.DirectionalLight0.Enabled = true; basicEffect.DirectionalLight1.Enabled = false; basicEffect.DirectionalLight2.Enabled = false;
代码的第一部分设置了World,View和Projection矩阵,用来将3D场景转换到2D屏幕,具体知识可参见教程2-1和4-2。因为在每个顶点中存储了纹理坐标,你还要将一张纹理传递到显卡并开启它,纹理的知识详见教程5-2。
接着设置光照。首先开启光照并定义一个环境光颜色,这个颜色是对象一直都受到的,无论它相对于光线的朝向如何。这里你定义了暗灰色,这样即使没有光线,物体仍会隐约可见。
如果显卡可以处理逐像素光照,你可以开启它,可参加教程6-3。
最后定义光源。使用BasicEffect时,可以一次使用三个光源。你需要设置光源方向和颜色,并开启光源。本例中只使用一个光源。
设置了BasicEffect后,就可以使用BasicEffect绘制三角形了:
basicEffect.Begin(); foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes) { pass.Begin(); device.VertexDeclaration = myVertexDeclaration; DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, 2); pass.End(); } basicEffect.End();
注意:如果你通过将basicEffect. LightingEnabled设置为false关闭了灯光,会以全部光强绘制场景。如前面“将光照施加到场景中”一段解释的那样,显卡会在每个像素的初始颜色上乘以光照强度。如果关闭灯光,显卡会简单地绘制每个像素的初始颜色。实际上对应lighting factor为1的情况,意思是全部光强。
使用世界矩阵
在绘制三角形前,你可以设置effect的世界矩阵。这样你可以将三角形移动到另一个位置或缩放旋转它们(见教程4-2)。构建良好的effects (例如BasicEffect)将这个变换施加到顶点中的法线数据上。例如,前面的代码中的矩形若发生旋转,法线也会跟着一起旋转。
下面的代码显示了这样一个例子。
归一化法线
注意:在阅读此节时,你需要知道normal (法线)和normalize(归一化)的区别。法线是垂直于三角形的方向。归一化表示使一个向量的长度为单位长度,当归一化一个向量时,你将它的长度减少(或增加)到1。
光照强度只应该由光线和法线间的夹角决定。但是,显卡的计算结果还取决于两者的长度。所以,你必须确保法线方向和光线方向的长度都为1,这可以通过归一化实现。
当两者都是单位长度时,光照强度只取决于两者间的夹角,这正是你想要的结果。
代码
首先要提供每个顶点的法线数据:
private void InitVertices() { vertices = new VertexPositionNormalTexture[6]; int i = 0; vertices[i++] = new VertexPositionNormalTexture(new Vector3(-1, 0, 1), new Vector3(0, 1, 0), new Vector2(1,1)); vertices[i++] = new VertexPositionNormalTexture(new Vector3(-1, 0, -1), new Vector3(0, 1, 0), new Vector2(0,1)); vertices[i++] = new VertexPositionNormalTexture(new Vector3(1, 0, -1), new Vector3(0, 1, 0), new Vector2(0,0)); vertices[i++] = new VertexPositionNormalTexture(new Vector3(1, 0, -1), new Vector3(0, 1, 0), new Vector2(0,0)); vertices[i++] = new VertexPositionNormalTexture(new Vector3(1, 0, 1), new Vector3(0, 1, 0), new Vector2(1,0)); vertices[i++] = new VertexPositionNormalTexture(new Vector3(-1, 0, 1), new Vector3(0, 1, 0), new Vector2(1,1)); myVertexDeclaration = new VertexDeclaration(device, VertexPositionNormalTexture.VertexElements); }
在使用BasicEffect绘制三角形前,你需要设定它的参数定义光照环境:
basicEffect.View = fpsCam.ViewMatrix; basicEffect.Projection = fpsCam.ProjectionMatrix; basicEffect.Texture = myTexture; basicEffect.TextureEnabled = true; basicEffect.LightingEnabled = true; basicEffect.DirectionalLight0.Direction = new Vector3(1, 0, 0); basicEffect.DirectionalLight0.DiffuseColor = Color.White.ToVector3(); basicEffect.DirectionalLight0.Enabled = true;
最后绘制三角形。下面的代码绘制了两个相同的三角形九次,每次都使用一个不同的世界矩阵。世界矩阵首先选择三角形然后将它沿x轴移动四个单位(参见教程5-2学习矩阵乘法的顺序)。
for (int i = 0; i < 9; i++) { basicEffect.World = Matrix.CreateTranslation(4, 0, 0) * Matrix.CreateRotationZ((float)i * MathHelper.PiOver2 / 8.0f); basicEffect.Begin(); foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes) { pass.Begin(); device.VertexDeclaration = myVertexDeclaration; device.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, 2); pass.End(); } basicEffect.End(); }
可参见教程5-1学习如何绘制三角形。