ProceduralPlanet for XNA 4.0

/* jwatte's ProceduralPlanet sample working on XNA 4.0!  Although I do see 
some small jaggy edges near the polls of the planet that was not a problem 
in the original.  It may have something to do with the fact that I changed the 
heightTexture from SurfaceFormat.Single to SurfaceFormat.Color because I 
was getting the following runtime error in XNA4 that did not occur on XNA3:  
"XNA Framework HiDef profile requires TextureFilter to be Point when using 
texture format Single."
*/
 
// Any ideas?
 
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using System.Runtime.InteropServices;
 
namespace ProceduralPlanet
{
  public class ProceduralPlanet : Microsoft.Xna.Framework.Game
  {
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
 
    public ProceduralPlanet()
    {
      graphics = new GraphicsDeviceManager(this);
      graphics.PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8;
      graphics.PreferMultiSampling = true;
      graphics.PreferredBackBufferHeight = 720;
      graphics.PreferredBackBufferWidth = 1280;
      graphics.SynchronizeWithVerticalRetrace = true;
      Content.RootDirectory = "Content";
    }
 
    protected override void Initialize()
    {
      base.Initialize();
    }
 
    [StructLayout(LayoutKind.Sequential)]
    public struct VertexPositionNormalTangentTexture
    {
      public Vector3 Position;
      public Vector3 Normal;
      public Vector3 Tangent;
      public Vector2 Texture;
      public static VertexElement[] VertexElements = new VertexElement[] {
        new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0),
        new VertexElement(0, 12, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Normal, 0),
        new VertexElement(0, 24, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Tangent, 0),
        new VertexElement(0, 36, VertexElementFormat.Vector2, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0),
      };
      public const int VertexSize = 44;
    }
 
    VertexDeclaration decl_;
    List<VertexPositionNormalTangentTexture> sphere_vertices = new List<VertexPositionNormalTangentTexture>();
    List<int> sphere_indices = new List<int>();
    VertexPositionNormalTangentTexture[] sphere_;
    int[] sphereIndices_;
    const int Slices = 36;
    const int Stacks = 24;
    Texture2D heightTexture_;
    Texture2D colorTexture_;
    Texture2D normalTexture_;
    Effect fx_;
    EffectParameter height_;
    EffectParameter diffuse_;
    EffectParameter normal_;
    EffectParameter world_;
    EffectParameter view_;
    EffectParameter projection_;
    EffectParameter lightDir_;
    SpriteFont font_;
    bool showHeight_ = false;
    bool showColor_ = false;
 
    protected override void LoadContent()
    {
      spriteBatch = new SpriteBatch(GraphicsDevice);
      decl_ = new VertexDeclaration(GraphicsDevice, VertexPositionNormalTangentTexture.VertexElements);
      font_ = Content.Load<SpriteFont>("Display");
      fx_ = Content.Load<Effect>("Planet");
      height_ = fx_.Parameters["HeightMap"];
      diffuse_ = fx_.Parameters["DiffuseMap"];
      normal_ = fx_.Parameters["NormalMap"];
      world_ = fx_.Parameters["World"];
      view_ = fx_.Parameters["View"];
      projection_ = fx_.Parameters["Projection"];
      lightDir_ = fx_.Parameters["LightDir"];
 
      //  generate vertices for a procedural sphere
      //sphere_ = new VertexPositionNormalTangentTexture[(Slices + 1) * (Stacks + 1)];
      for (int slice = 0; slice <= Slices; ++slice)
      {
        for (int stack = 0; stack <= Stacks; ++stack)
        {
          VertexPositionNormalTangentTexture v = new VertexPositionNormalTangentTexture();
          v.Position.Y = (float)Math.Sin(((double)stack / Stacks - 0.5) * Math.PI);
          if (v.Position.Y < -1) v.Position.Y = -1;
          else if (v.Position.Y > 1) v.Position.Y = 1;
          float l = (float)Math.Sqrt(1 - v.Position.Y * v.Position.Y);
          v.Position.X = l * (float)Math.Sin((double)slice / Slices * Math.PI * 2);
          v.Position.Z = l * (float)Math.Cos((double)slice / Slices * Math.PI * 2);
          v.Normal = v.Position;
          v.Tangent = new Vector3(l * (float)Math.Cos((double)slice / Slices * Math.PI * 2),
              0, -(float)Math.Sin((double)slice / Slices * Math.PI * 2));
          v.Texture = new Vector2((float)slice / Slices, (float)stack / Stacks);
          //sphere_[slice * (Stacks + 1) + stack] = v;
          sphere_vertices.Add(v);
        }
      }
      //  use indexed triangles to paint the sphere
      //sphereIndices_ = new int[Slices * Stacks * 6];
      sphere_indices = new List<int>(Slices * Stacks * 6);
      for (int i = 0; i < Slices * Stacks * 6; i++) sphere_indices.Add(0);
      int six = 0;
      for (ushort slice = 0; slice < Slices; ++slice)
      {
        for (ushort stack = 0; stack < Stacks; ++stack)
        {
          ushort v = (ushort)(slice * (Stacks + 1) + stack);
          sphere_indices[six++] = v;
          sphere_indices[six++] = (ushort)(v + 1);
          sphere_indices[six++] = (ushort)(v + (Stacks + 1));
          sphere_indices[six++] = (ushort)(v + (Stacks + 1));
          sphere_indices[six++] = (ushort)(v + 1);
          sphere_indices[six++] = (ushort)(v + (Stacks + 1) + 1);
        }
      }
 
      //  generate the textures used for drawing
      GenerateTextures();
    }
 
