diff --git a/TrueCraft.Client/Rendering/Camera.cs b/TrueCraft.Client/Rendering/Camera.cs index 9cd06d2..30f6967 100644 --- a/TrueCraft.Client/Rendering/Camera.cs +++ b/TrueCraft.Client/Rendering/Camera.cs @@ -136,6 +136,20 @@ namespace TrueCraft.Client.Rendering effect.World = Matrix.Identity; } + /// + /// Applies this camera to the specified effect. + /// + /// The effect to apply this camera to. + public void ApplyTo(AlphaTestEffect effect) + { + if (_isDirty) + Recalculate(); + + effect.View = _view; + effect.Projection = _projection; + effect.World = Matrix.Identity; + } + /// /// Returns the bounding frustum calculated for this camera. /// diff --git a/TrueCraft.Client/Rendering/ChunkMesh.cs b/TrueCraft.Client/Rendering/ChunkMesh.cs index 26b934b..3ed766a 100644 --- a/TrueCraft.Client/Rendering/ChunkMesh.cs +++ b/TrueCraft.Client/Rendering/ChunkMesh.cs @@ -29,6 +29,23 @@ namespace TrueCraft.Client.Rendering Vertices = vertices; SetSubmesh(0, indices); } + + /// + /// + /// + /// + /// + /// + /// + /// + 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); + } /// /// diff --git a/TrueCraft.Client/Rendering/ChunkRenderer.cs b/TrueCraft.Client/Rendering/ChunkRenderer.cs index accd367..69e1259 100644 --- a/TrueCraft.Client/Rendering/ChunkRenderer.cs +++ b/TrueCraft.Client/Rendering/ChunkRenderer.cs @@ -19,7 +19,7 @@ namespace TrueCraft.Client.Rendering /// A daemon of sorts that creates meshes from chunks. /// Passing meshes back is NOT thread-safe. /// - public class ChunkRenderer + public class ChunkRenderer : Renderer { public class ChunkSorter : Comparer { @@ -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 MeshGenerated; - - private ConcurrentQueue ChunkQueue { get; set; } private GraphicsDevice GraphicsDevice { get; set; } private IBlockRepository BlockRepository { get; set; } - private List> RenderThreads { get; set; } public ChunkRenderer(GraphicsDevice graphics, IBlockRepository blockRepository) + : base() { - ChunkQueue = new ConcurrentQueue(); - RenderThreads = new List>(); - 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(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 OpaqueVerticies = new List(); + public readonly List Verticies = new List(); public readonly List OpaqueIndicies = new List(); - public readonly List TransparentVerticies = new List(); public readonly List TransparentIndicies = new List(); public readonly HashSet DrawableCoordinates = new HashSet(); } 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)); - } } } } diff --git a/TrueCraft.Client/Rendering/Mesh.cs b/TrueCraft.Client/Rendering/Mesh.cs index 43d8cd8..9e937ca 100644 --- a/TrueCraft.Client/Rendering/Mesh.cs +++ b/TrueCraft.Client/Rendering/Mesh.cs @@ -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); diff --git a/TrueCraft.Client/Rendering/Renderer.cs b/TrueCraft.Client/Rendering/Renderer.cs new file mode 100644 index 0000000..8591742 --- /dev/null +++ b/TrueCraft.Client/Rendering/Renderer.cs @@ -0,0 +1,193 @@ +using System; +using System.Threading; +using System.Collections.Concurrent; + +namespace TrueCraft.Client.Rendering +{ + /// + /// Abstract base class for renderers of meshes. + /// + /// The object to render into a mesh. + public abstract class Renderer : IDisposable + { + private readonly object _syncLock = + new object(); + + /// + /// + /// + public event EventHandler> MeshCompleted; + + private volatile bool _isRunning; + private Thread _rendererThread; + private ConcurrentQueue _items, _priorityItems; + private volatile bool _isDisposed; + + /// + /// Gets whether this renderer is running. + /// + public bool IsRunning + { + get + { + if (_isDisposed) + throw new ObjectDisposedException(GetType().Name); + return _isRunning; + } + } + + /// + /// Gets whether this renderer is disposed of. + /// + public bool IsDisposed + { + get { return _isDisposed; } + } + + /// + /// + /// + protected Renderer() + { + lock (_syncLock) + { + _isRunning = false; + _rendererThread = new Thread(DoRendering); + _items = new ConcurrentQueue(); _priorityItems = new ConcurrentQueue(); + _isDisposed = false; + } + } + + /// + /// Starts this renderer. + /// + public void Start() + { + if (_isDisposed) + throw new ObjectDisposedException(GetType().Name); + + if (_isRunning) return; + lock (_syncLock) + { + _isRunning = true; + _rendererThread.Start(null); + } + } + + /// + /// + /// + /// + 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(item, result, true); + if (MeshCompleted != null) + MeshCompleted(this, args); + } + else if (_items.TryDequeue(out item) && TryRender(item, out result)) + { + var args = new RendererEventArgs(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); + } + } + + /// + /// + /// + /// + /// + /// + protected abstract bool TryRender(T item, out Mesh result); + + /// + /// Stops this renderer. + /// + public void Stop() + { + if (_isDisposed) + throw new ObjectDisposedException(GetType().Name); + + if (!_isRunning) return; + lock (_syncLock) + { + _isRunning = false; + _rendererThread.Join(); + } + } + + /// + /// Enqueues an item to this renderer for rendering. + /// + /// + /// + 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); + } + } + + /// + /// Disposes of this renderer. + /// + public void Dispose() + { + if (_isDisposed) + return; + + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes of this renderer. + /// + /// + 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. + } + + /// + /// Finalizes this renderer. + /// + ~Renderer() + { + Dispose(false); + } + } +} diff --git a/TrueCraft.Client/Rendering/RendererEventArgs.cs b/TrueCraft.Client/Rendering/RendererEventArgs.cs new file mode 100644 index 0000000..f14ff60 --- /dev/null +++ b/TrueCraft.Client/Rendering/RendererEventArgs.cs @@ -0,0 +1,39 @@ +using System; + +namespace TrueCraft.Client.Rendering +{ + /// + /// + /// + /// + public sealed class RendererEventArgs : EventArgs + { + /// + /// + /// + public T Item { get; private set; } + + /// + /// + /// + public Mesh Result { get; private set; } + + /// + /// + /// + public bool IsPriority { get; private set; } + + /// + /// + /// + /// + /// + /// + public RendererEventArgs(T item, Mesh result, bool isPriority) + { + Item = item; + Result = result; + IsPriority = isPriority; + } + } +} diff --git a/TrueCraft.Client/TrueCraft.Client.csproj b/TrueCraft.Client/TrueCraft.Client.csproj index b274693..f389d45 100644 --- a/TrueCraft.Client/TrueCraft.Client.csproj +++ b/TrueCraft.Client/TrueCraft.Client.csproj @@ -68,6 +68,8 @@ + + diff --git a/TrueCraft.Client/TrueCraftGame.cs b/TrueCraft.Client/TrueCraftGame.cs index b747325..6cc10aa 100644 --- a/TrueCraft.Client/TrueCraftGame.cs +++ b/TrueCraft.Client/TrueCraftGame.cs @@ -28,11 +28,9 @@ namespace TrueCraft.Client private IPEndPoint EndPoint { get; set; } private ChunkRenderer ChunkConverter { get; set; } private DateTime NextPhysicsUpdate { get; set; } - private List ChunkMeshes { get; set; } + private List ChunkMeshes { get; set; } private ConcurrentBag PendingMainThreadActions { get; set; } - private ConcurrentBag IncomingChunks { get; set; } - private ConcurrentBag IncomingTransparentChunks { get; set; } - private List TransparentChunkMeshes { get; set; } + private ConcurrentBag 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(); - TransparentChunkMeshes = new List(); - IncomingChunks = new ConcurrentBag(); - IncomingTransparentChunks = new ConcurrentBag(); + ChunkMeshes = new List(); + IncomingChunks = new ConcurrentBag(); PendingMainThreadActions = new ConcurrentBag(); 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 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++)