Skip to main content
GameDev.net gamedev.net
🔒 Locked

Optimizing my triangle drawing routine?

Started by Matei May 16, 2004 at 8:04 PM 6 replies 5.2k views
Original Post
Matei
Matei
I've written a small software 3D renderer in Java, with the purpose of using it in a game, and I'm at the point where I'm thinking of calling it done and starting the actual game programming. However, I still think my draw triangle routine is kind of slow, especially for textured triangles. (Although it is at least 10 times faster than Graphics.fillPolygon for flat triangles, because Java uses GDI internally!). Here is the relevant part, that draws a textured, Gouraud-shaded triangle. (The real method is quite a bit longer and handles special cases like flat shading and untextured triangles). Do you have any suggestions as to how to make it faster, or should I stick with this? I realize that I'm using arrays to store parameter values on each side of the triangle, which is slightly slower than walking down the sides with some kind of dx/dy step, but this should be practically inperceptible with caching and pipelining, and it happens an order of magnitude less than the inner loop code; I saw this technique in some 10-year-old articles, and if it worked then, it should work now. I'm also sure that this method really is the main bottleneck for the program; I timed the T&L part of the code and the rendering part, and rendering takes much more time. Also, rendering with flat shading is 1.5 to 2 times faster than using Gouraud shading.
/**
 * Draws a triangle given x,y screen coordinates, a w (1/z) coordinate, texture
 * coordinates (u, v multiplied by w), and material colors at each vertex.
 * The pixels and wBuffer are such that the pixel at (x,y) is in pixels[y*displayWidth + x].
 */
private void rasterizeTriangle(ProcessedTriangle p) {
	// get y's as ints

	int y0 = (int) p.y[0];
	int y1 = (int) p.y[1];
	int y2 = (int) p.y[2];
	
	// find min and max y values

	int yMin = y0, yMax = y0;
	if(y1 < yMin) yMin = y1;
	else if(y1 > yMax) yMax = y1;
	if(y2 < yMin) yMin = y2;
	else if(y2 > yMax) yMax = y2;
	
	// interpolate parameters accross the edges, assuming the points are clockwise

	// "w" is 1/z, and "u","v" are premultiplied by w, for perspective-correct texturing

	
	interpolateInt(p.x[0], y0, p.x[1], y1, (y0 > y1 ? leftX : rightX));
	interpolateInt(p.x[1], y1, p.x[2], y2, (y1 > y2 ? leftX : rightX));
	interpolateInt(p.x[0], y0, p.x[2], y2, (y2 > y0 ? leftX : rightX));

	interpolateFloat(p.w[0], y0, p.w[1], y1, (y0 > y1 ? leftW : rightW));
	interpolateFloat(p.w[1], y1, p.w[2], y2, (y1 > y2 ? leftW : rightW));
	interpolateFloat(p.w[0], y0, p.w[2], y2, (y2 > y0 ? leftW : rightW));
	
	interpolateColor(p.color[0], y0, p.color[1], y1, (y0 > y1 ? leftCol : rightCol));
	interpolateColor(p.color[1], y1, p.color[2], y2, (y1 > y2 ? leftCol : rightCol));
	interpolateColor(p.color[0], y0, p.color[2], y2, (y2 > y0 ? leftCol : rightCol));
	
	interpolateFloat(p.u[0], y0, p.u[1], y1, (y0 > y1 ? leftU : rightU));
	interpolateFloat(p.u[1], y1, p.u[2], y2, (y1 > y2 ? leftU : rightU));
	interpolateFloat(p.u[0], y0, p.u[2], y2, (y2 > y0 ? leftU : rightU));
	
	interpolateFloat(p.v[0], y0, p.v[1], y1, (y0 > y1 ? leftV : rightV));
	interpolateFloat(p.v[1], y1, p.v[2], y2, (y1 > y2 ? leftV : rightV));
	interpolateFloat(p.v[0], y0, p.v[2], y2, (y2 > y0 ? leftV : rightV));

	int texWidth = p.texture.width;
	int texHeight = p.texture.height;
	int maxU = texWidth-1, maxV = texHeight-1;
	
	int yPos = yMin * displayWidth;

	// loop through the scanlines, and do some poor man's clipping

	for(int y = yMin; y < yMax; y++, yPos+=displayWidth) if(y>=0) {
		if(y>=displayHeight) break;
		
		int x1 = leftX[y];
		int x2 = rightX[y];
		if(x2 - x1 < 0) continue;

		// get values of parameters at each edge of the scanline

		float w1 = leftW[y], w2 = rightW[y];
		float u1 = leftU[y], u2 = rightU[y];
		float v1 = leftV[y], v2 = rightV[y];
		float r1=leftCol[y][0], r2=rightCol[y][0];
		float g1=leftCol[y][1], g2=rightCol[y][1];
		float b1=leftCol[y][2], b2=rightCol[y][2];
		float a1=leftCol[y][3], a2=rightCol[y][3];
		
		// get gradients (slopes) accross the scanline

		float s = 1.0f/(x2-x1);
		float gw = (w2-w1)*s;
		float gu = (u2-u1)*s, gv = (v2-v1)*s;
		float gr = (r2-r1)*s, gg=(g2-g1)*s, gb=(b2-b1)*s, ga=(a2-a1)*s;
		
		// use fixed-point math for the colors; convert r,g,b, and their gradients to ints

		// first multiply by 255 since they are from 0 to 1, then by 256 for 8 "binary places"

		int ir = (int)(r1*(255*256)), igr = (int)(gr*(255*256));
		int ig = (int)(g1*(255*256)), igg = (int)(gg*(255*256));
		int ib = (int)(b1*(255*256)), igb = (int)(gb*(255*256));
		int ia = (int)(a1*(255*256)), iga = (int)(ga*(255*256));
		
		// for each point on the scanline; again, some poor man's clipping is occuring

		for(int i=yPos+x1; i<yPos+x2; i++) {
			if(i>=yPos+displayWidth) break;
			if(i>=yPos) {
				if(wTest(w1, i)) {
					// perspective correct texture mapping

					float z = 1.0f/w1;
					int u = (int) (texWidth*u1*z);
					int v = (int) (texHeight*v1*z);
					if(u<0) u=0; else if(u>maxU) u=maxU;
					if(v<0) v=0; else if(v>maxV) v=maxV;
					
					// get the texture color

					int col = p.texture.pixels[v*texWidth+u];
					
					// get the real color, by blending material with texture

					int r = (((col&0x00ff0000)>>16)*ir) >> 16;
					int g = (((col&0x0000ff00)>>8)*ig) >> 16;
					int b = ((col&0x000000ff)*ib) >> 16;
					int a = (((col&0xff000000)>>>24)*ia) >> 16;
					if(r<0) r=0; else if(r>255) r=255;
					if(g<0) g=0; else if(g>255) g=255;
					if(b<0) b=0; else if(b>255) b=255;
					if(a<0) a=0; else if(a>255) a=255;
					
					// set the pixel color and the w buffer element

					pixels[i] = alphaBlend(pixels[i], r, g, b, a);
					wBuffer[i] = w1;
				}
			}
			ir+=igr; ig+=igg; ib+=igb; ia+=iga;
			u1+=gu; v1+=gv;
			w1+=gw;
		}
	}
}

