问题
你想绘制一个颜色漂亮的三角形并可以完全控制三角形上的颜色。
解决方案
你的显卡允许你指定一张图像,从这张图像可以采样想要的颜色。
这意味着你需要将一张2D图像导入到XNA项目中,并在绘制三角形之前将它传递到显卡。对每个顶点,你要指定2D图像的哪个位置对应顶点。
工作原理
首先将一张2D图像导入到项目中,如教程3-1所示。在LoadContent方法中将它链接到一个变量:
myTexture = content.Load<Texture2D>("texture");
然后你要定义三角形的顶点。如前面的教程所述,你需要指定每个顶点的3D位置。这次你无需定义颜色而是定义在2D纹理中的对应位置,这一步需要在每个顶点中存储2D纹理坐标。如果三角形的每个顶点对应一个2D纹理坐标,显卡就会将你指定的2D图像的某一部分填充到三角形内部。
当指定纹理坐标时,要记住纹理的左上为坐标(0,0),右上为(1,0)。这意味着第二个坐标值表示垂直位置,所以右下坐标为(1,1)。图5-4显示了对应不同纹理坐标的纹理区域。
你需要存储一个2D纹理坐标而不是每个顶点的颜色。不是存储一个包含VertexPositionColor元素的数组,而是创建一个包含VertexPositionTexture元素的数组,如这个名称所述,它将存储纹理坐标和一个3D位置:
private void InitVertices() { vertices = new VertexPositionTexture[9]; int i = 0; vertices[i++] = new VertexPositionTexture(new Vector3(-10, 1, -5), new Vector2(0,0)); vertices[i++] = new VertexPositionTexture(new Vector3(-7, 5, -5), new Vector2(0.5f, 1)); vertices[i++] = new VertexPositionTexture(new Vector3(-4, 1, -5), new Vector2(1,0)); vertices[i++] = new VertexPositionTexture(new Vector3(-3, 1, -5), new Vector2(0, 0.5f)); vertices[i++] = new VertexPositionTexture(new Vector3(0, 5, -5), new Vector2(1, 0)); vertices[i++] = new VertexPositionTexture(new Vector3(3, 1, -5),new Vector2(1, 1)); vertices[i++] = new VertexPositionTexture(new Vector3(4, 1, -5), new Vector2(0.25f, 0.5f)); vertices[i++] = new VertexPositionTexture(new Vector3(7, 5, -5), new Vector2(0.5f, 0)); vertices[i++] = new VertexPositionTexture(new Vector3(10, 1, -5), new Vector2(1, 1)); myVertexDeclaration=new VertexDeclaration(device, VertexPositionTexture.VertexElements); }
这里定义的九个顶点拥有对应图5-4中的三个三角形的纹理坐标。通过指定纹理坐标,你可以显示如图5-4所示的三角形。
注意: 三角形在3D空间中的实际位置定义为一个Vector3。例如,图5-4中的第一个三角形是倒置的,因为两个顶点的Y坐标是1,中间一个顶点的Y坐标为5,这使得三角形中间的字母XNA也是颠倒绘制的(译者注:图5-4中的第一个三角形中的XNA并没有颠倒,但在源代码中是颠倒的,怀疑作者截图有误)。
你还需要告知显卡顶点现在包含不同的信息。所以需要基于VertexPositionTexture的VertexElement创建一个VertexDeclaration,这样可以在绘制三角形前将它传递到显卡。
定义了顶点后,你就做好了绘制三个三角形的准备:
device.RenderState.CullMode = CullMode.None; basicEffect.World = Matrix.Identity; basicEffect.View = fpsCam.ViewMatrix; basicEffect.Projection = fpsCam.ProjectionMatrix; basicEffect.Texture = myTexture; basicEffect.TextureEnabled = true; basicEffect.Begin(); foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes) { pass.Begin(); device.VertexDeclaration=myVertexDeclaration; device.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, vertices, 0, 3); pass.End(); } basicEffect.End();
你需要将纹理传递到显卡,这一步需要将这个纹理设置为BasicEffect的当前纹理。然后,不像前一个教程那样使用定义在顶点中的颜色,本教程通过将 TextureEnabled设置为true指定BasicEffect从纹理采样颜色。因为你使用了一个不同的顶点格式,所以需要将新的VertexDeclaration传递到显卡。
最后绘制三个三角形。
纹理寻址模式
如前所述,纹理坐标(0,0)对应纹理的左上点的像素,(1,1)对应右下点。
但这并没有限定纹理坐标的值必须在[0,1]范围内,例如设定为(1.5f, –0.5f)也可以。在这种情况中,你需要通过设置U和V(U是第一个纹理坐标,V是第二个)的纹理模式让XNA知道如何处理这样的坐标。下面的代码定义了一个U和V坐标超过[0,1]范围的三角形:
private void InitVertices() { vertices = new VertexPositionTexture[3]; int i = 0; vertices[i++] = new VertexPositionTexture(new Vector3(-3, -3, -1), new Vector2(-0.5f,1.5f)); vertices[i++] = new VertexPositionTexture(new Vector3(0, 5, -1), new Vector2(0.5f, -1.5f)); vertices[i++] = new VertexPositionTexture(new Vector3(3, -3, -1), new Vector2(1.5f,1.5f)); myVertexDeclaration=new VertexDeclaration(device, VertexPositionTexture.VertexElements); }
然后你要通过设置纹理采样器的寻址模式让XNA知道如何处理这些坐标,这用来从纹理采样颜色。
TextureAddressMode.Clamp
使用下列代码将UV模式设置为Clamp:
device.SamplerStates[0].AddressU = TextureAddressMode.Clamp; device.SamplerStates[0].AddressV = TextureAddressMode.Clamp;
这个寻址模式让所有的纹理坐标截取到[0,1]范围内,所有小于0的坐标截取到0,大于1的截取到1。
结果是,超出[0,1] 范围的纹理坐标的所有像素的颜色为纹理边缘的颜色,如图5-5所示。图中显示的三角形是前面已经定义的。
下面是显卡在从图像采样颜色前首先映射的纹理坐标的例子:
- (-0.5f, 1.5f) → (0, 1)
- (0.5f, -1.5f) → (0.5f, 0)
- (1.5f, 1.5f) → (1,1)
TextureAddressMode.Wrap
这是默认的纹理寻址模式,在使用了一个不同的模式后你可以使用下列代码重新选择使用wrap模式:
device.SamplerStates[0].AddressU = TextureAddressMode.Wrap; device.SamplerStates[0].AddressV = TextureAddressMode.Wrap;
使用这个模式,显卡会从坐标加上或减去1直到坐标仍回到[0,1]范围。
这会导致原始纹理被复制,如图5-5右边所示。
下面是一些纹理坐标映射的例子:
- (-0.5f, 1.5f) → (0.5f, 0.5f)
- (1.2f, -0.2f) → (0.2f,0.8f)
- (-0.7f, -1.2f) → (0.3f,0.8f)
TextureAddressMode.Mirror
使用下列代码将纹理寻址模式设置为:
device.SamplerStates[0].AddressU = TextureAddressMode.Mirror; device.SamplerStates[0].AddressV = TextureAddressMode.Mirror;
这个模式导致原始纹理被复制,但与Wrapping模式不同的是,复制的图像与相邻的原始图像是互为镜像的。复制的图像在原始图像的上方和下方是垂直镜像的,左方和右方是水平映射的。对角线上的复制沿着两边,翻转180度,如图5-6左图所示。
这个模式很有用,因为可以在放大图像避免边缘锐化。
下面是一些坐标映射的例子:
- (-0.5f, 1.5f) → (0.5f, 0.5f)
- (0.5f, -1.5f) → (0.5f, 0.5f)
- (1.2f, 1.7f) → (0.8f, 0.3f)
TextureAddressMode.MirrorOnce
你可以选择MirrorOnce模式:
device.SamplerStates[0].AddressU = TextureAddressMode.MirrorOnce; device.SamplerStates[0].AddressV = TextureAddressMode.MirrorOnce;
这个模式将[-1,1] 区域内的纹理坐标镜像到[0,1]区间,而所有超出[-1,1]区间的坐标会截取到–1 (小于–1的值)或1 (大于1的值),如图5-5右边所示。
下面是采样坐标的映射例子:
- (-0.5f, 1.5f) → (0.5f, 1)
- (0.5f, -1.5f) → (0.5f, -1)
- (1.2f, 1.7f) →(1, 1)
TextureAddressMode.Border
你可以给纹理坐标超出[0,1]区间的像素设置一个指定的颜色,这个颜色叫做 BorderColor,可以使用下列代码存储在SamplerState中:
device.SamplerStates[0].BorderColor = Color.LightSeaGreen; device.SamplerStates[0].AddressU = TextureAddressMode.Border; device.SamplerStates[0].AddressV = TextureAddressMode.Border;
当调试时这个模式很有用,因为它清晰地显示了所有超出[0,1]区间的纹理坐标,所有超出此区间的像素都使用BorderColor绘制。
当心:在XBox360平台上只能使用白色作为边界颜色。
代码
首先需要定义顶点,因为需要保护3D位置和对应的纹理位置,你需要使用VertexPositionTexture elements。别忘了初始化对应的VertexDeclaration。
private void InitVertices() { vertices = new VertexPositionTexture[3]; int i = 0; vertices[i++] = new VertexPositionTexture(new Vector3(-3, -3, -1), new Vector2(-0.5f, 1.5f)); vertices[i++] = new VertexPositionTexture(new Vector3(0, 5, -1), new Vector2(0.5f, -1.5f)); vertices[i++] = new VertexPositionTexture(new Vector3(3, -3, -1), new Vector2(1.5f, 1.5f)); myVertexDeclaration= new VertexDeclaration(device,VertexPositionTexture.VertexElements); }
有了存储在数组中的顶点和一个有效的VertexDeclaration,你可以设置一个纹理寻址模式并绘制三角形:
protected override void Draw(GameTime gameTime) { device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0); //draw triangles device.RenderState.CullMode = CullMode.None; basicEffect.World = Matrix.Identity; basicEffect.View = fpsCam.ViewMatrix; basicEffect.Projection = fpsCam.ProjectionMatrix; basicEffect.Texture = myTexture; basicEffect.TextureEnabled = true; device.SamplerStates[0].AddressU=TextureAddressMode.Mirror; device.SamplerStates[0].AddressV = TextureAddressMode.Mirror; basicEffect.Begin(); foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes) { pass.Begin(); device.VertexDeclaration = myVertexDeclaration; device.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, vertices, 0, 1); pass.End(); } basicEffect.End(); base.Draw(gameTime); }