Orbiter-Forum  

Go Back   Orbiter-Forum > Blogs > martins
Register Blogs Orbinauts List Social Groups FAQ Projects Mark Forums Read

Rating: 10 votes, 5.00 average.

Developer masterclass: Creating 2-D panels the new way

Posted 09-09-2010 at 04:28 AM by martins

Orbiter 2010 introduces a new way to define 2-D panels. Documentation for this is still thin on the ground (but under development), and currently only the stock DG provides an example for it.

I thought that maybe a small tutorial series may be the way to nudge developers towards adopting the new style. So here goes ...

Why should I switch to the new interface?
The new panel style has a number of advantages over the legacy (2006) method:
  • It is better supported by modern render engines, and therefore by external Orbiter graphics clients, present and future
  • It can avoid visual artefacts caused by driver incompatibilities
  • It improves frame rates
  • It allows cool animation effects using smooth mesh transformations
  • It provides an automatic scaling mechanism
So how is it different from the legacy method?
The old method defined panels as bitmaps that were blitted on top of the render window. Animations were performed by modifying the bitmap (either using GDI drawing, or blitting small bitmap elements representing switches, sliders, instrument needles, etc.) into it.
The new method defines the panel as a textured 2-D mesh. In effect, it works like a flat virtual cockpit. You get all the benefits of the mesh representation (complex shapes, vertex animation, using textures with transparency). It directly uses the 3-D render engine's functionality to display the panel on the screen, rather than using an old-fashioned blitting operation after the renderer is done.

Lesson 1: The panel request callback function
Whenever the vessel switches to a new 2-D panel cockpit view (either from an outside view or another cockpit view), it calls the clbkLoadPanel2D callback function. This is the point where we need to define the panel geometry and functions. For now, we are going to implement only a single main panel:
Code:
bool MyVessel::clbkLoadPanel2D (int id, PANELHANDLE hPanel,
  DWORD viewW, DWORD viewH)
{
  switch (id) {
  case 0: 
    DefineMainPanel (hPanel);
    ScalePanel (hPanel, viewW, viewH);
    return true;
  default:
    return false;
  }
}
Note that clbkLoadPanel2D has been introduced in the VESSEL3 interface, so your vessel class must be derived from VESSEL3 to make use of it. clbkLoadPanel2D is the equivalent of clbkLoadPanel for the old-style 2-D panel interface. If your vessel defines clbkLoadPanel2D, it should not also define clbkLoadPanel.
The id parameter defines the panel (the main panel has always id 0, but additional neighbour panels can be defined as well). The hPanel object is a handle that is required by various functions during the definition of the panel. The viewW and viewH parameters define the width and height of the viewport in pixels, which can be useful for scaling purposes.
Now we need to implement the DefineMainPanel function which defines the panel mesh, textures and active areas.

Lesson 2: The panel mesh
2-D instrument panels are defined as 2D meshes. Orbiter uses the same mesh format for 2-D meshes as it does for 3-D meshes (used e.g. to describe vessel and virtual cockpit geometries), with the exception that the vertex z-coordinates for 2-D meshes are ignored and should be set to 0.
The mesh coordinate system to which the mesh vertex coordinates refer can be freely chosen by the developer. A convenient convention is to set the bottom left corner of the mesh to coordinates (0,0), and the top right corner to coordinates (px,py), where px and py are the width and height of the panel background texture in pixels. With this convention, mesh coordinates correspond to the pixel positions of the background texture.
As a first example, let’s start with a simple rectangular panel, which can be defined with 4 vertices and 2 triangles. If we plan for a panel texture of dimension 1280x400, then the mesh would look like this:
Code:
Vertex coordinate list (0,0,0), (0,400,0), (1280,400,0), (1280,0,0)
Triangle index list (0,2,1), (2,0,3)
In principle it is possible to put this mesh definition into a standard Orbiter mesh file, and read it when required with oapiLoadMesh. However, this mesh is so simple that it is more efficient to define it directly in the vessel code. Defining the 2D panel mesh in the code will later also have the advantage that we are better able to control animations and moving parts which require direct access to the vertex lists. The main panel mesh definition could look like this:
Code:
void MyVessel::DefineMainPanel (PANELHANDLE hPanel)
{
  static DWORD panelW = 1280;
  static DWORD panelH =  400;
  float fpanelW = (float)panelW;
  float fpanelH = (float)panelH;
  static NTVERTEX VTX[4] = {
    {      0,      0,0,   0,0,0,   0,0},
    {      0,fpanelH,0,   0,0,0,   0,0},
    {fpanelW,fpanelH,0,   0,0,0,   0,0},
    {fpanelW,      0,0,   0,0,0,   0,0}
  };
  static WORD IDX[6] = {
    0,2,1,
    2,0,3
  };

  if (hPanelMesh) oapiDeleteMesh (hPanelMesh);
  hPanelMesh = oapiCreateMesh (0,0);
  MESHGROUP grp = {VTX, IDX, 4, 6, 0, 0, 0, 0, 0};
  oapiAddMeshGroup (hPanelMesh, &grp);
  SetPanelBackground (hPanel, 0, 0, hPanelMesh, panelW, panelH, 0,
    PANEL_ATTACH_BOTTOM | PANEL_MOVEOUT_BOTTOM);
}
Here, hPanelMesh is assumed to be a MESHHANDLE object defined as a member of MyVessel. The call to oapiCreateMesh creates an empty mesh, to which the group for the main panel background is added by oapiAddMeshGroup.
Since the hPanelMesh object may be shared with other cockpit panel views, we need to check if it is allocated already, and delete it before defining the new one, using the oapiDeleteMesh function. For this to work, it must be initialised it to NULL in the constructor:
Code:
MyVessel::MyVessel(OBJHANDLE hObj, int fmodel)
{
  ...
  hPanelMesh = NULL;
  ...
}
To avoid memory leaks, the destructor should delete the mesh if required:
Code:
MyVessel::~MyVessel ()
{
  ...
  if (hPanelMesh) oapiDeleteMesh (hPanelMesh);
  ...
}
The SetPanelBackground call in our DefineMainPanel function registers the panel mesh with Orbiter. Its parameters are:
  • The panel handle, as provided by clbkLoadPanel2D
  • A list of textures, and the number of textures in the list (set to 0 for now – we’ll come back to that in Lesson 4 below)
  • The panel mesh handle
  • The width and height of the panel in mesh units
  • The panel base line
  • The viewport attachment and scroll flags