/**
 * Puts the v value for each index from i1 to i2 into a float[] dest,
 * linearly interpolating between the values v1 and v2.
 * The functions interpolateInt and interpolateColor are similar.
 */
private void interpolateFloat(float v1, int i1, float v2, int i2, float[] dest) {
	if(i1 > i2) {
		float td = v1; v1 = v2; v2 = td;
		int ti = i1; i1 = i2; i2 = ti;
	}
	else if(i1 == i2) { return; }
	float gradient = (v2-v1)/(float)(i2-i1);
	float v = v1;
	for(int i = i1; i <= i2; i++) {
		if(i>=0 && i<dest.length)
			dest[i] = v;
		v += gradient;
	}
}


/** The w buffer test; should be inlined by the compiler. */
private final boolean wTest(float w, int pos) {
	return w <= wBuffer[pos];
}

/** Alpha blending; again, should be inlined by the compiler. */
private final int alphaBlend(int color, int r, int g, int b, int a) {
	if(a==0) return color;
	else if(a==255) return ((r<<16) | (g<<8) | b);
	int cb = color & 0x000000ff;
	int cg = (color>>8) & 0x000000ff;
	int cr = (color>>16) & 0x000000ff;
	cr = ((cr<<8) + (r-cr) * a) >> 8;
	cg = ((cg<<8) + (g-cg) * a) >> 8;
	cb = ((cb<<8) + (b-cb) * a) >> 8;
	return (cr << 16) | (cg << 8) | cb;
}


/************
Just for reference, this is how I draw the backbuffer;
This gets about 70 FPS with a blank scene, so it shouldn't be *too* big a problem. I think it's the fastest way available in Java to use a writable surface.
************/

/** Creates the pixels and w buffers (whenever component is resized) */
private void createBuffers() {
	backBuffer = new BufferedImage(displayWidth, displayHeight, BufferedImage.TYPE_INT_RGB);
	DataBufferInt dataBuffer = (DataBufferInt) backBuffer.getRaster().getDataBuffer();
	pixels = dataBuffer.getData();
	wBuffer = new float[pixels.length];
}

