29.天空球SkyDomeSceneNode类

12.3 从游戏引擎(GameBase)开始和3D系列4.6 天空球中都是使用一个半球模型生成天空球,并在其上附加天空纹理。而我的引擎中的天空球是编程实时创建的,这样灵活性更大。代码如下:

namespace StunEngine.SceneNodes
{
    /// 
    /// 天空球
    /// 
    public class SkyDomeSceneNode :Renderable3DSceneNode 
    {
        #region 构造函数和成员变量

        /// 
        /// 天空球使用的材质
        /// 
        private SingleTextureMaterial material;        

        /// 
        /// 水平方向上的切片数量
        /// 
        private int sphereCylinders;

        /// 
        /// 竖直方向上的切片数量
        /// 
        private int sphereSlices;        

        /// 
        /// 北极到球最下面的夹角,使用PI表示一个完整的球,PI/2表示半球
        /// 
        private float verticalRadians = MathHelper.PiOver2;

        /// 
        /// 创建一个默认天空球,半球,水平方向切片数量为16,竖直方向切片数量为32。
        /// 
        /// 引擎
        /// 天空球纹理
        /// 水平方向上圆周带的数量
        /// 竖直方向上整个圆周的切片数量          
        public SkyDomeSceneNode(StunXnaGE game, Scene setScene, string cloudMap) : this(game, setScene, cloudMap, 16, 32, MathHelper.PiOver2) { }
        
        /// 
        /// 创建一个天空球。
        /// 
        /// 引擎
        /// 天空球纹理
        /// 水平方向上圆周带的数量
        /// 竖直方向上整个圆周的切片数量
        /// 天空球的上下移动偏移量,负值代表向下移动
        /// 北极到球最下面的夹角,使用PI表示一个完整的球,PI/2表示半球
        public SkyDomeSceneNode(StunXnaGE game,Scene setScene, string cloudMap, int cylinders, int slices, float verticalRadians)
            : base(game,setScene)
        {
            this.sphereCylinders = cylinders;
            this.sphereSlices = slices;                      
            this.verticalRadians = verticalRadians;

            material = new SingleTextureMaterial(engine, engine .Content .Load ("Effects\\SkyDome"));
            this.material.Texture1Name = cloudMap;

            Imaterial = (IMaterial)material;

            //不对天空球进行剔除操作
            this.DisableCulling = true;
            this.DisableUpdateCulling = true;            
        }        

        #endregion        

        /// 
        /// 获取天空球使用的材质
        /// 
        public SingleTextureMaterial Material
        {
            get { return material; }
        }          
        
        /// 
        /// 初始化天空盒。 
        /// 
        public override void Initialize()
        {
            this.UpdateOrder = SceneNodeOrdering.EnvironmentMap.GetValue();
            base.Initialize();
            //创建天空球顶点数据
            this.mesh = MeshBuilder.CreateSkyDome(engine.GraphicsDevice, sphereSlices, sphereCylinders, verticalRadians);
        }        
        
        /// 
        /// 绘制天空盒。
        ///         
        public override int Draw(GameTime gameTime,bool useReflection)
        {
            //关闭深度缓冲
            engine.GraphicsDevice.RenderState.DepthBufferEnable = false;
            engine.GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

            int totalPrimitives = base.Draw(gameTime, useReflection);             
            
            //绘制结束后别忘了打开深度缓冲
            engine.GraphicsDevice.RenderState.DepthBufferWriteEnable = true;
            engine.GraphicsDevice.RenderState.DepthBufferEnable = true;

            //返回绘制的三角形数量
            return totalPrimitives;
        }        

        #region 单元测试

#if DEBUG

        /// 
        /// 测试SkyDomeSceneNode类
        /// 
        public static void TestSkyDomeSceneNode()
        {
            SkyDomeSceneNode skyDome = null;

            TestGame.Start("测试SkyDomeSceneNode类",
                delegate
                {
                    skyDome = new SkyDomeSceneNode(TestGame.engine, TestGame.scene, "Textures/SkyDome", 16, 32, MathHelper.Pi * 0.6f);
                    TestGame.scene.AddNode(skyDome);

                    TestGame.scene.IsShowMouse = false;

                },
                delegate
                {
                    //按空格键切换一张天空球纹理
                    if (Input.KeyboardSpaceJustPressed)
                    {
                        if (skyDome.Material.Texture1Name == "Textures/SkyDome")
                            skyDome.Material.Texture1Name= "Textures/Grass";
                        else
                            skyDome.Material.Texture1Name= "Textures/SkyDome";
                    }
                });
        }
#endif
        #endregion
    }
}

因为SkyDomeSceneNode类使用的材质不是GenericMaterial而是SingleTextureMaterial,所以它是从Renderable3DsceneNode类继承的。

使用的effect文件也是另外的SkyDome.fx,使用的像素着色器非常简单,只是从纹理采样颜色,其实就相当于Generic.fx的SimpleTextured。代码如下:

//-----------------------------------------------------------------------------------------------
//	天空球
//-----------------------------------------------------------------------------------------------

shared uniform extern float4x4	gProjection : PROJECTION; // 共享的投影矩阵

uniform extern float4x4 gWorld: WORLD;;					 // 世界矩阵
uniform extern float4x4 gView: VIEW;					 // 视矩阵

uniform extern float3	gCameraPos;	//	相机位置

// 天空球纹理
uniform extern texture gTexture1;
sampler textureSampler = sampler_state { texture =
 ; magfilter = LINEAR; minfilter = LINEAR; mipfilter=LINEAR; AddressU = mirror; AddressV = mirror;};

//SkyDome使用的顶点着色器输出结构
struct SkyDomeVSOut
{
    float4 Position			: POSITION0;
    float2 TexCoord			: TEXCOORD0;
};


