Premature Optimization – The Root of All Evil

“We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil”

– Donald Knuth

Correct > Fast

I’ve always tried to keep the above quote in mind when programming. My first priority is to get my programs working correctly, and only then do I go back and try to make them more efficient, usually with the help of a good profiler. Recently, the importance of profiling before optimizing was re-inforced in a big way.

Filling the gaps

When converting clothing from one Poser figure to another, my CrossDresser clothing conversion application welds the seams of the clothing together. Different figures often have different polygon groupings, and when regrouping a clothing for a new figure, seams can appear at the boundaries of the original groups if the clothing creator didn’t weld the original object.

Welding an object in a consistent way is a somewhat complicated algorithm. I decided to tackle it in the following way:

  1. Create a nearest neighbor class using a kd-tree containing the vertices of the object.
  2. Create a graph object with each vertex of the object being a vertex in the graph.
  3. For each vertex in the object, use the nearest neighbor class to find all vertices within the weld tolerance.
  4. Add edges to the graph between each pair of vertices that are within the weld tolerance of each other.
  5. Walk the graph until each vertex has been visited, and generate a list of vertices to weld.
  6. Weld the specified vertices.

Too slow

Given the complex nature of the weld algorithm, I wasn’t surprised when it was somewhat slow. After being annoyed with it for several months, I found the time to go back and try to optimize it. Initially, I guessed that the algorithm I was using was simply inefficient, and needed to be redesigned from scratch to be more efficient. However, before I invested my time in a massive rewrite, I ran the code through the a code profiler to see where the bottlenecks were.

Nothing can stop us now

As it turns out, my guess was completely wrong! The actual bottleneck was in a place that I would have never guessed. In the code that implemented a breadth-first search of the graph, I needed a way to track which vertices had been accessed, so I initializing a boolean array filled with “false.” This array had one entry for each vertex in the graph, and the entry was set to true when the vertex was visited. Unfortunately, this array was initialized every time a new breadth-first search was started. Since the weld graph was very sparse (almost all subgraphs consisted of two or less vertices), this happened a huge number of times. Almost 90% of the running time was taken up with initializing an empty variable! When I replaced the array with an empty set of vertices which was given a new entry whenever a vertex was visited, the running time dropped immensely.

Don’t guess. Know.

Although I was already a believer in profiling before optimizing, this was a very good example of why you should do so. I’ve found in most cases that when it comes to guessing where a bottleneck might be, I might as well be blind. :)

Procedural Landscape Generation with Virtual Machines

One of the first 3d graphics programs I tried way back when was the Persistence of Vision raytracer. POV-Ray really appealed to me since I was a hard-core programmer and not much of an artist. I could make high-quality graphics, even though I could barely draw tolerable stick-figures. I’ve since moved on to using more advanced programs such as 3d Studio, but the idea of creating art with pure code has never left me.

Playing God

Recently, I’ve spent some time playing Minecraft, which generates unique randomized worlds that are created on the fly and are virtually infinite in extent. By using properly designed procedural algorithms, Minecraft creates a surface world of plains, mountains, and oceans populated by trees, plants, and villages with elaborate cave systems below. Granted, the worlds are still very blocky and simple, but the variety is still pretty amazing. One of my more ambitious projects is to extend this idea beyond blocky voxel worlds, and be able to create a full 3d world completely procedural.

“And God said ‘Let there be light!'” … *Click*

I recently started on the first step of this journey. You can’t have a world without ground, so for the past few months, I’ve been working on a procedural landscape generator. I already had modules for handling geometry meshes, so I just needed a method for creating procedural heightmaps. It’s easy enough to iterate over the vertices in a high resolution plane, and set the height as a function of the vertex’s x and y coordinates. However, I didn’t want to limit the landscape to a set of pre-canned functions. I wanted to be able to make whatever landscape I could think of without needed to add new equations to the program and re-compile it every time I had a new idea.

The Genesis Machine

The solution I decided on was to parse a Lisp-style domain specific language into opcodes and use a custom-built virtual machine to execute the landscape heightmap function. This was my first experience with virtual machine programming, and it was very enlightening. In future posts, I’ll describe the pieces of this prototype landscape generator. But in the meantime, here are a few samples of what it’s capable of right now.

A procedurally generated 3d crater

Crater

