User:Ayin/New terrain graphics system proposal

From The Battle for Wesnoth Wiki

This page is a working document for a proposal for a complete rewrite of the terrain graphics system. It may be considered as a counter-proposal to the extended terrains proposal.

Motivations for the proposal

The current terrain-graphics system is a powerful, albeit complex, system whose purpose is to transform maps (which are, basically, 2-dimensional arrays containing 1-character terrains) into a list of graphics for each location in the map.

The base for the system, and the root of a terrain_graphics rule, is: "pattern of terrains => associated images". In my (Ayin (talk)) opinion, pattern-based rules are the way to go: those are simple to understand, and allow nearly anything.

However, the current system (see documentation here TerrainGraphicsWML, or, better, here) has several drawbacks, which I will try to detail below:

Very WML-intensive

The basic, pattern => images system, is quite hard to type effectively in WML.

One simple, standard rule takes about 20 to 50 lines of WML. Considering that completely defining a "castle" with a keep takes about 7 rules (using rotation symetries), and a basic transition takes 4 rules (again, using rotation symetries), this means about 200 lines of WML to completely define a transition.

To solve this problem, the WML macro system is used quite intensively. Considering that the terrain-graphics.cfg file only contains macros and comments, and is about 800 lines long, and considering each macro rule generates, more or less, 40 lines of WML, this amounts to many lines of WML for the engine. Some testing shown this amounts to about 13000 lines in Wesnoth 1.1-svn. This is about 1/4 of the WML cache.

The problem with this use of macros is that directly coding terrain_graphics rules is pretty much deprecated: it is recommended to use macros which re-use already-created rulesets instead of directly using new ones. We are more or less falling in the situation of Sendmail, where what was originally a "config file" became "program logic", and where the "config file" uses some macro system to transform higher-level macros into the actual low-level file which is then parsed by the compiled program.

This is a bad thing.

Relatively slow

Although there have been many optimizations in this way, rebuilding the list of terrain graphics (when, for example, a terrain has changed) still is pretty slow on older computers.

Quite a complex system

The pattern => images system is something very simple, conceptually. However, to be able to properly generate terrain graphics, the following subtelties must be taken into account:

Rule overlapping
The main problem with this approach is to specify precedence, and mutual-exclusiveness, of rules. For example, rules can be mutually-exclusive:
  • on an hex (only one rule of this "kind" can apply on a given hex)
  • on a hex border (only one rule of this "kind" can apply on a given hex border)
  • or on a hex corner (only one rule of this "kind" can apply on a given hex corner)
Furthermore, some rules have to be independant one from the other (for example, rules defining the base terrains and transitions, and rules defining the overlay graphics, are totally independant), and some rules are mutually exclusive (for example, all rules defining base terrain transitions are mutually exclusives on hex borders, all rules defining base terrains are mutually exclusive on hexes, all rules defining some kind of castle-like fence are mutually exclusive on hex corners).
To solve this problem, a rule may apply "flags" on some hexes, and check for the presence, or absence, of flags before being applyied.
Image ordering and adjustment
Images will necessarily have to overlap. The overlapping order of images is the source of many potential graphical glitches. A simple, layer-based mode to specify the overlapping order of images is not enough, nor is the "drawing in the order the rules are defined" solution.
The current system uses an ordering model based on 2 different shapes for images:
  • Images representing terrains layered over the floor. Those use a simple layer-based model
  • Images representing terrains standing vertically on the floor. Those have x,y coordinates, and are layered in the logical order. Specifying those coordinates seems quite hard to many terrain developers.
Using symetry properties
Most rules (hopefully) have 6-way rotational symetry properties. If they had not, the already huge amount of WML used would actually have to be multiplied by 6. However, defining template-like rules which spawn 6 actual, rotated rules is not trivial:
  • The rule itself can be rotated, but not the images themselves. Said images must have different filenames. The rule rotation must then have a way to specify the name of those files.
  • Same for flags and rule overlapping
  • Same (worse?) for layering of "vertical" images.

Hard to understand

Overall, the system has many possibilities, which have successfully been used to expand the graphics possiblities on Wesnoth. However, it looks like that few (no?) people understand it enough to use it effectively, and that the only solution, for graphics designers, to implement new ideas or to fix glitches is to ask me, on IRC, how to do it. As said above, the system is split in two parts:

  • A WML part which can be considered program logic, which is quite complex. For a long time I (Ayin (talk)) was the only one to actually commit changes in this part. There has only been one other commit, made by Darth Fool for use with the ruins graphics. He has promptly forget everything he knew about terrain WML and continues to point people at Ayin when questions are asked of him.
  • A WML-macro parts which is more-or-less accessible to artists, but which still retains a large part of black magic. (For example: "{TERRAIN_ADJACENT_CORNER_PROB K q  !NQqCK 56,76 sunken-ruinkeep3-wall-1 25}": who know what those "56,76" numbers mean? And what would happen if those were changed?)

I've been starting to think the problem is that the system is too powerful, and that going all-WML may not necessarily be a good idea. Being generic is good, being too generic may prove harmful.

Proposals

Part 1: Make terrain_graphics WML driven by terrain WML

