Rebased rendering enhancements and a bug fix

This commit is contained in:
William Moorehouse 2015-06-18 12:45:00 -04:00
commit aaddec8951
8 changed files with 334 additions and 124 deletions

View File

@ -136,6 +136,20 @@ namespace TrueCraft.Client.Rendering
effect.World = Matrix.Identity;
}
/// <summary>
/// Applies this camera to the specified effect.
/// </summary>
/// <param name="effect">The effect to apply this camera to.</param>
public void ApplyTo(AlphaTestEffect effect)
{
if (_isDirty)
Recalculate();
effect.View = _view;
effect.Projection = _projection;
effect.World = Matrix.Identity;
}
/// <summary>
/// Returns the bounding frustum calculated for this camera.
/// </summary>

View File

@ -30,6 +30,23 @@ namespace TrueCraft.Client.Rendering
SetSubmesh(0, indices);
}
/// <summary>
///
/// </summary>
/// <param name="chunk"></param>
/// <param name="device"></param>
/// <param name="vertices"></param>
/// <param name="opaqueIndices"></param>
/// <param name="transparentIndices"></param>
public ChunkMesh(ReadOnlyChunk chunk, GraphicsDevice device, VertexPositionNormalTexture[] vertices, int[] opaqueIndices, int[] transparentIndices)
: base(device, 2, true)
{
Chunk = chunk;
Vertices = vertices;
SetSubmesh(0, opaqueIndices);
SetSubmesh(1, transparentIndices);
}
/// <summary>
///
/// </summary>

View File

