11
\$\begingroup\$

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?

\$\endgroup\$
0

2 Answers 2

24
\$\begingroup\$

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 a LineList.

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:

enter image description here

\$\endgroup\$
3
  • \$\begingroup\$ What about a line with the same start and end point, wouldn't that produce a point? Or would it simply be invisible? \$\endgroup\$ Jan 30, 2012 at 10:37
  • \$\begingroup\$ @eBusiness Good question! I just gave it a try, and indeed it doesn't render anything. \$\endgroup\$ Jan 30, 2012 at 10:50
  • \$\begingroup\$ @eBusiness I just found a way though, but I think it's a bit silly. I'll add it for completeness anyway. \$\endgroup\$ Jan 30, 2012 at 10:57
2
\$\begingroup\$

This is tagged as XNA so I'm assuming that's what you're asking about?

If so, this article should be helpful for lines:

http://msdn.microsoft.com/en-us/library/bb196414(v=xnagamestudio.40).aspx

Of course you can use your own view/projection matrices instead of theirs. Basically, PrimitiveType.LineList or LineStrip is how to draw lines at the GPU level.

As for points, you can no longer use PrimitiveType.PointList to draw points anymore in XNA 4.0. Instead you would have to make very small triangles. This sample provides a good baseline:

http://create.msdn.com/education/catalog/sample/primitives_3d

For previous versions of XNA, if you're using one of those, you can go ahead and read the Point part of the XNA 3.0 version of the article posted above:

http://msdn.microsoft.com/en-us/library/bb196414(v=xnagamestudio.30).aspx

Note the link has:

graphics.GraphicsDevice.RenderState.PointSize = 10;

Obviously change that to 1.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .