I'm trying to understand how I can transform 2D vectors (x, y) to projection space. Something hasn't quite clicked for me yet. As per my previous question, here's the effect I'm trying to achieve:
My object model is entirely in 2D world space, but I want to render it as per above. That is, position the camera above at about 45 degrees looking down on my scene. Objects further away will appear smaller and move towards the center of the screen.
In an effort to visualize this, I put together a WPF/XNA application that renders a bunch of dots in world, view, and projection spaces. The parameters for the view and projection matrices can be altered in real-time and the resulting projections update accordingly.
However, things aren't being rendered as I expected/hoped. Here's what I see:
I was expecting to see the projection space render a road off into the distance similar to the earlier screenshot.
Can someone take a look at the code and see what it is I'm doing wrong? Apologies for the wall of code, but I didn't want to leave anything out as I may have done something stupid anywhere.
Vector2Collection:
namespace PerspectiveTest
{
using System.Collections.ObjectModel;
using Microsoft.Xna.Framework;
public sealed class Vector2Collection : ObservableCollection<Vector2>
{
}
}
VectorSurface:
using System.Linq;
using System.Windows;
using System.Windows.Media;
using XNA = Microsoft.Xna.Framework;
namespace PerspectiveTest
{
public sealed class VectorSurface : FrameworkElement
{
public static readonly DependencyProperty BrushProperty = DependencyProperty.Register(
"Brush",
typeof(Brush),
typeof(VectorSurface),
new FrameworkPropertyMetadata(OnPropChanged));
public static readonly DependencyProperty MatrixProperty = DependencyProperty.Register(
"Matrix",
typeof(XNA.Matrix),
typeof(VectorSurface),
new FrameworkPropertyMetadata(XNA.Matrix.Identity, OnPropChanged));
public static readonly DependencyProperty VectorsProperty = DependencyProperty.Register(
"Vectors",
typeof(Vector2Collection),
typeof(VectorSurface),
new FrameworkPropertyMetadata(OnPropChanged));
public Brush Brush
{
get { return (Brush)this.GetValue(BrushProperty); }
set { this.SetValue(BrushProperty, value); }
}
public XNA.Matrix Matrix
{
get { return (XNA.Matrix)this.GetValue(MatrixProperty); }
set { this.SetValue(MatrixProperty, value); }
}
public Vector2Collection Vectors
{
get { return (Vector2Collection)this.GetValue(VectorsProperty); }
set { this.SetValue(VectorsProperty, value); }
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (this.Vectors == null)
{
return;
}
var matrix = this.Matrix;
var transformed = new XNA.Vector3[this.Vectors.Count];
var vectors3 = this.Vectors.Select(x => new XNA.Vector3(x.X, x.Y, 0)).ToArray();
XNA.Vector3.Transform(vectors3, ref matrix, transformed);
foreach (var vector in transformed)
{
drawingContext.DrawEllipse(this.Brush, null, vector.ToWpfPoint(), 1, 1);
}
}
private static void OnPropChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
((VectorSurface)dependencyObject).InvalidateVisual();
}
}
public static class PointExtensions
{
public static Point ToWpfPoint(this XNA.Vector2 vector)
{
return new Point(vector.X, vector.Y);
}
public static Point ToWpfPoint(this XNA.Vector3 vector)
{
return new Point(vector.X, vector.Y);
}
}
}
MainWindow.xaml.cs:
using System.ComponentModel;
using System.Windows;
using XNA = Microsoft.Xna.Framework;
namespace PerspectiveTest
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
private readonly Vector2Collection vectors;
private XNA.Vector3 cameraPosition;
private XNA.Vector3 cameraTarget;
private XNA.Vector3 cameraUp;
private float fieldOfView;
private float aspectRatio;
private float nearPlaneDistance;
private float farPlaneDistance;
public event PropertyChangedEventHandler PropertyChanged;
public Vector2Collection Vectors
{
get { return this.vectors; }
}
public XNA.Vector3 CameraPosition
{
get { return this.cameraPosition; }
set
{
if (this.cameraPosition != value)
{
this.cameraPosition = value;
this.OnPropertyChanged("CameraPosition");
this.CreateViewMatrix();
}
}
}
public XNA.Vector3 CameraTarget
{
get { return this.cameraTarget; }
set
{
if (this.cameraTarget != value)
{
this.cameraTarget = value;
this.OnPropertyChanged("CameraTarget");
this.CreateViewMatrix();
}
}
}
public XNA.Vector3 CameraUp
{
get { return this.cameraUp; }
set
{
if (this.cameraUp != value)
{
this.cameraUp = value;
this.OnPropertyChanged("CameraUp");
this.CreateViewMatrix();
}
}
}
public float FieldOfView
{
get { return this.fieldOfView; }
set
{
if (this.fieldOfView != value)
{
this.fieldOfView = value;
this.OnPropertyChanged("FieldOfView");
this.CreateProjectionMatrix();
}
}
}
public float AspectRatio
{
get { return this.aspectRatio; }
set
{
if (this.aspectRatio != value)
{
this.aspectRatio = value;
this.OnPropertyChanged("AspectRatio");
this.CreateProjectionMatrix();
}
}
}
public float NearPlaneDistance
{
get { return this.nearPlaneDistance; }
set
{
if (this.nearPlaneDistance != value)
{
this.nearPlaneDistance = value;
this.OnPropertyChanged("NearPlaneDistance");
this.CreateProjectionMatrix();
}
}
}
public float FarPlaneDistance
{
get { return this.farPlaneDistance; }
set
{
if (this.farPlaneDistance != value)
{
this.farPlaneDistance = value;
this.OnPropertyChanged("FarPlaneDistance");
this.CreateProjectionMatrix();
}
}
}
public MainWindow()
{
InitializeComponent();
this.vectors = new Vector2Collection();
this.PopulateVectors();
this.cameraPosition = new XNA.Vector3(75, 200, 150);
this.cameraTarget = new XNA.Vector3(75, 0, 0);
this.cameraUp = new XNA.Vector3(0, 1, 0);
this.fieldOfView = XNA.MathHelper.Pi / 4;
this.aspectRatio = 1f;
this.nearPlaneDistance = 1f;
this.farPlaneDistance = 200f;
this.CreateViewMatrix();
this.CreateProjectionMatrix();
this.DataContext = this;
}
private void PopulateVectors()
{
for (var i = 0; i < 150; i += 3)
{
this.Vectors.Add(new XNA.Vector2(60, i));
this.Vectors.Add(new XNA.Vector2(90, i));
}
}
private void CreateViewMatrix()
{
this.viewSpace.Matrix = XNA.Matrix.CreateLookAt(this.cameraPosition, this.cameraTarget, this.cameraUp);
this.CreateProjectionMatrix();
}
private void CreateProjectionMatrix()
{
var viewMatrix = this.viewSpace.Matrix;
var perspectiveMatrix = XNA.Matrix.CreatePerspectiveFieldOfView(this.fieldOfView, this.aspectRatio, this.nearPlaneDistance, this.farPlaneDistance);
XNA.Matrix combinedMatrix;
XNA.Matrix.Multiply(ref viewMatrix, ref perspectiveMatrix, out combinedMatrix);
this.projectionSpace.Matrix = combinedMatrix;
}
private void OnPropertyChanged(string propertyName)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
MainWindow.xaml:
<Window x:Class="PerspectiveTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PerspectiveTest"
Title="MainWindow"
SizeToContent="WidthAndHeight">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label FontWeight="Bold" HorizontalAlignment="Center">World Space</Label>
<Label Grid.Column="1" HorizontalAlignment="Center" FontWeight="Bold">View Space</Label>
<Label Grid.Column="2" HorizontalAlignment="Center" FontWeight="Bold">Projection Space</Label>
<Border Grid.Row="1" BorderThickness="1" BorderBrush="Gray" Margin="50, 10, 50, 50">
<local:VectorSurface x:Name="worldSpace" Vectors="{Binding Vectors}" Brush="Black" Width="150" Height="150"/>
</Border>
<Border Grid.Row="1" Grid.Column="1" BorderThickness="1" BorderBrush="Gray" Margin="50, 10, 50, 50">
<local:VectorSurface x:Name="viewSpace" Vectors="{Binding Vectors}" Brush="Red" Width="150" Height="150"/>
</Border>
<Border Grid.Row="1" Grid.Column="2" BorderThickness="1" BorderBrush="Gray" Margin="50, 10, 50, 50">
<local:VectorSurface x:Name="projectionSpace" Vectors="{Binding Vectors}" Brush="Green" Width="150" Height="150"/>
</Border>
<Label Grid.Row="2">Camera Position:</Label>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding CameraPosition}"/>
<Label Grid.Row="3">Camera Target:</Label>
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding CameraTarget}"/>
<Label Grid.Row="4">Camera Up:</Label>
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding CameraUp}"/>
<Label Grid.Row="5">Field of View:</Label>
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding FieldOfView}"/>
<Label Grid.Row="6">Aspect Ratio:</Label>
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding AspectRatio}"/>
<Label Grid.Row="7">Near Plane Distance:</Label>
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding NearPlaneDistance}"/>
<Label Grid.Row="8">Far Plane Distance:</Label>
<TextBox Grid.Row="8" Grid.Column="1" Text="{Binding FarPlaneDistance}"/>
</Grid>
</Window>