@ -19,7 +19,7 @@ namespace TrueCraft.Client.Rendering
/// A daemon of sorts that creates meshes from chunks.
/// Passing meshes back is NOT thread-safe.
/// </summary>
public class ChunkRenderer
public class ChunkRenderer : Renderer<ReadOnlyChunk>
{
public class ChunkSorter : Comparer<Mesh>
{
@ -39,85 +39,16 @@ namespace TrueCraft.Client.Rendering
}
}
public class MeshGeneratedEventArgs : EventArgs
{
public bool Transparent { get; set; }
public ChunkMesh Mesh { get; set; }
public MeshGeneratedEventArgs(ChunkMesh mesh, bool transparent)
{
Transparent = transparent;
Mesh = mesh;
}
}
public event EventHandler<MeshGeneratedEventArgs> MeshGenerated;
private ConcurrentQueue<ReadOnlyChunk> ChunkQueue { get; set; }
private GraphicsDevice GraphicsDevice { get; set; }
private IBlockRepository BlockRepository { get; set; }
private List<Tuple<Thread, CancellationTokenSource>> RenderThreads { get; set; }
public ChunkRenderer(GraphicsDevice graphics, IBlockRepository blockRepository)
: base()
{
ChunkQueue = new ConcurrentQueue<ReadOnlyChunk>();
RenderThreads = new List<Tuple<Thread, CancellationTokenSource>>();
var threads = Environment.ProcessorCount - 2;
if (threads < 1)
threads = 1;
for (int i = 0; i < threads; i++)
{
var token = new CancellationTokenSource();
var worker = new Thread(() => DoChunks(token));
worker.IsBackground = true;
RenderThreads.Add(new Tuple<Thread, CancellationTokenSource>(worker, token));
}
BlockRepository = blockRepository;
GraphicsDevice = graphics;
}
public void QueueChunk(ReadOnlyChunk chunk)
{
ChunkQueue.Enqueue(chunk);
}
public void QueueHighPriorityChunk(ReadOnlyChunk chunk)
{
// TODO: Overwrite existing chunks
Task.Factory.StartNew(() =>
{
ProcessChunk(chunk, new RenderState());
});
}
public void Start()
{
RenderThreads.ForEach(t => t.Item1.Start());
}
public void Stop()
{
RenderThreads.ForEach(t => t.Item2.Cancel());
}
private void DoChunks(CancellationTokenSource token)
{
var state = new RenderState();
while (true)
{
if (token.Token.IsCancellationRequested)
return;
ReadOnlyChunk chunk;
if (ChunkQueue.TryDequeue(out chunk))
{
ProcessChunk(chunk, state);
}
Thread.Sleep(1);
}
}
private static readonly Coordinates3D[] AdjacentCoordinates =
{
Coordinates3D.Up,
@ -128,20 +59,30 @@ namespace TrueCraft.Client.Rendering
Coordinates3D.West
};
protected override bool TryRender(ReadOnlyChunk item, out Mesh result)
{
var state = new RenderState();
ProcessChunk(item, state);
result = new ChunkMesh(item, GraphicsDevice, state.Verticies.ToArray(),
state.OpaqueIndicies.ToArray(), state.TransparentIndicies.ToArray());
return (result != null);
}
private class RenderState
{
public readonly List<VertexPositionNormalTexture> OpaqueVerticies = new List<VertexPositionNormalTexture>();
public readonly List<VertexPositionNormalTexture> Verticies = new List<VertexPositionNormalTexture>();
public readonly List<int> OpaqueIndicies = new List<int>();
public readonly List<VertexPositionNormalTexture> TransparentVerticies = new List<VertexPositionNormalTexture>();
public readonly List<int> TransparentIndicies = new List<int>();
public readonly HashSet<Coordinates3D> DrawableCoordinates = new HashSet<Coordinates3D>();
}
private void ProcessChunk(ReadOnlyChunk chunk, RenderState state)
{
state.OpaqueVerticies.Clear();
state.Verticies.Clear();
state.OpaqueIndicies.Clear();
state.TransparentVerticies.Clear();
state.TransparentIndicies.Clear();
state.DrawableCoordinates.Clear();
@ -198,8 +139,8 @@ namespace TrueCraft.Client.Rendering
int[] i;
var v = BlockRenderer.RenderBlock(provider, descriptor,
new Vector3(chunk.X * Chunk.Width + coords.X, coords.Y, chunk.Z * Chunk.Depth + coords.Z),
state.OpaqueVerticies.Count, out i);
state.OpaqueVerticies.AddRange(v);
state.Verticies.Count, out i);
state.Verticies.AddRange(v);
state.OpaqueIndicies.AddRange(i);
}
else
@ -207,18 +148,11 @@ namespace TrueCraft.Client.Rendering
int[] i;
var v = BlockRenderer.RenderBlock(provider, descriptor,
new Vector3(chunk.X * Chunk.Width + coords.X, coords.Y, chunk.Z * Chunk.Depth + coords.Z),
state.TransparentVerticies.Count, out i);
state.TransparentVerticies.AddRange(v);
state.Verticies.Count, out i);
state.Verticies.AddRange(v);
state.TransparentIndicies.AddRange(i);
}
}
var opaque = new ChunkMesh(chunk, GraphicsDevice, state.OpaqueVerticies.ToArray(), state.OpaqueIndicies.ToArray());
var transparent = new ChunkMesh(chunk, GraphicsDevice, state.TransparentVerticies.ToArray(), state.TransparentIndicies.ToArray());
if (MeshGenerated != null)
{
MeshGenerated(this, new MeshGeneratedEventArgs(opaque, false));
MeshGenerated(this, new MeshGeneratedEventArgs(transparent, true));
}
}
}
}

View File

@ -150,7 +150,7 @@ namespace TrueCraft.Client.Rendering
if ((index < 0) || (index > _indices.Length))
throw new ArgumentOutOfRangeException();
if (_vertices == null || _vertices.IsDisposed || _indices[index] == null || _indices[index].IsDisposed)
if (_vertices == null || _vertices.IsDisposed || _indices[index] == null || _indices[index].IsDisposed || _indices[index].IndexCount < 3)
return; // Invalid state for rendering, just return.
effect.GraphicsDevice.SetVertexBuffer(_vertices);

View File

