I found it slightly disturbing that the Descent source code made the whole deal much easier to understand than any other tools I looked at - but I suppose considering the data format was probably optimised (quite well thank you very much) toward reading, as opposed to making it easy to write to, that is expectable.
But, for the few people that want to know anything more about D1/2 polymodels (I realise these are an increasingly rare breed these days), a summary of my findings above and beyond what the Descent Network information will tell you:
How D1/2 process polymodels
Basically, the block data in Descent polymodels are not just sensibly-organised data; it seems quite odd when you look at it that way. It is more closely akin to a very basic scripting language, where each block gives instructions to the renderer, such as draw this or that first, jump to this position in the model data, generate a polygon, or stop reading.
One other important fact is that
the renderer is recursive, which explains a lot of the features that seem really odd otherwise.
Block data: EOF
A very simple block, but also quite intriguing because of the patterns it usually turns up in; often there is not just one EOF at a point but several. Took me a while to figure out just how many you needed at each point and what aspect of the structure they are terminating.
Well, it's really quite simple - the EOF block tells the model processor to stop, that's all. And no, this doesn't mean the model stops at the first EOF, since as I said the data processor is recursive; it just means you've reached the end of the current 'node' - but many may be open at once.
Block data: DEFPOINTS
We know that this is rarely ever used, but not why it exists at all, it seems.
Easy explanation is that it's redundant, and superceded. A quick examination of
Mike Menefee's IDTA specs will show you that the formerPts field is missing compared to DEFP_START; the reason this matters is that formerPts tells you what points
aren't in the current submodel. Thus DEFPOINTS can only safely be used for the root submodel of the model, and it isn't really needed for that. Hence why no-one uses it.
Block data: SORTNORM
I was (obviously) having trouble with this one also, but it turns out to be probably THE most important block in the model, because it controls the rendering sequence. The point and normal data are used to define the plane as per linear algebra. n_Points is always zero, because planes don't have them; I'm personally not sure why they even bothered to put the field there because the Descent code just ignores it.
zFront and zBack are the fun bit. They are offsets from the start of the current block to wherever the renderer should jump next to process the rest of the model; far from being useless they are actually crucial...
Essentially, the renderer determines what side of the plane the viewer is on, and draws either zFront or zBack first to suit. Then it proceeds with the other, before going onward from the current point in the polymodel. (It can actually do this oddly enough; although no modelling programs that I've seen will let you, there
are robots in Descent 2 with polygon data after a SORTNORM. I don't know why they did this, since all it does is to guarantee those polygons are drawn last, no matter which side of the splitting plane they are on; I may be able to figure it out once I get around to checking which polygons received that treatment.)
I should note that I've never seen a negative offset given for either zFront and zBack; while this doesn't guarantee it wouldn't work - I suspect it would - there is no actual reason to do that, as you can always put child data later on unless you have loops in the polymodel, which is obviously a bad idea.
But it looks like you CAN do it. It's just quite stupid and will eventually cause an out of memory crash or something along those lines.
Block data: RODBM
This block draws a bitmap that is always facing you. It is not a conventional polygon, since it only has two points to anchor the top and bottom of the sprite (hence 'rod'); it also has values to change how thick it is at either end.
Would it have any actual use? I don't know. Effectively it is the same system as used to render hostages, but attached to a polymodel. There are some interesting things you could do with it such as engine glow (maybe; it'd be distorted from above or below), but most other applications seem too problem-fraught to use. I'm not really sure why the block exists, personally.
Block data: SUBCALL
SUBCALL in nature is similar to SORTNORM except it only jumps to one location, not two; it also does a few other fancy things.
Firstly, the vector vmsStartPoint. This defines the reference point around which any vertices in the submodel are rotated; the actual rotation does not occur until a DEFP_START is read though. I've never seen more than one DEFP_START in a submodel, and there appears to be no reason to do so; but I haven't yet seen any sign that you couldn't theoretically do that - it'd just be inefficient.
The 'offset' field just after that defines the point to which the interpreter should jump for the actual submodel data. Once it is done with that it will continue on in the polymodel, but an EOF block usually follows a SUBCALL anyway.
SUBCALL is not needed for the root segment.
Block data: GLOW
GLOW is a fairly simple little trick; it just tells the renderer to assign a preselected brightness to the following polygon instead of calculating it based on surroundings. This is used in the Pyro-GX's engines, for instance (at least I think it is
).
Summary of model structure
Effectively, a Descent polymodel is a recursive binary tree, where the interpreter renders one node at a time (where a node consists of any sequence of blocks up until an EOF block). Each node can have 0, 1 or 2 children; it will have 0 if it is a leaf, obviously; it will have 1 child node if it contains a SUBCALL, and it will have two if it contains a SORTNORM.
The renderer starts from the root node and proceeds through the rest of the polymodel in a depth-first manner. When it strikes a node with two children (SORTNORM), the branch it will take depends on the orientation of the normal.
Technically, a 'node' could have more than two children if there were two SORTNORMs not separated by an EOF block, but there is no good reason to do this.
Anyway, that should be enough for now, and perhaps it might be useful... at the very least, it should be usable reference material for later on...