D3D Variable Width Line Drawing in Hardware (w/ screenshot)
Wednesday, 13. August 2008, 02:43:28
What I'm going to describe here is an alternate way to draw lines. The real advantage with this method is that it's your own and you can use it as you want. If you need to change the color in any way before blending, you can change the pixel shader as you please. A secondary advantage is that it doesn't use quads (ok, it uses ONE quad) and doesn't use textures. A third advantage is that this one method will draw lines of any size whether it's one pixel or many or even fractional widths and lengths. One current disadvantage is that it's for a single line, not line strips though that may be fixed in the future.
The theory behind this algorithm is rather simple, but it's a little involved when it comes to the implementation. Assume you had a texture of a line. If you wanted to draw a line, you'd simply place your quad according to the shape of the line. The texture could even have a transparent border to allow for antialiasing. That's the basic idea behind this method, but we don't actually use a texture. We compute it on the fly within the pixel shader.
First thing we need are four sets of coordinates.
struct TXVertex
{
public:
float x, y, z;
DWORD diffuse;
float u, v;
};
TXVertex Vunit[] = {
{-1.0,1.0,1.0, D3DCOLOR_ARGB(255,255,255,255), 0.0,0.0},
{1.0,1.0,1.0, D3DCOLOR_ARGB(255,255,255,255), 1.0,0.0},
{1.0,-1.0,1.0, D3DCOLOR_ARGB(255,255,255,255), 1.0,1.0},
{-1.0,-1.0,1.0, D3DCOLOR_ARGB(255,255,255,255), 0.0,1.0}
};
D3DVERTEXELEMENT9 VLdecl3[] =
{
{ 0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0},
{ 0, 12,D3DDECLTYPE_D3DCOLOR, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR, 0},
{ 0, 16, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},
D3DDECL_END()
};
You don't need the color in these structures, but I leave it in if you want to have lines with a gradient. Just update the vertex shader to pass these colors to the pixel shader instead of the color that we store in a constant. Also, if you use culling, make sure you have the correct orientation. I don't use culling for 2D graphics.
BTW, if you have no idea what the above code is or does, then perhaps you should read up on vertex shaders, pixel shaders, vertex declarations, streams and custom vertex buffers.
So we start off with a box that covers the entire screen. The coordinates go from (-1,-1) to (+1,+1). If you understand D3D, then you know that negative values are on the left and bottom of the screen. We'll have to flip our screen coordinates later on.
Our line drawing routine will take the two endpoints along with a color and line width as inputs. From this input, we will need to give the vertex shader a transformation matrix, the color, line widths in texture space and the inverses of the texture size.
Before going into anything, I like to use clipping. We'll save our on screen coordinates for later use.
Here's the function prototype for the DrawLine rountine.
// Assumes render target is already set.
void __fastcall TfrmMain::DrawLine(float x1, float y1, float x2, float y2,
D3DCOLOR color, float linewidth);
// Save Clip boundaries with a few extra pixel border to allow for antialiasing. float X1 = min(x1,x2); float X2 = max(x1,x2); float Y1 = min(y1,y2); float Y2 = max(y1,y2); RECT r; r.left = (int)(X1-linewidth-2.0); r.top = (int)(Y1-linewidth-2.0); r.right = (int)(X2+linewidth+2.0); r.bottom = (int)(Y2+linewidth+2.0);
First, let's concentrate on the line widths. For this, we convert our screen coordinates to our D3D box. Here, we have a problem. Say your screen is 1920x1080. In D3D, this is mapped onto (-1,-1)-(+1,+1). Obviously 1920x1080 is not square, but our D3D box definitely is. So we need to make an adjustment. What we do for now is take the larger dimension and use that for both the width and height. Then we center our 1920x1080 coordinates. We do this by adding half the gap to our coordinates. gap = 1920-1080 = 840. This means we would have a 420 pixel gap on top and bottom. By adding this gap to our Y coordinates, we have everything centered.
float Scale; // The larger of the Width or Height.
if (ClientWidth>ClientHeight)
{
y1+=(float)(ClientWidth-ClientHeight)/2.0;
y2+=(float)(ClientWidth-ClientHeight)/2.0;
Scale = (float)ClientWidth;
}
else
{
x1+=(float)(ClientHeight-ClientWidth)/2.0;
x2+=(float)(ClientHeight-ClientWidth)/2.0;
Scale = (float)ClientHeight;
}
That's it for centering. x1,y1,x2,y2 are the endpoints to our DrawLine routine. ClientWidth and ClientHeight are the dimensions of our D3D render area in pixels.
Remember how the negative coordinates are on the bottom? Now's the time to flip our coordinates in the Y dimension.
// Flip y because smaller y coordinates should be on bottom. y1 = Scale-y1; y2 = Scale-y2;
We now try to convert these coordinates to our D3D box.
// Convert to [-Scale/2.0,+Scale/2.0) // The added 0.5 is the center of the pixel. x1 -= Scale/2.0+0.5; y1 -= Scale/2.0; x2 -= Scale/2.0+0.5; y2 -= Scale/2.0; // Convert points to [-1,1) range because we use square pixels. float fX1 = x1*2.0/Scale; float fY1 = y1*2.0/Scale; float fX2 = x2*2.0/Scale; float fY2 = y2*2.0/Scale;
We now have our coordinates in the proper range. From this point, we try to build the smallest rectangle that will fit around our line if the line was horizontal. But it isn't horizontal, therefore we calculate the angle of our line relative to the horizontal. Before we do that, it's easier if we only deal with -PI/2 to +PI/2. All we need to do to guarantee this is to have the first X coordinate be smaller than the second.
// -PI/2 to + PI/2
if (fX1>fX2)
{
// Swap
float tmp = fX1;
fX1 = fX2;
fX2 = tmp;
tmp = fY1;
fY1 = fY2;
fY2 = tmp;
}
// Find angle of the line from the horizontal.
float angle = atan2(fY2-fY1,fX2-fX1);
Now all we need is the bounding box. If we were using a texture, the height would be the width of the line. I prefer to use powers of two because video cards are optimized to handle this. It also produces better results on video cards that use fixed point hardware. The width of the texture would simply be the length of the line. We leave a couple pixels on either side to allow for antialiasing.
// Find the height that's a power of two that fits around our line. // +4.0 is a 2 pixel buffer on each side to guarantee antialiasing. int textureHeight = NextPow2((int)(linewidth+4.0)); // Length of line. float fLineLength = sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)); // Find the width of the box that fits around the line. // +LineWidth is in case we want to draw rounded ends. // +4.0 is a 2 pixel buffer at each end of the line to guarantee // antialising. int textureWidth = NextPow2((int)(fLineLength+4.0));
The final part of this fake texture is to calculate the percentage of the width and height that the line takes up relative to the said fake texture.
float fLineLengthU = (fLineLength+1.0)/(float)textureWidth; float fLineWidthV = (float)(linewidth+1.0)/(float)textureHeight;
The added 1.0 will be explained later on. It serves a specific purpose for antialiasing. You may be wondering why we even bothered converting our original coordinates to the D3D box since we haven't needed them yet. And as to the angle, it could have been calculated with the original coordinates. Well, we do need this conversion. We need it to know by how much to translate our D3D box.
That's now the goal. To take the D3D box and transform it until it is the exact same size as our fake texture. It should also be positioned and rotated correctly.
// Now that we have all the data, it's time to compute some matrices.
D3DXMATRIX result;
D3DXMATRIX mat;
D3DXMatrixIdentity(&result);
// Scale according to textureWidth and textureHeight
D3DXMatrixScaling(&mat,((float)(textureWidth*2))/Scale,
((float)(textureHeight*2))/Scale,1.0);
result=result*mat;
// Rotate by angle.
D3DXMatrixRotationZ(&mat,angle);
result=result*mat;
// Translate center of line
float fXX = (fX1+fX2)/2.0;
float fYY = (fY1+fY2)/2.0;
D3DXMatrixTranslation(&mat,fXX,fYY,0.0);
result=result*mat;
We have one thing left to do before our transformation is complete. So far, we've used a square box. We need to scale the Y coordinate to the appropriate size. If we take our original screen size of 1920x1080, then the bottom of the screen would be 1080/1920 = 0.5625. However, D3D takes the bottom of the screen as 1.0. Since we know how we want to scale it, it's easy to accomplish.
// Project onto screen.
if (ClientWidth>ClientHeight)
{
// For a 1024x768 screen, we'd have
// 0.75 that needs to be scaled to 1.0
// For a 1920x1080 screen, we'd have
// 0.5625 that needs to be scaled to 1.0
// Height/Width * scale = 1.0
// scale = Width/Height
D3DXMatrixScaling(&mat,1.0,(float)ClientWidth/(float)ClientHeight,1.0);
}
else
{
D3DXMatrixScaling(&mat,1.0,(float)ClientHeight/(float)ClientWidth,1.0);
}
result = result*mat;
We now have our transformation matrix in result. We have our line widths and lengths in texture space. We have the color given by the user. The only thing left to calculate is the inverse of our imaginary texture width and height.
The reason we need it is for antialiasing. This is why we added 1.0 to our line length and width earlier (half a pixel on each side). Say the edge of the line crosses a pixel right down the middle of it, the pixel intensity should be 50% as it only covers half the pixel. How do we get this value of 50%? Well, we need to allow for our line boundary to go a little further to allow for other fractional results. But how much further? We know pixel coordinates are centered in the middle of pixel (at least that's the way I'm using them). And at the center, that's half a pixel. This is why we add half a pixel on each side of the line. At the very last pixel on the edge of the line, we then calculate how far we are from the line boundary. Any result between 0.0 and 1.0 relative to ONE pixel on screen will give us our alpha value. In order to get this percentage, we need to divide by our texture sizes. Now you see why we need the inverse. Pixel values above 1.0 are well within the line and we can simply saturate those results so that the value remains at 1.0 (full intensity).
If you have a video card that uses floating point numbers internally, you can simply store the texture width and height (and if those don't look like inverses, read on). For this part, I decided to test on an older video card on one of my test boxes. I do have a newer video card that can handle floating point. But using a few extra computations, even older cards that use pixel shader 1.4 can use this algorithm. Fixed point hardware is only guaranteed to have a range of -8 to +8. However, you can't actually store +8.
Here's a little bit of trivia. Fixed point hardware on old ATI cards use 16 bits internally. One sign bit, 3 bits for integers and 12 bits for the fractional part. 3 bits can only store a maximum number of 7. I have tested the hardware and know this to be true. When you try to store the number 8, it simply sets all bits. It's VERY close to 8, but it isn't 8. It causes precisions errors if you try to use this number. So we're limited to factors of 4. I suppose we could use factors of 7, but I don't like uneven numbers. Again, if you have floating point precision in pixel shader 2 and above, don't mess with this. I offer this information here because it means the line drawing algorithm will work on even more hardware.
Say our imaginary texture width is 256. 1/256 is a fractional value. So what's the problem? Well, pixel shaders see ALL textures widths as 1.0. So if we're dealing with fractional parts to start off, the width of 1 pixel is 1/256. The actual inverse of that is 256. We end up multiplying when we thought we'd be dividing. This is quite a large number if we can only store +4 in our pixel shader. Instead, we divide our width by 4 until we get something smaller. We then send all those values (4's, 2's and 1's) to the pixel shader to be multiplied back together with the value that needs to be operated on. This will keep the value well under 1.0 every time if it's on the edge of the line and will saturate to 1.0 (full intensity) if it's well within the line. 12 fractional bits means we can have texture sizes up to 4096 (twice what the hardware can handle in real texture sizes meaning that our lines will be even more precise). 12 bits divided by 2 bits gives 6 values. That means we need to give 12 values to the pixel shader because both the width and height need to be passed. We can't use pixel shader constants because these range from -1 to +1. Not only that, but only the values of 1.0 and values of 1/255 to 15/255 are accurately represented as values of 1/256 to 15/256. The reason is that constants are 8 bits on old hardware. Eight bits can store 256 values, but the maximum value is 255. Remember that everything in pixel shaders is stored as fractions. The only way to represent 1.0 is having 255/255 meaning that all others values will also be divided by 255. Internally, they use 4 extra bits to add extra precision. These come into play after 15/255 and this is why higher values get out of whack. Shifting left from these small values is possible, but it's too much of a hassle. So how do we get integer values to the pixel shader?
We use texture coordinates. Texture coordinates are very high precision and don't have any of the limitations presented here. The texture repeat size is usually as large as the biggest texture size. On the current test video card, the max texture size is 2048 and so is the repeat size. That means that texture coordinates can store numbers up to 2048 as full integers. Unfortunately, we can't use this feature because pixel shader registers still have the limitations mentioned above. But what we can do is use them to pass data to the pixel shader. Pixel shader 1.4 has 6 texture units and can grab 3 values from each texture unit for a total of 18 values. We're using the first texture unit for our texture coordinates. So texture unit 0 is taken up. Texture unit 1 is used to pass the line width and length. That leaves 4 texture units for 12 values, exactly the amount we need.
Again, if you have a newer card, just send the two sizes as is. For example, a line length of 512 and width of 16, you'd send the values (512,16) in texture unit 2 and be done with it.
// Our vertex shader wants the following inputs. // C0 transformation matrix // C1 transformation matrix // C2 transformation matrix // C3 transformation matrix // C4 Color // C5 x = Half Line Length U, z = Half Line Width V // C6 Scale x, Scale y, Scale x, 0.0 // C7 Scale x, Scale y, Scale y, 0.0 // C8 Scale x, Scale y, Scale x, 0.0 // C9 Scale x, Scale y, Scale y, 0.0 // If using pixel shader 2.0 or above, use this instead: // C6 Texture Width, Texture Height // Constants to pass to the vertex shader. float inputs[4*10]; // Store our transformation matrix. D3DXMatrixTranspose(&mat,&result); memcpy(inputs,mat,sizeof(float)*16); // Store color D3DXCOLOR c(color); memcpy(&inputs[4*4],&c,sizeof(float)*4); // Store line length and width. inputs[4*5] = fLineLengthU/2.0; inputs[4*5+1] = fLineWidthV/2.0; inputs[4*5+2] = 0.0; inputs[4*5+3] = 0.0; // If you only want to support pixel shader 2.0 and above, use this instead. // inputs[4*6] = (float)textureWidth; // inputs[4*6+1] = (float)textureHeight; // inputs[4*6+2] = 0.0; // inputs[4*6+3] = 0.0; float W = (float)textureWidth; float H = (float)textureHeight; inputs[4*6] = (W>=4.0)?4.0:W; inputs[4*6+1] = (H>=4.0)?4.0:H; W = (W>=4.0)?W/4.0:1.0; H = (H>=4.0)?H/4.0:1.0; inputs[4*7] = (W>=4.0)?4.0:W; inputs[4*7+1] = (H>=4.0)?4.0:H; W = (W>=4.0)?W/4.0:1.0; H = (H>=4.0)?H/4.0:1.0; inputs[4*8] = (W>=4.0)?4.0:W; inputs[4*8+1] = (H>=4.0)?4.0:H; W = (W>=4.0)?W/4.0:1.0; H = (H>=4.0)?H/4.0:1.0; inputs[4*9] = (W>=4.0)?4.0:W; inputs[4*9+1] = (H>=4.0)?4.0:H; W = (W>=4.0)?W/4.0:1.0; H = (H>=4.0)?H/4.0:1.0; inputs[4*6+2] = (W>=4.0)?4.0:W; inputs[4*7+2] = (H>=4.0)?4.0:H; W = (W>=4.0)?W/4.0:1.0; H = (H>=4.0)?H/4.0:1.0; inputs[4*8+2] = (W>=4.0)?4.0:W; inputs[4*9+2] = (H>=4.0)?4.0:H; W = (W>=4.0)?W/4.0:1.0; H = (H>=4.0)?H/4.0:1.0; inputs[4*7+3] = 0.0; inputs[4*8+3] = 0.0; inputs[4*9+3] = 0.0; inputs[4*10+3] = 0.0; // Set constants for vertex shader inputs. d3d->pD3DDevice->SetVertexShaderConstantF(0, inputs, 10);
Now, we can draw the line. The vertex declarations from the top of the article make a comeback.
// Set stream data and vertex shader. d3d->pD3DDevice->SetVertexDeclaration(VDline); d3d->pD3DDevice->SetStreamSource(0,VVunit,0,sizeof(TXVertex)); d3d->pD3DDevice->SetVertexShader(vshaderLine); // Clear the texture units as we don't use them other than for // passing values around. d3d->pD3DDevice->SetTexture(0,NULL); d3d->pD3DDevice->SetTexture(1,NULL); d3d->pD3DDevice->SetTexture(2,NULL); d3d->pD3DDevice->SetTexture(3,NULL); d3d->pD3DDevice->SetTexture(4,NULL); d3d->pD3DDevice->SetTexture(5,NULL); // Set pixel shader. d3d->pD3DDevice->SetPixelShader(shaderLine); // Clipping d3d->pD3DDevice->SetRenderState(D3DRS_SCISSORTESTENABLE, TRUE); d3d->pD3DDevice->SetScissorRect(&r); // Draw the line! d3d->pD3DDevice->DrawPrimitive(D3DPT_TRIANGLEFAN,0,2); // Reset state. d3d->pD3DDevice->SetRenderState(D3DRS_SCISSORTESTENABLE, FALSE); d3d->pD3DDevice->SetVertexShader(NULL); d3d->pD3DDevice->SetPixelShader(NULL);
Finally, we get to the vertex and pixel shaders. The vertex shader is rather simple. The only thing it does is pass along the values calculated above to the pixel shader. And yes, I know I should use HLSL.
bool __fastcall TfrmMain::InitLine(bool bPersistent)
{
if (bPersistent)
{
// Create vertex shader
char *VshaderLine =
"vs.1.1\n"
"dcl_position v0\n"
"dcl_texcoord v1\n"
"// Inputs\n"
"// C0 transformation matrix\n"
"// C1 transformation matrix\n"
"// C2 transformation matrix\n"
"// C3 transformation matrix\n"
"// C4 Color\n"
"// C5 x = Line Length U, y = Line Width V\n"
"// C6 Scale x, Scale y, Scale x, 0.0\n"
"// C7 Scale x, Scale y, Scale y, 0.0\n"
"// C8 Scale x, Scale y, Scale x, 0.0\n"
"// C9 Scale x, Scale y, Scaly y, 0.0\n"
"//\n"
"// Algorithm\n"
"// We need to output the following\n"
"// Pos = v0 * transformation matrix\n"
"// oD0 = Color (C4)\n"
"// oT0 = v1\n"
"// oT1 = Line Length U, Line Width V (C5)\n"
"// oT2 = Scaling factors (C6)\n"
"// oT3 = Scaling factors (C7)\n"
"// oT4 = Scaling factors (C8)\n"
"// oT5 = Scaling factors (C9)\n"
"// For pixel shader 2.0 and above, use this for scaling factors\n"
"// C6 Texture Width, Texture Height, 0.0, 0.0\n"
"// oT2 = C6\n"
"\n"
"def c50, 0.0, 0.0, 0.0, 0.0\n"
"// Output texture coordinates\n"
"mov oT0.xy, v1\n"
"mov oT0.z, c50\n"
"// Output line length and width\n"
"mov oT1.xyz, c5\n"
"// Output mul 1\n"
"mov oT2.xyz, c6\n"
"// Output mul 2\n"
"// For pixel shader 2 and above, oT3 to oT5 are not needed.
"mov oT3.xyz, c7\n"
"// Output mul 3\n"
"mov oT4.xyz, c8\n"
"// Output mul 4\n"
"mov oT5.xyz, c9\n"
"// Output position\n"
// Apply matrix transformation.
"dp4 oPos.x, v0, c0\n"
"dp4 oPos.y, v0, c1\n"
"dp4 oPos.z, v0, c2\n"
"dp4 oPos.w, v0, c3\n"
"// Output color\n"
"mov oD0, c4\n";
if (NULL==(vshaderLine = CompileVertexShader(VshaderLine)))
{
return false;
}
}
else
{
if (D3D_OK!=d3d->pD3DDevice->CreateVertexDeclaration(VLdecl3, &VDline))
{
MessageBox(d3d->hwnd,"Could not create vertex declaration for line.","ERROR",MB_OK);
return false;
}
vertexdecl.push_back(VDline);
}
return true;
}
You need to call this twice on startup (once with bPersistent as true and once as false). Then you need to call it with bPersistent as false whenever you reset the D3D device. Deleting the vertex declaration is not shown here. That's up to you. I use a vector as you can see to store all my vertex declarations.
Now for the pixel shader. I know I should have used HLSL for both shaders, but I like assembly. I normally use HLSL, but this example is for pixel shader 1.4. I'm sure it'll be easy enough to create one for higher versions of pixel shaders though this one will work just fine as it's converted anyways.
bool __fastcall TfrmMain::SetupShaders(bool bPersistent)
{
if (!bPersistent)
{
}
else
{
char *SshaderLine =
"// From vertex shader.\n"
"// V0 = Color\n"
"// T0 = C\n"
"// T1 = Half Line Length U, Half Line Width V\n"
"// T2 = Scale x, Scale y, Scale x\n"
"// T3 = Scale x, Scale y, Scale y\n"
"// T4 = Scale x, Scale y, Scale x\n"
"// T5 = Scale x, Scale y, Scale y\n"
"ps.1.4\n"
"def c0, 1.0, 1.0, 1.0, 1.0\n"
"texcrd r0.xyz, t0.xyz\n" // Grab texture coordinates.
"texcrd r1.xyz, t1.xyz\n" // Grab line length and width.
"texcrd r2.xyz, t2.xyz\n" // Grab one set of scale factors.
// Calculate the positive distance from the center of the texture.
// The center is always 0.5, so the bias modifier comes in handy.
// cnd compares with 0.5, so again we luck out.
// r0.xy = (r0.xy>0.5)?r0-0.5:-(r0-0.5)
"cnd r0.xy, r0, r0_bias, -r0_bias\n"
// Calculate if the current texture coordinate is within the
// line boundaries.
// Negative result means the current coordinate is too far and this
// will be used with the texkill instruction.
// r0.xy = r1.xy - r0.xy
"sub r0.xy, r1, r0\n"
// We needed an extra instruction in the next phase, so we put it
// here. This is the beginning of calculating the percentage of the
// pixel that is on the edge of the line which causes antialiasing.
// Start scaling our distance values so that the one pixel on the edge
// of the line is represented between 0.0 and 1.0
// Values that end up fully inside the line boundaries will be 1.0
// and we can use saturation to keep it there.
// r1.xy = saturate(r0.xy * r2.xy);
"mul_sat r1.xy, r0, r2\n"
// Phase 2
"phase\n"
// Cancel drawing if our pixel is not within the line.
// if (r0<0) return; // Stop drawing if return
"texkill r0\n"
// Get remaining scaling factors.
// Pixel shaders 2 and above can skip these,
// however they should still multiply together the x and y values
// from the last multiply instruction (r1)
// and then multiply that result with the color's alpha as shown below.
"texcrd r3.xyz, t3.xyz\n"
"texcrd r4.xyz, t4.xyz\n"
"texcrd r5.xyz, t5.xyz\n"
"mul_sat r1.xy, r1, r3\n"
"mul_sat r1.xy, r1, r4\n"
"mul_sat r1.xy, r1, r5\n"
"mul_sat r1.x, r1, r2.z\n"
"+mul_sat r1.a, r1.y, r3.z\n"
"mul_sat r1.x, r1, r4.z\n"
"+mul_sat r1.a, r1, r5.z\n"
// Multiply the x and y (now stored in a) together to get
// the final alpha modifier.
// r1.a = r1.x * r1.a
"mul r1.a, r1, r1.x\n"
// Multiply the color's alpha with our computed intensity.
// This is what makes antialiasing possible.
// Pixels fully within the line will have an intensity of 1.0
// because of saturation.
// r1.a = r1.a * diffuse.a
"mul r1.a, r1, v0\n"
// This is precomputed pixel values. If you don't use
// precomputed values, simply copy the rgb values along
// with the newly computed alpha.
// r0 is the output color!
// r0.rgb = diffuse.rgb * r1.aaa
// r0.a = 1-r1.a
"mul r0.rgb, v0, r1.a\n"
"+mov r0.a, 1-r1\n";
if (NULL==(shaderLine = CompileShader(SshaderLine))) return false;
}
return true;
}
One thing to note here is that we use the same antialiasing algorithm for both the sides and endpoints. This means that even the ends will be antialiased.
This code is taken from Project V and is not meant to be used as is. I have specific uses that are most likely different from yours. This algorithm is shown here so that the reader can understand how the results were accomplished in order to write your own line drawing code. You may copy the code found here and use it as you wish if you find any of it useful, (*disclaimer*) but you use it at your own risk and I cannot be held liable for any use thereof. What I really like about this algorithm is that video cards don't need to provide hardware line drawing implementations. It can all be done with what's already there with extremely old hardware. And that's part of the point I was trying to make and is why I used pixel shader 1.4.
I'm thinking of using a couple of clipping planes on each side of the line to see if that would help performance beyond standard clipping. You already have the coordinates of the line, so it's extremely easy to set up.
That's it for this algorithm. I hadn't seen it anywhere though I didn't really look very hard. I hope this proves useful to someone. I know I like the fact that ALL lines of ALL sizes are drawn using the exact same algorithm. Fractional positioning and fractional line width with pixel perfect rendering are also nice. The vertex shader is dirt simple. The pixel shader is too as it only really checks if the current texture coordinates are within the boundaries of the line and then scales that value to get the alpha value. The hard part is the preparation, but that takes no time at all computationally as it's only done once per line. I hope to extend this to line strips later on. I may also add rounded ends too.
Enjoy!
Update:
Thought I should show a screenshot of this algorithm in action.
(Click on image to see full version. BTW, the framerate shown is meaningless because the screen only updates when something changes.)



How to use Quote function: