Developer masterclass: Creating 2-D panels the new way

martins

Orbiter Founder
Orbiter Founder
Joined
Mar 31, 2008
Messages
2,448
Reaction score
462
Points
83
Website
orbit.medphys.ucl.ac.uk
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:
[COLOR=Gray]// vessel class interface
class MyVessel: public VESSEL3
{
public:
  ...[/COLOR]
  static SURFHANDLE panel2dtex;
[COLOR=Gray]  ...
};[/COLOR]

[COLOR=Gray]// static member initialisation
[/COLOR]SURFHANDLE MyVessel::panel2dtex = NULL;

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

// module cleanup[/COLOR] [COLOR=Gray]
DLLCLBK void ExitModule (HINSTANCE hModule)
{
  ...
[/COLOR]  oapiDestroySurface (MyVessel::panel2dtex);
[COLOR=Gray]  ...
}[/COLOR]
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:
[COLOR=Gray]void MyVessel::DefineMainPanel (PANELHANDLE hPanel)
{
  static DWORD panelW = 1280;
  static DWORD panelH =  400;
  float fpanelW = (float)panelW;
  float fpanelH = (float)panelH;
[/COLOR]  static DWORD texW   = 2048;
  static DWORD texH   =  512;
  float ftexW   = (float)texW;
  float ftexH   = (float)texH;
[COLOR=Gray]  static NTVERTEX VTX[4] = {
    {      0,      0,0,   0,0,0,            [/COLOR]0.0f,1.0f–fpanelH/ftexH[COLOR=Gray]},
    {      0,fpanelH,0,   0,0,0,            [/COLOR]0.0f,1.0f              [COLOR=Gray]},
    {fpanelW,fpanelH,0,   0,0,0,   [/COLOR]fpanelW/ftexW,1.0f              [COLOR=Gray]},
    {fpanelW,      0,0,   0,0,0,   [/COLOR]fpanelW/ftexW,1.0f-fpanelH/ftexH[COLOR=Gray]}
  };
  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, [/COLOR]&panel2dtex, 1, [COLOR=Gray]hPanelMesh, panelW, panelH, 0,
    PANEL_ATTACH_BOTTOM | PANEL_MOVEOUT_BOTTOM);
}[/COLOR]
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.
 

Xyon

Puts the Fun in Dysfunctional
Administrator
Moderator
Orbiter Contributor
Addon Developer
Webmaster
GFX Staff
Beta Tester
Joined
Aug 9, 2009
Messages
6,929
Reaction score
795
Points
203
Location
10.0.0.1
Website
www.orbiter-radio.co.uk
Preferred Pronouns
she/her
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". :/
 
Last edited:

Moach

Crazy dude with a rocket
Addon Developer
Joined
Aug 6, 2008
Messages
1,581
Reaction score
62
Points
63
Location
Vancouver, BC
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

:cheers:
 

martins

Orbiter Founder
Orbiter Founder
Joined
Mar 31, 2008
Messages
2,448
Reaction score
462
Points
83
Website
orbit.medphys.ucl.ac.uk
Xyon;bt1840 said:
One slight niggle, though: It's spelled "Lesson". :/
Er, yes :lol:. Corrected now. This is what you get if you spend the night writing tutorials instead of sleeping.
 

martins

Orbiter Founder
Orbiter Founder
Joined
Mar 31, 2008
Messages
2,448
Reaction score
462
Points
83
Website
orbit.medphys.ucl.ac.uk
Moach;bt1845 said:
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.
 

jedidia

shoemaker without legs
Addon Developer
Joined
Mar 19, 2008
Messages
10,892
Reaction score
2,141
Points
203
Location
between the planets
Now, this was really helpfull, I'm finally seeing something on my screen :lol:

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.
 

BruceJohnJennerLawso

Dread Lord of the Idiots
Addon Developer
Joined
Apr 14, 2012
Messages
2,585
Reaction score
0
Points
36
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?
 
Top