A procedurally generated volcano

Volcano

A procedurally generated valley

Valley

Procedurally generated mountains

Mountains

A Better Gravity Simulation

Years ago, when I was still in college and had lots of free time, I played around with physics simulations of various kinds. One that most programmers try at some point is a classic gravitational simulation. It’s great fun to setup complex arrangements of planets and stars, turn on the simulation, and watch star systems evolve.

I am become death, the destroyer of worlds

After trying numerous generic simulations, I decided to try something a bit more complex: A collision simulator. I altered the force equations for the particles, so that when they got close enough together, they would repulse instead of attract. This caused particles to stick together in clumps. By simulating small systems, then saving the results, and throwing the clumps into more clumps, I eventually worked up to a planetery collision simulation with over 1,000 particles. I exported the results to the POV-Ray renderer, and entered the animation into the Internet Ray Tracing Competition.

Is this simulation ever going to finish?

Recently, I started thinking about better ways to do animations like that. The major issue I ran into when creating the full planetary collision was slowness. A gravity simulation with n particles needed to calculate n2 forces every frame in order to update the particles’ positions. This becomes slow very quickly when you increase the number of particles. If you’re doing scientific simulations, then this is probably the best you can do since accuracy is more important than speed. However, if your goal is to make a “cool” animation, then you can get away with some “cheating” as long as it’s not too drastic.

From up here, they all look like little ants

I’ve done some recent work with kd-trees as a way to create a more efficient nearest neighbor algorithm. This data structure is useful for organizing points in space so that you can search more efficiently in multiple dimensions. Another similar data structure is the octree which has a more regular structure than the kd-tree.

Since my goal is aesthetic rather than scientific, I can speed up force calculations by grouping all of the particles into an octree. Since a group of points becomes more and more like a single point the further away they get, I can get the force from a particular octree node by averaging the positions of the particles inside the node and treating the node as a single particle. Thus, the force from a large group of points far away from another point can be calculated with a single operation instead of many.

Although complete accuracy isn’t necessary for a purely visual effect, I don’t want the simulation to be too unrealistic. So, before I start implementing an octree-based simulation, I need to know how much “cheating” I can get away with. To do this, I wrote a PHP script (pasted below) that creates 2x2x2 cubes of 10 random masses at various distances from the origin, and then calculates the magnitude and angle error of the averaged force compared to the actual force.

Results

As shown in the graphs below, the accuracy is acceptable at surprisingly low distances. When the edge of the cube is as close to the origin as it is to the center of the cube, the magnitude error is less than 10%, and the angle error is less than 6 degrees. In order to bring the errors down to 1% and 1 degres respectively, the distance of the cube from the origin needs to be only 2.5 times the side length of the cube! So, even if there are 100,000 points in the scene, a point which is even somewhat isolated from the rest of the points may only need a handful of force calculations to determine its next position.

Errors for a 2x2x2 cube of 10 points at a distance X from the origin

Errors for a 2x2x2 cube of 10 points at a distance X from the origin

Creative Commons License
This PHP code snippet is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.


class Vector
{
	public $x;
	public $y;
	public $z;
	
	public function __construct($x = null, $y = null, $z = null)
	{
		if($x instanceof Vector)
		{
			$z = $x->z;
			$y = $x->y;
			$x = $x->x;
		}
		
		$this->set($x,$y,$z);
	}
	
	public function set($x,$y,$z)
	{
		$this->x = $x;
		$this->y = $y;
		$this->z = $z;
	}
	
	public function length()
	{
		return sqrt($this->length2());
	}
	
	public function length2()
	{
		return self::dot($this,$this);
	}
	
	public function normalize()
	{
		$len = $this->length();
		
		$v = new Vector($this);
		$v->x /= $len;
		$v->y /= $len;
		$v->z /= $len;
		
		return $v;
	}
	
	public function scaleTo($len)
	{
		$v = $this->normalize();
		$v->x *= $len;
		$v->y *= $len;
		$v->z *= $len;
		return $v;
	}
	
	public function scaleBy($len)
	{
		$v = new Vector($this);
		$v->x *= $len;
		$v->y *= $len;
		$v->z *= $len;
		return $v;
	}
	
