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 Krypton.Lights;
using Krypton.Common;
namespace Krypton
{
public enum LightMapSize
{
Full = 1,
Fourth = 2,
Eighth = 4,
}
///
/// A GPU-based 2D lighting engine
///
public class KryptonEngine : DrawableGameComponent
{
// The Krypton Effect
private string mEffectAssetName;
private Effect mEffect;
private CullMode mCullMode = CullMode.CullCounterClockwiseFace;
// The goods
private List mHulls = new List();
private List mLights = new List();
// World View Projection matrix, and it's min and max view bounds
private Matrix mWVP = Matrix.Identity;
private bool mSpriteBatchCompatabilityEnabled = false;
private BoundingRect mBounds = BoundingRect.MinMax;
// Blur
private float mBluriness = 0.25f;
private RenderTarget2D mMapBlur;
// Light maps
private RenderTarget2D mMap;
private Color mAmbientColor = new Color(35,35,35);
private LightMapSize mLightMapSize = LightMapSize.Full;
///
/// Krypton's render helper. It helps render. It also needs to be re-written.
///
public KryptonRenderHelper RenderHelper { get; private set; }
///
/// Gets or sets a value indicating how Krypton should cull geometry. The default value is CullMode.CounterClockwise
///
public CullMode CullMode
{
get { return this.mCullMode; }
set { this.mCullMode = value; }
}
///
/// The collection of lights krypton uses to render shadows
///
public List Lights { get { return this.mLights; } }
///
/// The collection of hulls krypton uses to render shadows
///
public List Hulls { get { return this.mHulls; } }
///
/// Gets or sets the matrix used to draw the light map. This should match your scene's matrix.
///
public Matrix Matrix
{
get { return this.mWVP; }
set
{
if (this.mWVP != value)
{
this.mWVP = value;
// This is totally ghetto, but it works for now. :)
// Compute the world-space bounds of the given matrix
var inverse = Matrix.Invert(value);
var v1 = Vector2.Transform(new Vector2(1, 1), inverse);
var v2 = Vector2.Transform(new Vector2(1, -1), inverse);
var v3 = Vector2.Transform(new Vector2(-1, -1), inverse);
var v4 = Vector2.Transform(new Vector2(-1, 1), inverse);
this.mBounds.Min = v1;
this.mBounds.Min = Vector2.Min(this.mBounds.Min, v2);
this.mBounds.Min = Vector2.Min(this.mBounds.Min, v3);
this.mBounds.Min = Vector2.Min(this.mBounds.Min, v4);
this.mBounds.Max = v1;
this.mBounds.Max = Vector2.Max(this.mBounds.Max, v2);
this.mBounds.Max = Vector2.Max(this.mBounds.Max, v3);
this.mBounds.Max = Vector2.Max(this.mBounds.Max, v4);
this.mBounds = BoundingRect.MinMax;
}
}
}
///
/// Gets or sets a value indicating weither or not to use SpriteBatch's matrix when drawing lightmaps
///
public bool SpriteBatchCompatablityEnabled
{
get { return this.mSpriteBatchCompatabilityEnabled; }
set { this.mSpriteBatchCompatabilityEnabled = value; }
}
///
/// Ambient color of the light map. Lights + AmbientColor = Final
///
public Color AmbientColor
{
get { return this.mAmbientColor; }
set { this.mAmbientColor = value; }
}
///
/// Gets or sets the value used to determine light map size
///
public LightMapSize LightMapSize
{
get { return this.mLightMapSize; }
set
{
if (this.mLightMapSize != value)
{
this.mLightMapSize = value;
this.DisposeRenderTargets();
this.CreateRenderTargets();
}
}
}
///
/// Gets or sets a value indicating how much to blur the final light map. If the value is zero, the lightmap will not be blurred
///
public float Bluriness
{
get { return this.mBluriness; }
set { this.mBluriness = Math.Max(0, value); }
}
///
/// Constructs a new instance of krypton
///
/// Your game object
/// The asset name of Krypton's effect file, which must be included in your content project
public KryptonEngine(Game game, string effectAssetName)
: base(game)
{
this.mEffectAssetName = effectAssetName;
}
///
/// Initializes Krpyton, and hooks itself to the graphics device
///
public override void Initialize()
{
base.Initialize();
this.GraphicsDevice.DeviceReset += new EventHandler(GraphicsDevice_DeviceReset);
}
///
/// Resets kryptons graphics device resources
///
private void GraphicsDevice_DeviceReset(object sender, EventArgs e)
{
this.DisposeRenderTargets();
this.CreateRenderTargets();
}
///
/// Load's the graphics related content required to draw light maps
///
protected override void LoadContent()
{
// This needs to better handle content loading...
// if the window is resized, Krypton needs to notice.
this.mEffect = this.Game.Content.Load(this.mEffectAssetName);
this.RenderHelper = new KryptonRenderHelper(this.GraphicsDevice, this.mEffect);
this.CreateRenderTargets();
}
///
/// Unload's the graphics content required to draw light maps
///
protected override void UnloadContent()
{
this.DisposeRenderTargets();
}
///
/// Creates render targets
///
private void CreateRenderTargets()
{
var targetWidth = GraphicsDevice.Viewport.Width / (int)(this.mLightMapSize);
var targetHeight = GraphicsDevice.Viewport.Height / (int)(this.mLightMapSize);
this.mMap = new RenderTarget2D(GraphicsDevice, targetWidth, targetHeight, false, SurfaceFormat.Color, DepthFormat.Depth24Stencil8, 0, RenderTargetUsage.PlatformContents);
this.mMapBlur = new RenderTarget2D(GraphicsDevice, targetWidth, targetHeight, false, SurfaceFormat.Color, DepthFormat.Depth24Stencil8, 0, RenderTargetUsage.PlatformContents);
}
///
/// Disposes of render targets
///
private void DisposeRenderTargets()
{
KryptonEngine.TryDispose(this.mMap);
KryptonEngine.TryDispose(this.mMapBlur);
}
///
/// Attempts to dispose of disposable objects, and assigns them a null value afterward
///
///
private static void TryDispose(IDisposable obj)
{
if (obj != null)
{
obj.Dispose();
obj = null;
}
}
///
/// Draws the light map to the current render target
///
/// N/A - Required
public override void Draw(GameTime gameTime)
{
this.LightMapPresent();
}
///
/// Prepares the light map to be drawn (pre-render)
///
public void LightMapPrepare()
{
// Prepare and set the matrix
var viewWidth = this.GraphicsDevice.ScissorRectangle.Width;
var viewHeight = this.GraphicsDevice.ScissorRectangle.Height;
// Prepare the matrix with optional settings and assign it to an effect parameter
Matrix lightMapMatrix = this.LightmapMatrixGet();
this.mEffect.Parameters["Matrix"].SetValue(lightMapMatrix);
// Obtain the original rendering states
var originalRenderTargets = this.GraphicsDevice.GetRenderTargets();
// Set and clear the target
this.GraphicsDevice.SetRenderTarget(this.mMap);
this.GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.Stencil, this.mAmbientColor, 0, 1);
// Make sure we're culling the right way!
this.GraphicsDevice.RasterizerState = KryptonEngine.RasterizerStateGetFromCullMode(this.mCullMode);
// put the render target's size into a more friendly format
var targetSize = new Vector2(this.mMap.Width, this.mMap.Height);
// Render Light Maps
foreach (var light in this.mLights)
{
// Loop through each light within the view frustum
if (light.Bounds.Intersects(this.mBounds))
{
// Clear the stencil and set the scissor rect (because we're stretching geometry past the light's reach)
this.GraphicsDevice.Clear(ClearOptions.Stencil, Color.Black, 0, 1);
this.GraphicsDevice.ScissorRectangle = KryptonEngine.ScissorRectCreateForLight(light, lightMapMatrix, targetSize);
// Draw the light!
light.Draw(this.RenderHelper, this.mHulls);
}
}
if (this.mBluriness > 0)
{
// Blur the shadow map horizontally to the blur target
this.GraphicsDevice.SetRenderTarget(this.mMapBlur);
this.RenderHelper.BlurTextureToTarget(this.mMap, LightMapSize.Full, BlurTechnique.Horizontal, this.mBluriness);
// Blur the shadow map vertically back to the final map
this.GraphicsDevice.SetRenderTarget(this.mMap);
this.RenderHelper.BlurTextureToTarget(this.mMapBlur, LightMapSize.Full, BlurTechnique.Vertical, this.mBluriness);
}
// Reset to the original rendering states
this.GraphicsDevice.SetRenderTargets(originalRenderTargets);
}
///
/// Returns the final, modified matrix used to render the lightmap.
///
///
private Matrix LightmapMatrixGet()
{
if (this.mSpriteBatchCompatabilityEnabled)
{
float xScale = (this.GraphicsDevice.Viewport.Width > 0) ? (1f / this.GraphicsDevice.Viewport.Width) : 0f;
float yScale = (this.GraphicsDevice.Viewport.Height > 0) ? (-1f / this.GraphicsDevice.Viewport.Height) : 0f;
// This is the default matrix used to render sprites via spritebatch
var matrixSpriteBatch = new Matrix()
{
M11 = xScale * 2f,
M22 = yScale * 2f,
M33 = 1f,
M44 = 1f,
M41 = -1f - xScale,
M42 = 1f - yScale,
};
// Return krypton's matrix, compensated for use with SpriteBatch
return this.mWVP * matrixSpriteBatch;
}
else
{
// Return krypton's matrix
return this.mWVP;
}
}
///
/// Gets a pixel-space rectangle which contains the light passed in
///
/// The light used to create the rectangle
/// the WorldViewProjection matrix being used to render
/// The rendertarget's size
///
private static Rectangle ScissorRectCreateForLight(ILight2D light, Microsoft.Xna.Framework.Matrix matrix, Vector2 targetSize)
{
// This needs refining, but it works as is (I believe)
var lightBounds = light.Bounds;
var min = KryptonEngine.VectorToPixel(lightBounds.Min, matrix, targetSize);
var max = KryptonEngine.VectorToPixel(lightBounds.Max, matrix, targetSize);
var min2 = Vector2.Min(min, max);
var max2 = Vector2.Max(min, max);
min = Vector2.Clamp(min2, Vector2.Zero, targetSize);
max = Vector2.Clamp(max2, Vector2.Zero, targetSize);
return new Rectangle((int)(min.X), (int)(min.Y), (int)(max.X - min.X), (int)(max.Y - min.Y));
}
///
/// Takes a screen-space vector and puts it in to pixel space
///
///
///
///
///
private static Vector2 VectorToPixel(Vector2 v, Matrix matrix, Vector2 targetSize)
{
Vector2.Transform(ref v, ref matrix, out v);
v.X = (1 + v.X) * (targetSize.X / 2f);
v.Y = (1 - v.Y) * (targetSize.Y / 2f);
return v;
}
///
/// Takes a screen-space size vector and converts it to a pixel-space size vector
///
///
///
///
///
private static Vector2 ScaleToPixel(Vector2 v, Matrix matrix, Vector2 targetSize)
{
v.X *= matrix.M11 * (targetSize.X / 2f);
v.Y *= matrix.M22 * (targetSize.Y / 2f);
return v;
}
///
/// Retrieves a rasterize state by using the cull mode as a lookup
///
/// The cullmode used to lookup the rasterize state
///
private static RasterizerState RasterizerStateGetFromCullMode(CullMode cullMode)
{
switch (cullMode)
{
case (CullMode.CullCounterClockwiseFace):
return RasterizerState.CullCounterClockwise;
case (CullMode.CullClockwiseFace):
return RasterizerState.CullClockwise;
default:
return RasterizerState.CullNone;
}
}
///
/// Presents the light map to the current render target
///
private void LightMapPresent()
{
RenderHelper.DrawTextureToTarget(this.mMap, this.mLightMapSize, BlendTechnique.Multiply);
}
}
}