Mr valentine suggested i make a example post on programatically creating a skybox and tesselating it to a sphere which i was doing. and so here it is. I somewhat prefer to post this for posterity, on the site. Also so it can be found more readily in a google search by anyone who maybe looking.
This will be a three part code post that covers a single Game1 example class.
Though this is in a single Game1 class Its heavily commented and not optimized at all. It maybe considered a working prototype and full example.
The two skybox algorithms will go into the second post
The sub-division method itself will be in the third part
Both of which belong to the SkyBoxTesselator class.
At the very bottom is the custom vertex structure used.
The Game1 class is here to serve as a example of its usage and generated the below gif.
This can be run with the console on to see quite a lot of data output as well by setting the bool at the top of the tesselator class to true. Google how to turn on console output in vs on a windows app if you donât know how and wish to see it.
How to run the example ?
The code shown can all be copy pasted from top to bottom into a single Game1 class.
You change the namespace to your projects namespace then add the images.
It runs stand alone in a MonoGame project.
External requirements besides MonoGame
This example requires only that you load two skybox images.
Hopefully this will be found useful by some. lol because it nearly gave me a migraine to write it.
MonoGame is a great api and this is a little attempt to give back.
The keyboard controls can be found in the update method primarily wasd t r f g
Here are the two images i used for the example shown in the above gif.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace SkyCubeToSphereTesselationExample
{
/// <summary>
/// Much of this in game1 is not really optimal.
/// However i squeezed it down because...
/// The skybox and sphere algorithms are large.
/// </summary>
public class Game1 : Game
{
#region This region defines some constants we might like to have on hand
public const float PI = (float)(Math.PI);
public const float PI2 = (float)(Math.PI * 2.0d);
public const float TODEGREES = 360.0f / PI2;
public const float TORADIANS = PI2 / 360.0f;
# endregion
#region This_region defines some useful states
RasterizerState rs_regular = new RasterizerState()
{
FillMode = FillMode.Solid,
};
RasterizerState rs_solid_ccw = new RasterizerState()
{
FillMode = FillMode.Solid,
CullMode = CullMode.CullCounterClockwiseFace
};
RasterizerState rs_wireframe_cullnone = new RasterizerState()
{
FillMode = FillMode.WireFrame,
CullMode = CullMode.None
};
DepthStencilState ds_depthtest_lessthanequals = new DepthStencilState()
{
DepthBufferEnable = true,
DepthBufferFunction = CompareFunction.LessEqual
};
DepthStencilState ds_depthtest_disabled = new DepthStencilState()
{
DepthBufferEnable = false,
};
#endregion
#region These will be our commonly used variables graphics textures ect
public static GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont font;
// our example class
SkyBoxTesselator sbt = new SkyBoxTesselator();
private Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, GraphicsDeviceManager.DefaultBackBufferWidth / GraphicsDeviceManager.DefaultBackBufferHeight, .5f, 2000);
private Matrix view = Matrix.Identity;
private Matrix camera = Matrix.Identity;
private Matrix worldSkybox = Matrix.Identity;
private Matrix worldGlobe = Matrix.Identity;
private float globeSpinAngle = 0f;
BasicEffect beffect;
private Texture2D currentSkyBoxTexture;
private Texture2D skyBoxImageA;
private Texture2D skyBoxImageB;
/// <summary>
/// This is a custom vertex struct defined at the bottom of the class
/// It allows for position normals colors and u v texture coordinates.
/// </summary>
private VertexPositionNormalColorUv[] vertices_skybox;
private int[] indices_skybox;
int tesselations = 0;
float timenow = 0f;
float pausetimeset = 0f;
#endregion
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
Window.AllowUserResizing = true;
this.IsFixedTimeStep = true;
this.TargetElapsedTime = TimeSpan.FromSeconds(1d / 60);
this.IsMouseVisible = true;
this.Window.ClientSizeChanged += HeyCallMeWhenTheUserResizesTheWindow;
}
public void HeyCallMeWhenTheUserResizesTheWindow(object sender, EventArgs e) { /* we are here when the windows being resized */ }
protected override void Initialize()
{
graphics.PreferredBackBufferWidth = 900;
graphics.PreferredBackBufferHeight = 600;
graphics.ApplyChanges();
base.Initialize();
}
protected override void UnloadContent() { }
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
//font = Content.Load<SpriteFont>("YourMonoGameCreatedFont");
skyBoxImageB = Content.Load<Texture2D>("skyBoxCrossImage_Example");
skyBoxImageA = Content.Load<Texture2D>("skyboxCrossImage_Desert");
currentSkyBoxTexture = skyBoxImageA;
beffect = new BasicEffect(GraphicsDevice);
beffect.TextureEnabled = true;
beffect.Texture = currentSkyBoxTexture;
beffect.Projection = projection;
// slightly raise our camera position
camera.Translation = new Vector3(0f, 3f, 0f);
CreateOrSetupSkyBoxOrSphere(out vertices_skybox, out indices_skybox, true, 0, 50f, true, true);
}
/// <summary>
/// This is a sample setup method for illustration.
/// The direct versions specifys also the facing order from 0 to 5 for front back right left ect...
/// Standard order is 0,1,2,3,4,5 for each parameter in sequence.
///
/// Texture order is not standardize for skyboxes so ive added the ability to swap order...
/// With many cross shaped skybox images the order is 0,1,2,3,4,6,5
/// With square skyboxs they may have many orders.
/// for example i think blender outputs 0,1,5,4,3,2 for its square images
/// </summary>
public void CreateOrSetupSkyBoxOrSphere(out VertexPositionNormalColorUv[] vertices_sborss, out int[] indices_sborss, bool createSphere, int numberOfTesselations, float scaleDistance, bool skyBoxUseImageCrossType, bool skyBoxCrossImageLeft)
{
if (skyBoxUseImageCrossType)
{
sbt.PrivateCreateSkyboxFromCrossImage(out vertices_sborss, out indices_sborss, scaleDistance, false, skyBoxCrossImageLeft, 0, 1, 2, 3, 5, 4);
}
else
{
sbt.PrivateCreateSkyboxFromSixBlockImage(out vertices_sborss, out indices_sborss, scaleDistance, false, 0, 1, 5, 4, 3, 2);
}
// this particular sphere type needs a preexisting cube type to be created like a skybox
// because its basically a... (single texture), seperate quad faced cube subdivision method
if (createSphere && numberOfTesselations > 0)
{
sbt.SkyBoxToSphere(vertices_sborss, indices_sborss, out vertices_sborss, out indices_sborss, numberOfTesselations, scaleDistance);
}
}
protected override void Update(GameTime gameTime)
{
KeyboardState ks = Keyboard.GetState();
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || ks.IsKeyDown(Keys.Escape))
Exit();
// keypress pause that only applys to some keys you don't want to fire multiple times
timenow = (float)gameTime.TotalGameTime.TotalSeconds;
if (pausetimeset < timenow)
{
// recreate a cube
if (ks.IsKeyDown(Keys.K)) { CreateOrSetupSkyBoxOrSphere(out vertices_skybox, out indices_skybox, false, 0, 2f, true, true); pausetimeset = timenow + .7f; }
// tesselate
if (ks.IsKeyDown(Keys.T))
{
sbt.Subdivide(vertices_skybox, indices_skybox, Vector3.Zero, 100f, 1, out vertices_skybox, out indices_skybox); pausetimeset = timenow + .7f;
tesselations += 1;
}
if (ks.IsKeyDown(Keys.R))
{
sbt.Subdivide(vertices_skybox, indices_skybox, Vector3.Zero, 100f, -1, out vertices_skybox, out indices_skybox); pausetimeset = timenow + .7f;
tesselations -= 1;
}
// flip to wirefram or solid
if (ks.IsKeyDown(Keys.F)) { GraphicsDevice.RasterizerState = rs_wireframe_cullnone; pausetimeset = timenow + .7f; }
if (ks.IsKeyDown(Keys.G)) { GraphicsDevice.RasterizerState = rs_regular; pausetimeset = timenow + .7f; }
}
// This is just a quick hack-in input camera slash view matrix.
if (ks.IsKeyDown(Keys.W)) { camera = camera * Matrix.CreateFromAxisAngle(camera.Right, +.0123f); }
if (ks.IsKeyDown(Keys.S)) { camera = camera * Matrix.CreateFromAxisAngle(camera.Right, -.0123f); }
if (ks.IsKeyDown(Keys.A)) { camera = camera * Matrix.CreateFromAxisAngle(camera.Up, +.0123f); }
if (ks.IsKeyDown(Keys.D)) { camera = camera * Matrix.CreateFromAxisAngle(camera.Up, -.0123f); }
var up = ((new Vector3(0f,.99f,0f) - camera.Up) *.05f) + camera.Up;
camera = Matrix.CreateWorld(Vector3.Zero, camera.Forward, up);
view = Matrix.CreateLookAt(Vector3.Zero, camera.Forward, camera.Up);
// set the cameras view to the basic effect
beffect.View = view;
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
currentSkyBoxTexture = skyBoxImageA;
DrawCustomVerticesWithBasicEffect(currentSkyBoxTexture, worldSkybox, vertices_skybox, indices_skybox);
globeSpinAngle += .01f;
worldGlobe = Matrix.Identity * Matrix.CreateScale(.03f);
worldGlobe *= Matrix.CreateRotationY(globeSpinAngle);
worldGlobe.Translation = new Vector3(0, 0, -10f);
currentSkyBoxTexture = skyBoxImageB;
DrawCustomVerticesWithBasicEffect(currentSkyBoxTexture, worldGlobe, vertices_skybox, indices_skybox);
base.Draw(gameTime);
}
public void DrawCustomVerticesWithBasicEffect(Texture2D texture, Matrix world, VertexPositionNormalColorUv[] verts, int[] indexs)
{
beffect.Texture = texture;
beffect.World = world;
foreach (EffectPass pass in beffect.CurrentTechnique.Passes)
{
pass.Apply();
GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, verts, 0, 2, indexs, 0, indexs.Length / 3, VertexPositionNormalColorUv.VertexDeclaration);
}
}
continued belowâŚ