World
Wide Guide | Knowledge Bank
| Kukushkin's Notebook |
Design Fundamentals
The scenery complexity is limited not only by the
drawing speed. FS5 has also internal limits on complexity of scenery files
and the amount of information it can hold in memory. Some of these limits
can be tricked out, thus allowing the creation of more complex sceneries
than would be possible by a straightforward programming.
Visibility tests in SDL instructions work by changing the execution order and thus avoiding executing instructions for drawing objects that are far away. They do not affect the way the scenery is stored in memory. Unlike that, visibility tests for Area() blocks work by loading or removing individual Area()-blocks from memory. FS5 does not keep all visual scenery in memory all the time. Each Area()-block is loaded separately and only if visibility conditions determined by the block type and its range parameter are satisfied. Only SDL code in blocks that are present in memory is executed.
FS5 periodically checks all Area()-blocks, both active (present in memory) and inactive. Each block has a minimum (often zero) and a maximum visibility range.
When FS5 determines that SOME blocks need to be loaded into or removed from memory, it compares for EACH block its distance to the viewpoint with its visibility range. It loads all blocks that are within their visibility range from the viewpoint and removes all other blocks from memory. It is very important to understand that blocks are not loaded/unloaded simply when the aircraft crosses their visibility borders, but their visibility checks must be triggered by some event and, when triggered, are made for all blocks at the same time, not only for the block that triggered them.
FS5 allows the viewpoint to move a certain distance away from the visibility range of an active Area()-block without triggering the reloading process. Similarly, the viewpoint can move a certain distance into the visibility range of an inactive block. In both cases, the reloading process is triggered only when this distance is exceeded. For this reason, the distance at which an Area()-block is deactivated is greater than the distance at which it is activated. This is often called a hysteresis effect.
The reloading process is also triggered after a situation reset or the Ok button in the scenery library dialog box is pressed.
The maximum possible visibility range for Area(5)s is 40km, which is also the minimum range for Area(8)s. The maximum possible visibility range for Area(8)s and Area(B)s is 130 km. The maximum visibility range can be further restricted by the range parameter of the Area() instruction.
The total size of all active Area()-blocks from all sceneries is limited to slightly less than 256 KBytes. Blocks that do not fit into this limit are discarded. Because it is never known which block will be actually discarded, the scenery must ensure it fits well into this limit. Furthermore, it should leave a significant portion of these 256K free in order not to cause conflicts with other hypothetical sceneries that might be active in the same area.
This limit is normally not reached by small sceneries. However, it can easily be reached in more complex sceneries or when a scenery displays many complex objects. Because such objects are often developed as macros and simply called from the main source file, the (small) size of the source can easily create an illusion that the scenery occupies much less memory than it really does.
The simplest solution here is to reduce visibility ranges of Area()s. This would reduce the number of active Area()s, thus reducing the amount of memory they occupy. However, the visual quality of the scenery would suffer because of unrealistic low visibility ranges of objects.
There are also some tricks that allow reducing the total size of Area()s concurrently present in memory almost without reducing the visual quality of the scenery.
The basic idea here is that the whole complexity of objects or landscape can be visible only from a relatively small distance. From a bigger distance, most memory-consuming details are simply not visible because of the limited screen resolution. So the code that displays these details does not have to be present in memory as long as the object is visible, but only when it is close enough for these details to be seen.
In most cases it is impossible to "add" details to an object already drawn in its basic shape. For this reason, two (or more) object drawing routines are normally developed which display the object or the part of the landscape at different detail levels. More detailed (and memory-consuming) routines should be put in blocks with a smaller visibility range.
The simplest variant is to put a detailed routine into an Area(5)-block with the maximum visibility range and to put a less detailed routine into an Area(8)-block. The center coordinates for both blocks should be the same, of course. Because the maximum visibility range for Area(5) is also the minimum range for Area(8), both blocks can never be active at the same time. The maximum visibility here would be determined by the range parameter of Area(8). A typical code should look like this:
Area( 5 ... ... 255 ) ; The block with the detailed routine
PerspectiveCall( :P )
Jump( : )
:P
... ; Draw a detailed version of the object
EndA
Area( 8 ... ) ; The block with the less detailed routine
PerspectiveCall( :P )
Jump( : )
:P
... ; Draw a less detailed version
EndA
While this approach is the easiest to implement, it is not very flexible because the minimum visibility range for Area(8) cannot be changed to a different value. Sometimes a much smaller visibility range for the detailed routine is desired.
In such cases, the visibility range for Area(5) has to be set to a value below the maximum. This makes it impossible to put the less detailed routine in an Area(8), because its minimum visibility range cannot be adjusted to match the range for Area(5). The less detailed routine thus has to be put into an Area(B) or another Area(5), depending from its maximum visibility range. While this would cause both routines to be in memory when the viewpoint comes close to the object, it requires much less memory when the less detailed routine is in use. Because the less detailed routine is supposed to consume a relatively small amount of memory, the overhead of having both routines in memory on certain occasions can be neglected.
Because the Area()-block containing the less detailed routine would remain in memory even when the more detailed routine would be active, the less detailed routine must somehow determine whether the block containing the more detailed routine is active. This can be done by putting its Area()-Block immediately after the block with the more detailed routine. The detailed routine should set some variable to some distinctive value, and the less detailed routine should check this variable. A good variable for this purpose is 033C. It normally contains the distance to the RefPoint in 256meter units and is re-initialized each time a RefPoint is defined or a PerspectiveCall() executed. For this reason, changing its value does not have any side effects lasting beyond the next RefPoint()/PerspectiveCall() instruction. Also, this variable normally contains only small values because of the big unit size. A typical code should look like this:
Area( 5 ... ) ; The block containing the detailed routine
PerspectiveCall( :P ) ; PerspectiveCall() the detailed
routine
SetVar( 033c 32766 ) ; Set the variable immediately before
exiting
Jump( : ) ; this Area() block
:P
... ; Draw a detailed version of the object
EndA
Area( B ... ) ; The block with the less detailed routine
IfVarRange( :Draw 033c 32766 32766 ) ; Was the previous block
executed?
SetVar( 033c 0 ) ; Yes - avoid confusing Area() blocks that
follow
Jump( : ) ; and exit the block without drawing anything
:Draw ; No - draw the less detailed version
PerspectiveCall( :P ) ; PerspectiveCall() will also reset
033C
... ; Draw a less detailed version of the object
EndA
PerspectiveCall()s here should be removed and drawing routines called or jumped to directly if a 2-D object is to be drawn.
Note: it is very important to reset the variable 033C to a reasonable value in the second block. Otherwise, some other blocks implementing the same algorithm could assume detailed versions of their objects have already been drawn even if this would not be the case.
The size of an Area() block is limited to 16364 bytes without the Area() header. FS5 discards bigger blocks. Normally, splitting the code in Area()-blocks not exceeding this limit is not a problem. However, it can be a bit difficult if a PerspectiveCalled object has to be split this way.
Because a PerspectiveCalled routine must be located within one block, the object has to be split into two or more PerspectiveCalled parts, and each of them put into a separate Area()-block. The main difficulty here is to determine the locations for RefPoints of separate parts in order to ensure the correct drawing order. Splitting objects is discussed in more detail in "How to place 3D objects".
Because FS5 has to collect all addresses of PerspectiveCalled and LayerCalled routines before executing them, the number of these routines executed per frame is limited. Exact numbers can be found in the FSASM documentation.
It is impossible to increase this limit, so the only way of increasing the number of 3-D objects seen at the same time is to merge them into bigger objects displayed with a common PerspectiveCall().
Sometimes 3-D objects perform a visibility test inside the PerspectiveCalled routine, and do not draw themselves in certain cases, like a big distance to the viewpoint or a low scenery density setting. In such cases, the visibility test can be performed before PerspectiveCall() instead. For example, the code fragment
PerspectiveCall( :P ) ; Collect the object drawing routine
address
...
:P
Perspective
RefPoint( ... )
IfVarRange( :Do_not_draw 0346 2 4 ) ; Check the scenery
density
... ; Draw the object
would always consume one PerspectiveCall(). It could be rewritten as:
IfVarRange( :Do_not_draw 0346 2 4 ) ; Check the scenery density
first
PerspectiveCall( :P ) ; Collect the object drawing routine
address
...
:P
Perspective
RefPoint( ... )
... ; Draw the object
In this case, the address of the PerspectiveCalled routine would only be collected after a passed visibility test.
Before collecting the routine address, PerspectiveCall() performs the visibility test for the RefPoint used for sorting. The address is discarded if this visibility test fails. For this reason, simple tests using V1= or V2= can be performed inside the routine as well, thus sparing an additional RefPoint() instruction.
The visibility range of each RefPoint is limited to 32767 RefPoint. This is caused by the 16-bit nature of the graphics engine. If the distance between the viewpoint and a RefPoint exceeds this limit, the visibility test fails and a jump to the label specified in the RefPoint instruction is performed.
This means all objects drawn around the RefPoint must be well within the 32767-unit boundary. Otherwise, the visibility of such objects would be very short from the side opposite to the RefPoint.
One specific problem is the visibility on the map view. Unlike other view windows, the zoom factor of the map view is modeled by changing the altitude of the viewpoint. At high altitudes, the (mostly vertical) distance can easily exceed the visibility range of RefPoints with small scale factors. The minimum scale factor visible from any altitude is 9.
Solutions include either not using RefPoints with small scale factors or displaying a simplified version of the object with a larger scale factor if the visibility test in the first RefPoint fails:
RefPoint( ... :Less_detail ... )
... ; Draw a detailed version of the object
Jump( ... )
:Less_detail
RefPoint( ... ) ; A RefPoint with a bigger scale factor
... ; Draw a less detailed version with a bigger scale factor
Note however that the above example would not work correctly if the first RefPoint would immediately follow Perspective() in a 3-D object drawing routine, because PerspectiveCall() would discard the routine address should the visibility test fail. In such objects, one extra RefPoint is needed in order to pass the visibility test made by PerspectiveCall().
The total number of texture files FS5 can load at once is limited by the amount of expanded memory available. While sceneries that use only stock textures normally stay well within this limit, photorealistic and pseudophotorealistic sceneries can use more texture files than available RAM. This would cause some textures not to appear and also significantly reduce the performance. The simplest solution is to reduce the visibility range of some textures, thus requiring less textures to stay in memory at the same time. Also, it is possible to generate reduced resolution texture files. Each of them would hold reduced-resolution versions of multiple texture files. Reduced resolution textures could then be used for textured surfaces that are too far away to be seen in detail.
The graphics card can handle only one palette at a time. If sceneries active in a given area attempt to load more than one custom palette file, some textures will appear in wrong colors. Also, the Palette() [FSASM: PaletteFile] instructions performs _very_ slowly when it is instructed to load a palette different from the current one.
For this reason, sceneries that use multiple custom palettes should be carefully programmed in order to prevent palette conflicts. Also, the visibility range for the custom palette and textures using it should be set to a reasonable value in order to prevent possible palette conflicts with neighboring sceneries that can also use custom palettes.