Lesson 3: Scaling the panel
Now we have to think about scaling the panel to the viewport. This will be done in the ScalePanel method that has already been called in clbkLoadPanel2D.
By default, we want to scale the panel so that it fills the width of the viewport, independent of its actual size. This can be done painlessly by using the VESSEL3::SetPanelScaling method. This is a big improvement over the old-style panel definitions, which only provided an awkward global scaling option. In addition, we can also define a zoom option that magnifies the panel. This will only display a part of the panel, but other parts can be scrolled in. This is particularly useful for small viewport sizes, where scaling the panel to fit would make it too small to use. The user can switch between standard and magnified scaling with the mouse wheel.
The scaling parameters passed to SetPanelScaling are magnification factors that describe how many viewport pixels should be covered by one mesh unit. The implementation of ScalePanel could therefore look like this:
Code:
void MyVessel::ScalePanel (PANELHANDLE hPanel, DWORD viewW, DWORD viewH)
{
  double defscale = (double)viewW/1280.0;
  double magscale = max (defscale, 1.0);
  SetPanelScaling (hPanel, defscale, magscale);
}
The defscale factor makes sure that the panel (defined as size 1280) stretches over the full viewport width (viewW). The magscale factor magnifies the panel such that 1 mesh unit covers one screen pixel if the viewport width is smaller than the panel width. This is a sensible convention, but of course you are free to implement different scaling strategies for your panels.

Lesson 4: Adding a panel background texture
We now want to draw a texture over the bare panel mesh. The texture serves the same function as the bitmap in the old-style panel definitions, but it must be stored in DDS format, rather than BMP format. You may have to experiment with the compression format, but usually DXT1 is best if no or only binary transparency is required, or DXT5 if continuous transparency is required.
Another important restriction is the fact that textures must have sizes that are multiples of 2. So for our 1280x400 texture we will have to create a 2048x512 pixel texture. For now this is a lot of waste, but we can use the same texture to add additional panels and active elements later on. Sometimes you may also be able to reduce the required texture size by clever mesh design and re-using the same texture elements multiple times (e.g. defining the right half of the panel as a mirror of the left).
The panel texture is a global resource (it is shared by all vessels of the MyVessel class), so we can make it static and load it during module initialisation:
Code:
// vessel class interface
class MyVessel: public VESSEL3
{
public:
  ...
  static SURFHANDLE panel2dtex;
  ...
};

// static member initialisation
SURFHANDLE MyVessel::panel2dtex = NULL;

// module initialisation
DLLCLBK void InitModule (HINSTANCE hModule)
{
  ...
  MyVessel::panel2dtex = oapiLoadTexture (“MyVessel\\panel2d.dds”);
  ...
}

