-
Notifications
You must be signed in to change notification settings - Fork 2
Visualization Tool
The Visualization program/"vistool" is an interactive graphics tool for viewing reconfiguration sequences of modular robots. See here for a brief demonstration video.
This tool is no longer under development. It has been superceded by WebVis, a browser-based visualization tool..
The vistool is compiled using Make. Simply run the 'make' command from the /Visualization/ directory. Windows x64 compilation uses MinGW-g++, which can be obtained using Cygwin. ARM compilation uses clang++ and is currently only tested on Mac M1 devices.
Make for Windows can be acquired here.
To get the MinGW-g++ distribution needed for compilation, download the Cygwin Setup file from here. Run it and install the 'mingw64-x86_64-gcc-core' and 'mingw64-x86_64-gcc-g++' packages. Nominally, these will install to 'C:/cygwin64/bin'. Add this directory to your system's environment variables.
A number of static libraries are compiled directly into the vistool executable.
- GLFW3 -- For creating windows, establishing OpenGL contexts, and handling user input
- GLAD -- (header-only) For convenient loading of OpenGL function pointers
- GLM -- (header-only) Maths library designed for use with OpenGL
Precompiled binaries for GLFW3 are included in the visualization program and statically linked into the main program. On Windows, this file is /Visualization/lib/win64/libglfw3.a. This library links against the MinGW-w64 runtime. If you would like to build the program using another compiler, GLFW hosts other precompiled binaries, as well as source files to build your own binaries.
After compilation, run the program ("main" or "main.exe") from the command-line. Specify a Scenario file to use by giving the program a command-line argument. (Scenario files must be located in the /Scenarios/ subfolder).
- Animation
- Spacebar -- Toggle auto-animation
- F/B -- Enable [F]orwards/[B]ackwards auto-animation
- Up/Down arrow key -- Speed up/Slow down animation
- Right/Left arrow key -- Perform a single move (forwards/reverse)
- Camera
- W/A/S/D -- Navigate the world (forward/strafe-left/backward/strafe-right)
- E/D -- Navigate the world (up/down)
- Right-click -- Hold right-click and move the mouse to look around/change the camera's orientation
- Scroll up/Scroll down -- Zoom in/out
- R -- Reset the camera's orientation to the default location
- P -- Toggle between perspective (default) and orthographic projection
"Scenarios" are pre-specified configurations of modular robots and their moves. They are stored in "Scenario Files" typically suffixed with .scen. Scenario files should be located in the /Scenarios/ subfolder. A Scenario can be loaded by running the vistool with the name of the Scenario as a command-line argument. (E.g. a Scenario 'example_scenario.scen' can be loaded by running 'main.exe example_scenario")
The vistool operates in a right-handed coordinate system. When the program is launched, the camera is initialized facing the negative-z direction, from a location of x=0, y=0, z>0. In this orientation, the positive-x is towards the right of the screen; positive-y is towards the top of the screen; and positive-z is facing away from the screen (towards you).
Modules in a scenario file can have any non-negative ID, as long as there are no conflicting IDs. It may be helpful, however, to cleverly assign module IDs such that they encode information on where the module exists in space. One such convention is to concatenate the Z (if applicable), X, and Y coordinate of the metamodule with the (Z), X, and Y coordinate of the module within the metamodule. (However, this approach has the drawback of not allowing negative coordinates for metamodules, so all metamodule coordinates must be translated to be greater than (0, 0, 0).)
Scenario files are broken into 3 blocks. Each block contains one or more lines containing comma-separated integer values. Blocks are separated by a blank line. Any text after a double-backslash (//) will be treated as a comment.
Each module in a Scenario file must be part of a "Group", which specifies the module's color and size. This block defines each of these groups. A group entry is structured as:
{Group ID}, {Red value 0-255}, {Green value 0-255}, {Blue value 0-255}, {Scale factor 0-100}
This block defines the starting location of each module, as well as the group to which each module belongs. A module entry is structured as:
{Module ID}, {Group ID}, {X location}, {Y location}, {Z location}
This block defines the sequence of moves the modules will follow when animating the Scenario. Every move specifies a "mover", and an "anchor direction code". The "mover" is simply the module ID of the module to move. The anchor direction code is an integer in the range [-3, 6]:
- -3: Sliding move, prioritizing motion along the z-axis
- -2: Sliding move, prioritizing motion along the y-axis
- -1: Sliding move, prioritizing motion along the x-axis
- 0: Sliding move, generic (module will slide in a straight line to its destination)
- 1: Pivot move which "pulls away" from the +x face of the module
- 2: Pivot move which "pulls away" from the +y face of the module
- 3: Pivot move which "pulls away" from the +z face of the module
- 4: Pivot move which "pulls away" from the -x face of the module
- 5: Pivot move which "pulls away" from the -y face of the module
- 6: Pivot move which "pulls away" from the -z face of the module
A move entry is structured as:
{Mover ID}, {Anchor Direction Code}, {Delta x}, {Delta y}, {Delta z}
Sliding moves can be specified by giving a negative value for the anchor ID. For "combined"/"corner" sliding moves, the value corresponds to the axis of the first slide: -1 for the x-axis, -2 for the y-axis, and -3 for the z-axis. For example: an entry of '8 -3 0 1 -1' encodes a move which will translate module ID 8 +1 in the y direction and -1 in the z direction. Because the anchor ID is set to -3, the z-translation will occur first, followed by the y-translation.
The vistool is a C++ application using OpenGL. The entry point is the main()
function, defined in main.cpp
.
- On program launch, before entering main(), a Camera object is instantiated (
Camera.cpp
). This object is stored on the stack and is global to the program. It handles the data and functionality required to create the world view on each frame. 2/ In main(): a number of setup functions (defined insetuputils.cpp
) are executed to create and open a window, establish an OpenGL context, and register callback functions to handle user-input (callback functions defined inuserinput.cpp
). - Then, the shaders and texture are loaded (
Shader.cpp
andloadTexture()
defined insetuputils.cpp
); respectively, these are stored in/resources/shaders/
and/resources/textures/
. - A Vertex-Attribute-Object (VAO) is created (
_createCubeVAO()
inCube.cpp
) which stores the vertices of a cube and informs OpenGL how to interpret the vertex data. - The Scenario file is loaded (
Scenario.cpp
). The cubes specified in the file are extracted to an ObjectCollection object (ObjectCollection.cpp
), and the moves are extracted to a MoveSequence object (MoveSequence.cpp
). The cubes are also registered to the global-object registry (glob_objects
), which is a hashmap associating integer Cube IDs with their associated Cube objects. - The main loop begins: timing variables are updated, camera speed/position is updated based on user input, moves/animations are fetched if needed, and all cubes are drawn to the screen (animating if needed).
On each frame:
- User input is processed via the
processInput()
function defined inuserinput.cpp
. This function handles keyboard input: the WASD/QE keys are queried and used to update the camera's speed. 'R' callscamera.reset()
; P toggles the camera's perspective flag and callscamera.resetProjMat()
; Esc closes the window, terminating the main loop/ending the program; and spacebar toggles the global animation flagglob_animate
. - If the
readyForNewAnim
boolean (initialized to true) is true, the next move is popped from the loaded Scenario's MoveSequence. The associated Cube is then identified from the global object registry, and the cube'sstartAnimation()
method is called with this Move. - The loaded Scenario's ObjectCollection has its
.drawAll()
method called.- The ObjectCollection's shader is invoked. The current time, camera's view matrix, and camera's projection matrix are all passed to the shader.
- The ObjectCollection's texture and VAO are bound.
- For each object in the Object Collection, the object's
.draw()
method is called.- The model matrix is calculated by translating the object by its position value.
- The transform matrix is initialized, scaled by the object's scale, and rotated by the object's cumulative-rotation.
- If the object currently has a Move associated with it, the transform matrix is multiplied by the output of
.processAnimation()
, which does the following:- A temporary "animation" transform matrix is initialized.
- If the Move is a sliding move, a translation matrix is calculated by interpolating between the cube's starting position, ending position, and current animation progress.
- If the move is a pivot move, the angle of rotation is calculated based on the current animation progress. The animation-transformation matrix is then pre-translated by the Move's "pre-translation" value; rotated by the current angle, about the Move's rotation axis; and post-translated by the negative of the Move's "pre-translation".
- If the animation progress has exceeded 1.0:
- The Cube's position is updated to the ending position (calculated by current position + delta position).
- The Move object is deleted, and the Cube sets its active Move to NULL.
- The
markWhenAnimFinished
bool is set to true, signalling to the main loop that a new move can be fetched and started.
- The transformation matrix, model matrix, and color are sent to the shader. (If there is surface normal data available, it is also sent to the shader.)
- glDrawElements() is invoked to draw the object to the screen.
The Camera class is used to instantiate a singleton-like/global object which stores the position, speed, orientation, FOV, zoom, and projection-style (perspective vs. orthographic) of the camera; as well as the view- and projection-matrices calculated from this data. On each frame, the camera calls the .calcViewMat()
method to construct a new view matrix (as the camera's position and orientation may have changed due to user-input). A .resetProjMat()
method is provided, which updates the projection matrix whenever the perspective, zoom, or FOV is changed (via user input). A .reset()
method is also available which restores the camera to its default settings (this method is called during construction).
The Cube class is used to create Cube objects, which should be stored in an ObjectCollection (using its .addObj()
method) as well as the global-objects registery (glob_objects
). A Cube object stores the ID, position, scale, and color of a cube in the scene. It also defines a method for drawing the cube to the scene (.draw()
), which may call animation-processing methods if the cube is undergoing a movement (.startAnimation()
and .processAnimation()
). On each frame, the .draw() method is called; this constructs the transformation and model matrices for the cube (accounting for any active animation), sends them to the shader, and draws the cube to the scene. Cube.cpp
also defines _createCubeVAO()
and contains vertex and surface-normal data for a cube.
The Move class is used to represent movements of Cube objects. A Move object stores the ID of the associated cube, the "anchor direction" the cube should pivot away from, the delta-position of the movement, and a boolean flag indicating whether or not the move is a "sliding" move. Based on this information, the Move object also calculates values for "pre-translation", maximum-angle, and rotation-axis; which are each used by the associated Cube object when the Cube is drawn (specifically, Cube.processAnimation()
uses this information to construct transformation/model matrices). The Move class defines .copy()
and .reverse()
methods, which return new Move objects representing the same move, or the "reversed" move, respectively. A Move should be stored in a MoveSequence.
The MoveSequence class is effectively a deque of Move objects with additional functionality provided to allow for "undo" operations. The .pop()
method pops the first Move from the MoveSequence, and stores a copy of the Move in an internal stack (the "undo-stack"). The .undo()
method pops the top Move on the undo-stack and returns it to the front of the MoveSequence.
The ObjectCollection class is used to conveniently store sets of objects which share the same VAO, texture, and shader. Currently, only one ObjectCollection instance is created when the vistool is run (the set of cubes extracted from the Scenario file). ObjectCollection defines .drawAll()
, which loads the shader, sends the view- and projection-matrices to the shader, binds the texture and VAO, and finally calls each internal object's .draw()
method.
The Scenario class is used to load Scenario files. Its constructor takes a filepath to a Scenario file and does some rudimentary string parsing to construct an ObjectCollection and MoveSequence, which can be extracted using .toObjectCollection
and .toMoveSequence()
.
The Shader class is used to load shader source files, compile them, link them, and provide an interface to invoke and send data to the shader. This class is taken from LearnOpenGL.com.