Part 1 of the proposal is to make terrain WML define which rules are to be associated to a given terrain. For example:

 [terrain]
    id=grassland
    name= _ "Grassland"
    char=g
    graphics=grassland
 [/terrain]
 [terrain]
   symbol_image=village-elven-tile
   id=village
   name= _ "Village"
   char=t
   heals=true
   gives_income=true
   graphics=grassland,elven-village
 [/terrain]
 [terrain]
   symbol_image=forest-tile
   id=forest
   name= _ "Forest"
   char=f
   graphics=grassland,forest
 [/terrain]

The terrain_graphics rules would not apply on terrain characters anymore, but would, instead, apply on symbols like "grassland", "forest", "village", etc.

Part 2: Use modular terrain graphics code

Basics

If only coders can modify the rulesets which build terrains, that's OK, but then, there's no point anymore to define rulesets using WML then.

The proposal is to have a common C++ API which would be used to define terrain graphics modules. Terrain graphics WML would then be used to instantiate such modules with parameters.

The following WML elements would be present:

module
The name of the module, as defined in the game code.
terrain_type_X
A "terrain type" parameter. Modules may require 1, 2, or more terrain types. Those are module-specific. Those actually correspond to the graphics WML element of the terrain WML (see part 1 of the proposition)
image_basename
A parameter from which the module will derive the actual name of the images used (for example, if this is "grassland", the module "base-terrain" will determine that the names of the actual files to use are "grassland.png", "grassland-nw.png", etc.

Other

  • Specify several image probabilities
  • Specify animations
  • Other types of per-module parameters?

Examples

 [terrain_graphics]
   module="base-terrain"
   terrain_type_1="grassland"
   terrain_type_2="! grassland"
   image_basename="grassland:80,grassland-v2:10,grassland-v3:10"
 [/terrain_graphics]
 [terrain_graphics]
   module="overlay-image"
   terrain-type_1="village"
   image_basename="human-village"
 [/terrain_graphics]

The castles would still have a bit of complexity in them, as there are many kinds of 3-way transitions possibles.

 [terrain_graphics]
   module=wall
   terrain_type_1="castle"
   terrain_type_2="!castle,!keep,!encampment"
   terrain_type_3="!castle,!keep,!encampment"
   image_basename="castle-convex"
 [/terrain_graphics]
 [terrain_graphics]
   module=wall
   terrain_type_1="!castle,!keep,!encampment"
   terrain_type_2="castle"
   terrain_type_3="castle"
   image_basename="castle-concave"
 [/terrain_graphics]

Modules to create

According to what is currently done in the code, the following modules would need to be developed:

  • base-terrain. Basic terrain with standard transitions. to remplace TERRAIN_BASE, TERRAIN_BASE_PROB, and TERRAIN_ADJACENT.
  • wall. To replace terrain-adjacent-corner.
  • overlay-image. To replace SHEX and BUILDING.
  • canyon.
  • bridge.

Module API

This is a proposal for a terrain_graphics module API. Modules should, ideally, be very short, independant fragments of C++ code.

 // Represents an image set as defined in the [terrain_graphics] WML, and handling probs
 // and animations transparently.
 // operator+(const std::string&) is defined, to add a suffix to animated or probabilized images.
 class imageset;
 
 class terrain_pattern;
 
 class module 
 {
 public:
     module(gamemap& map, imageset& set);
     
     virtual void fill_terrains_for(const gamemap::location&)
 protected:
     const gamemap& map() const;
     void insert_image_flat(int layer, imageset&) const;
     void insert_image_vertical(pos, imageset&) const;
     
     bool tile_matches(const gamemap::location& loc, const terrain_pattern& pattern);
     const gamemap::location adjacent_tile(const gamemap::location& loc, int position);
 
     imageset& image_basename() const; // Corresponds to image_basename in the WML
     std::vector<terrain_pattern> terrain_types; // Corresponds to terrain_types in the WML
 
     std::string corner_name(int corner);
     std::string edge_name(int edge);
}
 // Sample code for the "wall" module
 module_wall : public module
 {
 public:
     virtual void fill_terrains_for(const gamemap::location& loc)
     {
        for (int i = 0; i < 6; ++i) {
          if (tile_matches(loc, tile_terrain_types[0]) &&
              tile_matches(ajacent_tile(loc, i),  terrain_types[1]) &&
              tile_matches(adjacent_tile(loc, i+1), terrain_types[2])) {
 
             insert_image_vertical(i, image_basename() + corner_name(i));
          }
     }
 }

Part 3: Sanitize the vertical layering code

The vertical layering system, using x,y coordinates for the "base point" of a vertical item, is more complicated than necessary. The following points must be taked into account:

  • Only y-layering is important. x-layering was only used when rotating rules, which, according to part 2, will be directly managed by modules.
  • The "pixel size" of a tile, and x-layering of images, are totally unrelated concepts. There is no need for 72 layers per hexes. 3, 5, or 7 layers should be enough.
  • Calculating the rectangular coordinates of a point in a hex-system is a pain. Coordinates should be expressed relatively to hexes, not in cartesian coordinates.
  • The "backside" layer of an hex should always be in front of the "frontside" layer of hexes behind it. This rule should save us many headaches about graphic glitches.
This page was last edited on 16 October 2005, at 12:17.