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.
|