31.高级地形AdvancedTerrainSceneNode类
AdvancedTerrainSceneNode类的代码主要参考自3D系列4.4 多纹理地形,与这篇文章的不同之处在于它创建的是TriangleList类型的顶点,而我使用的是数据量更少的TriangleStrip,使用TriangleStrip的解释可参见27.简单地形SimpleTerrainSceneNode类,代码如下,关键步骤请见代码注释:
namespace StunEngine.SceneNodes { // 高级地形使用的自定义顶点格式,储存了顶点位置、法线和纹理坐标,还有每个顶点的4个权重 public struct VertexMultitextured { public Vector3 Position; public Vector3 Normal; public Vector2 TextureCoordinate; public Vector4 TexWeights; public static int SizeInBytes = (3 + 3 + 2 + 4) * sizeof(float); public static VertexElement[] VertexElements = new VertexElement[] { new VertexElement( 0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0 ), new VertexElement( 0, sizeof(float) * 3, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Normal, 0 ), new VertexElement( 0, sizeof(float) * 6, VertexElementFormat.Vector2, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0 ), new VertexElement( 0, sizeof(float) * 8, VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 1 ), }; } ////// 基于高度图的地形。 /// public class AdvancedTerrainSceneNode : Renderable3DSceneNode { #region 构造函数和成员变量 ////// 地形使用的材质 /// private AdvancedTerrainMaterial material; ////// 高度图文件名称 /// string heightMapName; ////// 高度图 /// Texture2D heightMap; ////// 地形最小高度 /// private float minimumHeight = float.MaxValue; ////// 地形最大高度 /// private float maximumHeight = float.MinValue; ////// 地形高度,默认为30,即高度介于0至30之间 /// private float height = 30.0f; ////// 地形纹理宽度 /// private int terrainWidth; ////// 地形纹理高度 /// private int terrainHeight; ////// 高度数据数组 /// float[,] heightData; ////// 创建一个默认AdvancedTerrainSceneNode对象。使用引擎中的高度图和四张纹理,不进行缩放 /// /// 引擎 /// 所属场景 public AdvancedTerrainSceneNode(StunXnaGE game, Scene setScene) : this(game, setScene, "Textures/heightmap128", "Textures/sand", "Textures/Grass", "Textures/rock", "Textures/snow", Vector3.One) { } ////// 创建一个AdvancedTerrainSceneNode对象。这个对象放置在(0,0,0) - (heightmap width, maxheight, -heightmap height)范围内 /// /// 引擎 /// 所属场景 /// 高度贴图名称 /// 纹理1(沙地) /// 纹理2(草地) /// 纹理3(岩石) /// 纹理4(雪地) /// 地形缩放 public AdvancedTerrainSceneNode(StunXnaGE game, Scene setScene,string setHeightMapName, string setTexture1,string setTexture2,string setTexture3,string setTexture4, Vector3 setScale): base(game,setScene) { // 设置材质 material = new AdvancedTerrainMaterial(engine, engine.Content.Load("Effects\\AdvancedTerrain")); material .DiffuseTextureName=setTexture1; material .DetailTextureName =setTexture2; material .Texture3Name =setTexture3 ; material .Texture4Name =setTexture4 ; //将每张纹理平铺次数设为4次 this.Material.DiffuseUVTile = this.Material.DetailUVTile =this.Material.Texture3UVTile =this.Material .Texture4UVTile =new Vector2(4.0f, 4.0f); // 实现IMaterial接口 Imaterial = (IMaterial)material; this.heightMapName = setHeightMapName; //不对地形进行剔除操作 this.DisableCulling = true; this.DisableUpdateCulling = true; this.pose.SetScale(ref setScale); } #endregion #region 属性 /// /// 获取地形使用的材质 /// public AdvancedTerrainMaterial Material { get { return material; } } ////// 返回高度数据数组 /// public float[,] HeightData { get { return heightData; } } ////// 获取或设置地形高度的缩放值,默认为30,即高度介于0至30之间。这个值发生改变后需要重新创建地形顶点 /// public float Height { get { return height; } set { height = value; //加载高度图数据 heightData = Utility.LoadHeightData(heightMap, ref terrainWidth, ref terrainHeight, ref minimumHeight, ref maximumHeight, height); CreateTerrain(); } } ////// 获取范围在[0, infinite]之间的地形最小高度。 /// public float MinHeight { get { return minimumHeight; } } ////// 获取范围在[0, infinite]之间的地形最大高度。 /// public float MaxHeight{ get { return maximumHeight; } } #endregion public override void Initialize() { this.UpdateOrder = SceneNodeOrdering.Terrain.GetValue(); base.Initialize(); CreateTerrain(); } internal override void LoadContent() { base.LoadContent(); //加载高度图 heightMap = engine.Content.Load(heightMapName); //加载高度图数据 heightData = Utility.LoadHeightData(heightMap, ref terrainWidth, ref terrainHeight, ref minimumHeight, ref maximumHeight, height); } /// /// 绘制地形。 /// /// public override int Draw(GameTime gameTime,bool useReflection) { // 因为地形上施加了缩放,所以WorldPose.WorldMatrix不正确,需要修正。 this.pose.WorldMatrix = pose.TranslateMatrix; return base.Draw(gameTime, useReflection); } ////// 如果重置了地形缩放则重新建立地形。 /// public override void OnScaleChange() { CreateTerrain(); } #region 生成地形使用的方法 ////// 基于高度图和缩放创建顶点缓冲和索引缓冲。 /// private void CreateTerrain() { 略... } ////// 创建地形顶点 /// ///private VertexMultitextured[] CreateTerrainVertices() { //创建一个VertexMultitextured类型的数组保存所有顶点。地形需要terrainWidth * terrainHeight个顶点。 VertexMultitextured[] terrainVertices = new VertexMultitextured[terrainWidth * terrainHeight]; // 在两个循环中创建所有顶点。里面的一个循环创建一行上的顶点,当一行完成后,第一个for循环切换到下一行,直到定义完所有行的顶点。 for (int x = 0; x < terrainWidth; x++) { for (int z = 0; z < terrainHeight; z++) { //z值是负的,因此地形是建立在向前(-Z)方向的。而高度信息取自heightData数组。并乘以对应地形的缩放分量。 terrainVertices[x + z * terrainWidth].Position = new Vector3(x * pose.Scale.X, heightData[x, z] * pose.Scale.Y, -z * pose.Scale.Z); // 指定纹理坐标,即将一张纹理平铺在地形上,纹理的平铺次数是由材质决定的,默认设置为4次 terrainVertices[x + z * terrainWidth].TextureCoordinate.X = (float)x / terrainWidth ; terrainVertices[x + z * terrainWidth].TextureCoordinate.Y = (float)z / terrainHeight ; // 将高度映射到纹理权重,以30高度为参考 // 如果地形最大高度为30,则因子factor为1,位于高度为15的像素第一张沙地纹理的权重为0,第二张草地纹理的权重为1/2,第三张岩石纹理的权重为1/6,第四张雪地纹理的权重为0 // 如果地形最大高度为60,则因子factor为2,位于高度为30的像素第一张沙地纹理的权重为0,第二张草地纹理的权重为/2,第三张岩石纹理的权重为1/6,第四张雪地纹理的权重为0 float factor = height / 30.0f; terrainVertices[x + z * terrainWidth].TexWeights.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, z] - 0) / (factor*8.0f), 0, 1); terrainVertices[x + z * terrainWidth].TexWeights.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, z] - 12*factor) / (factor*6.0f), 0, 1); terrainVertices[x + z * terrainWidth].TexWeights.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, z] - 20 * factor) / (factor * 6.0f), 0, 1); terrainVertices[x + z * terrainWidth].TexWeights.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, z] - 30* factor) / (factor * 6.0f), 0, 1); // 将求每个顶点的权重之和,然后将权重除以这个和,保证每个顶点的权重和为1,否则颜色会偏亮或偏暗 // 如上面的例子数据,total为0+1/2+1/6+=2/3,下面的计算将权重归一化为0,3/4,1/4,0 float total = terrainVertices[x + z * terrainWidth].TexWeights.X; total += terrainVertices[x + z * terrainWidth].TexWeights.Y; total += terrainVertices[x + z * terrainWidth].TexWeights.Z; total += terrainVertices[x + z * terrainWidth].TexWeights.W; terrainVertices[x + z * terrainWidth].TexWeights.X /= total; terrainVertices[x + z * terrainWidth].TexWeights.Y /= total; terrainVertices[x + z * terrainWidth].TexWeights.Z /= total; terrainVertices[x + z * terrainWidth].TexWeights.W /= total; } } return terrainVertices; } /// /// 创建地形顶点索引 /// ///private int[] CreateTerrainIndices() { 略... } /// /// 计算法线 /// private void GenerateNormals(ref VertexMultitextured[] vertices, ref int[] indices) { 略... } ////// 判断是否在地形上 /// /// 给定x坐标 /// 给定z坐标 ///private bool IsValidPosition(int x, int z) { return (x >= 0 && x < terrainWidth - 1 && z >= 0 && z < terrainHeight - 1); } /// /// 通过双线性插值获取指定点的精确高度 /// /// /// ///public float GetExactHeightAt(float xCoord, float zCoord) { 略... } #endregion #region 单元测试 #if DEBUG /// /// 测试AdvancedTerrainSceneNode类 /// public static void TestAdvancedTerrainSceneNode() { AdvancedTerrainSceneNode terrain = null; // 相机上一帧的位置。 Vector3 oldPosition=Vector3 .Zero; TestGame.Start("测试AdvancedTerrainSceneNode类", delegate { // terrain = new AdvancedTerrainSceneNode(TestGame.engine, TestGame.scene); TestGame.scene.AddNode(terrain); //将地形中心放置在坐标原点 Vector3 position=new Vector3(-64,0,64); terrain.Pose.SetPosition(ref position); // 开启地形的雾化 terrain .Material.FogEnabled = true; // 设置雾化颜色 TestGame.scene.FogColor = new Vector4(0.8f, 0.8f, 0.8f, 1.0f); TestGame.engine.BackGroundColor = Color.CornflowerBlue; //不显示光标 TestGame.scene.IsShowMouse = false; // 设置点光源和聚光灯 TestGame.scene.sunModel.Visible = false; position=new Vector3(-15, 20, 0); TestGame.scene.pointLight.Position = position; TestGame.scene.pointLightModel.Pose.SetPosition(ref position); position = new Vector3(-5, 15, 0); TestGame.scene.spotLight .Position = position; TestGame.scene.spotLightModel .Pose.SetPosition(ref position); TestGame.scene.floor.Visible = false; }, delegate { // 按数字1键则切换地形上的纹理 if (Input.KeyboardKeyJustPressed(Keys.D1)) { if (terrain.Material.DiffuseTextureName == "Textures\\sand") terrain.Material.DiffuseTextureName = "Textures\\Rock"; else terrain.Material.DiffuseTextureName = "Textures\\sand"; } // 按数字2键切换地形的高度 if (Input.KeyboardKeyJustPressed(Keys.D2)) { if (terrain.Height == 30.0f) terrain.Height = 60.0f; else terrain.Height = 30.0f; } // 按数字3键切换地形的缩放 if (Input.KeyboardKeyJustPressed(Keys.D3)) { Vector3 scale=Vector3 .One ; Vector3 position = new Vector3(-64f, 0f, 64f); if (terrain.Pose.Scale == scale) { scale = new Vector3(2.0f, 2.0f, 2.0f); terrain.Pose.SetScale(ref scale); position = new Vector3(-128f, 0f, 128f); terrain.Pose.SetPosition(ref position); } else { scale = Vector3.One; terrain.Pose.SetScale(ref scale ); terrain.Pose.SetPosition(ref position); } } // 实现相机跟随地形的移动 Vector3 camPosition, tempPosition; TestGame.scene.Camera.GetPosition(out camPosition); // 如果相机发生移动 if ((camPosition - oldPosition) != Vector3.Zero) { // 检查相机与地形的高度差 float height = 5.0f + terrain.GetExactHeightAt(camPosition.X, -camPosition.Z); float diff = height - camPosition.Y; if (diff != 0) { tempPosition = new Vector3(camPosition.X, height, camPosition.Z); TestGame.scene.Camera.Pose.SetPosition(ref tempPosition); TestGame.scene.Camera.UpdateViewMatrix(false); } TestGame.scene.Camera.GetPosition(out oldPosition); } }); } #endif #endregion } }
其中创建顶点的方法与27.简单地形SimpleTerrainSceneNode类是类似的,不同之处在于使用了自定义的顶点结构VertexMultitextured,额外添加了纹理的权重信息,但3D系列4.4 多纹理地形一文中自定义顶点结构有个问题:TextureCoordinate数据类型应是Vector2,此文中设为了Vector4,好像没有必要,增加了数据量,我改成了Vector2没有发生问题。而SimpleTerrain中使用的是XNA框架预定义的VertexPositionNormalTexture结构。
创建地形顶点索引的CreateTerrainIndices方法、计算法线的GenerateNormals方法、判断是否在地形上的IsValidPosition方法和通过双线性插值获取指定点的精确高度的GetExactHeightAt方法与27.简单地形SimpleTerrainSceneNode类是相同的,不再赘述。(好像应该创建一个地形基类让代码可以重用,还是以后实现吧)
高级地形使用的effect文件AdvancedTerrain.fx代码如下:
#include "Standard.inc" #include "Lights.inc" uniform extern texture gTexture3; // 第三张纹理 uniform extern float2 gTexture3UVTile; // 第三张纹理在UV方向的平铺次数 uniform extern texture gTexture4; // 第四张纹理 uniform extern float2 gTexture4UVTile; // 第四张纹理在UV方向的平铺次数 // 第三张纹理的采样器 sampler2D texture3Sampler = sampler_state { Texture =; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; // 第四张纹理的采样器 sampler2D texture4Sampler = sampler_state { Texture = ; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; AddressU = mirror; AddressV = mirror; }; // 顶点着色器的输入结构 struct VS_INPUT { float4 Position : POSITION0; float3 Normal : NORMAL; float2 TextureCoords : TEXCOORD0; float4 TextureWeights : TEXCOORD1; }; // 顶点着色器的输出结构 struct VS_OUTPUT { float4 Position : POSITION0; float3 WorldNormal : TEXCOORD0; float2 TextureCoords : TEXCOORD1; float4 WorldPosition : TEXCOORD2; float4 TextureWeights : TEXCOORD3; float Depth : TEXCOORD4; // 像素的深度值 }; VS_OUTPUT MultiTexturedVS(VS_INPUT Input) { VS_OUTPUT Output = (VS_OUTPUT)0; float4x4 preViewProjection = mul (gView, gProjection); float4x4 preWorldViewProjection = mul (gWorld, preViewProjection); Output.Position = mul(Input.Position, preWorldViewProjection); Output.WorldNormal = mul(normalize(Input.Normal), gWorld); Output.TextureCoords = Input.TextureCoords; float4 worldPosition = mul(Input.Position, gWorld); Output.WorldPosition = worldPosition / worldPosition.w; Output.TextureWeights = Input.TextureWeights; // 深度值就是顶点在屏幕空间中的z坐标,因为这是一个4x4矩阵乘法的结果,所以使用前需要首先除以w分量: Output.Depth = Output.Position.z/Output.Position.w; return Output; } float4 MultiTexturedPS(VS_OUTPUT Input):COLOR0 { // 定义两个变量处理混合:混合开始时离开相机的距离blendDistance,混合初末位置的范围blendWidth float blendDistance = 0.99f; float blendWidth = 0.005f; // 所有像素都有一个介于0和1之间的深度值,0对应近裁平面,1对应远裁平面。 // 使用以下代码,所有离开相机的距离小于0.99的像素的blendfactor为0,所有大于0.99+0.005=0.995的像素的blendfactor为1,两者之间的像素会获得一个经过线性插值的blendfactor float blendFactor = clamp((Input.Depth-blendDistance)/blendWidth, 0, 1); // 计算每个像素的颜色 float4 farColor; farColor = tex2D(textureSampler, Input.TextureCoords* gDiffuseUVTile)*Input.TextureWeights.x; farColor += tex2D(detailSampler, Input.TextureCoords* gDetailUVTile)*Input.TextureWeights.y; farColor += tex2D(texture3Sampler, Input.TextureCoords*gTexture3UVTile)*Input.TextureWeights.z; farColor += tex2D(texture4Sampler, Input.TextureCoords*gTexture4UVTile)*Input.TextureWeights.w; // 对近距离的像素,将纹理坐标乘以3,这样纹理会缩小3倍,即细节增加3倍。 float4 nearColor; float2 nearTextureCoords = Input.TextureCoords*3; nearColor = tex2D(textureSampler, nearTextureCoords*gDiffuseUVTile)*Input.TextureWeights.x; nearColor += tex2D(detailSampler, nearTextureCoords* gDetailUVTile)*Input.TextureWeights.y; nearColor += tex2D(texture3Sampler, nearTextureCoords*gTexture3UVTile)*Input.TextureWeights.z; nearColor += tex2D(texture4Sampler, nearTextureCoords*gTexture4UVTile)*Input.TextureWeights.w; // 有了像素的nearColor和farColor,根据混合因子blendFactor将它们混合在一起获得漫反射颜色 float4 diffuseColor = lerp(nearColor, farColor, blendFactor); // 将最终输出的颜色设为0 float3 finalColor = 0; // 归一化法线 float3 N = normalize(Input.WorldNormal); //---------------------------- // 遍历所有光源 //---------------------------- for(int i=0; i < gTotalLights; i++) { // 只处理可用的光源 if(gLights[i].enabled) { finalColor+= CalculateSingleLight(gLights[i], Input.WorldPosition, N,diffuseColor,gMaterialSpecular,gMaterialSpecPower); } } //---------------------------------------- // 添加环境光颜色和自发光颜色 //---------------------------------------- finalColor += (gAmbient * diffuseColor.rgb); finalColor += gMaterialEmissive; //--------------------------------------------------------------------------------- // 如果打开雾化,则计算雾化颜色 //--------------------------------------------------------------------------------- if(gFogEnabled) finalColor = LinearFog(finalColor, Input.WorldPosition); return float4(finalColor, diffuseColor.a); } technique MultiTextured { pass Pass0 { VertexShader = compile vs_3_0 MultiTexturedVS(); PixelShader = compile ps_3_0 MultiTexturedPS(); } }
具体解释还是请参见3D系列4.4 多纹理地形和3D系列4.5 在靠近相机的地方添加更多细节,还有代码的注释。
单元测试截图如下:
文件下载(已下载 1095 次)
发布时间:2010/4/29 下午4:08:55 阅读次数:6702