CS 315 Homework 4 - Dancing Robot
Due Fri Oct 18 at 11:59pm
Overview
In this assignment, you will create an animated, 3D model of a humanoid robot. Upon the press of a button, the robot will "dance" by moving its arms and legs in a hopefully less than frantic manner.
An example of a robot mid-dance (from an alternative implementation). Your robot need not be quite this fancy.
By creating this robot, you will have a chance to practice working with 3D transformations in OpenGL, as well as transformation hierarchies (creating series of transformations that can be used to easily describe characters with a "connected" body). You will also have a chance to do some animation, as well as to revisit the Android interaction widgets (i.e., buttons) from previous assignments.
This assignment should be completed individually.
Objectives
- Practice with 3D transformations in OpenGL
- Implement a hierarchical transformation system
- Practice with specifying animations
- Continue working with the OpenGL and Android APIs
Necessary Files
You will need to download and extract a copy of the starter code for this assignment. This project includes a basic GLES program to get you started--if you run the program you should be able to see a nice red cube spinning on the screen!
Be sure and read through the starter code; much of this is the same as the previous assignment and what we've gone over in class, but it has been refractored for readability (reduce time spent scrolling) and ease of use. There are also a few new pieces.
-
ModelFactory:
The "model" definitions (i.e., the coordinates of our triangles, quads, cubes, etc) has been refactored into a
ModelFactory
class. Instantiate the factory and then use its public methods (e.g.,getCubeData()
) to access thosefloat[]
and put them intoFloatBuffer
s for use in your program. -
Packed Buffers:
One of the more complicated pieces of code that has changed is that models are now defined as "packed" buffers--that is, the array of cubeData contains more than just the positions of the vertices. As an example, a packed buffer could interleave the positions and the colors of a model, so rather than having:
float[] positions = { x1, y1, z1, x2, y2, z2, ... } float[] colors = { r1, g1, b1, a1, r2, g2, b2, a2, ... }
We can combine these into a single array:float[] positions = { x1, y1, z1, r1, g1, b1, a1, x2, y2, z2, r2, g2, b2, a2, ... }
This actually will speed up programs (since vertex information is now in continugous memory), as well as making it easier to pass information around.
This requires us to change our
glVertexAttribPointer()
calls somewhat, since we can't just give an array to theposition
variable in our shader. Instead, we specify the stride of the array--this tells OpenGL how many items it should "step over". You can see this in action in thedrawPackedTriangleBuffer()
methodNote that specifically, the ModelFactory produces packed buffers that include position and normal data (color data is not passed using glVertexAttribPointer). A normal is a vector that is perpendicular to a particular triangle (to the plane that triangle is on)--we use normals for all kinds of things, including lighting!
-
A New Shader:
You may also notice the new, slightly more complex shader. This shader does some (very) simply lighting, in order to help show the depth of the models. Lighting is programmed such that vertices are "brighter" when they are facing a light source, and darker when they are facing away from it. We will talk about how the math works to calculate this in more detail later in the course. What you should notice is that the shader is passed a few additional
uniform>
s andattribute
s--specifically the modelView matrix and the normal for the vector. However, this is all handled for you so you don't need to change anything! - Axis Points: I've also included those handy axis points for reference and debugging.
Note that the zip file also contains a README.txt
file that you will need to fill out. DO FILL THIS OUT.
The Robot
Your robot will consist of several moving components: a torso, two two-segment arms, two two-segment legs, and a head. Each upper arm will draw in the reference frame of the torso, each lower arm in the reference frame of the corresponding upper arm, etc. Feel free to add additional components (e.g., "joints" to connect the segments, hands, etc)---but don't get bogged down!
- It's a good idea to start from a "paper" design of your robot, to figure out what you want it to look like and where objects will need to be placed relative to one another. When designing each individual component, consider how you might use a local coordinate system with the origin at the appropriate "pivot points" (you'll need to do this when you implement the robot!) For example: you might think that "the left arm is translated A and rotated B from the torso", writing down those notes.
Your robot will be built from copies of two basic primitive models (provided for you): cubes and spheres. Note that cubes can be scaled and stretched to turn them into rectangles, and spheres can be scaled to turn them into ellipsoids. You are welcome to add in other primitives as desired.
You will also need to program your robot to "dance" in place. This will involve creating a cyclical animation of the robot moving his arms and legs.
Details
Below are some further details about how this assignment should come together. Note that these instructions are less specific than previous ones--be sure to let me know if there are any problems so I can update the notes!
I suggest you complete this program in a few steps:
1. Create and display the Robot
Figures by Wayne O. Cochran
- Your first step should be to model the robot. Construct a series of model transformations and draw calls that will render your robot in a single, static pose. (i.e., standing still with arms at his sides)--this will be your "base" position.
- Feel free to use the cube and sphere models provided by the ModelFactory, scaling them and rotating them as you see fit. You can also make them different colors by passing a color array to the drawPackedTriangleBuffer() method.
-
Note that as you model your robot you should make sure to specify a local frame of reference for each body part. The frame of reference is a transform that takes represents how to get from the parent's coordinate system to your new local coordinate systems. In this way, you can create a transform hierarchy like we discussed in class. See the image for an example.
- Helpful hint: in order to help with animation, you might want the "origin" of each body part's reference frame to actually be at the "pivot point" between the current frame and the parent frame. So your origin for the arm's reference frame could actually be at the "shoulder."
- Helpful hint 2: avoid using scaling in defining your reference frame--it means that every future reference frame will also be scaled! Reference frames are about how to find the new "origin" of the local coordinate system, not how to draw the particular body part. Use a reference frame to get to the origin for your arm; the scaling/rotating/etc for position your arm (base) relative to its reference frame are not part of the reference frame!
-
You should calculate your list of reference frames (the transformations to get from parent's to child's origin) in the Robot's constructor, saving those frames as instance variables or such. This will allow you to easily use them in the code and save computation time.
-
Note: each reference frame will need to be a different array! You can't just keep pushing
mModelMatrix
onto the stack, because in Java you're just passing a reference. Instead, you'll need to make a bunch of separate 4x4 matrices (float[16]
).
-
Note: each reference frame will need to be a different array! You can't just keep pushing
- You should probably create separate "drawPart()" methods to help organize your code. These methods may include multiple models (i.e., all the fingers in a hand), though transforming those models may not influence your reference frame.
-
In order to control a particular piece of the robot then, we simply need to move into the correct frame, apply our transformations, draw what we want, and then move back out to the parent's frame to prepare for the next transformation. The basic algorithm we discussed in class (and what your helper method might look like):
1. save current reference frame (push my frame onto the stack) 2. apply global transformations we want children to inherit (animation, see below) 3. draw each child component //this is slightly different from class--post-order traversal rather than pre-order 4. apply local transformations we do NOT want children to inherit (local rotatoions, etc.) 5. draw this model's primitives 6. restore my parent's frame (by popping my frame off the stack!)
-
This drawing will be called in your
onDrawFrame()
method. - The pushed frame should be the "current reference frame"--so that is going to be a sequence of transformations between multiple "base" reference frames, as well as other rotations/translations that involved "moving" the arms. In effect, the pushed frame is the final composed matrix that moves from the world's origin to your current part's basis.
-
You'll need to store separate copies of the "current reference frame" on the stack! Remember that Java is pass by reference, which means if you just push
mModelMatrix
, you'll actually be simply pushing a second copy of the same matrix, and future modifications tomModelMatrix
will apply to everything on the stack. Instead, you'll want to save copies of your current reference frame--you can easily make a copy using theSystem.arraycopy()
method built into Android. -
Note that if needed you can compose arbitrary matrixes by using the built-in
Matrix.multiplyMM()
method. However, there is a known issue with this method if either of the operand methods are the same as the result. So if you try to do
Matrix.multiplyMM(matrixA, 0, matrixA, 0, matrixB, 0)
(multiplying A*B and putting the result in A), your result may not be correct. Instead, use a temporary matrix:Matrix.multiplyMM(temp, 0, matrixA, 0, matrixB, 0)
, and then assign that temp variable whereever you want. - Again, do not modify the variables on the stack as you draw! Each variable on the stack should stay "clean" after you've calculated it!
-
This drawing will be called in your
2. Adding animation
- Once you've gotten your robot drawn and posed, you can add animation. Think about what transformation (in some variable amount) you'll want to have happen in your animation--for example, maybe your animation is "rotate the arm by X degrees".
-
Inside your drawing code (where you are working with the stack), add a call that applies the transformations to your current reference frame. This will make it so that all children will also have their "base" reference frames transformed appropriately!
- Be careful about keeping local transformations distinct from inherited transformations! Look at the above algorithm!
- Note that you will need to keep track of different variables to represent the amounts of each transformations. This may mean a lot of instance variables; it's possible that a data structure might help make this organization nicer!
-
You can specify a "dance" by calculating how much each variable should be changing over any time frame--in effect, by creating a function
f(t)
that for some timet
represents the amount of transformation of a particular frame--and each frame will have a different function. Note that these functions will likely be piece-wise--use a lot of conditional statements to write what you want!-
Yes, this means you will probably have a giant if/else or switch block. That is expected. You are of course welcome to attempt more elegant solutions if desired, but don't get bogged down! As example code:
... if(lForearmRising) { left_forearm_angle -= 0.2f; if(left_forearm_angle < -100.0) lForearmRising = false; } ...
-
Yes, this means you will probably have a giant if/else or switch block. That is expected. You are of course welcome to attempt more elegant solutions if desired, but don't get bogged down! As example code:
- Your animation should loop correctly---it should be cyclical and be able to go forever once it is started. ♫ Never stop dancing! ♫
- Your animation should include moving all 4 limbs, including both segments of each limb. ♫ Move your whole body ♫
-
As I mentioned in class, life is better if animation is based on time, rather than framerate, as it becomes display independent. Thus if you want to "slow down" your animation, simply reduce the amount of movements (or change your function to
f(t/2))
.- However, if you really want to control framerate, you can do that by implementing your own "animation" thread and manually telling the GLSurfaceView to redraw. Google and StackOverview may have help for how to do this.
3. Animation Controls
- Once you've got your animation working (or before, if you want help testing), you should add some User Interface elements (e.g., buttons) that can be used to control the animation. A start and stop button would be sufficient, though more advanced controls are of course welcome.
-
You can do this by creating an XML layout (as you did in Homework 1), or by coding in a layout. The XML is actually easier (less new classes to deal with); use the structure from Homework 1 as an example.
-
Remember to override both constructors for your
GLSurfaceView;
I forgot to do this in the starter code! Your "initialization" code (where you instantiate theRenderer
, etc) should be called from both constructors! -
EDIT: There are a couple of extra details and changes needed to load an OpenGL canvas from the XML. There are two paths to success:
- You can make the inner GLSurfaceView class (called
GLBasicView
in the starter code) a separate full class in its own file, rather than an inner class. You can then simply refer to this class from the XML in the same way you referred to the DrawingView in the Hwk1 XML. -
You can leave
GLBasicView
an inner class, but then you need to (a) make itstatic
, and (b) reference it in the XML using the following syntax:<view class="your.package.name.GLDancingRobotActivity$GLBasicView" //note the $ android:id="@+id/gl_view" android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="100" \...or other attributes you see fit \/>
See here for details.
setContentView(R.layout.activity_main); _GLView = (GLSurfaceView)this.findViewById(R.id.gl_view);
That should get you where you need to go. I apologize for not setting this up correctly initially for you! If there are problems, please let me know. - You can make the inner GLSurfaceView class (called
-
Remember to override both constructors for your
- You might consider adding further interaction components--maybe you can drag to move the camera, or adjust a slighter to spin the robot, or include other buttons to move the robot's limbs even if it isn't dancing (as in the original from of this assignment).
Submitting
BEFORE YOU SUBMIT: make sure your code is fully functional, but also documented and readable. Again, grading will be based primarily on functionality. I cannot promise that I can find any errors in your code that keep it from running, so make sure it works. But I will look over your code to check your implementation, so please make sure that is readable :)
Upload your entire project (including all the src, resources, layouts, libraries, etc) to the Hwk4 submission folder on hedwig. Also include a filled-in copy of the README.txt file in your project directory!
The homework is due at the start of class on Fri, Oct 18. Late assignments will be considered most harshly.
Grading
This assignment will be graded based on approximately the following criteria:
- Your program renders a model of a 3D, humanoid robot [30%]
- Your robot has been structured using hierarchical transformations [20%]
- Your robot can perform an animated "dance" [25%]
- Your program includes interaction elements to control the dance [15%]
- Your code is documented and readable [5%]
- The README is completed [5%]