    const int MapShift = 9;
    const int MapSize = (1 << MapShift);
    const int MapAnd = (MapSize - 1);
    static Random rand = new System.Random();
 
    void GenerateTextures()
    {
      //  generate a height map
      float[] h = new float[MapSize * MapSize];
      //  use midpoint displacement in a breadth-first manner
      float distance = 1.0f;
      int countdown = 2;
      for (int generation = 0; generation <= MapShift; ++generation)
      {
        //  for each generation, perturb the lower-right corner of successively 
        //  smaller divided squares
        int add = (1 << (MapShift - generation));
        for (int y = add; y <= MapSize; y += add)
        {
          for (int x = add; x <= MapSize; x += add)
          {
            h[((y & MapAnd) * MapSize) + (x & MapAnd)] += (generation == 0) ? 0.5f : ((float)(rand.NextDouble() - 0.5) * distance);
          }
        }
        if (generation < MapShift)
        {
          System.Diagnostics.Debug.Assert(add > 1);
          int off = add >> 1;
          //  average the edges, and the midpoint of the square
          for (int y = 0; y < MapSize; y += add)
          {
            for (int x = 0; x < MapSize; x += add)
            {
              float h00 = h[((y & MapAnd) * MapSize) + (x & MapAnd)];
              float h01 = h[((y & MapAnd) * MapSize) + ((x + add) & MapAnd)];
              float h10 = h[(((y + add) & MapAnd) * MapSize) + (x & MapAnd)];
              float h11 = h[(((y + add) & MapAnd) * MapSize) + ((x + add) & MapAnd)];
              h[((y & MapAnd) * MapSize) + ((x + off) & MapAnd)] = (h00 + h01) * 0.5f;
              h[(((y + off) & MapAnd) * MapSize) + (x & MapAnd)] = (h00 + h10) * 0.5f;
              h[(((y + add) & MapAnd) * MapSize) + ((x + off) & MapAnd)] = (h10 + h11) * 0.5f;
              h[(((y + off) & MapAnd) * MapSize) + ((x + add) & MapAnd)] = (h01 + h11) * 0.5f;
              h[(((y + off) & MapAnd) * MapSize) + ((x + off) & MapAnd)] = (h00 + h01 + h10 + h11) * 0.25f;
            }
          }
        }
        if (countdown > 0)
          --countdown;
        else
          distance = distance * 0.5f;
      }
 
#if FILTER_IS_BROKEN
      //  now, filter the edges of the texture because the poles are smaller area than the equator
      for (int i = 0; i < MapSize / 4; ++i)
      {
        float f = 0;
        int bins = i;
        float weight = 1 - i / (MapSize / 4);
        int curbin = 0;
        int curcnt = 0;
        int binoffset = 0;
        int off = i * MapSize;
        for (int j = 0; j < MapSize; ++j)
        {
          int bin = j * bins / MapSize;
          if (bin != curbin)
          {
            f /= curcnt;
            for (int q = binoffset; q < j; ++q)
              h[off + q] = h[off + q] * (1 - weight) + f * weight;
            f = 0;
            curcnt = 0;
            curbin = bin;
            binoffset = j;
          }
          f += h[off + j];
          ++curcnt;
        }
        f /= curcnt;
        for (int q = binoffset; q < MapSize; ++q)
          h[off + q] = h[off + q] * (1 - weight) + f * weight;
        off = (MapSize - i - 1) * MapSize;
        f = 0;
        curbin = 0;
        curcnt = 0;
        binoffset = 0;
        for (int j = 0; j < MapSize; ++j)
        {
          int bin = j * bins / MapSize;
          if (bin != curbin)
          {
            f /= curcnt;
            for (int q = binoffset; q < j; ++q)
              h[off + q] = h[off + q] * (1 - weight) + f * weight;
            f = 0;
            curcnt = 0;
            curbin = bin;
            binoffset = j;
          }
          f += h[off + j];
          ++curcnt;
        }
        f /= curcnt;
        for (int q = binoffset; q < MapSize; ++q)
          h[off + q] = h[off + q] * (1 - weight) + f * weight;
      }
#endif
 
      if (heightTexture_ != null)
        heightTexture_.Dispose();
      heightTexture_ = new Texture2D(GraphicsDevice, MapSize, MapSize, 1, 
          TextureUsage.None, SurfaceFormat.Single);
      heightTexture_.SetData<float>(h);
 
      Color[] c = new Color[MapSize * MapSize];
      for (int y = 0; y < MapSize; ++y)
        for (int x = 0; x < MapSize; ++x)
          c[y * MapSize + x] = HeightToColor(h[y * MapSize + x], y);
      if (colorTexture_ != null)
        colorTexture_.Dispose();
      colorTexture_ = new Texture2D(GraphicsDevice, MapSize, MapSize, 0, 
          TextureUsage.AutoGenerateMipMap, SurfaceFormat.Color);
      colorTexture_.SetData<Color>(c);
 
      for (int y = 0; y < MapSize; ++y)
        for (int x = 0; x < MapSize; ++x)
          c[y * MapSize + x] = CalcNormal(h, x, y);
      if (normalTexture_ != null)
        normalTexture_.Dispose();
      normalTexture_ = new Texture2D(GraphicsDevice, MapSize, MapSize, 0,
          TextureUsage.AutoGenerateMipMap, SurfaceFormat.Color);
      normalTexture_.SetData<Color>(c);
    }
 
