Exploring Wings3D through the Erlang Shell
(for beginners)


Introduction

This tutorial if for beginners and aspiring plugin writers who would like to know more about the internals of Wings. It will attempt to teach you how to access the data (vertices, edges, faces, etc.) of the objects that you wish to export or modify. The Erlang shell is interactive (there's no need to compile the code) and, therefore, well suited for beginners. You must download the source code for wings to complete the tutorial.

The Erlang Shell

The first thing you need to do is start Wings, then locate and activate the shell window so that you can type some commands. This window is not the same as the console window that can be accessed from the Window Menu. Depending on which operating system you are running, here's what you need to do:

  • Windows: Click on 'Erlang' in the taskbar.
  • Linux: Copy the wings startup script to a file called wings_eshell and delete "-noinput" from the last line. Now, start wings with that script and you can type commands at the same terminal prompt.
  • Mac OSX: Find the directory where Wings resides by right clicking on the Wings icon and select "Show in Finder," then right click on Wings3D and select "Show Package Contents," then right click on Resources and select "Copy." Open a terminal and type or paste these commands, but replace the ROOTDIR path with the one you just copied.
bash
export ROOTDIR="/Applications/Wings3D 0.98.36.app/Contents/Resources"
export BINDIR=$ROOTDIR/bin
export ESDL_DIR=$ROOTDIR/lib
export EMU=beam
export PROGNAME=`echo $0 | sed 's/.*\///'`
exec "$BINDIR/erlexec" -run wings_start start_halt ${1+"$@"}

The State Record (St)

The state record is a global data structure that contains everything in your scene—objects, images, materials, etc. When you load a Wings file, it is decompressed (with zlib) and the state data structure is populated. As you model, the state changes constantly, and awaits to be written to disk. It is as simple as that. However, the state is a somewhat complex data structure that contains other data structures—lists, dictionaries, records, trees, etc.—that may be deeply nested. The state record is described in the wings.hrl include file.

Shell Commands

Now that the introductory material has been presented, let's see some commands in action. Recent releases of Wings have exposed a function that allows us to peek at the state. Enter wpa:get_state(). at the shell prompt and the current state record will be printed. The results depend on what you have in your scene.

Records and wings.hrl

Printing the state is practically useless, so a way to extract it's components is needed. This can be done with Erlang's powerful pattern matching. To access and match records from the shell, the record definitions in wings.hrl must be loaded. This can be done effortlessly by utilizing the built-in read-record function: rr(wings). Make sure the path matches the one on your system. This should be the result: [dlo,edge,st,view,we].

If you did not compile wings yourself, the above function may fail because of a missing epp.beam module. If this is the case, you can download the file here. To install it, simply unzip and copy the beam to your ebin directory:

WinXP: c:\Program Files\wings3d_0.98.35\lib\wings-0.98.35\ebin

OSX: /Applications/Wings3D.app/Contents/Resources/lib/wings-0.98.35/ebin

Linux: ~/wings-0.98.35/lib/wings-0.98.35/ebin

A First Example

The following code shows how to get the vertices of the first object in the scene. Copy and paste it into the shell. Don't forget to adjust the path to the hrl. To access the second object, change the 1 to a 2 in the line that starts with "We".


f(),
rr(wings),
St = wpa:get_state(),
#st{shapes=Shapes,sel=Sel} = St,
We = gb_trees:get(1, St#st.shapes),
Vs = array:sparse_to_list(We#we.vp).

If the first object you created is a cube, you should get a list of the verts similar the following:


[{-1.00000,-1.00000,1.00000},
 {-1.00000,1.00000,1.00000},
 {1.00000,1.00000,1.00000},
 {1.00000,-1.00000,1.00000},
 {-1.00000,-1.00000,-1.00000},
 {-1.00000,1.00000,-1.00000},
 {1.00000,1.00000,-1.00000},
 {1.00000,-1.00000,-1.00000}]

Unpacking Records

In the above example, St is a variable whose value is a state record, and We is a winged-edge record. These records are defined in wings.hrl.

There are two ways to access records. First, It's easy to see that St#st.shapes accesses the shapes field, and We#we.vp accesses the vertex positions field. This should be easy to follow.

The second way is through unpacking. The 4th line simply shows how records are unpacked and assigned to variables. Now the unused Shapes variable has the exact same value as St#st.shapes. The 5th line could also have been written as follows: We = gb_trees:get(1, Shapes).

The only confusing thing is that the unpacking (or pattern matching) of records might appear to be backwards, because you would think it should be written #st{Shapes=shapes,Sel=sel} = St (i.e., the capitalized variable name should be on the left. But since St is actually a record, the 4th line actually looks like this: #st{shapes=Shapes,sel=Sel} = #st{shapes=GBTREE,sel=LIST}. Therefore the unbound variable Shapes is matched to a gbtree, and sel is matched to a list, so the variable bounding rule is followed. In addition, the atom shapes matches shapes (and sel matches sel), so everything matches up just right.

Calling Other Functions

At this point, other functions may be called on the data extracted from the state record. For example, if the vertices are in a variable called Vs, the following will calculate the bounding box, the object center, and the radius of the smallest sphere which encloses the object. This makes good use of the built-in e3d_vec module, which contains many useful functions.


BBox = e3d_vec:bounding_box(Vs),
Center = e3d_vec:average(Vs),
[Pmin,Pmax] = BBox,
Size = e3d_vec:sub(Pmax,Pmin),
Radius = e3d_vec:len(Size),
io:fwrite("BBox: ~p\n", [BBox]),
io:fwrite("Center: ~p\n", [Center]),
io:fwrite("Size: ~p\n", [Size]),
io:fwrite("Radius: ~p\n", [Radius]).

These are the results that should be printed for a standard cube object.


BBox: [{-1.00000,-1.00000,-1.00000},{1.00000,1.00000,1.00000}]
Center: {0.00000e+0,0.00000e+0,0.00000e+0}
Size: {2.00000,2.00000,2.00000}
Radius: 3.46410

Non-State Functions

Functions that do not deal with the state may also be executed. Here's how to access all the lines that are printed from internal commands to the console window. Enter this code:


f(),
Lines = [Line || {Eol,Line} <- wings_console:get_all_lines()],
PrintString = fun(String) -> io:fwrite("~s\n", [String]) end,
lists:foreach(PrintString, Lines).

to see the following familiar results:


Trying OpenGL modes
  [{buffer_size,32},{depth_size,32},{stencil_size,8},{accum_size,16}]
Actual: RGBA: 8 8 8 8 Depth: 24 Stencil: 8 Accum: 16 16 16 16
Using GPU shaders.

Sending the Hotkeys List to the Printer

Wouldn't it be great to have a printed list of all the defined hotkeys? This can be easily done by entering:


f(),
Lines = wings_hotkey:listing(),
PrintString = fun(String) -> io:fwrite("~s\n", [String]) end,
lists:foreach(PrintString, Lines).

This could be a long list because it produces the hotkeys for each mode. But now you have the option to make a hardcopy to use as a handy reference.


Hotkeys in all modes
Space: Select|Deselect
+: Select|More
-: Select|Less
1: File|1 (user-defined)
@: File|Import|Obj|False (user-defined)
Shift+A: View|Frame
Shift+C: View|Show Colors (user-defined)
etc ...

Hotkeys List to HTML Table

Here's a format that is much better suited for printing the hotkeys list. The following code should open up a browser with the keys neatly listed in a table. Here I used a recursive anonymous fun (not to be confused with a function) to do the work.


f(),
FileName = "hotkeys.htm",
{ok, IoDevice} = file:open(FileName, write),
PrintKV = fun(Key, Val) ->
    Fmt = "<tr><td>~s</td><td>~s</td></tr>\n",
    io:fwrite(IoDevice, Fmt, [Key,Val]) end,
PrintRow = fun([], FunName) ->
    done;
    ([H1,H2|T], FunName) ->
    PrintKV(H1,H2), FunName(T, FunName) end,
PrintMode = fun(Caption, Keys) ->
    Table = "<table border=0 cellpadding=2 cellspacing=1 width=550>",
    io:fwrite(IoDevice, "~s\n", [Table]),
    io:fwrite(IoDevice, "<caption>~s</caption>\n", [Caption]),
    PrintRow(string:tokens(Keys,"\:\n"), PrintRow),
    io:fwrite(IoDevice, "~s\n", ["</table><br>\n"]) end,
PrintListing = fun([], FunName) ->
    done;
    ([H1,H2|T], FunName) ->
    PrintMode(H1,H2), FunName(T, FunName) end,
PrintStyle = fun() ->
    S = ["<style type=\"text/css\">
    tr, td, th, p {
    font-family: Verdana, Arial, Helvetica, sans-serif\;
    font-size: 12px\;
    line-height: 18px\;
    background-color: #C0C0C0\;
    }
    table { background-color: #666666\; }
    caption {
    font-family: Trebuchet MS, Verdana, Arial, Helvetica, sans-serif\;
    font-size: 18px\;
    font-weight: bold\;
    color: #555555\;
    }
    </style>
    <div align=\"center\">"],
    io:fwrite(IoDevice, "~s\n\n", [S]) end,
HotKeys = wings_hotkey:listing(),
PrintStyle(),
PrintListing(HotKeys, PrintListing),
file:close(IoDevice),
{Osfamily, Osname} = os:type(),
case Osname of
    nt -> os:cmd("start "++FileName);
    windows -> io:fwrite("Open~p\n", [FileName]);
    linux -> os:cmd("firefox "++FileName);
    darwin -> os:cmd("open "++FileName)
end.

Modules and Functions

You can easily find modules and functions by pressing tab in the console window. This will give you a list of modules. Enter the name of a module followed by a colon, then press tab to see a list of all the functions that the module exports. Any function that has /0 at the end signifies that it takes no parameters and you can simple execute it.

In the console, the tab key acts a completion helper. Most of the wings modules start with a "w". So type a "w" and press tab to see all the modules that start with that letter. By using the tab you can easily type long functions without cut and paste. Try to find and execute the following function by using a combination of typing and the tab key: wings_util:wings().

Vertex Selection Example

Many functions deal with the currently selected elements—vertices, edges, faces, or entire objects. Wings is flexible enough that it allows you to select elements on more than one object simultaneously. Don't forget to take this into account if you want to limit your selection to a single object. This actually came up as I was writing the shortest path selector. It didn't make much sense to select the shortest path (as a series of edges) between two vertices on two different objects because there are no edges connecting separate objects!

Here's an example of how to get the selected elements. The current mode—vertex, edge, face, or body—determines the type of the elements in the variable Sel. Select any two vertices on a cube and paste in these lines:


f(),
rr(wings),
St = wpa:get_state(),
#st{shapes=Shapes,selmode=Mode,sel=Sel} = St,
[{Id,SelectedVs}] = Sel,
We = gb_trees:get(Id, Shapes),
[Pa,Pb] = [wings_vertex:pos(Vert, We) || Vert <- gb_sets:to_list(SelectedVs)].

Now points A and B are bound to the variables Pa and Pb. Note that Pa is not necessarily the first point selected. Selections are not returned in the order that they were selected.

Face Selection Example

Here's an example of how to get the selected faces. Select any number of faces on a cube and paste in these lines:


f(),
rr(wings),
St = wpa:get_state(),
#st{shapes=Shapes,selmode=Mode,sel=Sel} = St,
[{Id,SelectedFs}] = Sel,
We = gb_trees:get(Id, Shapes),
[wings_face:vertex_positions(Face, We) || Face <- gb_sets:to_list(SelectedFs)].

You'll get a list of lists of tuples. This is the object in raw format. Note that SelectedVs was changed to SelectedFs (to avoid confusion) and a function from the wings_face module was utilized. It is possible write a complete exporter by using the shell and a few simple functions (or Erlang Anonymous Funs).

You'll get the e3d_mesh record if you paste in the following line. wings_export:make_mesh(We,[]).

A Simple Object Exporter

This code will export the first mesh to Wavefront OBJ, a common format. It will export the mesh data, but the materials and textures will be ignored. Why do you need this example if a full featured OBJ exporter is included with Wings? Because this one is meant to get you started.

I should mention that this example requires 0.98.36 because of a new function: e3d_util:raw_to_indexed(Raw). Or you may simply download the new e3d_util module and put in your ebin directory.

In addition, instead of printing the object to the console window, exporting to a file may be done by passing a filename instead of the atom none, for example: ObjExport(Vs2, Fs2, 'c:/temp/cube.obj'). Remember to quote the filename with single quotes.

As an exercise, see if you can modify the code to automatically triangulate the model before exporting. Hint: use We2 = wpa:triangulate(We).


f(),
ObjExport = fun (Verts, Faces, FileName) ->
    case (FileName==none) of
        true -> IoDevice = standard_io;
        false -> {ok, IoDevice} = file:open(FileName, write)
    end,
    PrintVert = fun(Vertex) ->
        {X,Y,Z} = Vertex,
        io:fwrite(IoDevice, "v ~9f ~9f ~9f\n", [X,Y,Z]) end,
    PrintIdx = fun(Index) ->
        io:fwrite(IoDevice, " ~w", [Index+1]) end,
    PrintFace = fun(Face) ->
        io:put_chars(IoDevice, "f"),
        lists:foreach(PrintIdx, Face),
        io:put_chars(IoDevice, "\n") end,
    io:fwrite(IoDevice, "# NumVerts: ~p\n", [length(Verts)]),
    io:fwrite(IoDevice, "# NumFaces: ~p\n", [length(Faces)]),
    io:fwrite(IoDevice, "g Mesh\n", []),
    lists:foreach(PrintVert, Verts),
    lists:foreach(PrintFace, Faces),
    file:close(IoDevice)
    end,
rr(wings),
St = wpa:get_state(),
#st{shapes=Shapes} = St,
We = gb_trees:get(1, St#st.shapes),
Vs = array:sparse_to_list(We#we.vp),
Fs = gb_trees:keys(We#we.fs),
Raw = [wings_face:vertex_positions(Face, We) || Face <- Fs],
{Vs2, Fs2} = e3d_util:raw_to_indexed(Raw),
ObjExport(Vs2, Fs2, none).

Modifying Objects

Many wings functions simply take the state as input, modify it, then return the modified state as output. That sounds pretty simple, and it is. If you were clever enough, you could call one of these functions from the shell and modify objects. The drawback for now is that wpa:put_state(). doesn't exist.

Another large set of functions take the We record (the Winged-Edge Data Structure) as input, modify it, and return it. Make sure to read the wings_we module as soon as your project requires it. For a little insight on the WEDS format try this:


f(),
wings_u:export_we("tmpweds.txt", wpa:get_state()),
{ok,Data} = file:read_file("tmpweds.txt"),
file:delete("tmpweds.txt"),
io:fwrite("~s", [binary_to_list(Data)]).

The above simply outputs dumps the winged-edge data structure to a file and then reads it back to you. This is useful if it's the first time you encounter this structure. Here's the output for a tetrahedron.


OBJECT 1: "tetrahedron1"
=======================
   mode=material next_id=7

Face table
===========

0: edge=1
1: edge=4
2: edge=1
3: edge=2

Edge table
===========

1: vs=0 ve=1
  a=none b=none
  left: face=2 pred=5 succ=3
  right: face=0 pred=2 succ=4
2: vs=0 ve=2
  a=none b=none
  left: face=0 pred=4 succ=1
  right: face=3 pred=3 succ=6
3: vs=0 ve=3
  a=none b=none
  left: face=3 pred=6 succ=2
  right: face=2 pred=1 succ=5
4: vs=1 ve=2
  a=none b=none
  left: face=1 pred=6 succ=5
  right: face=0 pred=1 succ=2
5: vs=1 ve=3
  a=none b=none
  left: face=2 pred=3 succ=1
  right: face=1 pred=4 succ=6
6: vs=2 ve=3
  a=none b=none
  left: face=1 pred=5 succ=4
  right: face=3 pred=2 succ=3

Ending Your Shell Session

Finally, the nicest and most elegant way to exit (or quit) Wings from the shell is to execute this simple function: q().

Conclusion

Now that you had a chance to code interactively, you will be more prepared to write your own functions and new features for Wings. I have tried to be as comprehensive as possible and this tutorial should give you a great head start on your journey to plugin writing. For other information dealing with primitives, see my other tutorial: How To Write Wings3D Plugins.


This page was last revised on October 12, 2006
Copyright © 2006 Anthony D'Agostino
All rights reserved.