@ -0,0 +1,193 @@
using System;
using System.Threading;
using System.Collections.Concurrent;
namespace TrueCraft.Client.Rendering
{
/// <summary>
/// Abstract base class for renderers of meshes.
/// </summary>
/// <typeparam name="T">The object to render into a mesh.</typeparam>
public abstract class Renderer<T> : IDisposable
{
private readonly object _syncLock =
new object();
/// <summary>
///
/// </summary>
public event EventHandler<RendererEventArgs<T>> MeshCompleted;
private volatile bool _isRunning;
private Thread _rendererThread;
private ConcurrentQueue<T> _items, _priorityItems;
private volatile bool _isDisposed;
/// <summary>
/// Gets whether this renderer is running.
/// </summary>
public bool IsRunning
{
get
{
if (_isDisposed)
throw new ObjectDisposedException(GetType().Name);
return _isRunning;
}
}
/// <summary>
/// Gets whether this renderer is disposed of.
/// </summary>
public bool IsDisposed
{
get { return _isDisposed; }
}
/// <summary>
///
/// </summary>
protected Renderer()
{
lock (_syncLock)
{
_isRunning = false;
_rendererThread = new Thread(DoRendering);
_items = new ConcurrentQueue<T>(); _priorityItems = new ConcurrentQueue<T>();
_isDisposed = false;
}
}
/// <summary>
/// Starts this renderer.
/// </summary>
public void Start()
{
if (_isDisposed)
throw new ObjectDisposedException(GetType().Name);
if (_isRunning) return;
lock (_syncLock)
{
_isRunning = true;
_rendererThread.Start(null);
}
}
/// <summary>
///
/// </summary>
/// <param name="obj"></param>
private void DoRendering(object obj)
{
while (_isRunning)
{
var item = default(T);
var result = default(Mesh);
lock (_syncLock)
{
if (_priorityItems.TryDequeue(out item) && TryRender(item, out result))
{
var args = new RendererEventArgs<T>(item, result, true);
if (MeshCompleted != null)
MeshCompleted(this, args);
}
else if (_items.TryDequeue(out item) && TryRender(item, out result))
{
var args = new RendererEventArgs<T>(item, result, false);
if (MeshCompleted != null)
MeshCompleted(this, args);
}
}
if (item == null) // We don't have any work, so sleep for a bit.
Thread.Sleep(100);
}
}
/// <summary>
///
/// </summary>
/// <param name="item"></param>
/// <param name="result"></param>
/// <returns></returns>
protected abstract bool TryRender(T item, out Mesh result);
/// <summary>
/// Stops this renderer.
/// </summary>
public void Stop()
{
if (_isDisposed)
throw new ObjectDisposedException(GetType().Name);
if (!_isRunning) return;
lock (_syncLock)
{
_isRunning = false;
_rendererThread.Join();
}
}
/// <summary>
/// Enqueues an item to this renderer for rendering.
/// </summary>
/// <param name="item"></param>
/// <param name="hasPriority"></param>
public void Enqueue(T item, bool hasPriority = false)
{
if (_isDisposed)
throw new ObjectDisposedException(GetType().Name);
if (!_isRunning) return;
lock (_syncLock)
{
if (hasPriority)
_priorityItems.Enqueue(item);
else
_items.Enqueue(item);
}
}
/// <summary>
/// Disposes of this renderer.
/// </summary>
public void Dispose()
{
if (_isDisposed)
return;
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes of this renderer.
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Stop();
lock (_syncLock)
{
_rendererThread = null;
_items = null; _priorityItems = null;
_isDisposed = true;
}
}
else
throw new NotSupportedException(); // We should 'encourage' developers to dispose of renderers properly.
}
/// <summary>
/// Finalizes this renderer.
/// </summary>
~Renderer()
{
Dispose(false);
}
}
}

View File