    static Color CalcNormal(float[] h, int x, int y)
    {
      float du = (h[x + y * MapSize] - h[((x+1) & MapAnd) + y * MapSize]) * NormalMapStrength;
      float dv = (h[x + y * MapSize] - h[x + ((y + 1) & MapAnd) * MapSize]) * NormalMapStrength;
      float z = (float)Math.Abs(du) + Math.Abs(dv);
      if (z > 1)
      {
        du /= z;
        dv /= z;
      }
      float dw = (float)Math.Sqrt(1.0f - du*du - dv*dv);
      return new Color(new Vector3(du * 0.5f + 0.5f, dv * 0.5f + 0.5f, dw));
    }
 
    const float SeaThreshold = 0.6f;
    const float MountainThreshold = 0.75f;
    const float SnowThreshold = 0.85f;
    const float NormalMapStrength = 4.0f;
 
    static Color HeightToColor(float h, int latitude)
    {
      float latScale = (100.0f + Math.Abs(latitude - MapSize / 2)) / 200.0f;
      float w;
      if (h < SeaThreshold)
      {
        w = SeaThreshold - h;
        if (w > 1) w = 1;
        return Color.Lerp(Color.Blue, Color.DarkBlue, w);
      }
      //  make things colder outside of the equator
      h = SeaThreshold + (h - SeaThreshold) * latScale;
      if (h < MountainThreshold)
      {
        w = (MountainThreshold - h) / (MountainThreshold - SeaThreshold);
        return Color.Lerp(Color.DarkGreen, Color.DarkKhaki, w);
      }
      if (h < SnowThreshold)
      {
        w = (SnowThreshold - h) / (SnowThreshold - MountainThreshold);
        return Color.Lerp(Color.LightGray, Color.DarkGray, w);
      }
      w = h - SnowThreshold;
      if (w > 0.5f) w = 0.5f;
      return Color.Lerp(Color.LightCyan, Color.White, w + 0.5f);
    }
 
    protected override void UnloadContent()
    {
    }
 
    GamePadState prev;
    KeyboardState prevK;
 
    protected override void Update(GameTime gameTime)
    {
      // Allows the game to exit
      GamePadState gps = GamePad.GetState(PlayerIndex.One);
      KeyboardState ks = Keyboard.GetState();
      if (gps.Buttons.Back == ButtonState.Pressed || ks.IsKeyDown(Keys.Escape))
        this.Exit();
      if ((gps.Buttons.A == ButtonState.Pressed && prev.Buttons.A != ButtonState.Pressed)
          || (ks.IsKeyDown(Keys.Space) && !prevK.IsKeyDown(Keys.Space)))
      {
        //  new planet
        GenerateTextures();
      }
      if ((gps.Buttons.B == ButtonState.Pressed && prev.Buttons.B != ButtonState.Pressed)
        || (ks.IsKeyDown(Keys.Back) && !prevK.IsKeyDown(Keys.Back)))
      {
        showHeight_ = !showHeight_;
        showColor_ = false;
      }
      if ((gps.Buttons.Y == ButtonState.Pressed && prev.Buttons.Y != ButtonState.Pressed)
        || (ks.IsKeyDown(Keys.Y) && !prevK.IsKeyDown(Keys.Y)))
      {
        showColor_ = !showColor_;
        showHeight_ = false;
      }
      prev = gps;
      prevK = ks;
 
      base.Update(gameTime);
    }
 
    protected override void Draw(GameTime gameTime)
    {
      GraphicsDevice.Clear(Color.Black);
 
      //  set up the display parameters
      diffuse_.SetValue(colorTexture_);
      normal_.SetValue(normalTexture_);
      height_.SetValue(heightTexture_);
      world_.SetValue(Matrix.CreateRotationY(-(float)
        ((gameTime.TotalGameTime.TotalSeconds % 100) * 2 * Math.PI / 100)));
      view_.SetValue(Matrix.CreateTranslation(0, 0, -4));
      projection_.SetValue(Matrix.CreatePerspective(
            (float)GraphicsDevice.PresentationParameters.BackBufferWidth / GraphicsDevice.PresentationParameters.BackBufferHeight,
            1.0f, 1.0f, 1000.0f));
      lightDir_.SetValue(Vector3.Normalize(new Vector3(1, 2, 3)));
 
      fx_.Begin();
      fx_.CurrentTechnique.Passes[0].Begin();
      GraphicsDevice.VertexDeclaration = decl_;
      GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionNormalTangentTexture>(
          PrimitiveType.TriangleList, sphere_vertices.ToArray(), 0, sphere_vertices.Count, sphere_indices.ToArray(), 0,
          sphere_indices.Count / 3);
      fx_.CurrentTechnique.Passes[0].End();
      fx_.End();
 
      spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.None);
      if (showHeight_)
      {
        spriteBatch.Draw(heightTexture_, new Rectangle(30, 20, MapSize, MapSize), Color.Red);
        spriteBatch.DrawString(font_, "Height", new Vector2(30, 20 + MapSize), Color.Red);
      }
      if (showColor_)
      {
        spriteBatch.Draw(colorTexture_, new Rectangle(30, 20, MapSize, MapSize), Color.White);
        spriteBatch.DrawString(font_, "Color", new Vector2(30, 20 + MapSize), Color.White);
      }
      spriteBatch.End();
 
      base.Draw(gameTime);
    }
  }
}