In my previous post I tried to break an OBJ down into a few basic sections:
- Global properties of the OBJ.
- Raw Mesh Data
- Commands, which in turn set per-batch state and then draw the batches.
The performance cost of an OBJ feature often has a lot to do with where in the OBJ the command shows up, e.g. is it global or per batch.
Global properties tend to affect OBJ performance on a one-time basis. For example, if you use cockpit regions, you pay a fairly large penalty for having the panel texture be set up even if you only apply that panel texture to a single texture. Sure enough, COCKPIT_REGION is in the global properties section of an OBJ.
Per-batch properties affect the OBJ in two ways:
- Every command you see in the commands section is going to involve some CPU intervention. A very long commands section is more work for an OBJ.
- Every time there are attributes between TRIS commands, it defines a new “batch” – that is, a separate instruction to the graphics card to draw a new and distinct setup. Think of this as shutting down the factory to reconfigure the assembly line.
Generally batch count is more important than total commands. In other words, in evaluating this:
TRIS 0 300
ATTR_light_level 0 1 some_dataref
TRIS 300 12
the fact that there are two attributes is less interesting than the fact that there are two batches (the two TRIS commands run with different state). Even if you got rid of the no-blend attribute, you’d still have two batches because of the light-level change.
The most powerful aspect of the OBJ format is bulk data handling – that is, you have to add a huge number of triangles before the number of triangles becomes a performance problem.
For this reason, you should never use an attribute to reduce geometry count. A few examples:
- Don’t use ATTR_no_cull to reduce triangle count – simply issue the indices of the triangle twice.
- Don’t use ATTR_flat_shade to reduce vertex count – simply use more vertices with correct per-vertex normals to simulate flat shading.
- Prefer texturing to materials whenever possible.
Finally a note on weighting: for airplanes, where the total number of objects is low (a few dozen) global object properties often matter most. For example, on an airplane, choosing to use huge panel regions, or huge textures can make a big difference in performance. By comparison, batches aren’t that expensive unless you do something really crazy.
By comparison, for scenery, batches matter more; X-Plane will share the global properties of objects across hundreds or thousands of objects, but each batch hurts framerate. So when making autogen-style scenery, batches are most important.
A number of people are working on an update to Jonathan’s Blender X-Plane export scripts; this post is aimed at shedding some light on some of the recent changes to the OBJ format. X-Plane 9 introduced a number of new OBJ features (manipulators, invisible geometry and camera collisions, dataref-driven control of emissive texturing, normal maps, and a number of new light billboard options). If you simply read the new OBJ commands in the order they were added to the format, it’s just a soup of funny names. But there is some logic to how the OBJ format is extended.
The World’s Simplest OBJ
Here is a very simple OBJ file, broken up by my annotations. First we have the header and global section:
POINT_COUNTS 24 0 0 36
the global section describes properties universal to the entire OBJ. For example, what textures will be used to draw the object?
We picked up a few new global properties in the version 9 run:
- Normal maps are declared globally for the entire OBJ.
- The metrics of any panel regions to be used are declared globally.
We may pick up new global attributes in the future; if we do, they will be properties that apply to the entire obj.
Next comes the data section:
VT 0.449997 0.300003 0.860001 1.000000 0.000000 0.000000 0.000000 0.000000
VT 0.449997 0.300003 0.000000 1.000000 0.000000 0.000000 0.000000 1.000000
VT 0.449997 -0.509995 0.860001 1.000000 0.000000 0.000000 1.000000 0.000000
IDX10 0 1 2 3 2 1 4 5 6 7
I have removed a lot of the data section, because there’s not much to be said about it. The data section contains the raw data for the meshes in your OBJ, and it hasn’t changed since the OBJ 8 format was introduced.
The third and final section is the most interesting one: the commands section.
ATTR_LOD 0 3000
TRIS 0 36
The commands section describes how the data is used in the form of serial instructions to X-Plane. Most changes to the OBJ format have come in the form of new commands. We can categorize our commands into a few buckets:
- Drawing commands create “stuff”. There aren’t very many drawing commands, and new ones don’t appear very frequently. TRIS and LINES are the main commands, but the smoke commands also fall into this category, as do the light billboard commands. The new light billboard command LIGHT_PARAM is the only new drawing command for version 9, and it probably warrants its own blog post.
- Attribute commands change how stuff is drawn – they effectively define properties for drawing on all triangles that can be modified. We picked up a number of new attributes: manipulators (controlling how the mouse works), light level control, solid camera, draw disabling, deck style hard surfaces, and panel regions. (While you must declare the panel region locations globally, a panel region is enabled for a specific batch.)
- ATTR_LOD is sort of an exception, because it defines the structure of the model (e.g. a model with LOD really contains several separate command lists, of which only one is used).
Most new extensions to the OBJ format come in the form of new attributes. Attributes generally apply to a specific mesh within your model, not to the entire model.
Note that attributes can be thought of as “per mesh” or “per batch” properties, because they affect only the batches of mesh (TRIS commands) between the attribute being turned on and turned back off again.
Where Will New Features Appear?
I try to post some of my crazier ideas regarding OBJ on the scenery system RFCs page. Looking at the extensions, we can see how these extensions will all be either global, drawing primitive, attribute, or OBJ structure extensions. (I am not promising that any of these RFCs will be implemented, just showing how the OBJ format grows.)
- Additive LOD. This is a change to the structure of an OBJ, but it doesn’t actually change the format, just the legal LOD values.
- Explicit OBJ Height. This is a global property on the OBJ.
- Global Texture Variants would be a global property on the OBJ’s textures.
- Global Object Attributes are new global properties – they move some per-batch attributes to be object-wide.
- Draped Object Geometry would be per batch.
In summary, the vast majority of proposed extensions are new per batch or per object properties.
Next: what are the implications on performance of the various sections of an OBJ?
In past posts I have tried to describe the implications of DSF base meshes, which are “fully baked”. The basic idea is: the base mesh is fully formed ahead of time as a single unit. This is a trade-off:
- The advantage is performance. The sim has no work to do except draw that base mesh as fast as possible.
- The disadvantage is flexibility. The sim has no easy way to modify that base mesh.
By comparison, DSF overlays are not fully baked – you can add 8 overlays to an area and they will all run on top of each other. There is a real performance cost to this. Compare the performance of a huge number of draped orthophotos (via .pol files, an overlay technique) with a real orthophoto base mesh cut with MeshTool. You’ll easily get 100 fps on the DSF base mesh, but you won’t come close with the overlay.
If you want to compare X-Plane to a first person shooter, consider the “cost” of overlays as one of the reasons why FPS games appear to be higher performance than general purpose flight simulators like X-Plane and MSFS. In a FPS, each level is likely to be fully baked, with only one level loaded at a time. This is equivalent to X-Plane’s DSF base mesh. The FPS game doesn’t need to manage overlays that are put together at runtime in unpredictable combinations, and this lets the FPS engine optimize for performance.
(In fact, the FPS engine might be able to optimize a second way, if third party level packs are not available. Not only can a level be ‘fully baked’, but it can be fully baked specifically for that particular rendering engine. By comparison, a DSF base mesh will run with X-Plane 8 or 9 – clearly it isn’t specifically optimized for just one version of X-Plane.)
If you look at the scenery system “overview” I wrote around the time of X-Plane 8’s release (this overview is now pretty out of date; I really need to update it) you’ll see this:
There are now two scenery formats – one for editing and one for distributing scenery. Both are new.
DSF stands for “distribution scenery file” – the idea is that DSF was meant to be a container for fully baked finished scenery, optimized for small size on DSF and fast loading, but not editing. Our internal tools use another file formatm “.xes” to contain imported global scenery data before it is baked. Originally I thought that we would provide an editor to .xes files, but that has not happened. With MeshTool, you provide input data in more common public formats like SRTM HGT or GeoTiff, and .shp (shapefiles). You can think of .shp and .tif as the editing formats for MeshTool and base DSFs.
So how do we make it easy for users to edit scenery? I believe OpenStreetMap is the answer. The common request we get from users is for a way to edit the vector source data for global scenery (or sometimes, the request is to edit the features created by vector data). In other words: how does a user edit the coastlines, water bodies, and roads? With OpenStreetMap, OSM itself becomes the “editing” format for X-Plane scenery with DSF as the final result of baking.
After about a week of on and off hacking, I have finally knocked down one of the major stumbling blocks to getting WED 1.1 up to beta quality: exporting UV-mapped (texture mapped) bezier polygons that cross DSF borders. It works! Well, sort of.
If you have tried to program polygon cutting algorithms, you can appreciate the difficulty of an algorithm that:
- Clips polygons robustly (including holes and other weird topology) and
- Maintains a UV mapping while doing this and
- Works with bezier curves and not just line segments.
WED now does all three! This was the ugliest and hardest part of the DSF exporter, and a big missing piece from going beta.
Of course, there is one problem: X-Plane can’t read the bezier curves.
The problem is a simple defect in how X-Plane manages DSFs.
- A valid bezier polygon, fully inside the DSF tile, may have control handles that go outside the tile.
- X-Plane can’t handle any DSF coordinates outside the tile.
I am not sure what I will do about this, but in the short term, I fear X-Plane will remain limited. Probably the best short term option is to have WED at least flag such problematic bezier polygons; it is possible to approximate them or edit them to make the export work.
There is still a little bit more exporter code to write, including the line segment exporter (which is separate from the polygon exporter), but with luck the whole DSF export path should be cleaned up in the next few days.
Meshes in X-Plane, whether modeled in an OBJ, or generated as the results of other “3-d clutter” (road .net files, .for forest files, etc.) can be either one or two sided. So first: two-sided geometry is bad in most cases.
In order to understand why two sided geometry is bad, we must consider the alternative. The alternative to two-sided geometry is to simply create each triangle twice, with one facing in each direction. We can do this in an OBJ without making new vertices – because vertices are referenced by indices, we only need more indices, and indices are cheap.
Thus we have an alternative to two sided geometry, namely “doubled” one-sided geometry.
The first problem with two sided geometry is performance: for a small number of triangles, it is must faster to simply emit additional indices than to change the drawing mode to two-sided drawing on the CPU.
Thus in an object it is virtually never a good idea to use two-sided geometry. That ATTRibute will always be worse.
What about for the other clutter? Forests are currently always two-sided, but that’s okay; X-Plane enables two sided drawing just once, then draws a huge number of trees. Same with roads. For facades, there is a cost to using two sided geometry, so only use it for facades that must be two sided, like fences; do not use it for buildings.
Now the second problem with two sided geometry is lighting: X-Plane does not calculate lighting values separately for the two sides of the two-sided geometry. So if you have directionally lit models with two sided geometry, the lighting will look wrong. This is the second reason to use doubled geometry instead.
Things Are Starting To Look Up
There is a work-around to this problem of incorrect lighting on two-sided geometry: “up normals”. With up normals, the normal vector for the triangle (which is used to determine how light “bounces” off the triangle) is set to face straight up. The result is a triangle with brightest lighting at high noon, regardless of which way the triangle actually faces.
The good: the triangle looks the same on both sides and has sort of a “flat” lighting – it doesn’t look wrong when the sun is setting. The bad: the triangle has “flat” lighting – it looks non-3d.
We use up normals for forests because the forests are made of two-quad trees…the trees look less fake if directional lighting hints don’t make the two quads as obvious. You can simulate this in ac3d using the “make up normal” command for vegetation quads you put in your own models.
For roads, the geometry is two-sided, so we use up normals to avoid having the back of a road element look funny. Some day we may do something more sophisticated.
Fixing Facade Lighting
Facade lighting behavior will be changed in the next 950 release candidate. Before 950, facades would receive up normals, always. Starting in 950, facades will get correct normals if they are one sided and up normals if they are two-sided. This avoids artifacts with two-sided facades, but will make one-sided closed buildings look much better.
Normal maps in X-Plane 940 have a funny property: if you flip the normal map horizontally or vertically, the bumps change direction. Things that “stuck out” now “stick in” and vice versa. (If you flip the normal map horizontally and vertically, the two cancel out and the normal map is not reversed.)
You can understand this by thinking of your normal map as a physical piece of metal with bumps punched in it. Flipping it horizontally really means rotating it horizontally to see the other side – now you see the back side of the bumps. Same with the vertical flip. Flip both and you have flipped it twice and it’s front-side forward again.
In a move that is either just in the nick of time or dangerously reckless, I have tweaked the normal map shader for 950 RC1 (coming out “real soon”) to detect and “fix” a flipped normal. In 950 rc1 the bumps in your normal map will always point in the same direction as the normal of your mesh, even if your UV map is flipped horizontally or vertically.
What does this mean to you, the modeler? It means that you can now mirror your normal map from the left to the right side of the airplane and the normal map will still have the bumps “sticking out” like you want.
I crammed this into 950 RC 1 because it looks like it’s a useful change that will restore flexibility to authors making highly detailed airplanes. Mirroring a symmetric airplane (which results in a horizontally mirrored normal map) is a pretty common thing to do, and if the text is applied as decals, this can be a big win in texture space savings.
I figured best to get the tweak in here now so people could take advantage of it. Plus, what’s an RC1 without an RC2?
The file loading code in 950 beta 1 for Windows is slower than 945. Sometimes. This will be “fixed” in beta 2. Here’s what happened:
The scenery system uses a number of small files. .ter files, multiple images, .objs, etc. This didn’t seem like a problem at first, and having everything in separate text files makes it easier to take apart a scenery pack and see what’s going on.
The problem is that as computers get bigger and faster, rather than a scenery pack growing bigger files, they are growing more files. The maximum texture size has doubled from 1024×1024 to 2048×2048. But with paged orthophotos, multicore, and a lot of VRAM, you could easily build a scenery pack with 10,000 images per DSF.
That’s exactly what people are doing, and the problem is that loading all of those tiny files is slow. Your hard drive is the ultimate example of “cheaper by the dozen” – it can load a single huge file at a high sustained data rate. But the combination of opening and closing files and jumping between them is horribly inefficient. 10,000 tiny .ter files is a hard drive’s worse nightmare.
In 950 beta 1 I tried to rewrite part of the low level file code to be quicker on Windows. It appeared to run 20% faster on my test of the LOWI demo area, so I left it in beta 1, only to find out later that it was about 100% slower on huge orthophoto scenery packs. I will be removing these “optimizations” in beta 2 to get back to the same speed we had before. (None of this affects Mac/Linux – the change was only for Windows.)
The long term solution (which we may have some day) is to have some kind of “packing” format to bundle up a number of small files so that X-Plane can read them more efficiently. An uncompressed zip file (that is, a zip where the actual contents aren’t compressed, just strung together) is one possible candidate – it would be easy for authors to work with and get the job done.
In the short term, for 950 beta 2, I am experimenting with code that loads only a fraction of the paged orthophoto textures ahead of time – this means that some (hopefully far away part) of the scenery will be “gray” until loaded, but the load time could be cut in half.
There is one thing you can do if you are making an orthophoto scenery pack: use the biggest textures you can. Not only is it good from a rendering perspective (fewer, larger textures means less CPU work telling the video card “it’s time to change textures”) but it’s good for loading too – fewer larger textures means fewer, larger total files, which is good for your hard disk.
(Thanks to Cam and Eric for doing heavy performance testing on some of the 950 beta builds!)
Life would be easier if the Earth was a big cube.* I was reminded of this yesterday when I discovered (thanks to a bug report from an author) that X-Plane has been drawing objects with slightly incorrect heading for the last who-knows-how-long amount of time.
The bug (present in 945 and going back who-knows-how-long) is that the object’s heading can be off by up to 1 full degree clockwise or counter-clockwise. (This maximum error occurs at the north pole – average error for real use cases is more likely to be about 0.35 degrees.) The amount of rotation depends on how the scenery system is shifted.
The case the author sent me was a converted scenery pack, where a pair of parallel runways were modeled partly out of draped polygons (which don’t exhibit the bug), but with markings modeled as an OBJ. Depending on the scenery system’s “centering” the markings would be a little bit to the left or right of the draped polygon.
A brief aside: do not model your scenery this way! A bug is a bug, and X-Plane 950 beta 1 fixes this problem, but there are a number of reasons not to use a giant OBJ to model your runways.
- OBJs do not “hug” the terrain, and the Earth is round. You will never really get clean, artifact-free flat surfaces unless they can follow the terrain contour. Even if you “flatten” the terrain, it’s not flat – it’s constant MSL.
- The case where I see this more often is with buildings, where authors build every single terminal building in one OBJ. X-Plane only “connects” the OBJ to the ground at one point (0,0,0 in the object). So to get a good ground connection, you need your objects to be smaller, so they can all “sit” on the ground. (Technically they need to be rotated slightly to wrap around the Earth, but for the scale of an airport this error is only about 1/30th of a degree or so – hard to see.)
- Finally, X-Plane chooses to draw or not draw an object on a whole-OBJ basis. So if you have a huge object, it’s always in view (because at any one time some part of it is in view). In fact, even if the object is not in view, the larger the object, the more the “sloppiness” of the visibility check causes false positives. (The visibility check is optimized for speed, so it really answers “this object is probably in view”, erring on the side of drawing too much by doing less analysis.) So the bigger your objects, the more they are drawn.
Enough ranting – in this case the scenery was the output of FS2XPlane, and the author hadn’t gotten to cleaning the results. Fortunately the interim product showed the rotation bug.
X-Plane 950b1 (in beta now) fixes this problem – rotational alignment of a very large OBJ with a draped polygon should match between X-Plane (with all frames of scenery system reference) vs. WED 1.1 preview 2 pretty much exactly.
*This post is in the long and distinguished tradition of “life would be easier if” musings relating to scenery, e.g. global scenery would be easier if the entire planet was paved in asphalt, cities would be easier if their road grids only ran perfectly north-south and east-west, clouds would be easier if they weren’t translucent, and rendering would be easier if nothing on the planet ever was translucent or cast a shadow. So apparently a programmer’s paradise is somewhere between “Mad Max” and a Dali painting…
There’s a slight performance win to be had by grouping taxiways by their surface type.
Now clearly if you have to have an “interlocked” pattern of asphalt on top of concrete, on top of asphalt, this isn’t an option.
But where you do have the flexibility to reorder, if you can group your work by surface type, X-Plane can sometimes cut down on the number of texture changes, which is good for framerate.
X-Plane will try to do this optimization for you, but X-Plane’s determination of “independent” taxiways (taxiways whose draw order can be swapped without a visual artifact) is a bit limited and can only catch simple cases.
For what it’s worth, interlocked patterns of surfaces were much more a problem with old X-Plane 6/7 type airport layouts, where the taxiways were sorted by size, and there could be hundreds of small pieces of pavement.
Some of the newer text file formats for art assets (.lin files, roads, etc.) require you to specify how wide the texture is.
Huh? Why does X-Plane need me to tell it how wide a texture is? Doesn’t it know?
Well, first, the answer is: no! When X-Plane builds the roads and draped lines, the texture that is needed may not actually be loaded. It may only be on the todo list of textures that need loading, and if you have orthophoto scenery, that list could be very, very long. There is no guarantee that the texture is loaded before the geometry gets built. If you have a multicore machine, both might happen at the same time!
So the first reason why the road and .lin files require you to declare the size of your texture is: to allow X-Plane to build the mesh on a separate core from the one that loads the texture, for faster loading. Isn’t multi-core fun?
There’s another reason, and it’s a little bit more subtle: you can change the size of your X-Plane textures. Imagine that you get a new graphics card and you decide to “upgrade” your scenery pack. So you double the size of all of your textures, and then go in to photoshop and add more detail.
But if X-Plane were to look at the size of the texture, then all of your .lin files will be wrong! For example, let’s say you have a .lin file with a line from pixels 0 to 16 across the texture. When you double the size of your texture, you’d have to go in and change this to be 0 to 32 pixels across the texture.
But here’s the trick: since X-Plane gets the size of the texture from you and not from the actual texture, you can just leave your .lin file the way it was – with the old texture size and the old coordinates. X-Plane will do the math out and generate correct texture mappings even though the texture size is double.
In other words, the important thing about texture coordinates is that they are self-consistent, not that they reflect the actual texture size. This is because internally all texture mapping is done in ratios.
(So why not just use ratios in the file format? Try entering the size of 10 or 12 lines as ratios and tell me whether you like it. The ability to enter the line coordinates in pixels is to make it easier to set up the line files from the image file.)