![]() |
| Most recent update (All By Hand(TM)): 30-Dec-2006 19:23 |
|
Oh yeah I used to know Quentin
The Frontier Galaxy VII: The Flat FactsTo see the difference between even the most advanced flat shading and the simplest texture mapping, just look at the computer images in the 1984 movie "The Last Starfighter". The CGI were produced on a Cray X-MP supercomputer, and it reportedly took about 4 minutes to render each frame. Nice, huh? Now look at Frontier. Fewer triangles, no advanced shading (just flat, using a 4-4-4 bit RGB palette) but they are textured. Better? (Aww, come on. Say "Better!")
The whole difference is that the world you see around you is not flat. It takes a large number of triangles before you can see anything like grains in a non-textured image, and you can get a same effect with just a single textured rectangle. Despite Frontier being a true 3D game, it uses flat images a lot. For example, in modern games the console is rendered in 3D because you can use all the different screen resolutions you want, and because, well, with modern hardware it is just slightly slower than blitting an image to the screen (and an additional trick to speed it up is to render the console once into a buffer and then render that one). For Frontier the console is just a bitmap somewhere in the main executable program; on-off buttons are separate bitmaps, so is the font, and so are the textures on the 3D models. The Image ListIn FE2 the graphics were scattered all around the program, and no doubt the programmers learned from that the hard way. In FFE there is a single list of pointers to all the bitmaps you'd ever want (strictly speaking, not every bitmap -- but the main lot is; also, probably not all the bitmaps you'd ever want, but just those from the game). There are a couple of essential parameters for bitmaps: width and height spring first to mind. Another thing is a bitmap hotspot coordinate: the bitmap coordinate which is taken to be (0,0) when blitted to the screen. Take, for example, the aiming cross; it is useful if you want to point it to, say, the center of the screen, and you don't have to subtract half its width and half its height of the screen coordinates. Just subtract the values in the hotspot, and presto, you have the correct screen position. That is a lie. The aiming cross has in fact a hotspot of (0,0), and, come to think of it, so have all the other bitmaps. Even worse is that the only piece of blitting code which actually uses the hotspot is the routine which blits the mouse pointer -- that's how I know what it is supposed to be. And the mouse pointer also has a (0,0) hotspot. It is quite useful to know whether a bitmap is masked is not -- if it uses transparent pixels instead of being a dull and boring square. One well known trick is to create a separate mask of the same size, in which the transparent pixels are black and opaque ones are white (or even shades of gray to perform alpha tricks). Another possibility is to compress the bitmap, where a special code is used for a "run" of transparent pixels (and the compression itself would also be quite useful). FFE uses a third common method: pixels with a certain color value are never drawn on the screen, and, no doubt because of the easy test, this usually is the value "0". It would be a real bummer if all images were always tested for transparent pixels; not only is it a waste of time if you can tell beforehand whether to test or not, but it would eat up a whole color of the precious limited number; the wise thing to do is to store a flag in the bitmap header for transparency.
Putting the useful and otherwise bits in the right order, we get this structure definition: typedef struct {
unsigned char Transparent;
unsigned char height;
unsigned short width;
unsigned short hot_x;
unsigned short hot_y;
} bitimage_t;
The final shorts are Each separate image in the image list points to a structure like this; right after the structure the actual image bytes follow, at least width × height, and followed by padding zeros where necessary to force the next structure to a 4-byte boundary (the compiler does that).
The images are all palette-indexed, so another useful thing would be the actual color palette. Frontier does some amazing tricks with the limited number of colors, and rather a large part of the code is devoted to shifting just the palette around, using all kinds of in-use and update flags. It appears the first 128 colors of the palette are fixed to the colors used in the UI; the top half is used for 3D modelling, and can be changed when necessary for every single frame. If you must know. The internal palette for the 3D models is 16x16x16 values; only four bits per color channel, where the actual VGA palette supports 6 bits and true color supports 8. The program maintains an array of 4096 bytes to translate between a "real" RGB value and the palette index used for that color; a large number of routines and flags select the "optimal" 128 actual colors out of those. It appears the 128 fixed colors also have their place in this array (though they probably have a flag saying don't change me).
The bottom of the 128 color palette looks like this -- you'll need it if you want to display the graphics yourself:
To peep into the colorful guts of FFE I wrote a small(ish) program, the Frontier Image Viewer. It's for Windows only, but if you're lucky to use that (... not even a joke intended...) you can see just about all images in the file firstenc.dat (the one supplied with your original FFE; it should be 2,101,248 bytes large). An additional Texture toggle switches to showing the texture bitmaps in the appropriate colors. Don't do that yet -- the story on the textures comes right after this.
If you look at the different images using the Image Viewer, you'll see they fall broadly into two categories. The "properly" colored images are those used in the UI, the dull grey ones are used for textures. Among the UI images you can find the journal headings, the different stars and planets from the System info screen, and an entire console in its "off" state (# 43). On top of this console the various buttons and indicators are drawn in either "off" or "on" state (they start at image #102), as well as the center scanner (images 254 to 261) and the different missile types (#276 and onwards). There is a special structure in the game, defining which bitmaps are on which x and y coordinates; this structure also defines the width and height of the "clickable" region for that button and which key press to send to the game. The structure gets updated every time the console needs to be redefined for a different function; fairly regular stuff all. The Textures ListThe dull grey images are used for 3D texture mapping. They appear dull grey because they only use the first 7 colors in the palette, and they get their (initial!) color from yet another structure: typedef struct {
int image;
int number_of_colors;
unsigned char rgb[7][3];
unsigned char padding;
} texture_t;
The Ambient colorEvery object should have some ambient coloring applied, that's the color of the "surroundings". In Frontier, this is bright white (when examining ships in the Shipyard), or the color of the main star in your current system. Recall that every 3D model has a Dot color (see Frontier Objects for more about this). The Red component of the dot color may have, apart from the actual dot color, a few extra bits set. Bit #7 The extra lighting is taken from this array: unsigned char Ambient[6][8][3] = {
7,7,7, 7,7,7, 7,7,7, 6,6,6, 5,5,5, 4,4,4, 3,3,3, 2,2,2, // 0 (default)
7,6,3, 7,5,2, 7,4,2, 6,3,1, 5,2,0, 4,1,0, 3,0,0, 2,0,0, // Red
7,7,5, 7,6,4, 7,5,2, 6,4,0, 5,3,0, 4,2,0, 3,2,0, 2,1,0, // Orange
7,7,7, 7,7,7, 6,6,6, 5,5,5, 4,4,4, 3,3,3, 2,2,2, 1,1,1, // White
7,7,7, 7,7,7, 6,7,7, 6,6,7, 5,5,7, 4,4,7, 3,3,6, 2,2,4, // Cyan
0,6,7, 0,5,7, 0,4,7, 0,3,7, 0,2,6, 0,1,5, 0,0,4, 0,0,2 // Blue
};
The The default white cannot be selected in the model color bits (a value of '0' here means "don't use it"). The only models which have this extra ambient data are the Brown dwarf substellar object and the regular stars. Only the ambient color of the primary star is used. If you visit Omicron Eridani (2,0) -- a system with a bright yellow 'G' star, a white dwarf, a type 'M' red star and a brown substellar object -- you will see that, no matter how close you get to one of the red stars, everything will be lighted in white. You may also be blasted out of the skies by a squadron of Imperial Traders long before you reach it.
Each color set has 8 brightness levels; they are used for flat shading. Coloring is simple: the values here are added to the current color and clipped when they are over 15, so the entire line for each texture pixel reads like actual_red = max (Ambient[CurrentAmbient][CurrentShade][0]
+ texture[CurrentTexture].rgb[texture_pixel][0], 15)
... for red, and the same for green and blue. Then this RGB color has to be looked up in the current color array but I'll spare you that one. Looking up the color index to use is the easy part. It is the index on
The texture list consists of 147 If you want to experiment with the textures, first make sure you know how to read arbitrary images from the image list. The list of pointers to the images start in
If you run the Image Viewer you can see that there are a few duplicates; for example, textures 1 and 2 appear to be the same. That is not an error in the program! The structures are defined exactly the same, but texture #1 is used in the 3D models, and #2 is not... There are a few same textures with different
.. where the image is the same (#50) but the The same four textures under (top) the brightest "Red Star" light and (bottom) a medium "Cyan Star" light look something like this:
The colors look quite different than in the game, these images aren't remapped to 6 levels of RGB and so use the full range possible. An example of textures with transparency is the background galaxy. The 3D model is sort of wrap-around poster, divided into squares, and uses these textures:
The entire Times font consists of textures with transparency. The 3D model for each character is a simple rectangle with the appropriate texture mapped onto it. Some characters look strangely stretched in the Image Viewer, but that's just the texture itself; the width of the destination rectangle is adjusted so the character looks normal again. The white line visible at the bottom in the images for characters "Z" and "/" does not appear on the textured characters -- it may well be an artefact of the texture code itself, causing the bottom-most line never to be used.
The texturing code itself is pretty straightforward. It examines whether the texture image is 64x64 or 128x128, and whether it contains transparent pixels or not. Based on that, one of four optimized routines is selected and fed with the source and destination coordinates; for a 64x64 pattern, the source coordinates are always (0,0), (63,0), (0,63) and (63,63) and for a 128x128 pattern always (0,0), (127,0), (0,127) and (127,127). It follows from this the texturizer can't draw any random part of a texture, the entire pattern is always used (and, it seems, apart from the bottom line). Do four different routines look like hyper-optimizing code to you? Not to me. Transparent texturing deserves a routine for itself, but there is not a large difference between using fixed sizes (64 and 128) and 'any' size. But what really drags the whole thing down for me is the restriction on the input coordinates. There is no actual reason to always use the same input coordinates for every texture map operation. The code, as it is now, is able to handle other input values! I confirmed this by changing the data in JJFFE on linesDATA_007791: dw 0x00,0x00, 0x3f,0x00 ; same as yours but rewritten as dw's dw 0x00,0x3f, 0x3f,0x3f ; coz that's what they areto other values; the game still runs fine, but with clipped textures! For example, this change:DATA_007791: dw 0x20,0x00, 0x3f,0x00 ; left in pair is x,right is y dw 0x20,0x3f, 0x3f,0x3fblits only the right half of the textures, as can be seen in the intro text. The Special case of PlanetsTwo images in the image list have an unexpected value in their
There is a second weird thing about these two. As explained above, textures use only 7 colors, which are defined in the There is a tantalising section in the data of the game where a triangle order is defined which smells like tesselation -- 0,2,5, then 0,1,2, then 2,3,5, then 3,4,1 and so on. This pattern repeats itself for smaller and smaller sets of triangles -- exactly what one would expect for subdivison of a sphere. There is also a large section with a number of RGB triplets which look promising. They aren't used in the 'regular' models (I figgered that much out) but I'll have to rewire one of my programs to plug them into these two textures; it might be the planet colors! Back to the Model MaterialsAt this point you know everything about the textures, except for one. Where are they used in the 3D models? The answer: just about everywhere! Fortunately, since you now also know all of the color model in the game, this is a brief section! The
If the There are a few borderline cases. Textures are only valid for the primitives triangle and square, but I seem to recall there are several lines, polygons, and/or balls with a valid texture in their definition. I'm not sure what should happen in that case; it might be as easy as using color #0 in the texture definition. The other borderline case are the jet flames; they are clearly textured but also clearly get their color in their own drawing command (usually the pine primitive). I still don't know what really happens when a pine shape is drawn, I can't imagine the code seeing the difference between a jet flame and a tree... The first is usually Fun with JJFFE: The Jjagged Edge CobraThe JJFFE source code is freely available, and if you have the correct compiler setup you can change the actual code of the game at will. Granted, you'd have to be proficient in C and even rate Elite in assembler to make changes to the executable part. Fortunately, the package also includes all data written out in easy-to-adjust ASCII texts; John Jordan made absolutely sure to get all data labels right so the game won't complain a bit if we, shall we say, surreptitiously sneak some structures in, compile, and see what we get... As mentioned before, there are a few duplicate textures with exactly the same parameters. An example is texture #2 (a green camou pattern); every occurrence, for example, on the Falcon Attack Fighter, uses texture #1. That means this is not used. That means we surely can find a use for it. That means hacking! The list of db 0x32, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0
db 0x0, 0x4, 0x0, 0x0, 0x6, 0x0, 0x0, 0x5, 0x0, 0x0, 0x3, 0x0
db 0x0, 0x2, 0x0, 0x0, 0x1, 0x0, 0x0, 0x1, 0x0, 0x0
... all written out in bytes coz it don't matter to the compiler (or the game) if the data is written out in bytes, words, or ASCII strings with escape codes. Remember that the first two items in the The next two lines define 22 bytes, of which the first 21 define 7 RGB color values (3 bytes each); the last one is padding. (Do you spot an error in my original comment on the "padding byte"?) Let's change the colors to something more interesting. Change these two lines to:
db 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x5, 0x3, 0x0, 0x6, 0x4, 0x0
db 0x7, 0x5, 0x0, 0x7, 0x6, 0x0, 0x7, 0x7, 0x0, 0x0
... defining the RGB triplets as 000, 100, 530, 640, 750, 760, and 770. When you recompile, the colors of this texture are redefined. Wait a minute! You can redefine all you want, but this particular texture is still not used anywhere! Let's find the Cobra Mk I definition. With the ShowMesh program you can find the model number: model #36. The model list can be found in the file Search for the data label itself (there should be a line "DATA_002686:" somewhere above this list); on this position, which is a
Now you'll have to use the output of ShowMesh to see what you must look for. In the output you can see this model uses the SetColor command: L94: 001A 400F 8200 4011 4009 400D 4001 4003 4013 4023 ; SetColor(GLOBAL[2],
Texture_015_Metal_Red_0,
Texture_017_Metal_Blue_0,
Texture_009_Metal_Black_0,
Texture_013_Metal_Green_0,
Texture_001_Camouflage_Green_0,
Texture_003_Camouflage_Gray_0,
Texture_019_Metal_Cyan_0,
Texture_035_Camouflage_Orange)
In the previous article I speculated somewhere global variable [2] might be the ship's internal ID; here it should select the main texture for the ship. I am not too happy about this: to use the result of this SetColor, some of the triangles and/or squares defined afterwards should have their Anyway. You can see a couple of lines below this command a useless (?) 0004 4003 050A 0B04 0002 ; Square(Texture_003_Camouflage_Gray_0, 10,11,5,4, Normal(2))
0007 20EE 0C0E 1002 ; Triangle(UNLIT | COLOR_CYAN, 12,14,16, Normal(2));
; Triangle(UNLIT | COLOR_CYAN, 13,15,17, Normal(3))
0003 4003 0001 0204 ; Triangle(Texture_003_Camouflage_Gray_0, 0,1,2, Normal(4))
0003 4003 0405 0206 ; Triangle(Texture_003_Camouflage_Gray_0, 4,5,2, Normal(6))
0008 4004 0602 0400 0008 ; Square(Texture_004_Camouflage_Gray_1, 6,2,4,0, Normal(8));
; Square(Texture_004_Camouflage_Gray_1, 7,3,5,1, Normal(9))
0007 4003 0406 080A ; Triangle(Texture_003_Camouflage_Gray_0, 4,6,8, Normal(10));
; Triangle(Texture_003_Camouflage_Gray_0, 5,7,9, Normal(11))
024B 0186 ; if (DISTANCE > 390) goto L190
191C 0000 ; Rotate -- default 0000
833C 0554 ; Rotate -- default 0554
000A 0888 060A 4646 3016
; Text(COLOR_WHITE, Normal(10), Vertex(70), Scale(6), VECTOR_FONT, 3016h)
191C 0040 ; Rotate -- default 0040
833C 0554 ; Rotate -- default 0554
000A 0888 060B 4648 3016
; Text(COLOR_WHITE, Normal(11), Vertex(72), Scale(6), VECTOR_FONT, 3016h)
L190: 0007 4004 0408 0A0C ; Triangle(Texture_004_Camouflage_Gray_1, 4,8,10, Normal(12));
; Triangle(Texture_004_Camouflage_Gray_1, 5,9,11, Normal(13))
0004 4003 010A 0B00 000E ; Square(Texture_003_Camouflage_Gray_0, 10,11,1,0, Normal(14))
0007 4004 0006 0A10 ; Triangle(Texture_004_Camouflage_Gray_1, 0,6,10, Normal(16));
; Triangle(Texture_004_Camouflage_Gray_1, 1,7,11, Normal(17))
0007 4003 0608 0A12 ; Triangle(Texture_003_Camouflage_Gray_0, 6,8,10, Normal(18));
; Triangle(Texture_003_Camouflage_Gray_0, 7,9,11, Normal(19))
These lines define the entire body of the Cobra! You can see the two textures used: #3 (Camouflage Gray) and #4 (also Camouflage Gray). This is a bit tragic, since they appear exactly the same -- AFAIK! What I want to do now, is replace every occurrence of the texture with the new one I re-defined before. Since it is texture #2, and we still want the The data in In my copy the byte set db 0x6e, 0x18, 0x1a, 0x6, 0x15, 0xf0, 0xe6, 0xff ; <- useful after all! db 0x4, 0x0, 0x3, 0x40, 0xa, 0x5, 0x4, 0xb db 0x2, 0x0, 0x7, 0x0, 0xee, 0x20, 0xe, 0xc db 0x2, 0x10, 0x3, 0x0, 0x3, 0x40, 0x1, 0x0 db 0x4, 0x2, 0x3, 0x0, 0x3, 0x40, 0x5, 0x4 db 0x6, 0x2, 0x8, 0x0, 0x4, 0x40, 0x2, 0x6 db 0x0, 0x4, 0x8, 0x0, 0x7, 0x0, 0x3, 0x40 db 0x6, 0x4, 0xa, 0x8, 0x4b, 0x2, 0x86, 0x1 db 0x1c, 0x19, 0x0, 0x0, 0x3c, 0x83, 0x54, 0x5 db 0xa, 0x0, 0x88, 0x8, 0xa, 0x6, 0x46, 0x46 db 0x16, 0x30, 0x1c, 0x19, 0x40, 0x0, 0x3c, 0x83 db 0x54, 0x5, 0xa, 0x0, 0x88, 0x8, 0xb, 0x6 db 0x48, 0x46, 0x16, 0x30, 0x7, 0x0, 0x4, 0x40 db 0x8, 0x4, 0xc, 0xa, 0x4, 0x0, 0x3, 0x40 db 0xa, 0x1, 0x0, 0xb, 0xe, 0x0, 0x7, 0x0 db 0x4, 0x40, 0x6, 0x0, 0x10, 0xa, 0x7, 0x0 db 0x3, 0x40, 0x8, 0x6, 0x12, 0xa, 0xd3, 0x0 Change the underlined bytes to the word Hit compile. My compiler does not automatically rebuild the assembler sources! Imagine this: I wrote all this before actually testing and thought for a moment it didn't work! ... and buy yourself a gold marbled Jjagged Edge Cobra!
As stated above the SetColor doesn't work properly for the Cobra's. In principle, you could restore multi-color Cobra's by changing the
So I tried another of my ideas: adding a new high-resolution image and applying it to a new texture. That worked out fine!
First create an appropriate image. Remember, only values 0 to 7 should be used, and it should be in one of the supported sizes 64x64 or 128x128. You'll need it as an ASCII file, so try this:
Then you have to patch in this new image into
If you recompile now, you'll have 348 images instead of 347; the program will never know you added it because it does no range checking. You still can't see it because it isn't used anywhere. In the file
You can safely recompile now (it should do so without any errors) but, although the texture is defined, it still isn't used anywhere. Go back to the file Whereever you changed Recompile and find yourself a modified Cobra Mk I.
A much simpler enhancement (although it requires some work) is to get rid of all 64x64 textures and replace them with 128x128 ones. To do so, copy the original texture from the Image Viewer into your favourite editor, enlarge it and replace the original image with the new one. You must take care to leave the seven color indexes of the original image unchanged. And do not forget to change the size in the data file! The top line of each image originally reads db 0x0, 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0 and you must change the This is less work because the game engine will notice the new image size and automatically uses the larger texture routine -- no other patches required. And it is not necessary to change all textures -- just the ones which get "ugly" when viewed close up. Besides, because you don't need to patch anything else, you can test each new texture immediately!
Based on original data and algorithms from Frontier:Elite 2 and Frontier:First Encounters by David Braben (Frontier Developments) Original copyright holders: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
![]() |
All the images you'd ever want... yeah, right! At least you got a nice Cobra -- send your thanks to jongware. |