SkyDomeVSOut SkyDomeVS(float4 position: POSITION, float2 texCoord : TEXCOORD0)
{
	SkyDomeVSOut Output=(SkyDomeVSOut)0;
	
	//根据相机位置平移世界矩阵
	gWorld[3]=float4(gCameraPos,1.0f);
	
	//将顶点坐标转换到世界坐标系
	float4x4 viewProjection = mul (gView, gProjection);
	float4x4 worldViewProjection = mul (gWorld, viewProjection);
	Output.Position = mul(position, worldViewProjection);
	
	Output.TexCoord = texCoord;
	    
    return Output;
}

float4 SkyDomePS(SkyDomeVSOut input) : COLOR0
{	
	//采样颜色纹理
	return tex2D(textureSampler, input.TexCoord);
}

technique SkyDome
{
    pass Pass0
    {       
        vertexshader = compile vs_3_0 SkyDomeVS();	
        pixelshader = compile ps_3_0 SkyDomePS();
    }
}

创建天空盒顶点数据的操作由MeshBuilder类中的静态方法CreateSkyDome完成,代码如下:

///        
/// 创建天空球顶点和索引
///         
/// 图形设备
/// 水平切片数量
/// 竖直切片数量
/// 北极到球最下面的夹角,使用PI表示一个完整的球,PI/2表示半球
/// 
public static Mesh CreateSkyDome(GraphicsDevice g, int cylinders, int slices, float verticalRadians)
{
    //===========================================================================
    //      
    //          x = r * cos(phi) * sin(theta)
    //          y = r * sin(phi) * sin(theta)
    //          z = r * cos(theta)
    //
    //          这里phi的范围是0 < phi <= 2Pi,theta范围是0 < theta < Pi
    //          半球的theta范围是[0, Pi/2]
    //          因为使用单位向量r=1,所以上述公式简化为:
    //
    //          x = cos(phi) * sin(theta)
    //          y = sin(phi) * sin(theta)
    //          z = cos(theta)
    //
    //------------------------------------------------------------------------------            
    //
    //        1) 我们必须考虑到XNA是右手坐标系,而+Z方向在屏幕之外,需要反转y/z坐标
    //
    //        2) 我们必须注意顶点是以逆时针顺序绘制的,因为我们是从天空球内部向外看的。
    //
    //        3) uv坐标必须正确计算。
    //
    //
    //          最终我们获得以下公式:
    //
    //          x =  cos(phi) * sin(theta)
    //          y =  cos(theta)
    //          z =  -1 * sin(phi) * sin(theta)
    //
    //===========================================================================
    
    int dwVertices = (slices + 1) * (cylinders + 1);
    int dwIndices = (3 * slices * (cylinders + 1)) * 2;
    int[] indices = new int[dwIndices];

    VertexPositionTexture[] vertices = new VertexPositionTexture[dwVertices];
    float stackAngle = verticalRadians / (float)slices;
    float sliceAngle = (float)(Math.PI * 2.0) / (float)cylinders;
    int wVertexIndex = 0;

    //生成顶点坐标、纹理坐标和索引
    int vertcount = 0;
    int Indexcount = 0;

    for (int stack = 0; stack < (slices + 1); stack++)
    {
        float r = (float)Math.Sin((float)stack * stackAngle);
        float y = (float)Math.Cos((float)stack * stackAngle);

        for (int slice = 0; slice < (cylinders + 1); slice++)
        {
            float z = r * (float)Math.Sin((float)slice * sliceAngle) * -1;
            float x = r * (float)Math.Cos((float)slice * sliceAngle);

            vertices[vertcount].Position = new Vector3(x, y, z);
            vertices[vertcount].TextureCoordinate = new Vector2(1f - (float)slice / (float)cylinders, (float)stack / (float)slices);
            vertcount++;
            if (!(stack == (slices - 1)))
            {
                indices[Indexcount] = wVertexIndex + (cylinders + 1);
                Indexcount++;
                indices[Indexcount] = wVertexIndex + 1;
                Indexcount++;
                indices[Indexcount] = wVertexIndex;
                Indexcount++;
                indices[Indexcount] = wVertexIndex + (cylinders);
                Indexcount++;
                indices[Indexcount] = wVertexIndex + (cylinders + 1);
                Indexcount++;
                indices[Indexcount] = wVertexIndex;
                Indexcount++;
                wVertexIndex++;
            }
        }
    }

    //顶点声明
    VertexDeclaration skyDomeVertexDeclaration = new VertexDeclaration(g, VertexPositionTexture.VertexElements);

    //创建顶点缓冲
    VertexBuffer skyDomeVertexBuffer = new VertexBuffer(g, typeof(VertexPositionTexture), dwVertices, BufferUsage.None);
    skyDomeVertexBuffer.SetData(vertices, 0, dwVertices);

    //创建索引缓冲
    IndexBuffer skyDomeIndexBuffer = new IndexBuffer(g, typeof(int), Indexcount, BufferUsage.None);
    skyDomeIndexBuffer.SetData(indices, 0, Indexcount);

    //创建mesh,使用TriangleList绘制天空球
    return new Mesh(PrimitiveType.TriangleList, skyDomeVertexBuffer, skyDomeVertexDeclaration, skyDomeIndexBuffer, dwVertices, dwIndices / 3);
}

以上代码与创建球体的代码是类似的,只不过索引的顺序正好反向,还可以实现半球。

你也可以参考3D系列4.19 渐变的天空球给天空添加渐变的颜色,使之更符合自然中的情况,也可以给添加另一个纹理实现云层的移动效果。

单元测试截图如下:

单元测试

文件下载(已下载 1369 次)

发布时间:2010/4/26 下午4:20:01  阅读次数:7149

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号