/** Paints the scene. */
private void paint3dScene(Graphics g) {
	// [clear buffers]

	// [do some math]

	// [call rasterizeTriangle a few thousand times]

	g.drawImage(backBuffer, 0, 0, null);	
}

 
[edited by - Matei on May 16, 2004 9:04:41 PM] [edited by - Matei on May 16, 2004 9:06:54 PM]
rypyr
rypyr
I've implemented a triangle drawing routine once myself but haven't yet optimized it yet.

Otherwise have very little experience with this sort of thing. Here are just some simple things that may not have any impacts:

- in your y-loop, you check if(y>=0) every iteration. Why not check this once before the y-loop begins and set the y-loop's starting point accordingly?
- same goes for if(y>=displayHeight)
- do you need variables x1, x2, r1, r2, g1, g2, b1, b2, a1, a2? i.e. can you use the array values directly?
- I assume constants like 255*256 are being compiled into 65280?
- the x-loop could be rewritten as:

int iEnd = yPos + x2;
for(int i=yPos+x1; i < iEnd; i++)

May save you an add every loop iteration (but may already be optimized out)
- yPos+displayWidth should be precalculated as well...
- for your interpolateFloat routine, you have an if that swaps some values...I wonder if it would be faster just to put your routine in there twice? That is, instead of swapping i1/i2 and v1/v2:


if(i1 == i2) { return; }
float gradient = (v2-v1)/(float)(i2-i1);
if(i1 > i2) {
// do it here
}
else {
// and do it here
}


- this won't speed things up but since you're doing a lot of clamping/clipping, why not make that another inlined function:


int clamp(int val, int min, int max)
{ return (val < min ? min : (val > max ? max : val)); }


Just a suggestion...

- where are leftW, rightW, leftCol, rightCol etc calculated and stored?

EDIT: Added code tags...


[ CodeDread ]

[edited by - rypyr on May 17, 2004 1:29:09 PM]

[edited by - rypyr on May 17, 2004 1:29:26 PM]
Matei
Matei
Thanks a lot for your comments. I''ll try to remove all the branching by adding a real clipping algorithm to ensure that only valid array addresses are accessed and simplify the code.

The leftW, rightW, leftCol, rightCol, etc, are fields in the GraphicsEngine class that are only created once, when the class is created. They have size equal to the display height. They don''t need to be cleared in any way, since each triangle drawn knows exactly what values to use (those between yMin and yMax) and calculates them. I find it easier to use these to interpolate things linearly accross a triangle than to write special cases for the different types of triangles possible.
OrangyTang
OrangyTang
1. Run a profiler over your code. All you need to do is stick the right command line args (something like -Xrunhprof:cpu=samples,file="C:\Profile.txt" ) then bung that output into something like HPJmeter.

2. Those two float->int casts right in your inner loop probably aren''t helping things, is there a way of avoiding the cast somehow?
Matei
Matei
Is there a way to get hprof to time the execution of particular lines to show which ones are slow? I tried running it before but I didn''t see anything of that sort.
duhroach
duhroach
Not sure if java allows it, but using SIMD functions like SSE2 can improve a rasterizer 10 fold. The ability to process 4 floats in one command cuts alot of overhead out.

~Main

==
Colt "MainRoach" McAnlis
Programmer
www.badheat.com/sinewave
ph33r
ph33r
Matei,

I have a few q''s about software rasterization, and would like to get some input from someone who has written one recently. If you wouldn''t mind giving me an email dave@coderdave.com so I can contact you, I would appreciate it. If not, its ok too.

Thanks
Tristan10
Tristan10
You might want to check out Tricks of the 3D Game
Programming Gurus by Mr. Andre LaMothe. In the book,
he takes you trough the process of building a complete
software engine: including tricks for optimisation.

From the back of my mind: it propably would be wise to
brake down your triangles into flat-top, and flat-bottom
ones. It might seem 'less clean' to you, but I assure you
it will be much faster.

Also try to avoid all floating-point math you're
doing in your interior loop; for example by converting to
fixed-point.

Another thing that he does in his book, is have special case
rendering routines for: Alpha-blended triangles, Gouraud shaded
triangles, flat shaded triangles and w-buffer rendering etc...
to avoid as much inner-loop branching as possible.

Furthermore; this function is going to be called an incredible
amount of times. I'm not sure if Java supports 'references' or
'pointers', but if it does: be sure to pass the triangle that
way.

The last thing I noticed: you declare a whole lot of variables
inside your inner loop, and each traversal their memory will
have to be freed and re-acquired causing a decent amount of
overhead. Declare them at the top of your function.

Good luck with your project! Let me know how these tricks
turn out; I'm rather curious :D

[edited by - Tristan10 on May 18, 2004 2:27:19 AM]

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.