Manders vs. Machine

A blog about PIX, XNA, and other game programming stuff

  • Importing and rendering XAML shape data in XNA Game Studio

     

    I've been wanting to find a better way to get 2D geometry content into my XNA Game Studio projects. Laying out lines and triangles on graph paper, then entering coordinates by hand into code files gets old fast! I was looking for a 2D illustration package that I could use instead, then translate from its data files into something I could read from XNA.

    Microsoft Expression Design is a pretty nice tool for laying out lines and shapes, and I was interested to see that it can export its documents as WPF XAML, which looked fairly straightforward to parse. I decided to see if I could use the XNA Content Pipeline to translate the XAML into lines and triangles that I could render.

    XAML is based on XML, so I thought at first that I would parse the XML nodes directly with something like XmlReader. Then I realized that it was pretty easy to let Windows Presentation Foundation take care of the parsing, and just traverse the resulting WPF object hierarchy to get the path information. The XAML files I generated in Expression Design had a top-level Canvas object, which contained Canvas child objects for each layer. These inner Canvas objects had various Shapes -- the ones I was interested in were Paths. A method called GetFlattenedPathGeometry took care of converting any curves into line segments -- then I just had to iterate through the line segments and record the coordinates.

    This was my first time attempting to extend the XNA Content Pipeline. It took a bit of background reading to get the hang of it, but it seems straightforward to me now that I understand the concepts. My Content Importer takes a XAML file as input and generates a "MyDataTypes.MyShape" object as output. I didn't need a Content Processor, since my app works with MyShape objects (there was no intermediate type needed). And thanks to XNA Game Studio 3.1's automatic XNB serialization, I didn't have to write a ContentTypeWriter or ContentTypeReader for MyShape.

    One minor thing I had to deal with was a coordinate system change. Expression Designer and WPF treat the top-left corner of the document as the origin, with Y increasing as you move down the screen. I am more used to Y increasing as you go up the screen, so all my content was appearing upside-down at first (though I could modify my projection matrix to match WPF as one way to address this). I decided to have my Content Importer perform the Y inversion with a "Y = CanvasHeight - Y" adjustment.

    Getting the line segments for a path was fairly easy. Getting the triangles for the interior of the path required a bit more work. Basically I needed a way to tessellate arbitrary convex polygons into triangles (this is sometimes called triangulation, since the term tessellation can mean other things).  Algorithms exist to do triangulation, but I was having trouble finding some pre-existing code to use. Then I learned that Direct2D has the ability to tessellate geometry, and decided to try using that.

    Direct2D is fairly new, and is built into Windows 7. It can be installed on Windows Vista SP2 if you've installed the "Platform Update for Windows Vista" -- see KB 971644. If you're still using Windows XP, I'm afraid you're out of luck -- you'll have to find another way to triangulate shapes. Headers/libs for Direct2D are available both in the latest Windows SDK and the August 2009 DirectX SDK.

    I wrote a standalone tool called D2DTessellate.exe to handle this triangulation task while importing content. It's written in C++, not C#, so I've provided a executable as well as the source code, in case you don't have Visual C++ or the necessary headers/libs installed. The tool reads a text file containing a list of points specifying a polygon, calls into Direct2D to tessellate it, then writes the resulting triangles to another text file. It's short and it works! My content importer invokes D2DTessellate.exe as needed for each polygon.

    Note that the Content Importer needs to use WPF and D2D, but everything is just "MyShape" by the time the game loads the content from the Content Pipeline. So games that use XAML-based content with this method can still run on the Xbox -- the game itself doesn't have any direct dependencies on those Windows-only technologies.

    Back in my XNA Game Studio game app, the main new thing I needed to add was a SimpleMesh class for drawing these lists of triangles associated with MyShape. I used non-indexed triangle lists, since that matched the data coming out of D2DTessellate. Since a lot of vertices are shared by multiple triangles, it would be possible to save a little space and improve perf by using index buffers and re-using vertex data instead of replicating it. My shapes were small enough that it didn't really matter. I created a spiffy zig-zag pixel shader as an example of a more interesting way to render the mesh triangles than a solid color. I also added a wireframe option to the test app, so you can see the tessellation.

    I'm really happy with the result -- now I can draw shapes in Expression Designer, export the XAML, and easily import and draw it in my XNA Game Studio apps. Rock on!

    -Mike

  • XNA RoundLine Code Released on CodePlex

    Since there still seems to be a lot of interest in it, I've updated my RoundLine code and released it on CodePlex. Here are the major changes from earlier versions:

    • Updated for XNA Game Studio 3.0
    • Uses shader instancing for improved performance: over 20,000 lines per frame at 60fps on the Xbox 360
    • Drawing code is in a separate file from interaction code (using partial classes), so you can easily skip the latter if you don't need it
    • Miscellaneous cleanup
    • A small demo app is included

    I hope you find it useful!

    -Mike

  • Check out the August 2008 version of PIX

    The August 2008 DirectX SDK has been released. You can get it here.

    Here are the PIX updates, as listed on the official "What's New" summary for the Aug08 release.

    PIX Improvements

    This release of PIX has many significant improvements.

    • Draw call timing for single-frame captures—PIX now calculates GPU-side timing information for Direct3D 9 and Direct3D 10 draw calls when performing single-frame captures or during playback.
    • Improved 32/64-bit support—PIX has improved 64-bit support and cross-platform compatibility. Run files can be played back on either platform regardless of whether the original application was 32-bit or 64-bit. By using the 64-bit version of PIX to analyze your application, you can take advantage of the larger address-space available on 64-bit Windows and reduce the chance of running out of memory.
    • Improved application support—PIX has improved support for applications that call both Direct3D 9 and Direct3D 10. It is also able to safely capture data from multithreaded applications.
    • Output Messages tab—A new Output Messages tab is available that better exposes debug output from the Render, Mesh, and Shader views. You can filter messages by view and sort them by event ID, timestamp, and type, as well as copy them to the clipboard.
    • Render tab—As you move through the draw calls in a run file, the Render view continues to display the back buffer on Present calls, but for other calls it now displays the currently-bound render target. Also, the status bar now shows the surface format information. In addition, pencil icons appear on all the tabs of surfaces or textures that are currently bound to the device as render targets.
    • Buffer Data view—The performance of the Buffer Data is significantly improved and large buffers are no longer painfully slow to examine.

    And here's a more detailed article on Gamasutra on new PIX functionality in the June 2008 and August 2008 versions of PIX.

     

    -Mike

  • My First XNA Community Game

    I've been working on a little arcade game to submit to the XNA Creators Club Community Games Beta. I finally finished it last weekend and submitted it Sunday night. It passed Peer Review and went live a few hours ago -- woohoo! It's not the most amazing game of all time, but it was still a lot of work to get all the details finished. I have ideas for more ambitious games to tackle soon...

    Check out "Gemini 08" in the Games Catalog. Anyone with an XNA Creators Club Premium Membership can play it on the Xbox 360 for free! If you have feedback on the game, you can submit it here.

    For more info about the XNA Creators Club and the Community Games Beta, check out this tour.

    -Mike

  • Polygonal Tessellation

    Now that I've got the hang of quickly drawing lots of shapes with shader instancing, what should I draw? All that geometric repetition got me thinking about the artwork of M. C. Escher, which led me to this page about regular and semi-regular tessellation. The diagrams near the bottom looked nice, so I started playing with drawing similar images with XNA code.

    Regular tessellations are tessellations of the plane with a single regular polygon. This is only possible with triangles, squares, and hexagons. Things get more interesting when you allow multiple kinds of polygons. Semiregular tessellations have the same sequence of polygons around each vertex, such as (4, 6, 12) for a square, hexagon, and dodecagon. There are eight possible semiregular tessellations.

    To draw these, I first needed to make meshes for various polygons: squares, hexagons, octagons, etc. And I needed to be able to move and rotate them around relative to each other. Rather than gobs of the usual sines and cosines, I thought I'd try using turtle graphics, like the old Logo programming language. Here's my quick and dirty turtle class:

    public class Turtle
    {
        public Vector2 pos = Vector2.Zero;
        public float theta = 0;
        public void Reset()
        {
            pos =
    Vector2.Zero;
            theta = 0;
        }
        public void Fwd(float amt)
        {
            Vector2 delta = new Vector2(amt * (float)Math.Cos(theta), amt * (float)Math.Sin(theta));
            pos += delta;
        }
        public void RotRadians(float radians)
        {
            theta += radians;
        }
        public void RotDegrees(float degrees)
        {
            RotRadians(
    MathHelper.ToRadians(degrees));
        }
    }

    So that was handy, both for creating polygons and for placing them relative to each other. Note that the origin of the polygon meshes is at one of the vertices, rather than in the center. That made it easy to place several polygons that share a vertex. (That is, they mathematically share a vertex in the tessellation...they don't share Direct3D vertices when they are rendered).

    I separated my mesh code from the InstancedShapeManager, so I could pass various different polygons to a single shape manager for drawing. I still kept separate lists for all the squares, all the hexagons, etc., and draw all polygons of a given type at once.

    The actual algorithm for generating the tessellations is a bit of a hack. I start at a vertex and rotate around, placing polygons as specified by the tessellation type. For example, for a (3, 4, 6, 4) tessellation, I would place a triangle, rotate 60 degrees, place a square, rotate 90 degrees, place a hexagon, rotate 120 degrees, and place a square. After placing each polygon, I'd enqueue a "task" to handle the vertex one unit away from the current vertex at the current orientation. When the current vertex was finished, I'd dequeue the next task and repeat the process. The neighboring vertices would have to start from a different orientation, which I called the "magic rotation" value and determined by trial and error. There's probably a simple way to derive it, but I didn't come up with it.

    I had to add code to keep from re-adding vertices which were already processed (or in the task queue), and also to keep from placing polygons where one had already been placed.

    I show square markers to represent unfinished vertex tasks. The green marker shows the current task, and red markers are enqueued tasks. When a task is complete, I remove the marker.

    I use arbitrary hues to color each polygon -- again, there's probably a clever algorithm for coloring them such that adjacent polys are not the same color -- but I didn't go there.

    The result is fun to watch, as the tessellation grows from the center point.

    I didn't get some of the tessellations working, such as (4, 6, 12). With these, some vertices are (4, 6, 12) and others are (12, 6, 4). I was able to cope with that, but then I couldn't come up with magic rotation values to make the subsequent polygons appear in the right place. Oh well.

    I'm probably not going to do anything further with this tessellation code (though it might be fun to try some hyperbolic tessellations), but it was fun to make some pretty pictures.

     

    -Mike

  • Debugging with PIX for Windows

    I haven't written anything about PIX in a while. But here's a great article written by Brian Richardson at GarageGames about using PIX to track down a rendering issue. Nice job, Brian!

    You can find the latest version of PIX in the March 2008 DirectX SDK.

     -Mike

  • It's full of stars: XNA 2-D Shader Instancing

    A lot of my XNA-based programs draw many instances of the same mesh (such as RoundLines), with each instance having different position, rotation, etc. I had been submitting these meshes to Direct3D one instance at a time, which is not great for performance. Instanced drawing is a good way to get many copies of simple meshes on-screen faster, but it takes a bit of extra work. It's not too difficult, though.

    The Instanced Model Sample is a great place to start learning about instancing. It's a little intimidating, though, to learn that there are three ways to do instancing, and they all require slightly different code and have different hardware requirements.

     

     

    Hardware Instancing

    Shader Instancing

    VFetch Instancing

    Works on Xbox?

    No

    Yes

    Yes

    Works on Windows?

    Yes (if vs_3_0)

    Yes

    No

    Need multiple streams?

    Yes

    No

    No

    Need modified vdecl?

    Yes

    Yes

    No

    Need replicated VB?

    No

    Yes

    No

    Need replicated IB?

    No

    Yes

    Yes

    Need inline asm?

    No

    No

    Yes

    Max instances/draw?

    Thousands

    50-200

    50-200

    If you want maximum performance in every possible situation, you should probably implement all three methods. But, being a bit of a minimalist, I'd rather pick one method that works pretty well in all situations. The "happy medium" method seems to be shader instancing. It works on both Windows and Xbox, it doesn't require multiple vertex streams or inline assembly, and gives good performance. Hardware instancing and VFetch instancing have some advantages on their respective platforms, but shader instancing is giving me the speed boost I need without a lot of extra code paths.

    In the Instanced Model Sample, a full 4x4 transformation matrix is specified for each instance. Since there are only 256 float4 registers available to the vertex shader, this limits the number of instances per draw to about 60. You can often get away with much less per-instance data, especially in 2-D. In my test program, I pass just four float values per instance: x, y, rotation, and hue. That all fits in a single float4, so I am submitting 200 instances per draw (I could have done closer to 256, but I want to leave room for some non-instance constant data). The per-instance data is going to vary depending on what functionality you want -- you might want a scaling factor or an alpha value, for example.

    For shader instancing, the vertex declaration needs to have an element for the instance number, stored in a texture coordinate. The only other element I need for my program is position, so it is a pretty small vdecl:

    public static VertexElement[] VertexElements = new VertexElement[]
    {
        new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0),
        new VertexElement(0, 12, VertexElementFormat.Single, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0),
    };

    When creating a mesh, I need to repeat the vertex data for each instance, for a total of verticesPerInstance * numInstances vertices. Each instance's copy specifies its instance index. Here's the setup for quads:

    int iVertex = 0;
    for (int iInstance = 0; iInstance < numInstances; iInstance++)
    {
        vertices[iVertex++] =
    new MyVertexElement(new Vector3(-1, -1, 0), iInstance);
        vertices[iVertex++] =
    new MyVertexElement(new Vector3(+1, -1, 0), iInstance);
        vertices[iVertex++] =
    new MyVertexElement(new Vector3(+1, +1, 0), iInstance);
        vertices[iVertex++] =
    new MyVertexElement(new Vector3(-1, +1, 0), iInstance);
    }

    The index data needs to be repeated as well, though you don't need to store the instance index here:

    int iIndex = 0;
    for (int iInstance = 0; iInstance < numInstances; iInstance++)
    {
        int iVertexBase = iInstance * 4;
        indices[iIndex++] = (
    short)(iVertexBase + 1);
        indices[iIndex++] = (
    short)(iVertexBase + 2);
        indices[iIndex++] = (
    short)(iVertexBase + 3);
        indices[iIndex++] = (
    short)(iVertexBase + 3);
        indices[iIndex++] = (
    short)(iVertexBase + 0);
        indices[iIndex++] = (
    short)(iVertexBase + 1);
    }

    The replication feels kind of wasteful, but for these tiny shapes you still end up with a very small VB and IB.

    Before drawing, set the per-instance data into an array, then hand it to the instanceData effect parameter.

    In the vertex shader, use the index to pull the per-instance data out of the instanceData array:

    VS_OUTPUT MyVS(
        float4 Pos : POSITION, 
        float Index : TEXCOORD0 )
    {
        VS_OUTPUT Out = (VS_OUTPUT)0;
        float tx = instanceData[Index].x;
        float ty = instanceData[Index].y;
        float rot = instanceData[Index].z;
        float hue = instanceData[Index].w;
        ...
    }

    That's basically it! You can see in my Draw() function how I break up the shapeList into batches of 200, since that's the maximum number I can draw at a time. This is still a huge speedup over drawing them one at a time.

    In the demo, I draw hexagrams just to show you can do something more interesting than quads. And I do some translation, rotation, and color cycling to exercise the per-instance information.

    I'm using GameTime.IsRunningSlowly to tell if I'm doing so much work per frame that the system can't keep up with the target rate (60fps). On the Xbox I can maintain 60fps with up to around 15,000 instances. I suspect that there is more graphics power still going unused...perhaps I am causing too much garbage collection to render more. On the PC, I can do over 40,000 instances -- your mileage may vary depending on your hardware configuration, of course.

    Instancing is great...I'll be using it a lot in my drawing code from now on.

    -Mike

  • Wendybrot Revisited: A Better XNA Mandelbrot Set Explorer

    (Insert standard gosh-I-haven't-blogged-in-a-while blurb here)

    (UPDATE: If you get an out-of-video-memory exception when starting the program, find the line of code that says "public const int numNodesToAllocate = 250;" and change 250 to a smaller number.)

    I decided to improve my old XNA Mandelbrot set explorer to take advantage of the GPU horsepower available on PCs and the Xbox. It was a fair bit of work, but the end result is much more interactive and fun to use than the old version.

    The core Mandelbrot set algorithm is a natural fit for a pixel shader. The harder part is making it incremental. Once you're zoomed in a fair bit, it can take hundreds or thousands of iterations to determine if a point is in the set or not, and it's more fun if you can zoom and pan around while progressively improving the image. So you want a program that can iterate a little, then move and draw, then iterate a little more. That is a little off the beaten path of the standard rendering pipeline.

    I decided to use render-to-texture and ping-pong between two 256x256 textures. So for the first frame, the shader reads from texture A, computes some iterations, and writes to texture B. Then texture B is rendered onscreen. Then in the next frame, the shader reads from texture B, computes some further iterations, and writes to texture A. Then texture A is rendered onscreen. Then we repeat the whole process. (Life would be easier if we could read to and write to the same texture, but Direct3D doesn't work that way).

    But instead of there just being one texture A and one texture B, there are a whole lot of them. The two-dimensional space is subdivided with a quadtree, and each node in the quadtree has a "texture A" and a "texture B." The program both iterates on and draws all visible nodes, every frame.

    Also, what I'm calling "texture A" for one node is actually more than one Direct3D texture. I need to maintain four pieces of state for each texel: the current calculated "x", the current calculated "y", the iteration count, and whether the point is "done." Ideally I'd use floats for "x" and "y", a uint for "iterations", and a boolean for "done." SurfaceFormat.Vector4 would have been sufficient (albeit a bit wasteful), but the Xbox doesn't support it. I decided to use three SurfaceFormat.Single textures, and encode "done" into the iterations count by negating the value if the texel is done.

    I had never tried using multiple render targets (MRT) before, but it worked fine. Instead of having the pixel shader output a float4, it needs to output a structure with values for each render target:

    // MRT output. Even though each RT only has a red channel,
    // we have to write out a four-component vector to each RT.
    struct PS_OUTPUT
    {
        float4 x : COLOR0;
        float4 y : COLOR1;
        float4 iterations : COLOR2;
    };

    The shader also needs to read from three textures, but that's just a matter of using three samplers, with a different texture bound to each sampler:

        float4 colorData = tex2D(dataSamp, pos);
        float4 colorData1 = tex2D(dataSamp1, pos);
        float4 colorData2 = tex2D(dataSamp2, pos);
        float x = colorData.r;
        float y = colorData1.r;
        float iterations = colorData2.r;
        bool done = false;
        if( iterations < 0 )
        {
            done = true;
            iterations = -iterations;
        }

        (Not shown here: update x, y, iterations, done)

        if( done )
            iterations = -iterations; 
        PS_OUTPUT output;
        output.x = float4(x,0,0,0);
        output.y = float4(y,0,0,0);
        output.iterations = float4(iterations,0,0,0);
        return output;

    Okay, so each node consists of six 256x256 32 bpp textures, or 1.5MB of texture memory. How many of these nodes do we need around, to be able to view a frame at full screen resolution? At 1280x720, there are generally about 50 to 100 nodes recalculated per frame. Since we want to zoom and pan around, it's helpful to have a few extra nodes. So I allocate a pool of 250 nodes up front when the program starts, and purge least-recently-used nodes when I need fresh ones for newly-exposed areas of the screen. So, yeah, that's 375MB of textures altogether, and 75-150MB of that is read/written per frame. That's a lot of texture memory to eat up, especially on an Xbox, but it works.

    There are actually three key pixel shaders. One is used to set the initial values for x, y, and iterations/done when a node is initialized. A second shader does the iterations. A third shader just reads the iterations/done info and translates it into a color for display.

    You can use the "B" button to tell the "CalcCore" pixel shader to perform 1, 4, or 16 iterations per pixel per frame. More iterations makes a given node calculate faster, and is nice if you're not panning/zooming, but the frame rate suffers a bit. As with previous versions of Wendybrot, you can use the right stick to play around with the color palette.

    The attached zip file contains all the source code, plus prebuilt ccgame packages for both Xbox and Windows. You can undefine "USE_RTT" in the source code to make Wendybrot use the old method of performing the iterations on the CPU instead of with the pixel shader -- but it's much slower! Also, you can try rendering at 1920x1080, but the frame rate struggles a bit here. Still, it's staggering to think about how much math and memory transfer is happening per frame.

    I hope you enjoy playing with the program and learn a few things from the code.

    -Mike

  • XNA RoundLines in Pixel Coordinate Space

    Two people recently sent me questions about setting up the world/view/projection matrices to work with my XNA RoundLine code. All of my programs so far have specified line coordinates in an abstract world coordinate space, with a view matrix based on a 2D camera and a projection matrix to adjust for nonsquare viewports. But if you want to write a 2D game that uses pre-authored bitmap graphics, it's often easier to work in a pixel-based coordinate system. The XNA SpriteBatch class, for instance, lets you specify coordinates in pixels, and things just work without having to think about the 3D transformation pipeline. So I spent a little time looking at what is involved in using RoundLines in a game that uses pixel-space coordinates.

    So let's say you want to specify RoundLine coordinates in this space where (0, 0) is the upper-left corner of the screen, and something like (640, 480) is the lower-right corner of the screen. Before any polygons get rasterized by Direct3D, they need to be transformed into clip space. In this space (ignoring the Z dimension), the upper-left corner is (-1, 1) and the lower-right corner is (1, -1). Anything outside this rectangle gets clipped. We want the projection matrix to transform the vertex coordinates between these two spaces. (We'll just use an identity matrix for the view matrix.)

     

    Horizontally, we want to change the coordinate system from (0 to 640) to (-1 to 1). So we divide x by 640 (so we're now 0 to 1), then multiply by 2 (so we're now 0 to 2), then subtract 1 (so we're now -1 to 1). Done!

    Vertically, things are similar except there is a vertical flip: we want to change the coordinate system from (0 to 480) to (1 to -1). So we divide y by 480 (so we're now 0 to 1), then multiply by 2 (so we're now 0 to 2), then multiply by -1 (so we're now 0 to -2), then add 1 (so we're now 1 to -1).

    Here's one way to do that in code:

     

    viewportWidth = (float)graphicsDevice.Viewport.Width;

    viewportHeight = (float)graphicsDevice.Viewport.Height;

    float scaleX = 1.0f / (viewportWidth / 2);

    float scaleY = 1.0f / (viewportHeight / 2);

    projMatrix = Matrix.CreateScale(scaleX, scaleY, 1) *

                 Matrix.CreateScale(1, -1, 1) *

                 Matrix.CreateTranslation(-1, 1, 0);

     

    There's one more twist involved. By inverting Y, the culling rule changes. Culling is more relevant in 3D apps than 2D -- it lets the pipeline discard polygons that are facing away from the camera, to save the time that would otherwise be wasted rendering them. The vertices for my RoundLine polygons were specified in clockwise order. But if you flip Y, clockwise becomes counter-clockwise:

    The RoundLine effect file was specifying "CW" culling for each technique. With the Y inversion, all the polygons became counter-clockwise and were getting clipped. The solution is to specify "None" culling instead, so changing the coordinate system doesn't inadvertently cull all the polygons.

    So I've attached a little sample program that draws a SpriteBatch image and some RoundLines together, doing everything in screenspace. Hopefully this code and explanation is useful, especially for people writing games in pixel space.

    -Mike

  • Shadows in 2D

    The discs of light used by my XNA 2D lighting code look decent, but shadows would really add to the realism. Shadows are a bit tricky because they are non-local: an object half-way across the screen can make the difference in whether a pixel should be light or dark. And you have to take account of every object in the scene in order to determine whether a point is in shadow. But there's a reasonable way to do 2D shadows that works well with my approach of drawing the lighting in a separate render target.

    Shadow volumes are a popular technique for doing shadows in 3D. It's not too hard to see how to use a similar technique in 2D. Basically, for each object that casts a shadow, modulate that object's shape based on the light source's position and, by rendering the updated shape, mark every pixel that is in shadow (the stencil buffer is often used for this). Then when you draw the lighting for a pixel, you check the shadow information to tell whether the point should be in shadow or not. Since I'm drawing the lighting in its own render target, I can also do it in the other order: draw the light beam first, then black out the pixels that are in shadow.

    Consider a point light that casts light for a finite distance, so its area of influence is a disc (shown in white below). Now consider a line segment that intersects the disc of light, and thus obstructs some of the light. Visualize a ray extending from the light to each endpoint of the line segment, and on to the edge of the disc. We want to black out the interior of that shape (shown in red below):

    There's probably a proper mathematical term for that kind of shape -- but I'm calling it a shadow wedge. The inside points are just the endpoints of the line segment -- call them p0 and p1. It's round on the outside, lining up with the light disc's perimeter, but with a slightly larger radius so there are no sparkles due to different tessellation than the light disc. I'm using 6 outer points (but of course you could use more if necessary) -- call them v0, v1, v2, v3, v4, and v5:

    v0 is the extension of the ray through p0, and v5 is the extension of the ray through p1. The other "p"s are at equal divisions of the angle between v0 and v5.

    Note that I'm trying to make the shadow wedge as "tight" as possible, so we don't waste pixel bandwidth. I could have made the shape a triangle with the light source as one vertex and the other two being projections through the endpoints out some great distance, and done a line test in the pixel shader to see whether to black out the pixel or not. But I'm trying to be efficient so I touch as few pixels as possible, and make it quicker to draw lots of shadow wedges.

    Here's where it gets fun. Given p0, p1, the light position, and the light radius, can you generate the shadow wedge in a vertex shader? It's a slightly strange job for a vertex shader, because the shader needs to do different things for the different points. (It's a better match for a D3D10 geometry shader, but that'll have to wait for another day). Here's how I did it...

    For the input vertices, instead of storing literal position info, I store the "p-ness vs. v-ness" in the x coordinate, and the "0-ness vs. 1-ness" in the y coordinate. So p0 has an x value of 1.0 (all p, no v) and a y value of 0.0 (all 0, no 1). v5 has an x value of 0.0 (no p, all v) and a y value of 1.0 (all 1, no 0). v1 through v4 have y values between 0 and 1, reflecting their increasing "1-ness."

                numVertices = 8; // p0, p1, v0, v1, v2, v3, v4, v5
                vertices[0] = new VertexPositionTexture(new Vector3(1, 0.0f, 0), new Vector2(0, 0));
                vertices[1] = new VertexPositionTexture(new Vector3(1, 1.0f, 0), new Vector2(0, 0));
                vertices[2] = new VertexPositionTexture(new Vector3(0, 0.0f, 0), new Vector2(0, 0));
                vertices[3] = new VertexPositionTexture(new Vector3(0, 0.2f, 0), new Vector2(0, 0));
                vertices[4] = new VertexPositionTexture(new Vector3(0, 0.4f, 0), new Vector2(0, 0));
                vertices[5] = new VertexPositionTexture(new Vector3(0, 0.6f, 0), new Vector2(0, 0));
                vertices[6] = new VertexPositionTexture(new Vector3(0, 0.8f, 0), new Vector2(0, 0));
                vertices[7] = new VertexPositionTexture(new Vector3(0, 1.0f, 0), new Vector2(0, 0));

    In the shader, I first translate to a space where the light source is at the origin. Then I find theta0 (the angle to p0) and theta1 (the angle to p1). Theta is somewhere between the two (based on the vertex's "0-ness vs. 1-ness"). So I find vLightSpace and pLightSpace, and use the vertex's "p-ness vs. v-ness" to decide which one to pick. There are a few extra bits of code to make sure we go the right direction from theta0 to theta1, and clamp p to not be bigger than the radius, but that's about it!

    VS_OUTPUT MyVS(
        float4 pos  : POSITION )
    {
        VS_OUTPUT Out = (VS_OUTPUT)0;
        float vVsP = pos.x; // v vs. p
        float zeroVsOne = pos.y; // v0 vs. v5, or p0 vs. p1

        // Use "lightspace", a coordinate system where the light source is at the origin
        float2 lineP0LightSpace = lineP0 - lightPos;
        float2 lineP1LightSpace = lineP1 - lightPos;
     
        float theta0 = atan2(lineP0LightSpace.y, lineP0LightSpace.x);
        float theta1 = atan2(lineP1LightSpace.y, lineP1LightSpace.x);

        // Make sure our lerp takes the shortest path from theta0 to theta1
        // (make sure theta0 and theta1 are <= PI apart from each other) 
        if( theta1 - theta0 > 3.14159 )
            theta1 -= 2 * 3.14159;
        if( theta0 - theta1 > 3.14159 )
            theta0 -= 2 * 3.14159;
     
        float theta = lerp(theta0, theta1, zeroVsOne);
        float2 vLightSpace = float2(lightRadius * cos(theta), lightRadius * sin(theta));

        // clamp p0 and p1 to the light's radius...otherwise things get weird
        if( distance( lineP0LightSpace, float2(0,0) ) > lightRadius )
        {
           lineP0LightSpace = float2( lightRadius * cos(theta0), lightRadius * sin(theta0) );
        }
        if( distance( lineP1LightSpace, float2(0,0) ) > lightRadius )
        {
           lineP1LightSpace = float2( lightRadius * cos(theta1), lightRadius * sin(theta1) );
        }
     
        float2 pLightSpace = lerp(lineP0LightSpace, lineP1LightSpace, zeroVsOne);
     
        // Translate back from "lightspace" to worldspace
     
        float2 v = vLightSpace + lightPos;
        float2 p = pLightSpace + lightPos;

        float2 mergedPos = lerp(v, p, vVsP);
        float4 mergedPos4 = float4(mergedPos.x, mergedPos.y, 0, 1);

        Out.position = mul(mergedPos4, viewProj);

        return Out;
    }

    Computing the shadow wedge in the vertex shader makes it possible to draw a whole lot of them quickly, so a light can be occluded by any shape whose silohouette is a series of line segments. I just pass in a list of my Line objects, plus the light position and radius, and the shadow wedges get drawn for each Line.

    In my case, I want to have my RoundLines cast shadows, rather than idealized "thin" lines. The code above is fine if the line has a radius of 0 (or "close enough" to 0), but what if it's thick? I solved this by drawing three shadow wedges instead of one, per RoundLine. The larger, middle wedge goes between the RoundLine's "endpoints". The other points I need are the tangents to the end discs where the ray from the light source just grazes them. This requires some trigonometry.

    We know the coordinates of p0 (one of the endpoints of the RoundLine), and we want to find p0-prime (the tangent point to the RoundLine). The tangent line is at a right angle to the radius line from p0 to p0-prime. We know the distance from the light to p0 (the hypotenuse), and the distance from p0 to p0-prime (the opposite side). sin(theta) = opposite/hypotenuse, so asin(opp/hyp) = theta. Once we have theta we can quickly find the length of the adjacent side, and thus p0-prime. Do the same thing for p1 to find p1-prime, and you're done! I call the little shadow wedges from p0 to p0-prime, and from p1 to p1-prime, the "sideburns" and have an option to turn them off if you're not using RoundLines. It might even work (and be faster) to simply draw a single shadow wedge from p0-prime to p1-prime.

    I tried some options to have the far half of the RoundLine be dark (in shadow), and the near half be properly lit by the light source. Because the endpoints of the RoundLines overlap, this had issues, and the sideburns were causing problems too. I decided to make the walls not be affected by the shadow -- I actually draw them on the final render target after the fullbright scene is multiplied by the light scene. This is a little less "realistic" than I originally had in mind, but as I was rambling about last time, it's not really clear how the walls should be lit in 2D anyway.

    My test app shows the fullbright scene, the lighting scene, and the combined scene all side by side at once. Press "A" to go to wireframe, and "B" to go to a mode that shows the shadow regions more clearly. I'm very happy with the final result. I've got the shadows working in the BentMaze app too -- I'll show that code next time.

    -Mike

  • BentMaze 1.2: Lights Out!

    I've been playing with 2D lighting lately so I can add it to my XNA maze game. Lighting in computer graphics, I think it's fair to say, is always a bit of a hack -- a matter of finding some middle ground between the (incredibly rich and complex) way light behaves in the real world on one hand, and what you can reasonably compute in real-time on the other hand. Even fancy global illumination models cannot hope to recreate all the subtleties of real-world lighting, at least in real-time. Especially for games, lighting is mainly a matter of doing something simple, deciding if it's "good enough," refining the model a bit, and repeating.

    At least in 3D, we have the real world as a gold standard to compare our rendered images to (if realism is our goal). But is there a "proper" way to light an imaginary 2D world? How should a light source in my maze light the walls? If objects in the world are opaque, shouldn't they block the light and thus be completely black when viewed from above? How can the virtual camera be "above" the 2D plane anyway? The viewer is seeing the world from a rather omniscient perspective that the flat inhabitants could never experience. There's not really a "right" way to light a 2D world, so one should feel free to come up with something that's plausible, pleasing to the eye, and is compatible with the desired gameplay.

    In the traditional 3D graphics pipeline, lighting is determined per-vertex or per-pixel as each polygon gets rendered, and the interaction between the incoming light and the object's material is dealt with before the pixel gets written to the frame buffer. However, you can also render your objects in unlit form, then determine lighting separately, then combine the two later for a final result, somewhat like the old precomputed light maps used to texture the walls in Quake. This approach is attractive in 2D because we can render the lighting as objects independent of the world. Consider a point light source with linear falloff. In 3D this produces a sphere of light that affects a volume of space, but it's tough to directly render the sphere in a way that we can combine with a rendered 3D scene. But in 2D, we can render a disc and easily blend this with our unlit 2D scene to produce a lit result.

    There are other advantages to separating out the lighting into its own render target. Lighting can be done at lower spatial resolution than the rest of the scene, to conserve pixel bandwidth. If monochrome lighting is all you need, you could use a monochrome pixel format and save memory. Or if you want to do high-dynamic range lighting, you could use a floating-point frame buffer for your lighting.

    Anyway, that's the approach I'm going with. I'm certainly not the first to go this direction to light a 2D game. Shawn Hargreaves talked about it near the end of this blog post. I followed the link to his demo app and was impressed to see that it still works on Windows XP, even though it's an old-school DOS game. Try it!

    Here's my full-brightness scene, drawn to a render target that is the same size as the back buffer:

    Here's my lighting, drawn to a different render target. The big white disc follows the orange dude around, and I threw in a few fixed colored lights too.  I'm just using boring discs at the moment, but they could be other shapes to represent spotlights, flickering torches, etc:

    And here's the combination of the two:

    I didn't use SpriteBatch to combine the buffers -- I wrote a little class to draw a fullscreen quad with a custom pixel shader to do the blending. This gives me the most flexibility to do more complex blending later.

    I also wrote a custom shader to do the grid-like "floor" pattern of the maze. By passing the coordinates of the fullscreen quad backwards through the projection and view matrices, I can get the original worldspace coordinates, so the grid moves appropriately with the camera.

    When two lights overlap, things look a little wonky -- you can see this in the middle screenshot above. I think there are two problems here. One is that the light map is getting saturated -- you're getting a "0.8 + 0.8 = 1.0" effect. With a HDR texture, we could properly combine the light values and then normalize the result. The other part of the issue is related to gamma. A linear ramp of pixel values from 0 to 1 doesn't produce a visually linear progression of brightness onscreen, due to the way monitors work. So really this gamma effect needs to be removed before combining lights, then re-added afterwards -- otherwise you get weird nonlinear boosts in brightness. See here for a nice discussion of the issue.

    So this lighting effect works well -- the maze is nice and dark except where I've placed some lights. But if you're like me, you might wonder if things could be taken a bit further along the "realism" scale. I'm already on top of it and will have some double-extra-cool stuff to talk about next time.

     

    -Mike

  • BentMaze 1.1: Collide and slide

    Man, I totally jinxed myself when I predicted that hooking up my RoundLine collision detection / sliding code to my XNA maze game wouldn't be painful. It was. But after a lot of hours of debugging, I've got it working well enough to call done and usable, though not perfect.

    The main problem arises because collision management is all about that exact threshold where an equation changes state, i.e., the moment at which we go from being just outside the wall to just inside the wall. If different parts of the code express insideness/outsideness slightly differently (different algorithms, different coordinate systems, etc.), things that are mathematically equivalent actually give different results due to floating-point rounding error. For instance, I could call my FindFirstIntersection() routine to get the parametric "t" value at which the disc exactly intersects the wall, then call my distance-to-line function and discover that moving to "t" would put me just slightly (like, 0.000000162 units) inside the wall. So the next time I try to move away from the wall, my FindFirstIntersection might return a tiny "t" number, indicating that I'm tapping on the wall's edge from the inside, and thus can't move away.

    I first tried to accommodate this by adding little tolerance fudge factors to a bunch of my equations. If you're within 0.00001 of the line, you can probably consider yourself exactly in contact with it, right? But this approach just shifted the breaking point around, and lead to endless random tweaking of these tolerances. And you still need to treat exact contact with the wall carefully. If you are "in contact" and trying to move closer to the wall, you need to set t=0 to stop further motion. If you are moving away from the wall, you need to not do this or else you'll be stuck to the wall forever.

    I decided to avoid tolerances as much as possible and instead use a single uniform collision test as a backup to verify the results from all other tests and calculations. The collision test I chose was that the distance squared from the disc to the line, as measured by one piece of code, absolutely must be greater than or equal to zero. Usually, this test would agree with my other results. But in the cases where the other tests were telling me to go to a point that would penetrate the line (according to my standard test), I would instead do a binary search between a known-good (non-penetrating) value and the known-bad (penetrating) value until I had a point that was very close to the line, but still passed my standard test. Eww -- my original vision of a clean, near-constant-time algorithm was suddenly very messy and variable in terms of execution time. There's still an upper bound -- I never iterate more than 10 times -- but it's frustrating because my whole objective with using algebra to compute these intersection points was to avoid the need to stab around with various values until finding an answer that was "close enough." However, I don't think a completely iterative solution would be sufficient. There's always going to be a need for some higher-level analysis. A straight binary search for collision is dangerous because t=0 might not intersect the line, t=1 might not intersect the line, and even t=0.5 might not intersect the line, but there is still some "t" value at which the disc does intersect the line, so some mathematical analysis of the situation is necessary. I couldn't find many useful resources on this topic on the Web, so it's hard to know how near or far my solution is to being optimal.

    But enough gloominess. What I've got works pretty well. If you collide with the rounded end of a line, you roll around it in a natural way. If you move into a corner of two walls, you snug right up into that corner and don't wobble around until you change direction. You can slide happily along the wall's edge. And I haven't yet found a case where the disc misses a collision. If you zoom way out so you can see most of the maze, the disc moves much faster in world-space units, yet it never fails to bang up against any walls in its way.

    That was more than enough collision code for me. I'm ready to get back to coding up some whizzy graphics.

    -Mike

  • Collision and wall sliding on a RoundLine

    I think I've got it! After a couple hours of head-scratching and geometry, my XNA code to handle collision between a disc and a RoundLine is looking correct -- at least in my test app. Soon my maze game will be a bit more challenging, since the walls will be impenetrable.

    Okay, so there's a lot of colorful lines and dots in that picture -- here's what it all means:

    • The red line is the target line, the one we're testing for collision against
    • The green line is the disc path line (in the screenshot, the start position is highlighted)
    • The red dot is the point at which the path first intersects the target line
    • The gray dot is the point at which the path last intersects the target line (this might be useful someday)
    • The orange line connects the red dot to the closest point on the target line
    • The blue line is the orange line rotated 90 degrees and made unit length
    • The cyan line is the projection of the post-intersection part of the path line onto the blue line.

    So, suppose the disc is trying to move along this green path. We let it proceed for the first part of the path. But due to the collision with the red line, we stop the disc at the red dot. But the disc shouldn't just stop dead here -- it feels better to have it slide along the wall. The cyan line shows this sliding path.  The disc should end up at the end of the cyan path.

    Here's what happens if you graze the tip of the RoundLine:

    The green lines I'm using for testing are way longer than are likely to occur in practice, unless the disc is moving really fast from one frame to the next. More typically, the path will be something more like this:

    The math turned out to be pretty basic geometry and algebra. My math skills are pretty rusty, so it took some time to figure out, but the result is straightforward and should be reasonably efficient.

    I broke the RoundLine intersection problem into three separate subproblems: one for each endpoint, and one for the line part.

    To solve the endpoint intersections, I translated the path so the endpoint was at (0, 0). Then I expressed the path parametrically for x and y:

    x = p1.X + (p2.X - p1.X) * t

    y = p1.Y + (p2.Y - p1.Y) * t

    Pythagoras says:

    x * x + y * y = dist * dist

    And as I learned last time, it's easier to solve for distance-squared than for distance.  Remember that in this case, the distance we care about is the sum of the disc's radius and the line's radius.  So dist is a constant value, as is dist * dist.

    If you substitute the top equations into the bottom one, you end up with a quadratic equation that can be solved for t using the quadratic formula.

    To solve the line intersections, I transformed the path so the target line is going from (0, 0) to (1, 0). In this space, y is the distance from the target line, and x can be used to find if the intersection points are between 0 and 1 (meaning, between the RoundLine's endpoints).

    So I compute all six possible intersection points, and find the minimum (red dot) and maximum (gray dot) that are still on the disc path (t between 0 and 1).

    Next up: determining which lines to test against, and dealing with multiple collisions. I don't foresee a lot of pain there, though. After that I'll plug this code into the maze game, and hopefully the disc's interaction with the walls will feel natural.

    -Mike

  • Graphing the distance to a RoundLine

    The next thing I want to add to my XNA maze game is collision detection, so my little orange disc can't go right through the maze walls. There seem to be two alternate general approaches that can be taken regarding collision detection:

    1. After reading the user input, determine whether moving the disc the way that the user requested would cause it to penetrate a wall. If so, alter the disc's path before applying it, so as to not penetrate the wall.
    2. After reading the user input, move the disc the way that the user requested, then determine whether that motion caused it to penetrate a wall. If so, "back up" the motion so it no longer penetrates the wall.

    Since using Latin phrases makes everything sound more scientific, these two approaches are termed a priori and a posteriori. The a priori approach seems more clean and straightforward to me, so I'm going to wander down that road and see how things go.

    Here is a careful breakdown of the problem I want to solve: We have a disc D, with radius Dr, which is at position Dp0 at time t=0. User input indicates that we are considering moving the disc (with constant velocity) to position Dp1 at time t=1, but a line might be in the way. For any (thick, round) line L, which also has a radius and two endpoints, compute the following:

    1. Will disc D overlap line L at any point, as it moves from Dp0 to Dp1?
    2. If so, at what time t between t=0 and t=1 does the overlap first occur?

    This problem has two twists in comparison to the usual point vs. line geometry problems. Firstly, both the disc and line have nonzero thickness, so these have to be accounted for. That is, we aren't looking for the "t" value where the mathematical point intersects the mathematical line, but rather the value where the distance between them first reaches a value equal to the sum of their radii. Secondly, the line is not infinite, but is more correctly a line segment with two endpoints. So the distance from the disc to the line is not a single polynomial equation.

    These twists make the problem complex enough mathematically that I was having a hard time getting my head around all the issues, let alone how to compute the solution. There's always the option of using brute force or some sort of binary search to find the first intersection. But it seems that a direct derivation of "t" shouldn't be that hard, and would be much more efficient, so I pressed on.

    I decided it was worth taking the time to write a program that would let me interactively explore, and graphically represent, the problem. So I wrote a program that lets me move a disc and its linear path around a target line, and graph "t vs. d" where "t" is the parametric position along the disc's path, and "d" is the distance between the disc and the line. The resulting program was quite helpful. Here's a look:

    The red line is the "target line" that we are measuring distance to.  The green line is the path taken by the disc (it's a happy coincidence that a disc, swept along a line, produces a RoundLine).  The orange line shows the shortest distance from the disc to the target line.  By pressing B and moving the right stick, you can arrange the disc's path however you want.  Then you can sweep the disc along its path and see the correlating point on the graph (marked in green).

    I wrote a Graph class to handle drawing graphs in a general way. For a while I had grandiose visions of making this class totally general and reusable and bursting with clever features, but in the end I only hooked up some basic functionality, and you have to specify some things (like grid size) manually. What's there is useful, but it would need more work to be a complete and reusable component. Here's a shot of it graphing y = x * sin(x), and that equation's first derivative:

    One great feature in C# is delegates, which are similar to function pointers. The graph class takes a "ComputeY" delegate, which it calls to evaluate the Y coordinate for every X value. The really slick thing about delegates is that they can be a class member function which references member variables -- to do this requires in C++ hoop-jumping, but it "just works" in C#. This is a huge help, since my "LineDistance" function needs to access class information about the disc position, target line, and graph mode.

    Back to the problem at hand. We are trying to find the "t" value at which the distance first drops below the value which is the sum of the disc's radius and the line's radius. In my program, the target line has radius 0.2, and the disc has radius 0.1, so we're looking for the place where the graph dips below 0.3 (the dark blue gridlines are spaced 0.1 units apart). When the target line's endpoints aren't relevant to the problem, the distance (graphed in white) is a simple linear function of t, so its derivative (graphed in gray) is a constant (well, two constants -- it flips when the disc crosses the target line):

    When the endpoint determines the distance, the distance is nonlinear. I thought it was quadratic at first, but it's hyperbolic, which means that even its derivative is nonlinear:

    In the most complex case, the distance goes from hyperbolic, to linear, to hyperbolic:

    Yuck. But look at what happens when we graph distance-squared, rather than distance:

    It becomes quadratic, with a sequence of linear derivatives, regardless of whether the endpoints are involved! The slope of the derivative is the only thing that changes for the various phases of the graph. It looks like it will be easier to compute the value of t at which d-squared first drops below (0.3)-squared.

    To someone with better mathematical intuition than me, this all may have been obvious.  But it didn't click for me til I could visualize it and tweak the variables.

    You might find this program a useful starting point for creating visualizations for other geometric/mathematical problems. In any case, it's fun to play with. Many people use Java to create similar visualizations -- this site is particularly impressive. Using XNA instead of Java (or MFC, or Mathematica, or Applesoft, or whatever) has pros and cons, but it got the job done for me, and that's the important thing. Oh, and it was fun -- that's important too.

    -Mike

  • BentMaze 1.0: A jittered maze and radial culling

    One natural fit for my XNA line drawing code is a maze game, since mazes consist of lots of thick lines, and they are easy to generate programmatically. So I had a look at some maze generation algorithms, decided to try the simple depth-first algorithm, and had myself a maze.

    My original implementation of the maze generator used recursion, but the recursion was using so much of the system stack that it was blowing up on the Xbox 360 for large mazes. Apparently on Windows you get a larger default stack size, which I suppose is reasonable since PCs tend to have more RAM. Fortunately, it wasn't too hard to change my implementation to an iterative version where I manage my own heap-allocated stack. This was much more stable and probably a bit more efficient, too.

    To give the maze a more organic feel, I "jittered" the grid coordinates by a random factor in X and Y. I really like the look of the jittered maze. If I allow X and Y to jitter by about 35% of the cell width, the "grid-ness" starts to fade away, while still guaranteeing an open path through the maze. It starts to look similar to a non-grid-based "amorphous maze" that I ran across, but is easier to generate.

    If you crank up the jitter factor too high, though, the lines start overlapping and you get a mess of scribbles (there's a too-much-coffee joke in there somewhere):

    Most maze generation algorithms don't have to generate a rectangular maze. If you define a set of cells as off-limits, the algorithm will happily work around them. My program creates a round maze, since circles and roundness seem to be a theme for my XNA code lately. Before generating the maze, I just make a pass through the cells, determine which ones are outside of the circle, and mark them as off-limits. I draw an "X" in each off-limits cell, but I could have just left them blank.

    A 50 x 50 maze consists of 3,125 line segments in my program, and unless you're zoomed way out, you only see a tiny percentage of them at a time. So some culling is advisable for improved performance. Culling is the process of eliminating geometry that you can quickly determine to be invisible, to avoid the overhead of sending it through the graphics pipeline. Line.cs has a function to return the distance from a point to the line segment, so the easiest culling routine to hook up was radial culling, meaning that all lines not within some radius of the center of the viewport get eliminated. I added a mode to the program to use a smaller cull radius than is necessary, to make sure the code was working. And I added some text to show how many lines were being drawn:

    So we know the coordinates of the lines in world space, and we know the position of the camera in world space, but how do we determine the radius of the circle that exactly encloses the view area, in world space? My approach was to take the upper-right corner of the viewport, which is (1, 1) in clip space, and run it backwards through the projection and view matrices, to get the coordinates in world space. The distance from that point to the camera's position is the radius that I need. Transforming backwards through the transformation pipeline isn't too hard, as long as you have (or can derive) the inverse projection and view matrices. I compute them at the same time that I compute the regular versions, which is easier (and faster) than figuring out the inverse of an arbitrary matrix. The result is a decent culling system. Of course, unless your viewport is literally circular, it's not perfect -- it will fail to cull some lines which are not visible. But it gets you within an order of magnitude of perfect culling, with very little effort. Since I allow rotation of the camera, an axis-aligned bounding box culling system would not be sufficient, and non-axis-aligned bounding box culling would have been a lot more work.

    So now I have a nifty maze generator and can move my little orange disc dude (and the camera) around inside it. Next I need to prevent the dude from moving through the walls. Stay tuned!

    The code works on the Xbox 360 and Windows (pixel shader 2.0 required). You can select from five different line styles (the animated linear style is quite mind-bending). You can also select from three different culling modes: cull to viewport radius, cull to 1/3 of viewport radius, and no culling. Notice the differences in the number of lines drawn and the improved smoothness, when culling is enabled.

    -Mike

More Posts Next page »

© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker