Please refer to the text of assignment 1 if you're unsure how to clone a repository. On the command line, you can achieve this using the following command:
$ git clone https://gitlab.cs.dartmouth.edu/cs77-fa17/assignment4_basecode.git
For visual tools such as SourceTree the process is slightly different, but the URL will be the same.
Before you start, open
firstname-lastname-report.html in your browser. This html file will be both your report and your submitted assignment. Check the developer console of your browser for any error messages (check the x-hour slides for how to open the console).
Note: If there are any compatibility issues with your browser/platform, let us know as soon as possible
Before starting with the assignment, we strongly encourage you to look through the provided framework. The webgl and utility framework is similar to the previous assignments; however, there are some files containing additional classes to which you'll have to add code to complete the assignment. All the locations where you are required to add code have been marked with "TODO" statements for your convenience. Also the provided files contain "NOTE" markers that provide vital information related to the assignment. The provided files have ample documentation to give you an understanding of the major segments of the code.
joint.js file contains the
Joint class that represents the actual joint or bone within a skeletal framework. The
skeleton.js file contains the
Skeleton class that stores the joints in a flat array. The skeletal system is traditionally employed in a hierarchical fashion where every joint contains a reference to its parent and the root of the hierarchy has its parent set to
null. Each joint in the hierarchy can be represented by a local transform with respect to its parent. The
mPosition field in the
Joint class stores this position of the joint with respect to its parent. The
mJointAxis represents the axis along which a joint can rotate. The
mJointAngle stores the current rotation angle of the joint. In addition to these fields, the joint also contains a
mLength field that stores the length of the corresponding bone in the skeleton hierarchy. You will be using this length to compute weights for vertices when performing linear blend skinning. Each joint is rendered as a wireframe cube with its corresponding length and orientation.
Note: Each joint of length
mLength is assumed to lie along the '+X' axis starting from origin (the joint's local frame). You will have to keep this in mind while performing Task 3.
skin.js file contains the
SkinMesh class that represents the mesh to which a skeleton can be bound to. The functions
createArmSkin() are utility functions that create meshes that you will be using to perform rigid and smooth skinning. The original untransformed vertices for the mesh are stored in the
mOriginalPositions. The render code however employs the
mTransformedPositions to correctly render the mesh that has been deformed by a skeleton bound to it. You can use the
setTransformedVertex() functions to access the original vertex locations and update the transformed vertex locations correspondingly. In the beginning both these positions would be the same since we have not applied any transformations to the underlying geometry. For smooth skinning, the
mWeights array is used to store the weights for each vertex with respect to all the joints in a bound skeleton. The
mJointIds array stores the corresponding joint ids for each weight. The
getVertexWeight() function can be used to retrieve a particular vertex's corresponding weight for a joint.
Note: Note that each vertex stores its corresponding weights and joint indices in a contiguous format. For example if there are two joints in the skeleton attached to the mesh, then vertex v(i) stores the weights for joint 'j0' and 'j1' in the
mWeights[i * 2 + 0] and
mWeights[i * 2 + 1] respectively. Further the indices are also stored as
mJointIds[i* 2 + 0] and
mJointIds[i * 2 + 1] respectively. In case of the mesh's skeleton having multiple joints, the multiplicative factor will be different.
Note that initially the
mSkeleton is set to
null. This is because no skeleton has been bound to the mesh. The skeleton has to be explicitly bound to the mesh when the joints in the mesh are appropriately oriented in the "binding pose". The basecode also has functionality to help you visualize weights for each vertex with respect to each joint. The
setShowWeights() function essentially strips out the weights for a joint 'i' and populates an underlying triangle mesh with the current state of the transformed vertices and weights. This functionality enables you to understand and debug your weighting code correctly.
Figure1: Weight visualizer
Task 1: Rigid Skinning (UG: 2 pts G: 1.5pts)
In the first task, you will implement rigid skinning. For this task you will work in the
skin.js files. The subtasks should be solved in sequential order to complete the entire task.
Subtask 1: Computing Transforms (UG: 1pt G: 0.75pt)
For the first subtask, you will have to compute the local, world and binding transforms of each joint. Remember each joint is specified with a joint location with respect to its parent position and has a rotation axis about which it can rotate.
A local transformation matrix can be computed with a translation component and a rotation component. Compute this transformation in the
Once you are confident that your local transformation is correct, you should implement the computation of the world transform matrix for a joint within the
getWorldMatrix(). Remember that a skeletal system is implemented in a hierarchical fashion with each joint containing a pointer to its parent. The world transform of the root of the hierarchy is the local transform of the root itself. Hence the world transform of a local joint can be computed recursively by moving up the hierarchy by chaining the transforms of the parent of the current node. Hint: You've already implemented something similar as a part of hierarchical transforms in assignment 4.
Once you have computed the world transform of the joints correctly, you can go ahead with computing the binding matrix of each joint. The binding matrix is computed when the joint is aligned with the associated mesh in the "binding pose". Remember the (inverse of the) binding matrix is used to transform a point in world-space to a point in the local space of a bone. You should implement this in the
Once you have computed the binding transform, you can move on to the next subtask to perform rigid skinning of the provided cylindrical mesh.
Subtask 2: Computing Rigid Skinning (UG: 1pt G: 0.75pt)
Once you've successfully computed the previous task, you can go ahead and implement the rigid skinning within the
rigidSkinning() function in the
skin.js file. Note that we have already provided the weights for the cylindrical mesh. Each vertex in the mesh has its weight set to one exactly for one joint. You can query this informtion by employing the
getRigidlyAttachedJoint() function. You can use this information to compute the tranformation of the vertex as the joint is transformed.
Figure 2: Rigid skinning
Task 2: Linear Blend Skinning (UG: 3pts G: 2.5pts)
In the second task you will perform linear blended skinning. This task also has subtasks that you will have to implement in a linear fashion. They build upon one another and successfully implementing them in this fashion will help you finish this part of the assignment.
Subtask 1: Point-Line Nearest Distance Computation (1 pt)
In this first subtask, you will have to compute the nearest distance from a point to a line segment. You will have to add your code to the
computeDistanceToLine() within the
skin.js file. This function takes in a point and the two vertices that make up the line. Note that there are a few cases you'll need to consider to properly compute the distance to a line segment. If the perpendicular projection of the point onto the line lies outside the segment end points, the shortest distance is to one of the two endpoints. Otherwise, the shortest distance is to the line.
Subtask 2: Computing Linear Blend Skinning (UG: 2pts G: 1.5pts)
Once you've implemented the nearest distance between a point and a line segment, you can go ahead and implement a weighting function for each vertex that employs this distance. As discussed in the class lecture, linear blend skinning is implemented by considering a weight for each joint that influences a vertex. The final transformed vertex is then a linear combination of weighted vertex positions that have been transformed by each joint vertex. You will have to implement a weighting strategy within the
computeLinearBlendedWeights() function that considers the inverse of the distance raised to the fourth power (1/distance4) of each vertex to each joint (the length of the joint and its position can be used to compute two endpoints of the line segment) in the underlying skeleton. You will store all the weights in
mWeights and the corresponding joint id within the
mJointIds as indicated earlier. You can use the previously discussed weight display code to help you debug problems associated with this task.
Note: Make sure that you normalize the weights you store for each vertex i.e. all the weights for a vertex sum up to 1.
Figure 3 : Linear blended skinning
Task 3: Skinning A Custom Mesh (1pt)
In this task, you will use all that you've learned in the previous tasks to rig a skeleton to the provided arm mesh. You are required to manually place joints at different locations along the arm. At a minimum, there should at least be 3 bones (the upper arm, middle arm and wrist). We have already provided the locations of the first two bones for your convenience. You can use these positions to get an idea of how to place the additional joints. To get full points for this task you should use additional joints within the fingers to increase the realism of the skinning. Slider elements in the UI are automatically added as you bind a skeleton containing bones to the meshes.
Figure 4 : Skinning a custom mesh
Note: As we have already indicated earlier, the joints are assumed to lie along the '+X' axis. In the arm case, the arm is actually along the '-X' axis. Hence you will have to appropriately orient your bones before actually binding them to the underlying mesh. You can compute the transforms required to position a joint and set them within each joint before attaching to the underlying mesh. You may want to add additional functions to the
Joint class to help you do this.
Procedural Animation of a Custom Mesh: (G: 1pt)
Graduate students must perform a procedural animation of the arm and undergraduates may do so for extra credit. At each time interval 't' you can update the rotation angle of each joint by a predefined angle increment and you can loop over all these frames. The more the number of joints you employ, the more realistic the arm animation would look like.
What to submit:
You should submit a zip file containing the entire folder for this assignment (including all js files, html files and the
resources folder). Rename
firstname-lastname-report.html to contain your name. Fill in the report with any problems encountered and comments about the assignment.
Note that we will grade the assignments in person. You will still need to come to office hours for grading. However, you should also submit the zip file so we know you completed your assignment on time, and that you didn't copy source code from others.