	public static function add($v1,$v2)
	{
		$v = new Vector($v1);
		$v->x += $v2->x;
		$v->y += $v2->y;
		$v->z += $v2->z;
		return $v;
	}
	
	public static function sub($v1,$v2)
	{
		$v = new Vector($v1);
		$v->x -= $v2->x;
		$v->y -= $v2->y;
		$v->z -= $v2->z;
		return $v;
	}
	
	public static function dot($v1,$v2)
	{
		return
			$v1->x * $v2->x +
			$v1->y * $v2->y +
			$v1->z * $v2->z;
	}
	
	public static function cross($v1,$v2)
	{
		return new Vector($v1->y*$v2->z - $v1->z*$v2->y, $v1->z*$v2->x - $v1->x*$v2->z, $v1->x*$v2->y - $v1->y*$v2->x);
	}
	
	public static function angle($v1,$v2)
	{
		$len2 = $v1->length() * $v2->length();
		$cosTheta = self::dot($v1,$v2) / $len2;
		$sinTheta = self::cross($v1,$v2)->length() / $len2;
		return atan2($sinTheta,$cosTheta) * 180.0 / 3.1415936535897;
	}
}

function random_float ($min,$max) {
   return ($min+lcg_value()*(abs($max-$min)));
}

class Mass
{
	public $mass;
	public $location;
	
	const G = 100.0;
	
	public function force()
	{
		return $this->location->scaleTo(self::G * $this->mass / $this->location->length2());
	}
	
	public static function average($massList)
	{
		$v = new Vector(0,0,0);
		$mass = 0.0;
		foreach($massList as $item)
		{
			$v = Vector::add($v,$item->location->scaleBy($item->mass));
			$mass += $item->mass;
		}
		$v = $v->scaleBy(1.0 / $mass);
		
		$m = new Mass;
		$m->mass = $mass;
		$m->location = $v;
		return $m;
	}
	
	public static function random($vOffset, $offsetVariance, $minMass, $maxMass)
	{
		$m = new Mass();
		$m->mass = random_float($minMass, $maxMass);
		$m->location = new Vector(
			random_float(-$offsetVariance,$offsetVariance),
			random_float(-$offsetVariance,$offsetVariance),
			random_float(-$offsetVariance,$offsetVariance)
		);
		$m->location = Vector::add($m->location,$vOffset);
		return $m;
	}
}

function getResults($offset)
{
	$offset = new Vector($offset,0,0);
	$variance = 1.0;
	$min = 1.0;
	$max = 10.0;
	$numMasses = 10;

	$masses = array();
	$forceActual = new Vector();
	for($i=0;$i<$numMasses;$i++)
	{
		$mass = Mass::random($offset,$variance,$min,$max);
		$masses[] = $mass;
		$forceActual = Vector::add($forceActual,$mass->force());
	}

	$forceAverage = Mass::average($masses)->force();

	$magDiff = $forceActual->length() - $forceAverage->length();
	$magDiffRatio = 100.0 * $magDiff / $forceActual->length();

	$angle = Vector::angle($forceActual,$forceAverage);
	
	return array(
		'forceActual' => $forceActual,
		'forceAverage' => $forceAverage,
		'magDiff' => $magDiff,
		'magDiffRatio' => $magDiffRatio,
		'angle' => $angle
	);
}

echo "<pre>";

$iterations = 20;
$limit = 20;
$increment = 0.05;
$results = array();
for($offset=0.1; $offset<=$limit; $offset+=$increment)
{
	$magDiffAvg = 0.0;
	$angleAvg = 0.0;
	for($i=0; $i<$iterations; $i++)
	{
		$curResults = getResults($offset);
		$magDiffAvg += abs($curResults['magDiffRatio']);
		$angleAvg += abs($curResults['angle']);
	}
	$magDiffAvg /= $iterations;
	$angleAvg /= $iterations;
	$results["$offset"] = array(
		'magDiffAvg' => $magDiffAvg,
		'angleAvg' => $angleAvg
	);
}

foreach($results as $offset => $data)
{
	extract($data);
	echo "{$offset}, {$magDiffAvg}, {$angleAvg}\n";
}

Zend View Helper and Fluent Interfaces

Lately, I’ve been building a new website using Zend Framework as a base. In addition to having easy to use bootstrap utilities and a solid MVC structure, Zend also has numerous view helpers to assist with common tasks.

…Left, Right, Left, Right, Left…

One common styling feature commonly seen in data grids is alternating row colors. Normally in PHP, you would handle that using something like:

<table>
	...
	<?php $row = 0; ?>
	<?php foreach($items as $item) { ?>
		<?php if($row == 2) $row = 0; ?>
		<tr class="row<?=$row++;?>">
			...
		</tr>
	<?php } ?>
	...
</table>

This works, but it’s not very elegant and pollutes the view namespace with unnecessary variables and code. It would be nice if there was a way to encapsulate the code into a single statement. Fortunately, Zend has a view helper called Cycle that does just that:

<table>
	...
	<?php foreach($items as $item) { ?>
		<tr class="row<?=$this->cycle(array(1,2))->next()?>">
			...
		</tr>
	<?php } ?>
	...
</table>

Much better! Now everything regarding the alternate rows is in a single place. Now, what if instead of alternating every row, you want to alternate every other row? There are two obvious alternatives:

<table>
	...
	<?php foreach($items as $item) { ?>
		<tr class="row<?=$this->cycle(array(1,1,2,2))->next()?>">
			...
		</tr>
	<?php } ?>
	...
</table>
<table>
	...
	<?php $row = 0; ?>
	<?php $cycle = $this->cycle(array(1,2)); ?>
	<?php foreach($items as $item) { ?>
		<?php if($row++%2==0) $cycle->next(); ?>
		<tr class="row<?=$cycle?>">
			...
		</tr>
	<?php } ?>
	...
</table>

Both of these have issues. The first would need a lot of repetition in the array if you wanted to alternate between five numbers every 4 rows instead. The second example is back to polluting the view namespace.

Fluent Interfaces

Declarative languages like SQL are useful because they allow the user to specific what they want without having to worry about the hairy implementation that goes on behind the scene. The user can say “I want this data satisfied these criteria” and the database engine will take care of putting together an efficient search query to get the desired data.

One way to get this kind of declarative power in other languages is to use what is called a “fluent” interface. The idea is to design your API so that you can chain configuration calls into a kind of domain specific language for that class. For example, a fluent cycling view helper would allow us to easily specify:

  1. What values to cycle between
  2. What string to insert the values into
  3. How often to switch to the next value

Something like:

$this->alternate()->between(array(1,2,3,'a','b','c'))->every(2)->using('this is the {N} row');

Using this is a loop would output:

this is the 1 row
this is the 1 row
this is the 2 row
this is the 2 row
this is the 3 row
this is the 3 row
this is the a row
this is the a row
this is the b row
this is the b row
this is the c row
this is the c row
this is the 1 row
...

Setting up a fluent interface like this isn’t very difficult. The critical step is to simply return $this from every method which sets parameters. Below is the class I implemented for my website which uses this fluent interface to alternate values in loops.

Creative Commons License
This PHP code snippet is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.

class ATP_View_Helper_Alternate
{
	protected $_every = 1;
	protected $_template = '{N}';
	protected $_modeMap = array(0=>1,1=>2);
	
	protected $_curCount = 0;
	protected $_curMode = 0;
	
	protected static $_alternators = array();
	
	public function alternate($template = '')
	{
		if(!isset(self::$_alternators[$template]))
		{
			self::$_alternators[$template] = new self();
			self::$_alternators[$template]->using($template);
		}
		
		return self::$_alternators[$template];
	}
	
	public function using($template = '')
	{
		if(strpos($template,'{N}')===false)
		{
			$template .= '{N}';
		}
		$this->_template = $template;
		return $this;
	}
	
	public function every($times)
	{
		$this->_every = intval($times) ? intval($times) : 1;
		return $this;
	}
	
	public function between($modes)
	{
		if(is_array($modes))
		{
			$this->_modeMap = array_values($modes);
		}
		else
		{
			$modes = intval($modes) ? intval($modes) : 2;
			$this->_modeMap = array();
			while(count($this->_modeMap)<$modes)
			{
				$this->_modeMap[] = count($this->_modeMap)+1;
			}
		}
		
		return $this;
	}
	
	public function __toString()
	{
		$str = str_replace("{N}",$this->_modeMap[$this->_curMode],$this->_template);
		
		$this->_curCount++;
		if($this->_curCount == $this->_every)
		{
			$this->_curCount = 0;
			$this->_curMode++;
			if($this->_curMode == count($this->_modeMap))
			{
				$this->_curMode = 0;
			}
		}
		
		return $str;
	}
}

Welcome Back (or: The Importance of Backups)

In June this year, our server went down. No problem. It gets overloaded occasionally, and kicking Apache would usually get things going again. So, I logged onto the server and realized that the problem was bigger than I thought. Something had happened to the server (I’m still not sure what), and the database was corrupted.

So, my blog database was lost…

…as well as the database for my wife’s 3d graphics business…

…and my SVN repository…

…and the last backup was 3 years old.

Whoops.

Backup. Yesterday. Do it Now!

Fortunately, we had local copies of the code for all of our projects, so the “only” thing we lost was 3 years worth of customer data. The website was back up and running in a few days, and we now have a weekly backup routine for the website.

Welcome Back!

This blog got put on the back burner for a while, but I finally found the time to get everything re-uploaded. I have some interesting projects I’m working on now, so hopefully, I’ll have more updates in the near future.

The Insanitizer

There are times when simple text does not adequately convey the horror of a certain situtation. Whether it is describing a terrible monstrosity eating someone’s very soul, an indescribable cosmic horror, or bad programming techniques, you sometimes need that extra something to boost your writing from very good to PURE AWESOME!!1

T͒he̷̳͐͊҉ ͥI͖ͣ͑ͬn̩̠san̪͛itĩ̟͞z͙e̲̔͛͜r̪̖ͧ̏ͫ̚

The Insanitizer is a text-generator that can add garbage characters and random formatting to any text. It has multiple parameters to customize the appearance of the final text.

Use Garbage / Use Formatting
These parameters determine what will be added to the final text. You can add either or both.
Garbage Amount / Formatting Amount
These parameters control the amount of the effect in the final text. The higher the number, the larger the effect.
Format Length
This determines the average length of the formatting blocks
Falloff
This parameter changes the density of the effect according to the position in the text. A value of 1.0 distributes the effect uniformly in the text. Numbers higher than 1.0 start with a larger effect near the beginning of the text that fades out towards the end. Numbers between 0.0 and 1.0 do the opposite, with a larger effect near the end of the text.

Try it out!

Note that in order for you to use the generated text on a webpage, the website needs to be setup to correctly handle the UTF-8 character set. If the website isn’t setup correctly, any insanitized text will not be displayed correctly. Also, in order to copy the formatting from your generated text, you will have to select the text generated below, and view the HTML source for it.

Minecraft to Max Converter (Part 3)

Now that the time-consuming hassles of the holidays, hard drive crashes, new computers, and new jobs are out of the way, I finally have time to work on the Minecraft exporter again. I dusted off the project a few days ago, and found that despite not working on the exporter since the Halloween update, it still mostly worked. It only required a few minor changes to exclude the Nether chunk files.

Refactoring the Code

I’ve never liked projects which require code changes to incorporate what are essentially configuration changes. The original version of the exporter contained custom code and a big set of ifs and switches to determine what geometry to load for a particular block. In other words, if the block IDs ever change or new objects are added, code changes and a re-compile would have been needed to include those changes into the exporter. So my first task this time around was to generalize the handling of the various Minecraft blocks and objects.

I removed all of the if torch do this else if door do this else if bleh code and replaced it with a new method that reads the block ids and custom object definitions out of a configuration file. The advantage of this is that changes to block IDs or objects now require a few changes to a text file instead of a re-compile. It also allows any users of the program to customize the output of the exporter to their needs. You can use the standard cube for objects like workbenches and furnaces, or replace them with custom objects of your own design. You can even do things like replace the bottom-most part of a tree with a custom object that has roots!

missing object test render

Error: torch geometry not found.

The new config system also allows you to test out your config file changes without needing the new custom objects immediately. If the exporter can’t find one of the objects specified in the config file, it will just put a nice big exclamation point in your scene in place of the object. That way, you can see at a glance which objects are missing. This test render shows a part of my world which contains numerous missing objects. I’m remaking all of the custom objects, including the torches and fences, which is why they show up as missing now.

Next Steps

Now that the geometry creation system has been revamped, the next step will be to create an actual interface around the test rig. Once that’s done, the utility should be ready for a beta release!

Minecraft to Max Converter (Part 2)

As I wrote about previously, I’m working on a utility to convert Minecraft levels into a Wavefront OBJ format so I can render my worlds in 3ds Max. Since my last post, I’ve added the following features:

  • You can now choose the region to render. This reduces the polygon count on the final object, especially if you’re just doing a surface render, and don’t need all of the cave polygons.
  • I exported the default texture set from Minecraft so I could apply the in-game textures to my final model.
  • Fixed some chunk boundary issues. Previously, if a block was at the edge of a chunk, it couldn’t tell if it should be rendered or not, since there was no next block to compare to. This resulted in holes in the final object. Now, the converter imports all of the chunks before doing the export, so it can check conditions across chunk boundaries.
  • The biggest feature I added was the ability to use custom geometry for certain blocks. So, now the exporter will automatically insert actual torches, doors, fences, flowers and steps where appropriate. It will also check the extra info for those objects in the level data, so the torches and doors are rotated correctly. There are many other custom geometries that I could add, but I don’t have any in the scenes I wanted to render, so I haven’t done them yet.

Rendering Minecraft scenes in 3ds Max

Once I have the level object imported into 3ds Max, I still need to hook up the materials and setup the lighting. I used the Mental Ray rendering engine with a dim skylight for ambient light. I also added a glowing material to the torch flames to get more dynamic lighting in the scene.

Renders

Still to-do

I still need to wrap the utility in a nice interface, and perhaps add some more custom geometry. Once I find the time to do that, I’ll package up the utility and release it here.

Minecraft to Max Converter (Part 1)

Minecraft house on a hill

Home Sweet Home

There’s a new computer game that has become rather popular lately.

Minecraft is the perfect game for people who enjoyed building with Lego bricks as a kid. The player wanders around an infinite natural landscape during the day gather natural resources such as wood from trees, and coal from caves. The player then uses these resources to build whatever they desire. However, when night falls, the player had better have made at least one shelter, because the darkness spawns zombies, skeletons, and other baddies whose sole purpose is to kill the player. You can only survive by crafting a secure shelter, or if that fails, a sword and armor.

Minecraft is a sandbox game, which means that there is no goal other than what the player decides to do. There are no levels to finish, bosses to defeat, or damsels to rescue. It is limited only by your imagination. Some Minecraft players have created very complex creations, including waterslides, the Mines of Moria, a recreation of the battle at Minas Tirith from the Lord of the Rings, and even a working computer! Yes, you read that right. Someone made a simulation of a computer inside a computer game.

Minecraft underground tree farm

Torches are just as good as sunlight, right?

I’m not quite that ambitious. My world consists of a modest house on a hillside, the start of a castle, and a few underground mines. The underground tree farm is pretty cool, though!

Beyond 8-bit graphics

The graphics for Minecraft are very simple compared to most modern computer games. Every piece of the terrain is a simple cube. Dynamic lighting is crude, and shadows are non-existent. Granted, these limitations are natural given the player’s ability to alter virtually anything in the environment, but as a hard-core 3D graphics snob, I thought it would be cool to see if I could render my world at a higher quality. Fortunately, notch, the author of Minecraft, decided to publish the file formats for the game’s level data. As a result, many people have created third-party utilities to visualize and interact with the game’s data. So, I decided to jump on the band-wagon, and write a Minecraft to 3D Studio converter.

I wrote up a nice C++ library to read notch’s NBT file format, and then created a test harness to iterate over the chunk files for my world, and convert the block data into a Wavefront Object, which I could then import into 3D Studio. After a few long nights of gnarly pointer shenanigans (death to the Big Endians!), scaling mismatches, and out-of-memory errors, I finally have a prototype. The gaps in the model are due to Minecraft’s level data being saved in strips. I stopped the conversion early once I could see that it was working correctly.

Minecraft world converted to 3D Studio

Woohoo! It works!

Future Plans

My next steps include building a base texture for the world, creating low-poly models for the in-game objects such as chests, workbenches, and furnaces, and lighting the world model with dynamically places torches and sunlight. I will also need an option to limit the region to convert. The region in the image above has over 1.6 million polygons, and is probably not even a quarter of my world. Then, once I wrap a nice interface around the converter, I’ll probably put it up for download here for everyone else to enjoy.