Skip to content

Visualization Tool

Kestrel280 edited this page Sep 18, 2024 · 16 revisions

Overview

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

Compiling

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.

Libraries/Dependencies

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

Build troubleshooting

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.

Basic Usage

Running the Program

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

Controls

  • 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

bzCTPjS "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")

Coordinates

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

Module ID Convention

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

Structure of a Scenario File

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.

Block 1: Group Definitions

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}

Block 2: Module Definitions

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}

Block 3: Move Definitions

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

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.

Program Structure / Technical Details

The vistool is a C++ application using OpenGL. The entry point is the main() function, defined in main.cpp.

  1. 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 in setuputils.cpp) are executed to create and open a window, establish an OpenGL context, and register callback functions to handle user-input (callback functions defined in userinput.cpp).
  2. Then, the shaders and texture are loaded (Shader.cpp and loadTexture() defined in setuputils.cpp); respectively, these are stored in /resources/shaders/ and /resources/textures/.
  3. A Vertex-Attribute-Object (VAO) is created (_createCubeVAO() in Cube.cpp) which stores the vertices of a cube and informs OpenGL how to interpret the vertex data.
  4. 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.
  5. 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).

Frame/Main-loop Breakdown

On each frame:

  • User input is processed via the processInput() function defined in userinput.cpp. This function handles keyboard input: the WASD/QE keys are queried and used to update the camera's speed. 'R' calls camera.reset(); P toggles the camera's perspective flag and calls camera.resetProjMat(); Esc closes the window, terminating the main loop/ending the program; and spacebar toggles the global animation flag glob_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's startAnimation() 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.

Camera.hpp

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

Cube.hpp

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.

Move.hpp

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.

MoveSequence.hpp

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.

ObjectCollection.hpp

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.

Scenario.hpp

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

Shader.hpp

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.