// module cleanup 
DLLCLBK void ExitModule (HINSTANCE hModule)
{
  ...
  oapiDestroySurface (MyVessel::panel2dtex);
  ...
}
where the panel texture is assumed to be located in file Textures\MyVessel\panel2d.dds.
We can now modify the DefineMainPanel method to make use of the background texture:
Code:
void MyVessel::DefineMainPanel (PANELHANDLE hPanel)
{
  static DWORD panelW = 1280;
  static DWORD panelH =  400;
  float fpanelW = (float)panelW;
  float fpanelH = (float)panelH;
  static DWORD texW   = 2048;
  static DWORD texH   =  512;
  float ftexW   = (float)texW;
  float ftexH   = (float)texH;
  static NTVERTEX VTX[4] = {
    {      0,      0,0,   0,0,0,            0.0f,1.0f–fpanelH/ftexH},
    {      0,fpanelH,0,   0,0,0,            0.0f,1.0f              },
    {fpanelW,fpanelH,0,   0,0,0,   fpanelW/ftexW,1.0f              },
    {fpanelW,      0,0,   0,0,0,   fpanelW/ftexW,1.0f-fpanelH/ftexH}
  };
  static WORD IDX[6] = {
    0,2,1,
    2,0,3
  };

  if (hPanelMesh) oapiDeleteMesh (hPanelMesh);
  hPanelMesh = oapiCreateMesh (0,0);
  MESHGROUP grp = {VTX, IDX, 4, 6, 0, 0, 0, 0, 0};
  oapiAddMeshGroup (hPanelMesh, &grp);
  SetPanelBackground (hPanel, &panel2dtex, 1, hPanelMesh, panelW, panelH, 0,
    PANEL_ATTACH_BOTTOM | PANEL_MOVEOUT_BOTTOM);
}
The texture coordinates for the mesh vertices have now been defined (where I am assuming that the main panel image is located in the lower left corner of the texture). The call to SetPanelBackground contains a pointer to the texture handle, and the number of textures (1). If your panel mesh references more than one texture, put them in a list, pass the list as the second parameter of SetPanelBackground, and the number of textures in the list as the third parameter.

At this point, you can compile your vessel code and run it in Orbiter. It isn’t very exciting yet (a static panel background texture covering the lower half of the screen), but it is the basis for the next steps. You should be able to scroll the panel up and down with the cursor keys.

Part 2 of this tutorial adds an MFD display to the new panel.
Views 14012 Comments 7
« Prev     Main     Next »
Total Comments 7

Comments

  1. Old Comment
    Xyon's Avatar
    I'm watching... :D

    This really illustrates the improvements you've made to the panels since VESSEL2. It seems much simpler to implement them than the old way, too, but maybe that's just me.

    One slight niggle, though: It's spelled "Lesson". :/
    Posted 09-09-2010 at 04:58 AM by Xyon Xyon is offline
    Updated 09-09-2010 at 03:52 PM by Xyon
  2. Old Comment
    Moach's Avatar
    wow cool! i'll read it in depth after i've had my lunch...

    for now, just a question, how can this new method be used for drawing to dynamic VC textures? - sorry if this is covered in the tutorial, as i mentioned, i have yet to read it properly

    Posted 09-09-2010 at 04:01 PM by Moach Moach is offline
  3. Old Comment
    Quote:
    Originally Posted by Xyon View Comment
    One slight niggle, though: It's spelled "Lesson". :/
    Er, yes . Corrected now. This is what you get if you spend the night writing tutorials instead of sleeping.
    Posted 09-09-2010 at 06:47 PM by martins martins is offline
  4. Old Comment
    Quote:
    Originally Posted by Moach View Comment
    for now, just a question, how can this new method be used for drawing to dynamic VC textures?
    Patience - dynamic mesh animations will be covered later. Although this tutorial is about 2D panels, not virtual cockpits, the same techniques will apply to those.
    Posted 09-09-2010 at 06:51 PM by martins martins is offline
  5. Old Comment
    Moach's Avatar
    cool! thanks
    Posted 09-09-2010 at 07:10 PM by Moach Moach is offline
  6. Old Comment
    jedidia's Avatar
    Now, this was really helpfull, I'm finally seeing something on my screen

    One note, in the line

    Code:
        {      0,      0,0,   0,0,0,            0.0f,1.0f–fpanelH/ftexH},
    The minus sign in front of fpanelH is actually a score, which produces an error.
    Posted 01-27-2012 at 10:34 AM by jedidia jedidia is offline
  7. Old Comment
    BruceJohnJennerLawso's Avatar
    Well, I tried to implement this in my project, and it all worked fine up until a rather strange error. When I put in the part in InitModule, like this,

    DLLCLBK void InitModule (HINSTANCE hModule)
    {
    g_hDLL = hModule;
    hFont = CreateFont (-20, 3, 0, 0, 150, 0, 0, 0, 0, 0, 0, 0, 0, "Haettenschweiler");
    hPen = CreatePen (PS_SOLID, 3, RGB (120,220,120));
    hBrush = CreateSolidBrush (RGB(0,128,0));
    ShuttleD::panel2dtex = oapiLoadTexture (“ShuttleD\\MainPanel.dds”);

    My compiler tells me that the name ShuttleD, under the filepath given in the last line, is undefined. That doesnt make any sense does it?
    Posted 08-15-2012 at 03:17 AM by BruceJohnJennerLawso BruceJohnJennerLawso is offline
 

All times are GMT. The time now is 03:54 AM.

Quick Links Need Help?


About Us | Rules & Guidelines | TOS Policy | Privacy Policy

Orbiter-Forum is hosted at Orbithangar.com
Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2019, vBulletin Solutions Inc.
Copyright ©2007 - 2017, Orbiter-Forum.com. All rights reserved.