I'd like to draw a line in 3D space that is always exactly one pixel wide on screen, no matter how far away from the camera it is. (And the same for single points too).
Any hints on how I could do that?
Answer
Rendering Lines - Method 1 (Primitives)
For simple lines in 3D space you can draw them using a LineList
or LineStrip
primitive. Here's the minimal amount of code you have to add to an empty XNA project to have it draw a line from (0,0,0) to (0,0,-50). The line should appear to have roughly the same width no matter where the camera is located.
// Inside your Game class
private BasicEffect basicEffect;
private Vector3 startPoint = new Vector3(0, 0, 0);
private Vector3 endPoint = new Vector3(0, 0, -50);
// Inside your Game.LoadContent method
basicEffect = new BasicEffect(GraphicsDevice);
basicEffect.View = Matrix.CreateLookAt(new Vector3(50, 50, 50), new Vector3(0, 0, 0), Vector3.Up);
basicEffect.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45f), GraphicsDevice.Viewport.AspectRatio, 1f, 1000f);
// Inside your Game.Draw method
basicEffect.CurrentTechnique.Passes[0].Apply();
var vertices = new[] { new VertexPositionColor(startPoint, Color.White), new VertexPositionColor(endPoint, Color.White) };
GraphicsDevice.DrawUserPrimitives(PrimitiveType.LineList, vertices, 0, 1);
I basically created a simple BasicEffect
to hold my view and projection transformations, and passed two vertices (storing position and color) to the GraphicsDevice.DrawUserPrimitives
method to be rendered as aLineList
.
Of course there are many ways to optimize it, most of which involve the creation of a VertexBuffer
to store all of the vertices, and batching as many lines as possible into a single Draw call but that's irrelevant to the question.
Rendering Points - Method 1 (SpriteBatch)
As for drawing points, this used to be easy using point sprites but they were removed from XNA 4.0. There's a few alternatives though. The easiest way is to create a 1x1 white Texture2D
object, and render it using SpriteBatch
in the correct screen location, which you can easily find using the Viewport.Project method.
You can create the required Texture2D
object like this:
Texture2D pixel = new Texture2D(GraphicsDevice, 1, 1);
pixel.SetData(new [] { Color.White });
And render it at location (x,y,z) like this:
// Find screen equivalent of 3D location in world
Vector3 worldLocation = new Vector3(0, 0, 50);
Vector3 screenLocation = GraphicsDevice.Viewport.Project(worldLocation, projectionMatrix, viewMatrix, Matrix.Identity);
// Draw our pixel texture there
spriteBatch.Begin();
spriteBatch.Draw(pixel, new Vector2(screenLocation.X, screenLocation.Y), Color.White);
spriteBatch.End();
Rendering Lines - Method 2 (SpriteBatch)
Alternatively, you can also draw lines using a SpriteBatch
using the technique described here. In this case you'd simply need to find the screen space coordinate for both ends of the 3D line (once again using Viewport.Project
) and then draw a regular line between them.
Rendering Points - Method 2 (Small Line with Primitives)
In the comments, eBusiness raised the following question:
What about a line with the same start and end point, wouldn't that produce a point? Or would it simply be invisible?
I gave it a try and rendering a LineList
using the same start and end points resulted in nothing being drawn. I found a way around it though so I'll describe it here for completeness.
The trick is not to use the same start and end points, but instead to draw a line so small, that it only appears as one pixel when drawn. So, in order to choose the correct end point, I first projected the world space point into screen space, moved it right one pixel in screen space, and finally projected it back into world space. That's the end point of your line in order to make it look like a dot. Something like this:
Vector3 GetEndPointForDot(Vector3 start)
{
// Convert start point to screen space
Vector3 screenPoint = GraphicsDevice.Viewport.Project(start, projection, view, Matrix.Identity);
// Screen space is defined in pixels so adding (1,0,0) moves it right one pixel
screenPoint += Vector3.Right;
// Finally unproject it back into world space
return GraphicsDevice.Viewport.Unproject(screenPoint, projection, view, Matrix.Identity);
}
Followed by rendering it as a normal line primitive.
Demonstration
Here's what I got drawing a white line in 3D space using a line list primitive, and red dots at both ends of the line using a 1x1 texture and SpriteBatch. The code used is pretty much what I wrote above. I've also zoomed in so you can confirm that they're exactly one pixel wide:
No comments:
Post a Comment