diff --git a/GenesisEngine.Specs/DomainSpecs/QuadMeshSpecs.cs b/GenesisEngine.Specs/DomainSpecs/QuadMeshSpecs.cs new file mode 100644 index 0000000..b9f2edd --- /dev/null +++ b/GenesisEngine.Specs/DomainSpecs/QuadMeshSpecs.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Machine.Specifications; +using Rhino.Mocks; +using Microsoft.Xna.Framework; + +namespace GenesisEngine.Specs.DomainSpecs +{ + [Subject(typeof(QuadMesh))] + public class when_the_mesh_is_initialized : QuadMeshContext + { + Because of = () => + _mesh.Initialize(_radius, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 7); + + It should_initialize_the_renderer = () => + _renderer.InitializeWasCalled.ShouldBeTrue(); + + It should_remember_its_level = () => + _mesh.Level.ShouldEqual(7); + } + + [Subject(typeof(QuadMesh))] + public class when_a_top_facing_mesh_creates_a_mesh : QuadMeshContext + { + Because of = () => + _mesh.Initialize(_radius, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); + + It should_project_center_point_into_spherical_mesh_space = () => + { + var centerPosition = _renderer.Vertices[_renderer.Vertices.Length / 2].Position; + centerPosition.ShouldBeCloseTo(Vector3.Zero); + }; + + It should_calculate_center_point_normal = () => + { + var centerNormal = _renderer.Vertices[_renderer.Vertices.Length / 2].Normal; + centerNormal.ShouldBeCloseTo(Vector3.Up); + }; + + It should_project_top_left_corner_into_spherical_mesh_space = () => + { + var vertex = _renderer.Vertices[0].Position; + AssertCornerIsProjected(vertex, Vector3.Up, Vector3.Left, Vector3.Forward); + }; + + It should_project_bottom_right_corner_into_spherical_mesh_space = () => + { + var vertex = _renderer.Vertices[_renderer.Vertices.Length - 1].Position; + AssertCornerIsProjected(vertex, Vector3.Up, Vector3.Backward, Vector3.Right); + }; + + // TODO: verify that it captures corner and center samples? + } + + [Subject(typeof(QuadMesh))] + public class when_a_mesh_is_below_the_horizon : QuadMeshContext + { + Establish context = () => + _mesh.Initialize(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 5); + + Because of = () => + _mesh.Update(new TimeSpan(), DoubleVector3.Down * 100, DoubleVector3.Zero, _clippingPlanes); + + It should_not_be_visible = () => + _mesh.IsVisibleToCamera.ShouldBeFalse(); + } + + [Subject(typeof(QuadMesh))] + public class when_a_mesh_is_not_below_the_horizon : QuadMeshContext + { + Establish context = () => + _mesh.Initialize(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 5); + + Because of = () => + _mesh.Update(new TimeSpan(), DoubleVector3.Up * 100, DoubleVector3.Zero, _clippingPlanes); + + It should_be_visible = () => + _mesh.IsVisibleToCamera.ShouldBeTrue(); + } + + //[Subject(typeof(QuadMesh))] + //public class when_a_mesh_is_updated : QuadMeshContext + //{ + // Establish context = () => + // { + // _mesh.Initialize(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); + // }; + + // Because of = () => + // _mesh.Update(); + //} + + [Subject(typeof(QuadMesh))] + public class when_a_mesh_is_drawn : QuadMeshContext + { + public static DoubleVector3 _cameraLocation; + public static Matrix _viewMatrix; + public static Matrix _projectionMatrix; + + Establish context = () => + { + _cameraLocation = DoubleVector3.Up; + _viewMatrix = Matrix.Identity; + _projectionMatrix = Matrix.Identity; + + _mesh.Initialize(10, Vector3.Up, Vector3.Backward, Vector3.Right, new QuadNodeExtents(-1.0, 1.0, -1.0, 1.0), 0); + }; + + Because of = () => + _mesh.Draw(_cameraLocation, _viewMatrix, _projectionMatrix); + + It should_draw_the_mesh = () => + _renderer.DrawWasCalled.ShouldBeTrue(); + + It should_draw_the_mesh_in_the_correct_location = () => + _renderer.Location.ShouldEqual(Vector3.Up * 10); + } + + [Subject(typeof(QuadMesh))] + public class when_a_mesh_is_disposed : QuadMeshContext + { + Establish context = () => + _mesh.Initialize(_radius, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); + + Because of = () => + _mesh.Dispose(); + + It should_dispose_the_renderer = () => + _renderer.DisposeWasCalled.ShouldBeTrue(); + } + + public class QuadMeshContext + { + public static readonly float _radius = 1; + public static DoubleVector3 _location; + public static QuadNodeExtents _extents = new QuadNodeExtents(-1.0, 1.0, -1.0, 1.0); + + public static IHeightfieldGenerator _generator; + public static MockQuadMeshRenderer _renderer; + public static ISettings _settings; + public static Statistics _statistics; + public static ClippingPlanes _clippingPlanes; + + public static QuadMesh _mesh; + + Establish context = () => + { + _generator = MockRepository.GenerateStub(); + + // We're using a hand-rolled fake here because of a bug + // in .Net that prevents mocking of multi-dimentional arrays: + // http://code.google.com/p/moq/issues/detail?id=182#c0 + _renderer = new MockQuadMeshRenderer(); + + _settings = MockRepository.GenerateStub(); + + _statistics = new Statistics(); + + _clippingPlanes = new ClippingPlanes(); + + _mesh = new QuadMesh(_generator, _renderer, _settings); + }; + + public static void AssertCornerIsProjected(Vector3 projectedVector, Vector3 normalVector, Vector3 uVector, Vector3 vVector) + { + var expectedVector = (normalVector * _radius) + (uVector * _radius) + (vVector * _radius); + expectedVector.Normalize(); + expectedVector *= _radius; + expectedVector -= normalVector * _radius; + + projectedVector.ShouldBeCloseTo(expectedVector); + } + } + + public class MockQuadMeshRenderer : IQuadMeshRenderer, IDisposable + { + public bool InitializeWasCalled { get; private set; } + public bool DrawWasCalled { get; private set; } + public bool DisposeWasCalled { get; private set; } + public Vector3 Location { get; private set; } + public VertexPositionNormalColored[] Vertices { get; private set; } + + public virtual void Initialize(VertexPositionNormalColored[] vertices, int[] indices) + { + this.InitializeWasCalled = true; + this.Vertices = vertices; + } + + public virtual void Draw(DoubleVector3 location, DoubleVector3 cameraLocation, Matrix originBasedViewMatrix, Matrix projectionMatrix) + { + this.DrawWasCalled = true; + this.Location = location; + } + + public void Dispose() + { + DisposeWasCalled = true; + } + } +} diff --git a/GenesisEngine.Specs/DomainSpecs/QuadNodeSpecs.cs b/GenesisEngine.Specs/DomainSpecs/QuadNodeSpecs.cs index 9e84f63..193f6b5 100644 --- a/GenesisEngine.Specs/DomainSpecs/QuadNodeSpecs.cs +++ b/GenesisEngine.Specs/DomainSpecs/QuadNodeSpecs.cs @@ -14,13 +14,13 @@ namespace GenesisEngine.Specs.DomainSpecs // cover everything. [Subject(typeof(QuadNode))] - public class when_the_node_is_initialized : QuadNodeContext + public class when_a_node_is_initialized : QuadNodeContext { Because of = () => - _node.InitializeMesh(_radius, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 7); + _node.Initialize(_radius, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 7); - It should_initialize_the_renderer = () => - _renderer.InitializeWasCalled.ShouldBeTrue(); + It should_initialize_the_mesh = () => + _mesh.AssertWasCalled(x => x.Initialize(_radius, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 7)); It should_increment_the_node_count_statistic = () => _statistics.NumberOfQuadNodes.ShouldEqual(1); @@ -33,69 +33,27 @@ public class when_the_node_is_initialized : QuadNodeContext } [Subject(typeof(QuadNode))] - public class when_a_top_facing_node_creates_a_mesh : QuadNodeContext - { - Because of = () => - _node.InitializeMesh(_radius, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); - - It should_project_center_point_into_spherical_mesh_space = () => - { - var centerPosition = _renderer.Vertices[_renderer.Vertices.Length / 2].Position; - centerPosition.ShouldBeCloseTo(Vector3.Zero); - }; - - It should_calculate_center_point_normal = () => - { - var centerNormal = _renderer.Vertices[_renderer.Vertices.Length / 2].Normal; - centerNormal.ShouldBeCloseTo(Vector3.Up); - }; - - It should_project_top_left_corner_into_spherical_mesh_space = () => - { - var vertex = _renderer.Vertices[0].Position; - AssertCornerIsProjected(vertex, Vector3.Up, Vector3.Left, Vector3.Forward); - }; - - It should_project_bottom_right_corner_into_spherical_mesh_space = () => - { - var vertex = _renderer.Vertices[_renderer.Vertices.Length - 1].Position; - AssertCornerIsProjected(vertex, Vector3.Up, Vector3.Backward, Vector3.Right); - }; - - // TODO: verify that it captures corner and center samples? - } - - [Subject(typeof(QuadNode))] - public class when_a_node_is_below_the_horizon : QuadNodeContext - { - Establish context = () => - _node.InitializeMesh(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 5); - - Because of = () => - _node.Update(new TimeSpan(), DoubleVector3.Down * 100, DoubleVector3.Zero, _clippingPlanes); - - It should_not_be_visible = () => - _node.Visible.ShouldBeFalse(); - } - - [Subject(typeof(QuadNode))] - public class when_a_node_is_not_below_the_horizon : QuadNodeContext + public class when_a_node_is_updated : QuadNodeContext { Establish context = () => - _node.InitializeMesh(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 5); + _node.Initialize(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 5); Because of = () => - _node.Update(new TimeSpan(), DoubleVector3.Up * 100, DoubleVector3.Zero, _clippingPlanes); + _node.Update(TimeSpan.Zero, DoubleVector3.Up * 11, DoubleVector3.Zero, _clippingPlanes); - It should_be_visible = () => - _node.Visible.ShouldBeTrue(); + It should_update_the_mesh = () => + _mesh.AssertWasCalled(x => x.Update(TimeSpan.Zero, DoubleVector3.Up * 11, DoubleVector3.Zero, _clippingPlanes)); } [Subject(typeof(QuadNode))] public class when_a_leaf_node_is_updated_and_the_camera_is_close : QuadNodeContext { Establish context = () => - _node.InitializeMesh(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 5); + { + _mesh.Stub(x => x.IsVisibleToCamera).Return(true); + + _node.Initialize(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 5); + }; Because of = () => _node.Update(new TimeSpan(), DoubleVector3.Up * 11, DoubleVector3.Zero, _clippingPlanes); @@ -107,7 +65,7 @@ public class when_a_leaf_node_is_updated_and_the_camera_is_close : QuadNodeConte { foreach (var subnode in _node.Subnodes) { - subnode.AssertWasCalled(x => x.InitializeMesh(Arg.Is.Anything, Arg.Is.Anything, Arg.Is.Anything, Arg.Is.Anything, Arg.Is.Anything, Arg.Is(6))); + subnode.AssertWasCalled(x => x.Initialize(Arg.Is.Anything, Arg.Is.Anything, Arg.Is.Anything, Arg.Is.Anything, Arg.Is.Anything, Arg.Is(6))); } }; @@ -125,7 +83,9 @@ public class when_a_leaf_node_is_updated_more_than_once_and_the_camera_is_close { Establish context = () => { - _node.InitializeMesh(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); + _mesh.Stub(x => x.IsVisibleToCamera).Return(true); + + _node.Initialize(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); _node.Update(new TimeSpan(), DoubleVector3.Up * 11, DoubleVector3.Zero, _clippingPlanes); }; @@ -141,7 +101,9 @@ public class when_a_max_level_leaf_node_is_updated_and_the_camera_is_close : Qua { Establish context = () => { - _node.InitializeMesh(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 19); + _mesh.Stub(x => x.IsVisibleToCamera).Return(true); + + _node.Initialize(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 19); _node.Update(new TimeSpan(), DoubleVector3.Up * 11, DoubleVector3.Zero, _clippingPlanes); }; @@ -160,11 +122,15 @@ public class when_a_nonleaf_node_is_updated_and_the_camera_is_far : QuadNodeCont Establish context = () => { + _mesh.Stub(x => x.IsVisibleToCamera).Return(true); + _nearCameraLocation = DoubleVector3.Up * 11; _farCameraLocation = DoubleVector3.Up * 15 * 10 * 2; - _node.InitializeMesh(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); + _node.Initialize(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); _node.Update(new TimeSpan(), _nearCameraLocation, DoubleVector3.Zero, _clippingPlanes); + + _mesh.Stub(x => x.WidthToCameraDistanceRatio).Return(2); }; Because of = () => @@ -191,7 +157,7 @@ public class when_a_nonleaf_node_is_updated_and_the_camera_is_near : QuadNodeCon { _cameraLocation = DoubleVector3.Up; - _node.InitializeMesh(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); + _node.Initialize(10, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); _node.Update(new TimeSpan(), _cameraLocation, DoubleVector3.Zero, _clippingPlanes); }; @@ -218,11 +184,13 @@ public class when_a_nonleaf_node_is_drawn : QuadNodeContext Establish context = () => { + _mesh.Stub(x => x.IsVisibleToCamera).Return(true); + _cameraLocation = DoubleVector3.Up; _viewMatrix = Matrix.Identity; _projectionMatrix = Matrix.Identity; - _node.InitializeMesh(10, Vector3.Up, Vector3.Backward, Vector3.Right, new QuadNodeExtents(-1.0, 1.0, -1.0, 1.0), 0); + _node.Initialize(10, Vector3.Up, Vector3.Backward, Vector3.Right, new QuadNodeExtents(-1.0, 1.0, -1.0, 1.0), 0); _node.Update(TimeSpan.Zero, DoubleVector3.Up, DoubleVector3.Zero, _clippingPlanes); }; @@ -232,6 +200,9 @@ public class when_a_nonleaf_node_is_drawn : QuadNodeContext It should_not_draw_the_node = () => _renderer.DrawWasCalled.ShouldBeFalse(); + It should_not_draw_the_mesh = () => + _mesh.AssertWasNotCalled(x => x.Draw(Arg.Is.Anything, Arg.Is.Anything, Arg.Is.Anything)); + It should_draw_the_subnodes_in_the_correct_location = () => { foreach (var subnode in _node.Subnodes) @@ -250,11 +221,13 @@ public class when_a_leaf_node_is_drawn : QuadNodeContext Establish context = () => { + _mesh.Stub(x => x.IsVisibleToCamera).Return(true); + _cameraLocation = DoubleVector3.Up; _viewMatrix = Matrix.Identity; _projectionMatrix = Matrix.Identity; - _node.InitializeMesh(10, Vector3.Up, Vector3.Backward, Vector3.Right, new QuadNodeExtents(-1.0, 1.0, -1.0, 1.0), 0); + _node.Initialize(10, Vector3.Up, Vector3.Backward, Vector3.Right, new QuadNodeExtents(-1.0, 1.0, -1.0, 1.0), 0); }; Because of = () => @@ -263,6 +236,9 @@ public class when_a_leaf_node_is_drawn : QuadNodeContext It should_draw_the_node = () => _renderer.DrawWasCalled.ShouldBeTrue(); + It should_draw_the_mesh = () => + _mesh.AssertWasCalled(x => x.Draw(_cameraLocation, _viewMatrix, _projectionMatrix)); + It should_draw_the_node_in_the_correct_location = () => _renderer.Location.ShouldEqual(Vector3.Up * 10); } @@ -271,7 +247,7 @@ public class when_a_leaf_node_is_drawn : QuadNodeContext public class when_a_leaf_node_is_disposed : QuadNodeContext { Establish context = () => - _node.InitializeMesh(_radius, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); + _node.Initialize(_radius, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); Because of = () => _node.Dispose(); @@ -288,7 +264,7 @@ public class when_a_nonleaf_node_is_disposed : QuadNodeContext { Establish context = () => { - _node.InitializeMesh(_radius, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); + _node.Initialize(_radius, Vector3.Up, Vector3.Backward, Vector3.Right, _extents, 0); _node.Update(new TimeSpan(), DoubleVector3.Up, DoubleVector3.Zero, _clippingPlanes); }; @@ -316,8 +292,8 @@ public class QuadNodeContext public static DoubleVector3 _location; public static QuadNodeExtents _extents = new QuadNodeExtents(-1.0, 1.0, -1.0, 1.0); + public static IQuadMesh _mesh; public static IQuadNodeFactory _quadNodeFactory; - public static IHeightfieldGenerator _generator; public static MockQuadNodeRenderer _renderer; public static ISettings _settings; public static Statistics _statistics; @@ -327,11 +303,11 @@ public class QuadNodeContext Establish context = () => { + _mesh = MockRepository.GenerateStub(); + _quadNodeFactory = MockRepository.GenerateStub(); _quadNodeFactory.Stub(x => x.Create()).Do((Func)(() => (IQuadNode)MockRepository.GenerateMock(typeof(IQuadNode), new Type[] { typeof(IDisposable) }))); - _generator = MockRepository.GenerateStub(); - // We're using a hand-rolled fake here because of a bug // in .Net that prevents mocking of multi-dimentional arrays: // http://code.google.com/p/moq/issues/detail?id=182#c0 @@ -344,7 +320,7 @@ public class QuadNodeContext _clippingPlanes = new ClippingPlanes(); - _node = new TestableQuadNode(_quadNodeFactory, _generator, _renderer, _settings, _statistics); + _node = new TestableQuadNode(_mesh, _quadNodeFactory, _renderer, _settings, _statistics); }; public static void AssertCornerIsProjected(Vector3 projectedVector, Vector3 normalVector, Vector3 uVector, Vector3 vVector) @@ -360,8 +336,8 @@ public static void AssertCornerIsProjected(Vector3 projectedVector, Vector3 norm public class TestableQuadNode : QuadNode { - public TestableQuadNode(IQuadNodeFactory quadNodeFactory, IHeightfieldGenerator generator, IQuadNodeRenderer renderer, ISettings settings, Statistics statistics) - : base(quadNodeFactory, generator, renderer, settings, statistics) + public TestableQuadNode(IQuadMesh mesh, IQuadNodeFactory quadNodeFactory, IQuadNodeRenderer renderer, ISettings settings, Statistics statistics) + : base(mesh, quadNodeFactory, renderer, settings, statistics) { } @@ -374,11 +350,6 @@ public double Width { get { return _extents.Width; } } - - public bool Visible - { - get { return _isVisible; } - } } public class MockQuadNodeRenderer : IQuadNodeRenderer, IDisposable diff --git a/GenesisEngine.Specs/GenesisEngine.Specs.csproj b/GenesisEngine.Specs/GenesisEngine.Specs.csproj index 99af2d4..c708198 100644 --- a/GenesisEngine.Specs/GenesisEngine.Specs.csproj +++ b/GenesisEngine.Specs/GenesisEngine.Specs.csproj @@ -87,6 +87,7 @@ + diff --git a/GenesisEngine/Domain/IQuadMesh.cs b/GenesisEngine/Domain/IQuadMesh.cs new file mode 100644 index 0000000..3b29bba --- /dev/null +++ b/GenesisEngine/Domain/IQuadMesh.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.Xna.Framework; + +namespace GenesisEngine +{ + public interface IQuadMesh + { + void Initialize(double planetRadius, DoubleVector3 planeNormalVector, DoubleVector3 uVector, + DoubleVector3 vVector, QuadNodeExtents extents, int level); + + bool IsVisibleToCamera { get; } + + double WidthToCameraDistanceRatio { get; } + + void Update(TimeSpan elapsedTime, DoubleVector3 cameraLocation, DoubleVector3 planetLocation, ClippingPlanes clippingPlanes); + + void Draw(DoubleVector3 cameraLocation, Matrix originBasedViewMatrix, Matrix projectionMatrix); + } +} \ No newline at end of file diff --git a/GenesisEngine/Domain/IQuadMeshFactory.cs b/GenesisEngine/Domain/IQuadMeshFactory.cs new file mode 100644 index 0000000..a3908e4 --- /dev/null +++ b/GenesisEngine/Domain/IQuadMeshFactory.cs @@ -0,0 +1,7 @@ +namespace GenesisEngine +{ + public interface IQuadMeshFactory + { + IQuadMesh Create(); + } +} \ No newline at end of file diff --git a/GenesisEngine/Domain/IQuadNode.cs b/GenesisEngine/Domain/IQuadNode.cs index 7d3e5ea..2cc855d 100644 --- a/GenesisEngine/Domain/IQuadNode.cs +++ b/GenesisEngine/Domain/IQuadNode.cs @@ -8,7 +8,7 @@ namespace GenesisEngine { public interface IQuadNode { - void InitializeMesh(double planetRadius, DoubleVector3 planeNormalVector, DoubleVector3 uVector, DoubleVector3 vVector, QuadNodeExtents extents, int level); + void Initialize(double planetRadius, DoubleVector3 planeNormalVector, DoubleVector3 uVector, DoubleVector3 vVector, QuadNodeExtents extents, int level); void Update(TimeSpan elapsedTime, DoubleVector3 cameraLocation, DoubleVector3 planetLocation, ClippingPlanes clippingPlanes); diff --git a/GenesisEngine/Domain/QuadMesh.cs b/GenesisEngine/Domain/QuadMesh.cs new file mode 100644 index 0000000..8a4298c --- /dev/null +++ b/GenesisEngine/Domain/QuadMesh.cs @@ -0,0 +1,392 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System.Diagnostics; + +namespace GenesisEngine +{ + public class QuadMesh : IQuadMesh, IDisposable + { + // XNA is a right-handed system so the positive z axis points out of the screen + // and the winding order is clockwise (counter-clockwise faces are culled). + + // TODO: make sure we're accessing 2D arrays in row-major order as per http://msdn.microsoft.com/en-us/magazine/cc872851.aspx + + // This should be 2^n+1 + readonly int _gridSize = 65; + + DoubleVector3 _locationRelativeToPlanet; + double _planetRadius; + DoubleVector3 _uVector; + DoubleVector3 _vVector; + DoubleVector3 _planeNormalVector; + protected QuadNodeExtents _extents; + + IHeightfieldGenerator _generator; + + VertexPositionNormalColored[] _vertices; + int[] _indices; + DoubleVector3[] _vertexSamples; + + IQuadMeshRenderer _renderer; + readonly ISettings _settings; + + public QuadMesh(IHeightfieldGenerator generator, IQuadMeshRenderer renderer, ISettings settings) + { + _generator = generator; + _renderer = renderer; + _settings = settings; + } + + public int Level { get; private set; } + + public bool IsVisibleToCamera { get; private set; } + + public double WidthToCameraDistanceRatio { get; private set; } + + // TODO: push this data in through the constructor, probably in a QuadMeshDefintion class, and make + // this method private. Except that would do real work in construction. Hmmm. When we explode this class + // into separate responsibilites, this problem may go away. + public void Initialize(double planetRadius, DoubleVector3 planeNormalVector, DoubleVector3 uVector, DoubleVector3 vVector, QuadNodeExtents extents, int level) + { + _planetRadius = planetRadius; + _planeNormalVector = planeNormalVector; + _uVector = uVector; + _vVector = vVector; + _extents = extents; + Level = level; + + _locationRelativeToPlanet = (_planeNormalVector) + (_uVector * ((_extents.West + (_extents.Width / 2.0)))) + (_vVector * ((_extents.North + (_extents.Width / 2.0)))); + _locationRelativeToPlanet = _locationRelativeToPlanet.ProjectUnitPlaneToUnitSphere() * _planetRadius; + + GenerateMeshVertices(); + CollectMeshSamples(); + + _renderer.Initialize(_vertices, _indices); + } + + void CollectMeshSamples() + { + // We just sample the corners and the middle for now + _vertexSamples = new DoubleVector3[] + { + _vertices[0].Position, + _vertices[_gridSize - 1].Position, + _vertices[_vertices.Length / 2].Position, + _vertices[_gridSize * (_gridSize - 1)].Position, + _vertices[_gridSize * _gridSize - 1].Position + }; + + // Move them back into real space + for (int x = 0; x < _vertexSamples.Length; x++) + { + _vertexSamples[x] += _locationRelativeToPlanet; + } + } + + private void GenerateMeshVertices() + { + _vertices = new VertexPositionNormalColored[_gridSize * _gridSize]; + + for (int u = 0; u < _gridSize; u++) + { + for (int v = 0; v < _gridSize; v++) + { + // TODO: pass in the vertex to be modified + _vertices[u * _gridSize + v] = GetVertexInMeshSpace(u, v); + } + } + + GenerateIndices(); + GenerateNormals(); + + if (_settings.ShowQuadBoundaries) + { + MarkQuadBoundaries(); + } + } + + void MarkQuadBoundaries() + { + MarkNorthBoundary(_extents.North == -1 ? Color.Green : Color.Red); + MarkSouthBoundary(_extents.South == 1 ? Color.Green : Color.Red); + MarkWestBoundary(_extents.West == -1 ? Color.Green : Color.Red); + MarkEastBoundary(_extents.East == 1 ? Color.Green : Color.Red); + } + + void MarkNorthBoundary(Color color) + { + for (int x = 0; x < _gridSize; x++) + { + _vertices[x].Color = color; + } + } + + void MarkSouthBoundary(Color color) + { + for (int x = 0; x < _gridSize; x++) + { + _vertices[_vertices.Length - x - 1].Color = color; + } + } + + void MarkWestBoundary(Color color) + { + for (int x = 0; x < _gridSize; x++) + { + _vertices[_gridSize * x].Color = color; + } + } + + void MarkEastBoundary(Color color) + { + for (int x = 0; x < _gridSize; x++) + { + _vertices[_gridSize * x + _gridSize - 1].Color = color; + } + } + + private VertexPositionNormalColored GetVertexInMeshSpace(int u, int v) + { + // Check out "Textures and Modelling - A Procedural Approach" by Ken Musgaves + + // Managing data in different reference frames (zero-based mesh, real-world-based mesh) is going + // to be tricky. We want to build a mesh where + // the center of the mesh is at 0,0 but the vertices are sphere-projected as though they were out in their + // correct place in the sphere. Then we want to keep track of the mesh's real-world location so we can do a + // camera-relative translation for rendering. + + // We start with a sea-level-based height and quad-relative coordinates. We first convert the quad + // coordinates into a planet-space vector that points to the equivalent point on the mesh plane. Then we project + // to a sphere by normalizing the vector and extending it by the planet radius plus heightfield height, which gives + // us the correct spherical distance from the center. Finally we translate the vector "downward" by the radius so + // that a zero-height point in the exact middle of the mesh surface would be at the origin. + + var sphereUnitVector = ProjectOntoSphere(u, v); + var terrainHeight = _generator.GetHeight(sphereUnitVector, Level, 8000); + + // TODO: Temporary water adjustment, needs to live somewhere else + if (terrainHeight < 0) + { + terrainHeight = 0; + } + + var realVector = ConvertToRealSpace(sphereUnitVector, terrainHeight); + var meshVector = ConvertToMeshSpace(realVector); + + return CreateVertex(meshVector, terrainHeight); ; + } + + VertexPositionNormalColored CreateVertex(DoubleVector3 meshVector, double terrainHeight) + { + var vertex = new VertexPositionNormalColored { Position = meshVector }; + + if (terrainHeight <= 0) + { + vertex.Color = Color.Blue; + } + else + { + vertex.Color = Color.White; + } + + return vertex; + } + + private DoubleVector3 ProjectOntoSphere(int u, int v) + { + var planeUnitVector = ConvertToUnitPlaneVector(u, v); + var sphereUnitVector = planeUnitVector.ProjectUnitPlaneToUnitSphere(); + + return sphereUnitVector; + } + + private DoubleVector3 ConvertToUnitPlaneVector(int u, int v) + { + // TODO: promote to class member + var stride = _extents.Width / (_gridSize - 1); + + var uDelta = _uVector * (_extents.North + (u * stride)); + var vDelta = _vVector * (_extents.West + (v * stride)); + var convertedVector = _planeNormalVector + uDelta + vDelta; + + return convertedVector; + } + + private DoubleVector3 ConvertToRealSpace(DoubleVector3 sphereUnitVector, double terrainHeight) + { + return sphereUnitVector * (_planetRadius + terrainHeight); + } + + private DoubleVector3 ConvertToMeshSpace(DoubleVector3 planetSpaceVector) + { + return planetSpaceVector - _locationRelativeToPlanet; + } + + private void GenerateIndices() + { + // TODO: I think we can generate the indices once and share it for all + // instances since it never changes. In the future we'll want to deal + // with adjacent nodes at different levels by constructing special + // index sets that blend them at the edge. + + _indices = new int[(_gridSize - 1) * (_gridSize - 1) * 6]; + int counter = 0; + for (int x = 0; x < _gridSize - 1; x++) + { + for (int y = 0; y < _gridSize - 1; y++) + { + int topLeft = x * _gridSize + y; + int lowerLeft = (x + 1) * _gridSize + y; + int topRight = x * _gridSize + (y + 1); + int lowerRight = (x + 1) * _gridSize + (y + 1); + + _indices[counter++] = topLeft; + _indices[counter++] = lowerRight; + _indices[counter++] = lowerLeft; + + _indices[counter++] = topLeft; + _indices[counter++] = topRight; + _indices[counter++] = lowerRight; + } + } + } + + private void GenerateNormals() + { + InitializeNormals(); + CalculateNormals(); + NormalizeNormals(); + } + + private void InitializeNormals() + { + for (int x = 0; x < _vertices.Length; x++) + { + _vertices[x].Normal = new Vector3(0, 0, 0); + } + } + + private void CalculateNormals() + { + // Iterate through each indexed vertex and gradually + // accumulate the normals in the vertices as we go. + + for (int i = 0; i < _indices.Length / 3; i++) + { + int index1 = _indices[i * 3]; + int index2 = _indices[i * 3 + 1]; + int index3 = _indices[i * 3 + 2]; + + Vector3 side1 = _vertices[index1].Position - _vertices[index3].Position; + Vector3 side2 = _vertices[index1].Position - _vertices[index2].Position; + Vector3 normal = Vector3.Cross(side1, side2); + + _vertices[index1].Normal += normal; + _vertices[index2].Normal += normal; + _vertices[index3].Normal += normal; + } + } + + private void NormalizeNormals() + { + for (int i = 0; i < _vertices.Length; i++) + _vertices[i].Normal.Normalize(); + } + + public void Update(TimeSpan elapsedTime, DoubleVector3 cameraLocation, DoubleVector3 planetLocation, ClippingPlanes clippingPlanes) + { + var cameraRelationship = GetDistanceFrom(cameraLocation); + var distanceFromCamera = cameraRelationship.ClosestDistance; + WidthToCameraDistanceRatio = distanceFromCamera / WidthInRealSpaceUnits(); + + DetermineVisibility(cameraLocation, planetLocation, cameraRelationship.ClosestVertex); + + SetClippingPlanes(cameraRelationship, clippingPlanes); + } + + MeshDistance GetDistanceFrom(DoubleVector3 location) + { + double closestDistanceSquared = double.MaxValue; + double furthestDistanceSquared = double.MinValue; + DoubleVector3 closestVertex = _vertexSamples[0]; + DoubleVector3 furthestVertex = _vertexSamples[0]; + + foreach (var vertex in _vertexSamples) + { + var distanceSquared = DoubleVector3.DistanceSquared(location, vertex); + if (distanceSquared < closestDistanceSquared) + { + closestDistanceSquared = distanceSquared; + closestVertex = vertex; + } + else if (distanceSquared > furthestDistanceSquared) + { + furthestDistanceSquared = distanceSquared; + furthestVertex = vertex; + } + } + + return new MeshDistance() + { + ClosestDistance = Math.Sqrt(closestDistanceSquared), + ClosestVertex = closestVertex, + FurthestDistance = Math.Sqrt(closestDistanceSquared), + FurthestVertex = furthestVertex + }; + } + + double WidthInRealSpaceUnits() + { + return _extents.Width * _planetRadius; + } + + void DetermineVisibility(DoubleVector3 cameraLocation, DoubleVector3 planetLocation, DoubleVector3 closestVertex) + { + var cameraDirection = DoubleVector3.Normalize(cameraLocation - planetLocation); + var nodeDirection = DoubleVector3.Normalize(closestVertex - planetLocation); + + var horizonAngle = Math.Acos(_planetRadius * 0.997 / DoubleVector3.Distance(planetLocation, cameraLocation)); + var angleToNode = Math.Acos(DoubleVector3.Dot(cameraDirection, nodeDirection)); + + IsVisibleToCamera = (horizonAngle > angleToNode); + } + + void SetClippingPlanes(MeshDistance cameraRelationship, ClippingPlanes clippingPlanes) + { + if (IsVisibleToCamera) + { + if (clippingPlanes.Near > cameraRelationship.ClosestDistance) + { + clippingPlanes.Near = cameraRelationship.ClosestDistance; + } + if (clippingPlanes.Far < cameraRelationship.FurthestDistance) + { + clippingPlanes.Far = cameraRelationship.FurthestDistance; + } + } + } + + public void Draw(DoubleVector3 cameraLocation, Matrix originBasedViewMatrix, Matrix projectionMatrix) + { + _renderer.Draw(_locationRelativeToPlanet, cameraLocation, originBasedViewMatrix, projectionMatrix); + } + + public void Dispose() + { + ((IDisposable)_renderer).Dispose(); + } + + private class MeshDistance + { + public DoubleVector3 ClosestVertex { get; set; } + public DoubleVector3 FurthestVertex { get; set; } + public double ClosestDistance { get; set; } + public double FurthestDistance { get; set; } + } + } +} + diff --git a/GenesisEngine/Domain/QuadMeshFactory.cs b/GenesisEngine/Domain/QuadMeshFactory.cs new file mode 100644 index 0000000..8bc970b --- /dev/null +++ b/GenesisEngine/Domain/QuadMeshFactory.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Xna.Framework; + +namespace GenesisEngine +{ + public class QuadMeshFactory : IQuadMeshFactory + { + readonly IQuadMeshRendererFactory _rendererFactory; + readonly IHeightfieldGenerator _generator; + readonly Settings _settings; + readonly Statistics _statistics; + + public QuadMeshFactory(IQuadMeshRendererFactory rendererFactory, IHeightfieldGenerator generator, Settings settings) + { + _rendererFactory = rendererFactory; + _generator = generator; + _settings = settings; + } + + public IQuadMesh Create() + { + var quadMeshRenderer = _rendererFactory.Create(); + return new QuadMesh(_generator, quadMeshRenderer, _settings); + } + } +} diff --git a/GenesisEngine/Domain/QuadNode.cs b/GenesisEngine/Domain/QuadNode.cs index 37b828b..33dc27e 100644 --- a/GenesisEngine/Domain/QuadNode.cs +++ b/GenesisEngine/Domain/QuadNode.cs @@ -10,40 +10,28 @@ namespace GenesisEngine { public class QuadNode : IQuadNode, IDisposable { - // XNA is a right-handed system so the positive z axis points out of the screen - // and the winding order is clockwise (counter-clockwise faces are culled). - // TODO: this class is a serious SRP violation and needs to be refactored ASAP! - // This should be 2^n+1 - readonly int _gridSize = 65; - DoubleVector3 _locationRelativeToPlanet; double _planetRadius; DoubleVector3 _uVector; DoubleVector3 _vVector; DoubleVector3 _planeNormalVector; protected QuadNodeExtents _extents; - protected bool _isVisible = true; - DoubleVector3[] _vertexSamples; - - IHeightfieldGenerator _generator; bool _hasSubnodes = false; protected List _subnodes = new List(); - VertexPositionNormalColored[] _vertices; - int[] _indices; - + IQuadMesh _mesh; IQuadNodeFactory _quadNodeFactory; IQuadNodeRenderer _renderer; readonly ISettings _settings; readonly Statistics _statistics; - public QuadNode(IQuadNodeFactory quadNodeFactory, IHeightfieldGenerator generator, IQuadNodeRenderer renderer, ISettings settings, Statistics statistics) + public QuadNode(IQuadMesh mesh, IQuadNodeFactory quadNodeFactory, IQuadNodeRenderer renderer, ISettings settings, Statistics statistics) { + _mesh = mesh; _quadNodeFactory = quadNodeFactory; - _generator = generator; _renderer = renderer; _settings = settings; _statistics = statistics; @@ -54,7 +42,7 @@ public QuadNode(IQuadNodeFactory quadNodeFactory, IHeightfieldGenerator generato // TODO: push this data in through the constructor, probably in a QuadNodeDefintion class, and make // this method private. Except that would do real work in construction. Hmmm. When we explode this class // into separate responsibilites, this problem may go away. - public void InitializeMesh(double planetRadius, DoubleVector3 planeNormalVector, DoubleVector3 uVector, DoubleVector3 vVector, QuadNodeExtents extents, int level) + public void Initialize(double planetRadius, DoubleVector3 planeNormalVector, DoubleVector3 uVector, DoubleVector3 vVector, QuadNodeExtents extents, int level) { _planetRadius = planetRadius; _planeNormalVector = planeNormalVector; @@ -64,285 +52,25 @@ public void InitializeMesh(double planetRadius, DoubleVector3 planeNormalVector, Level = level; _locationRelativeToPlanet = (_planeNormalVector) + (_uVector * ((_extents.West + (_extents.Width / 2.0)))) + (_vVector * ((_extents.North + (_extents.Width / 2.0)))); - _locationRelativeToPlanet = ProjectPlaneToSphere(_locationRelativeToPlanet) * _planetRadius; - - GenerateMeshVertices(); - CollectMeshSamples(); + _locationRelativeToPlanet = _locationRelativeToPlanet.ProjectUnitPlaneToUnitSphere() * _planetRadius; - _renderer.Initialize(_vertices, _indices); + _mesh.Initialize(planetRadius, planeNormalVector, uVector, vVector, extents, level); _statistics.NumberOfQuadNodes++; _statistics.NumberOfQuadNodesAtLevel[Level]++; } - void CollectMeshSamples() - { - // We just sample the corners and the middle for now - _vertexSamples = new DoubleVector3[] - { - _vertices[0].Position, - _vertices[_gridSize - 1].Position, - _vertices[_vertices.Length / 2].Position, - _vertices[_gridSize * (_gridSize - 1)].Position, - _vertices[_gridSize * _gridSize - 1].Position - }; - - // Move them back into real space - for (int x = 0; x < _vertexSamples.Length; x++) - { - _vertexSamples[x] += _locationRelativeToPlanet; - } - } - - private void GenerateMeshVertices() - { - _vertices = new VertexPositionNormalColored[_gridSize * _gridSize]; - - for (int u = 0; u < _gridSize; u++) - { - for (int v = 0; v < _gridSize; v++) - { - // TODO: pass in the vertex to be modified - _vertices[u * _gridSize + v] = GetVertexInMeshSpace(u, v); - } - } - - GenerateIndices(); - GenerateNormals(); - - if (_settings.ShowQuadBoundaries) - { - MarkQuadBoundaries(); - } - } - - void MarkQuadBoundaries() - { - MarkNorthBoundary(_extents.North == -1 ? Color.Green : Color.Red); - MarkSouthBoundary(_extents.South == 1 ? Color.Green : Color.Red); - MarkWestBoundary(_extents.West == -1 ? Color.Green : Color.Red); - MarkEastBoundary(_extents.East == 1 ? Color.Green : Color.Red); - } - - void MarkNorthBoundary(Color color) - { - for (int x = 0; x < _gridSize; x++) - { - _vertices[x].Color = color; - } - } - - void MarkSouthBoundary(Color color) - { - for (int x = 0; x < _gridSize; x++) - { - _vertices[_vertices.Length - x - 1].Color = color; - } - } - - void MarkWestBoundary(Color color) - { - for (int x = 0; x < _gridSize; x++) - { - _vertices[_gridSize * x].Color = color; - } - } - - void MarkEastBoundary(Color color) - { - for (int x = 0; x < _gridSize; x++) - { - _vertices[_gridSize * x + _gridSize - 1].Color = color; - } - } - - private VertexPositionNormalColored GetVertexInMeshSpace(int u, int v) - { - // Check out "Textures and Modelling - A Procedural Approach" by Ken Musgaves - - // Managing data in different reference frames (zero-based mesh, real-world-based mesh) is going - // to be tricky. We want to build a mesh where - // the center of the mesh is at 0,0 but the vertices are sphere-projected as though they were out in their - // correct place in the sphere. Then we want to keep track of the mesh's real-world location so we can do a - // camera-relative translation for rendering. - - // We start with a sea-level-based height and quad-relative coordinates. We first convert the quad - // coordinates into a planet-space vector that points to the equivalent point on the mesh plane. Then we project - // to a sphere by normalizing the vector and extending it by the planet radius plus heightfield height, which gives - // us the correct spherical distance from the center. Finally we translate the vector "downward" by the radius so - // that a zero-height point in the exact middle of the mesh surface would be at the origin. - - DoubleVector3 sphereUnitVector = ProjectOntoSphere(u, v); - var terrainHeight = _generator.GetHeight(sphereUnitVector, Level, 8000); - - // TODO: Temporary water adjustment, needs to live somewhere else - if (terrainHeight < 0) - { - terrainHeight = 0; - } - - DoubleVector3 realVector = ConvertToRealSpace(sphereUnitVector, terrainHeight); - DoubleVector3 meshVector = ConvertToMeshSpace(realVector); - - return CreateVertex(meshVector, terrainHeight); ; - } - - VertexPositionNormalColored CreateVertex(DoubleVector3 meshVector, double terrainHeight) - { - var vertex = new VertexPositionNormalColored(); - vertex.Position = meshVector; - - if (terrainHeight <= 0) - { - vertex.Color = Color.Blue; - } - else - { - vertex.Color = Color.White; - } - - return vertex; - } - - private DoubleVector3 ProjectOntoSphere(int u, int v) - { - DoubleVector3 planeVector = ConvertToUnitPlaneVector(u, v); - DoubleVector3 sphereUnitVector = ProjectPlaneToSphere(planeVector); - - return sphereUnitVector; - } - - private DoubleVector3 ConvertToUnitPlaneVector(int u, int v) - { - // TODO: promote to class member - double stride = _extents.Width / (_gridSize - 1); - - DoubleVector3 uDelta = _uVector * (_extents.North + (u * stride)); - DoubleVector3 vDelta = _vVector * (_extents.West + (v * stride)); - DoubleVector3 convertedVector = _planeNormalVector + uDelta + vDelta; - - return convertedVector; - } - - private DoubleVector3 ProjectPlaneToSphere(DoubleVector3 planeVector) - { - // http://mathproofs.blogspot.com/2005/07/mapping-cube-to-sphere.html - DoubleVector3 sphereUnitVector; - sphereUnitVector.X = planeVector.X * Math.Sqrt(1.0 - (Math.Pow(planeVector.Y, 2) / 2) - (Math.Pow(planeVector.Z, 2) / 2) + (Math.Pow(planeVector.Y, 2) * Math.Pow(planeVector.Z, 2) / 3)); - sphereUnitVector.Y = planeVector.Y * Math.Sqrt(1.0 - (Math.Pow(planeVector.Z, 2) / 2) - (Math.Pow(planeVector.X, 2) / 2) + (Math.Pow(planeVector.Z, 2) * Math.Pow(planeVector.X, 2) / 3)); - sphereUnitVector.Z = planeVector.Z * Math.Sqrt(1.0 - (Math.Pow(planeVector.X, 2) / 2) - (Math.Pow(planeVector.Y, 2) / 2) + (Math.Pow(planeVector.X, 2) * Math.Pow(planeVector.Y, 2) / 3)); - - return sphereUnitVector; - } - - private DoubleVector3 ConvertToRealSpace(DoubleVector3 sphereUnitVector, double terrainHeight) - { - return sphereUnitVector * (_planetRadius + terrainHeight); - } - - private DoubleVector3 ConvertToMeshSpace(DoubleVector3 planetSpaceVector) - { - return planetSpaceVector - _locationRelativeToPlanet; - } - - private void GenerateIndices() - { - // TODO: I think we can generate the indices once and share it for all - // instances since it never changes. In the future we'll want to deal - // with adjacent nodes at different levels by constructing special - // index sets that blend them at the edge. - - _indices = new int[(_gridSize - 1) * (_gridSize - 1) * 6]; - int counter = 0; - for (int x = 0; x < _gridSize - 1; x++) - { - for (int y = 0; y < _gridSize - 1; y++) - { - int topLeft = x * _gridSize + y; - int lowerLeft = (x + 1) * _gridSize + y; - int topRight = x * _gridSize + (y + 1); - int lowerRight = (x + 1) * _gridSize + (y + 1); - - _indices[counter++] = topLeft; - _indices[counter++] = lowerRight; - _indices[counter++] = lowerLeft; - - _indices[counter++] = topLeft; - _indices[counter++] = topRight; - _indices[counter++] = lowerRight; - } - } - } - - private void GenerateNormals() - { - InitializeNormals(); - CalculateNormals(); - NormalizeNormals(); - } - - private void InitializeNormals() - { - for (int x = 0; x < _vertices.Length; x++) - { - _vertices[x].Normal = new Vector3(0, 0, 0); - } - } - - private void CalculateNormals() - { - // Iterate through each indexed vertex and gradually - // accumulate the normals in the vertices as we go. - - for (int i = 0; i < _indices.Length / 3; i++) - { - int index1 = _indices[i * 3]; - int index2 = _indices[i * 3 + 1]; - int index3 = _indices[i * 3 + 2]; - - Vector3 side1 = _vertices[index1].Position - _vertices[index3].Position; - Vector3 side2 = _vertices[index1].Position - _vertices[index2].Position; - Vector3 normal = Vector3.Cross(side1, side2); - - _vertices[index1].Normal += normal; - _vertices[index2].Normal += normal; - _vertices[index3].Normal += normal; - } - } - - private void NormalizeNormals() - { - for (int i = 0; i < _vertices.Length; i++) - _vertices[i].Normal.Normalize(); - } - public void Update(TimeSpan elapsedTime, DoubleVector3 cameraLocation, DoubleVector3 planetLocation, ClippingPlanes clippingPlanes) { - // TODO: This algorithm could be improved to optimize the number of triangles that are drawn - - var cameraRelationship = GetRelationshipToCamera(cameraLocation); + _mesh.Update(elapsedTime, cameraLocation, planetLocation, clippingPlanes); - DetermineVisibility(cameraLocation, planetLocation, cameraRelationship.ClosestVertex); - - if (_isVisible) - { - if (clippingPlanes.Near > cameraRelationship.ClosestDistance) - { - clippingPlanes.Near = cameraRelationship.ClosestDistance; - } - if (clippingPlanes.Far < cameraRelationship.FurthestDistance) - { - clippingPlanes.Far = cameraRelationship.FurthestDistance; - } - } - - var distanceFromCamera = cameraRelationship.ClosestDistance; + // TODO: This algorithm could be improved to optimize the number of triangles that are drawn - if (_isVisible && distanceFromCamera < RealWidth() * 1 && !_hasSubnodes && Level < _settings.MaximumQuadNodeLevel) + if (_mesh.IsVisibleToCamera && _mesh.WidthToCameraDistanceRatio < 1 && !_hasSubnodes && Level < _settings.MaximumQuadNodeLevel) { Split(); } - else if (distanceFromCamera >= RealWidth() * 1.2 && _hasSubnodes) + else if (_mesh.WidthToCameraDistanceRatio > 1.2 && _hasSubnodes) { Merge(); } @@ -356,53 +84,6 @@ public void Update(TimeSpan elapsedTime, DoubleVector3 cameraLocation, DoubleVec } } - void DetermineVisibility(DoubleVector3 cameraLocation, DoubleVector3 planetLocation, DoubleVector3 closestVertex) - { - var cameraDirection = DoubleVector3.Normalize(cameraLocation - planetLocation); - var nodeDirection = DoubleVector3.Normalize(closestVertex - planetLocation); - - var horizonAngle = Math.Acos(_planetRadius * 0.997 / DoubleVector3.Distance(planetLocation, cameraLocation)); - var angleToNode = Math.Acos(DoubleVector3.Dot(cameraDirection, nodeDirection)); - - _isVisible = (horizonAngle > angleToNode); - } - - CameraRelationship GetRelationshipToCamera(DoubleVector3 location) - { - double closestDistanceSquared = double.MaxValue; - double furthestDistanceSquared = double.MinValue; - DoubleVector3 closestVertex = _vertexSamples[0]; - DoubleVector3 furthestVertex = _vertexSamples[0]; - - foreach (var vertex in _vertexSamples) - { - var distanceSquared = DoubleVector3.DistanceSquared(location, vertex); - if (distanceSquared < closestDistanceSquared) - { - closestDistanceSquared = distanceSquared; - closestVertex = vertex; - } - else if (distanceSquared > furthestDistanceSquared) - { - furthestDistanceSquared = distanceSquared; - furthestVertex = vertex; - } - } - - return new CameraRelationship() - { - ClosestDistance = Math.Sqrt(closestDistanceSquared), - ClosestVertex = closestVertex, - FurthestDistance = Math.Sqrt(closestDistanceSquared), - FurthestVertex = furthestVertex - }; - } - - private double RealWidth() - { - return _extents.Width * _planetRadius; - } - private void Split() { var subextents = _extents.Split(); @@ -410,7 +91,7 @@ private void Split() foreach (var subextent in subextents) { var node = _quadNodeFactory.Create(); - node.InitializeMesh(_planetRadius, _planeNormalVector, _uVector, _vVector, subextent, Level + 1); + node.Initialize(_planetRadius, _planeNormalVector, _uVector, _vVector, subextent, Level + 1); _subnodes.Add(node); } @@ -434,7 +115,7 @@ void DisposeSubNodes() public void Draw(DoubleVector3 cameraLocation, Matrix originBasedViewMatrix, Matrix projectionMatrix) { - if (!_isVisible) + if (!_mesh.IsVisibleToCamera) { return; } @@ -449,6 +130,7 @@ public void Draw(DoubleVector3 cameraLocation, Matrix originBasedViewMatrix, Mat else { _renderer.Draw(_locationRelativeToPlanet, cameraLocation, originBasedViewMatrix, projectionMatrix); + _mesh.Draw(cameraLocation, originBasedViewMatrix, projectionMatrix); } } @@ -460,14 +142,6 @@ public void Dispose() _statistics.NumberOfQuadNodes--; _statistics.NumberOfQuadNodesAtLevel[Level]--; } - - private class CameraRelationship - { - public DoubleVector3 ClosestVertex { get; set; } - public DoubleVector3 FurthestVertex { get; set; } - public double ClosestDistance { get; set; } - public double FurthestDistance { get; set; } - } } } diff --git a/GenesisEngine/Domain/QuadNodeFactory.cs b/GenesisEngine/Domain/QuadNodeFactory.cs index 2e41ec8..1eb186b 100644 --- a/GenesisEngine/Domain/QuadNodeFactory.cs +++ b/GenesisEngine/Domain/QuadNodeFactory.cs @@ -8,23 +8,24 @@ namespace GenesisEngine { public class QuadNodeFactory : IQuadNodeFactory { - readonly QuadNodeRendererFactory _rendererFactory; - readonly IHeightfieldGenerator _generator; + readonly IQuadMeshFactory _meshFactory; + readonly IQuadNodeRendererFactory _rendererFactory; readonly Settings _settings; readonly Statistics _statistics; - public QuadNodeFactory(QuadNodeRendererFactory rendererFactory, IHeightfieldGenerator generator, Settings settings, Statistics statistics) + public QuadNodeFactory(IQuadMeshFactory meshFactory, IQuadNodeRendererFactory rendererFactory, Settings settings, Statistics statistics) { + _meshFactory = meshFactory; _rendererFactory = rendererFactory; - _generator = generator; _settings = settings; _statistics = statistics; } public IQuadNode Create() { + var mesh = _meshFactory.Create(); var quadNodeRenderer = _rendererFactory.Create(); - return new QuadNode(this, _generator, quadNodeRenderer, _settings, _statistics); + return new QuadNode(mesh, this, quadNodeRenderer, _settings, _statistics); } } } diff --git a/GenesisEngine/Domain/TerrainFactory.cs b/GenesisEngine/Domain/TerrainFactory.cs index c93acf7..cb1c5f3 100644 --- a/GenesisEngine/Domain/TerrainFactory.cs +++ b/GenesisEngine/Domain/TerrainFactory.cs @@ -52,7 +52,7 @@ private IQuadNode CreateFace(double planetRadius, Vector3 normalVector) var orientationVectors = _faceOrientations[normalVector]; var u = orientationVectors[0]; var v = orientationVectors[1]; - face.InitializeMesh(planetRadius, normalVector, u, v, new QuadNodeExtents(-1.0, 1.0, -1.0, 1.0), 0); + face.Initialize(planetRadius, normalVector, u, v, new QuadNodeExtents(-1.0, 1.0, -1.0, 1.0), 0); return face; } diff --git a/GenesisEngine/GenesisEngine.csproj b/GenesisEngine/GenesisEngine.csproj index 44218c9..d875e44 100644 --- a/GenesisEngine/GenesisEngine.csproj +++ b/GenesisEngine/GenesisEngine.csproj @@ -97,6 +97,12 @@ + + + + + + @@ -141,6 +147,9 @@ + + + diff --git a/GenesisEngine/Math/DoubleVector3.cs b/GenesisEngine/Math/DoubleVector3.cs index 99ce31a..e3bb4ab 100644 --- a/GenesisEngine/Math/DoubleVector3.cs +++ b/GenesisEngine/Math/DoubleVector3.cs @@ -425,6 +425,18 @@ public static void TransformNormal(DoubleVector3[] sourceArray, int sourceIndex, } } + public DoubleVector3 ProjectUnitPlaneToUnitSphere() + { + // http://mathproofs.blogspot.com/2005/07/mapping-cube-to-sphere.html + DoubleVector3 sphereUnitVector; + + sphereUnitVector.X = X * Math.Sqrt(1.0 - (Math.Pow(Y, 2) / 2) - (Math.Pow(Z, 2) / 2) + (Math.Pow(Y, 2) * Math.Pow(Z, 2) / 3)); + sphereUnitVector.Y = Y * Math.Sqrt(1.0 - (Math.Pow(Z, 2) / 2) - (Math.Pow(X, 2) / 2) + (Math.Pow(Z, 2) * Math.Pow(X, 2) / 3)); + sphereUnitVector.Z = Z * Math.Sqrt(1.0 - (Math.Pow(X, 2) / 2) - (Math.Pow(Y, 2) / 2) + (Math.Pow(X, 2) * Math.Pow(Y, 2) / 3)); + + return sphereUnitVector; + } + public static DoubleVector3 Negate(DoubleVector3 value) { DoubleVector3 negatedVector; diff --git a/GenesisEngine/Renderers/IQuadMeshRenderer.cs b/GenesisEngine/Renderers/IQuadMeshRenderer.cs new file mode 100644 index 0000000..a5f9f89 --- /dev/null +++ b/GenesisEngine/Renderers/IQuadMeshRenderer.cs @@ -0,0 +1,7 @@ +namespace GenesisEngine +{ + public interface IQuadMeshRenderer : IRenderer + { + void Initialize(VertexPositionNormalColored[] vertices, int[] indices); + } +} \ No newline at end of file diff --git a/GenesisEngine/Renderers/IQuadMeshRendererFactory.cs b/GenesisEngine/Renderers/IQuadMeshRendererFactory.cs new file mode 100644 index 0000000..1f7b2dd --- /dev/null +++ b/GenesisEngine/Renderers/IQuadMeshRendererFactory.cs @@ -0,0 +1,7 @@ +namespace GenesisEngine +{ + public interface IQuadMeshRendererFactory + { + IQuadMeshRenderer Create(); + } +} \ No newline at end of file diff --git a/GenesisEngine/Renderers/IQuadNodeRenderer.cs b/GenesisEngine/Renderers/IQuadNodeRenderer.cs index 5d8585d..8c85eb2 100644 --- a/GenesisEngine/Renderers/IQuadNodeRenderer.cs +++ b/GenesisEngine/Renderers/IQuadNodeRenderer.cs @@ -8,6 +8,5 @@ namespace GenesisEngine { public interface IQuadNodeRenderer : IRenderer { - void Initialize(VertexPositionNormalColored[] vertices, int[] indices); } } diff --git a/GenesisEngine/Renderers/IQuadNodeRendererFactory.cs b/GenesisEngine/Renderers/IQuadNodeRendererFactory.cs new file mode 100644 index 0000000..c1cabdc --- /dev/null +++ b/GenesisEngine/Renderers/IQuadNodeRendererFactory.cs @@ -0,0 +1,7 @@ +namespace GenesisEngine +{ + public interface IQuadNodeRendererFactory + { + IQuadNodeRenderer Create(); + } +} \ No newline at end of file diff --git a/GenesisEngine/Renderers/QuadMeshRenderer.cs b/GenesisEngine/Renderers/QuadMeshRenderer.cs new file mode 100644 index 0000000..c4d8f87 --- /dev/null +++ b/GenesisEngine/Renderers/QuadMeshRenderer.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace GenesisEngine +{ + public class QuadMeshRenderer : IQuadMeshRenderer, IDisposable + { + private ContentManager _contentManager; + private GraphicsDevice _graphicsDevice; + private VertexDeclaration _vertexDeclaration; + private VertexBuffer _vertexBuffer; + private IndexBuffer _indexBuffer; + private int _numberOfVertices; + private int _numberOfIndices; + private Effect _effect; + private ISettings _settings; + + public QuadMeshRenderer(ContentManager contentManager, GraphicsDevice graphicsDevice, ISettings settings) + { + _contentManager = contentManager; + _graphicsDevice = graphicsDevice; + _settings = settings; + } + + public void Initialize(VertexPositionNormalColored[] vertices, int[] indices) + { + _vertexDeclaration = new VertexDeclaration(_graphicsDevice, VertexPositionNormalColored.VertexElements); + + _effect = _contentManager.Load("effects"); + + CreateVertexBuffer(vertices); + CreateIndexBuffer(indices); + } + + private void CreateVertexBuffer(VertexPositionNormalColored[] vertices) + { + _numberOfVertices = vertices.Length; + _vertexBuffer = new VertexBuffer(_graphicsDevice, _numberOfVertices * VertexPositionNormalColored.SizeInBytes, BufferUsage.WriteOnly); + _vertexBuffer.SetData(vertices); + } + + private void CreateIndexBuffer(int[] indices) + { + // TODO: according to the _Interactive Visualization_ paper, we should be able to pre-generate + // all possible index buffers once (tweaked on the edges to match lower LOD neighbors) and reuse + // them for all nodes. + + _numberOfIndices = indices.Length; + _indexBuffer = new IndexBuffer(_graphicsDevice, typeof(int), _numberOfIndices, BufferUsage.WriteOnly); + _indexBuffer.SetData(indices); + } + + public void Draw(DoubleVector3 location, DoubleVector3 cameraLocation, Matrix originBasedViewMatrix, Matrix projectionMatrix) + { + // TODO: Calculate a dynamic scaling factor based on the distance of the object from the camera? See + // the Interactive Visualization paper, page 24. + + Matrix translationMatrix = Matrix.CreateTranslation(location - cameraLocation); + Matrix worldMatrix = translationMatrix; + + _effect.CurrentTechnique = _effect.Techniques["Colored"]; + _effect.Parameters["xView"].SetValue(originBasedViewMatrix); + _effect.Parameters["xProjection"].SetValue(projectionMatrix); + _effect.Parameters["xWorld"].SetValue(worldMatrix); + + _effect.Parameters["xEnableLighting"].SetValue(true); + Vector3 lightDirection = new Vector3(1.0f, -1.0f, -1.0f); + lightDirection.Normalize(); + _effect.Parameters["xLightDirection"].SetValue(lightDirection); + _effect.Parameters["xAmbient"].SetValue(0.1f); + + if (_settings.ShouldDrawWireframe) + { + _graphicsDevice.RenderState.FillMode = FillMode.WireFrame; + } + else + { + _graphicsDevice.RenderState.FillMode = FillMode.Solid; + } + + _effect.Begin(); + foreach (EffectPass pass in _effect.CurrentTechnique.Passes) + { + pass.Begin(); + + _graphicsDevice.VertexDeclaration = _vertexDeclaration; + _graphicsDevice.Indices = _indexBuffer; + _graphicsDevice.Vertices[0].SetSource(_vertexBuffer, 0, VertexPositionNormalColored.SizeInBytes); + _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, _numberOfVertices, 0, _numberOfIndices / 3); + + pass.End(); + } + _effect.End(); + } + + public void Dispose() + { + _vertexDeclaration.Dispose(); + _vertexBuffer.Dispose(); + _indexBuffer.Dispose(); + + // Don't dispose _effect here because the ContentManager gives us a shared instance + } + } +} diff --git a/GenesisEngine/Renderers/QuadMeshRendererFactory.cs b/GenesisEngine/Renderers/QuadMeshRendererFactory.cs new file mode 100644 index 0000000..bbc1dd6 --- /dev/null +++ b/GenesisEngine/Renderers/QuadMeshRendererFactory.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace GenesisEngine +{ + public class QuadMeshRendererFactory : IQuadMeshRendererFactory + { + ContentManager _contentManager; + GraphicsDevice _graphicsDevice; + ISettings _settings; + + public QuadMeshRendererFactory(ContentManager contentManager, GraphicsDevice graphicsDevice, ISettings settings) + { + _contentManager = contentManager; + _graphicsDevice = graphicsDevice; + _settings = settings; + } + + public IQuadMeshRenderer Create() + { + return new QuadMeshRenderer(_contentManager, _graphicsDevice, _settings); + } + } +} diff --git a/GenesisEngine/Renderers/QuadNodeRenderer.cs b/GenesisEngine/Renderers/QuadNodeRenderer.cs index 1526ce7..e3f7025 100644 --- a/GenesisEngine/Renderers/QuadNodeRenderer.cs +++ b/GenesisEngine/Renderers/QuadNodeRenderer.cs @@ -12,12 +12,6 @@ public class QuadNodeRenderer : IQuadNodeRenderer, IDisposable { private ContentManager _contentManager; private GraphicsDevice _graphicsDevice; - private VertexDeclaration _vertexDeclaration; - private VertexBuffer _vertexBuffer; - private IndexBuffer _indexBuffer; - private int _numberOfVertices; - private int _numberOfIndices; - private Effect _effect; private ISettings _settings; public QuadNodeRenderer(ContentManager contentManager, GraphicsDevice graphicsDevice, ISettings settings) @@ -27,83 +21,12 @@ public QuadNodeRenderer(ContentManager contentManager, GraphicsDevice graphicsDe _settings = settings; } - public void Initialize(VertexPositionNormalColored[] vertices, int[] indices) - { - _vertexDeclaration = new VertexDeclaration(_graphicsDevice, VertexPositionNormalColored.VertexElements); - - _effect = _contentManager.Load("effects"); - - CreateVertexBuffer(vertices); - CreateIndexBuffer(indices); - } - - private void CreateVertexBuffer(VertexPositionNormalColored[] vertices) - { - _numberOfVertices = vertices.Length; - _vertexBuffer = new VertexBuffer(_graphicsDevice, _numberOfVertices * VertexPositionNormalColored.SizeInBytes, BufferUsage.WriteOnly); - _vertexBuffer.SetData(vertices); - } - - private void CreateIndexBuffer(int[] indices) - { - // TODO: according to the _Interactive Visualization_ paper, we should be able to pre-generate - // all possible index buffers once (tweaked on the edges to match lower LOD neighbors) and reuse - // them for all nodes. - - _numberOfIndices = indices.Length; - _indexBuffer = new IndexBuffer(_graphicsDevice, typeof(int), _numberOfIndices, BufferUsage.WriteOnly); - _indexBuffer.SetData(indices); - } - public void Draw(DoubleVector3 location, DoubleVector3 cameraLocation, Matrix originBasedViewMatrix, Matrix projectionMatrix) { - // TODO: Calculate a dynamic scaling factor based on the distance of the object from the camera? See - // the Interactive Visualization paper, page 24. - - Matrix translationMatrix = Matrix.CreateTranslation(location - cameraLocation); - Matrix worldMatrix = translationMatrix; - - _effect.CurrentTechnique = _effect.Techniques["Colored"]; - _effect.Parameters["xView"].SetValue(originBasedViewMatrix); - _effect.Parameters["xProjection"].SetValue(projectionMatrix); - _effect.Parameters["xWorld"].SetValue(worldMatrix); - - _effect.Parameters["xEnableLighting"].SetValue(true); - Vector3 lightDirection = new Vector3(1.0f, -1.0f, -1.0f); - lightDirection.Normalize(); - _effect.Parameters["xLightDirection"].SetValue(lightDirection); - _effect.Parameters["xAmbient"].SetValue(0.1f); - - if (_settings.ShouldDrawWireframe) - { - _graphicsDevice.RenderState.FillMode = FillMode.WireFrame; - } - else - { - _graphicsDevice.RenderState.FillMode = FillMode.Solid; - } - - _effect.Begin(); - foreach (EffectPass pass in _effect.CurrentTechnique.Passes) - { - pass.Begin(); - - _graphicsDevice.VertexDeclaration = _vertexDeclaration; - _graphicsDevice.Indices = _indexBuffer; - _graphicsDevice.Vertices[0].SetSource(_vertexBuffer, 0, VertexPositionNormalColored.SizeInBytes); - _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, _numberOfVertices, 0, _numberOfIndices / 3); - - pass.End(); - } - _effect.End(); } public void Dispose() { - _vertexDeclaration.Dispose(); - _vertexBuffer.Dispose(); - _indexBuffer.Dispose(); - // Don't dispose _effect here because the ContentManager gives us a shared instance } } diff --git a/GenesisEngine/Renderers/QuadNodeRendererFactory.cs b/GenesisEngine/Renderers/QuadNodeRendererFactory.cs index f1ab007..b566bac 100644 --- a/GenesisEngine/Renderers/QuadNodeRendererFactory.cs +++ b/GenesisEngine/Renderers/QuadNodeRendererFactory.cs @@ -7,7 +7,7 @@ namespace GenesisEngine { - public class QuadNodeRendererFactory + public class QuadNodeRendererFactory : IQuadNodeRendererFactory { ContentManager _contentManager; GraphicsDevice _graphicsDevice;