@ -0,0 +1,39 @@
using System;
namespace TrueCraft.Client.Rendering
{
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
public sealed class RendererEventArgs<T> : EventArgs
{
/// <summary>
///
/// </summary>
public T Item { get; private set; }
/// <summary>
///
/// </summary>
public Mesh Result { get; private set; }
/// <summary>
///
/// </summary>
public bool IsPriority { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="item"></param>
/// <param name="result"></param>
/// <param name="isPriority"></param>
public RendererEventArgs(T item, Mesh result, bool isPriority)
{
Item = item;
Result = result;
IsPriority = isPriority;
}
}
}

View File

@ -68,6 +68,8 @@
<Compile Include="Rendering\Font.cs" />
<Compile Include="Rendering\FontRenderer.cs" />
<Compile Include="Rendering\FontStyle.cs" />
<Compile Include="Rendering\Renderer.cs" />
<Compile Include="Rendering\RendererEventArgs.cs" />
<Compile Include="Rendering\TextureMapper.cs" />
<Compile Include="TrueCraftGame.cs" />
<Compile Include="MultiplayerClient.cs" />

View File

@ -28,11 +28,9 @@ namespace TrueCraft.Client
private IPEndPoint EndPoint { get; set; }
private ChunkRenderer ChunkConverter { get; set; }
private DateTime NextPhysicsUpdate { get; set; }
private List<ChunkMesh> ChunkMeshes { get; set; }
private List<Mesh> ChunkMeshes { get; set; }
private ConcurrentBag<Action> PendingMainThreadActions { get; set; }
private ConcurrentBag<ChunkMesh> IncomingChunks { get; set; }
private ConcurrentBag<ChunkMesh> IncomingTransparentChunks { get; set; }
private List<ChunkMesh> TransparentChunkMeshes { get; set; }
private ConcurrentBag<Mesh> IncomingChunks { get; set; }
public ChatInterface ChatInterface { get; set; }
private RenderTarget2D RenderTarget;
private BoundingFrustum CameraView;
@ -44,23 +42,23 @@ namespace TrueCraft.Client
private Microsoft.Xna.Framework.Vector3 Delta { get; set; }
private TextureMapper TextureMapper { get; set; }
private BasicEffect OpaqueEffect, TransparentEffect;
private BasicEffect OpaqueEffect;
private AlphaTestEffect TransparentEffect;
public TrueCraftGame(MultiplayerClient client, IPEndPoint endPoint)
{
Window.Title = "TrueCraft";
Content.RootDirectory = "Content";
Graphics = new GraphicsDeviceManager(this);
Graphics.SynchronizeWithVerticalRetrace = false;
Graphics.IsFullScreen = UserSettings.Local.IsFullscreen;
Graphics.PreferredBackBufferWidth = UserSettings.Local.WindowResolution.Width;
Graphics.PreferredBackBufferHeight = UserSettings.Local.WindowResolution.Height;
Client = client;
EndPoint = endPoint;
NextPhysicsUpdate = DateTime.MinValue;
ChunkMeshes = new List<ChunkMesh>();
TransparentChunkMeshes = new List<ChunkMesh>();
IncomingChunks = new ConcurrentBag<ChunkMesh>();
IncomingTransparentChunks = new ConcurrentBag<ChunkMesh>();
ChunkMeshes = new List<Mesh>();
IncomingChunks = new ConcurrentBag<Mesh>();
PendingMainThreadActions = new ConcurrentBag<Action>();
MouseCaptured = true;
@ -79,16 +77,16 @@ namespace TrueCraft.Client
SpriteBatch = new SpriteBatch(GraphicsDevice);
base.Initialize(); // (calls LoadContent)
ChunkConverter = new ChunkRenderer(Graphics.GraphicsDevice, Client.World.World.BlockRepository);
Client.ChunkLoaded += (sender, e) => ChunkConverter.QueueChunk(e.Chunk);
Client.ChunkModified += (sender, e) => ChunkConverter.QueueHighPriorityChunk(e.Chunk);
ChunkConverter.MeshGenerated += ChunkConverter_MeshGenerated;
Client.ChunkLoaded += (sender, e) => ChunkConverter.Enqueue(e.Chunk);
Client.ChunkModified += (sender, e) => ChunkConverter.Enqueue(e.Chunk, true);
ChunkConverter.MeshCompleted += ChunkConverter_MeshGenerated;
ChunkConverter.Start();
Client.PropertyChanged += HandleClientPropertyChanged;
Client.Connect(EndPoint);
var centerX = GraphicsDevice.Viewport.Width / 2;
var centerY = GraphicsDevice.Viewport.Height / 2;
Mouse.SetPosition(centerX, centerY);
Camera = new Camera(GraphicsDevice.Viewport.AspectRatio, 70.0f, 0.1f, 1000.0f);
Camera = new Camera(GraphicsDevice.Viewport.AspectRatio, 70.0f, 1.0f, 1000.0f);
UpdateCamera();
Window.ClientSizeChanged += (sender, e) => CreateRenderTarget();
MouseComponent.Move += OnMouseComponentMove;
@ -103,12 +101,9 @@ namespace TrueCraft.Client
false, GraphicsDevice.PresentationParameters.BackBufferFormat, DepthFormat.Depth24);
}
void ChunkConverter_MeshGenerated(object sender, ChunkRenderer.MeshGeneratedEventArgs e)
void ChunkConverter_MeshGenerated(object sender, RendererEventArgs<ReadOnlyChunk> e)
{
if (e.Transparent)
IncomingTransparentChunks.Add(e.Mesh);
else
IncomingChunks.Add(e.Mesh);
IncomingChunks.Add(e.Result);
}
void HandleClientPropertyChanged(object sender, PropertyChangedEventArgs e)
@ -119,7 +114,7 @@ namespace TrueCraft.Client
UpdateCamera();
var sorter = new ChunkRenderer.ChunkSorter(new Coordinates3D(
(int)Client.Position.X, 0, (int)Client.Position.Z));
PendingMainThreadActions.Add(() => TransparentChunkMeshes.Sort(sorter));
PendingMainThreadActions.Add(() => ChunkMeshes.Sort(sorter));
break;
}
}
@ -154,8 +149,9 @@ namespace TrueCraft.Client
OpaqueEffect.FogEnd = 1000f;
OpaqueEffect.FogColor = Color.CornflowerBlue.ToVector3();
TransparentEffect = new BasicEffect(GraphicsDevice);
TransparentEffect.TextureEnabled = true;
TransparentEffect = new AlphaTestEffect(GraphicsDevice);
TransparentEffect.AlphaFunction = CompareFunction.Greater;
TransparentEffect.ReferenceAlpha = 127;
TransparentEffect.Texture = TextureMapper.GetTexture("terrain.png");
base.LoadContent();
@ -287,16 +283,11 @@ namespace TrueCraft.Client
i.Update(gameTime);
}
ChunkMesh mesh;
Mesh mesh;
if (IncomingChunks.TryTake(out mesh))
{
var existing = ChunkMeshes.SingleOrDefault(m => m.Chunk.Chunk.Coordinates == mesh.Chunk.Chunk.Coordinates);
if (existing != null)
ChunkMeshes.Remove(existing);
ChunkMeshes.Add(mesh);
}
if (IncomingTransparentChunks.TryTake(out mesh)) // TODO: re-render transparent meshes
TransparentChunkMeshes.Add(mesh);
Action action;
if (PendingMainThreadActions.TryTake(out action))
action();
@ -347,37 +338,57 @@ namespace TrueCraft.Client
Camera.ApplyTo(TransparentEffect);
}
// Used for faking the disabling of color buffer writing.
private static readonly BlendState ColorWriteDisable = new BlendState()
{
ColorSourceBlend = Blend.Zero,
AlphaSourceBlend = Blend.Zero,
ColorDestinationBlend = Blend.One,
AlphaDestinationBlend = Blend.One
};
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.SetRenderTarget(RenderTarget);
GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
Graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
GraphicsDevice.SamplerStates[0] = SamplerState.PointClamp;
GraphicsDevice.SamplerStates[1] = SamplerState.PointClamp;
GraphicsDevice.BlendState = BlendState.NonPremultiplied;
int verticies = 0, chunks = 0;
GraphicsDevice.DepthStencilState = new DepthStencilState { DepthBufferEnable = true };
GraphicsDevice.DepthStencilState = DepthStencilState.Default;
for (int i = 0; i < ChunkMeshes.Count; i++)
{
if (CameraView.Intersects(ChunkMeshes[i].BoundingBox))
{
verticies += ChunkMeshes[i].GetTotalVertices();
chunks++;
ChunkMeshes[i].Draw(OpaqueEffect);
ChunkMeshes[i].Draw(OpaqueEffect, 0);
}
}
GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
for (int i = 0; i < TransparentChunkMeshes.Count; i++)
GraphicsDevice.BlendState = ColorWriteDisable;
for (int i = 0; i < ChunkMeshes.Count; i++)
{
if (CameraView.Intersects(TransparentChunkMeshes[i].BoundingBox))
if (CameraView.Intersects(ChunkMeshes[i].BoundingBox))
{
if (!TransparentChunkMeshes[i].IsDisposed)
verticies += TransparentChunkMeshes[i].GetTotalVertices();
TransparentChunkMeshes[i].Draw(TransparentEffect);
if (!ChunkMeshes[i].IsDisposed)
verticies += ChunkMeshes[i].GetTotalVertices();
ChunkMeshes[i].Draw(TransparentEffect, 1);
}
}
GraphicsDevice.BlendState = BlendState.NonPremultiplied;
for (int i = 0; i < ChunkMeshes.Count; i++)
{
if (CameraView.Intersects(ChunkMeshes[i].BoundingBox))
{
if (!ChunkMeshes[i].IsDisposed)
verticies += ChunkMeshes[i].GetTotalVertices();
ChunkMeshes[i].Draw(TransparentEffect, 1);
}
}
GraphicsDevice.DepthStencilState = DepthStencilState.Default;
SpriteBatch.Begin();
for (int i = 0; i < Interfaces.Count; i++)