**Meshes**
[A Graphics Codex Programming Project](../projects/index.html)
![Figure [teaser]: A scene composed of solids of revolution. Make objects like this in 100 lines of code!](teaser.jpg border=1)
Introduction
============================================================================================
In this project, you'll learn to create models like those in Figure [teaser] in about 100 lines
of code. You'll extend your solution to the [Cubes project](../cubes/index.html) or the [G3D
starter project](https://casual-effects.com/g3d/G3D10/samples/starter/) to create indexed face meshes by procedural generation. These meshes are the
mainstay of modern 3D graphics.
Prerequisites
---------------------------------------------------------------------------------------------
- Complete the [Cubes](../cubes/index.html) project
- Read in the [Graphics Codex](http://graphicscodex.com):
- A Model of Light
- Surface Geometry
Educational Goals
---------------------------------------------------------------------------------------------
- Learn to think topologically about meshes
- Gain expertise in working with file formats
- Design a simple mesh abstraction for procedural geometry
- Master some common trigonometric and modular arithmetic idioms from graphics
- Experiment informally with materials and lighting
- Improve workflow and project time management
File Formats
--------------------------------------------------------------------------------------------
Data files are interchange formats that allow the use of different tools for creating and
processing data. They allow the same input to be used with many programs, and are a way of
connecting programs to each other. By working with standard data file formats, we can increase
the number of programs that our own can interact with and gain access to large repositories of
information. The structure of an interchange format often differs from the in-memory data
structure that it maps to, because a file has different requirements for space and time
complexity of operations.
You'll continue to use the G3D `.Scene.Any` file format that you learned about on the Cubes
project for scenes. In this project, you'll go beyond loading the stock 3D models from those
scenes. Instead, you'll generate your own meshes and save them in a standard interchange
format.
There are many 3D file formats, and many of those are quite complex. The "Simple 3D File
Formats" topic in the _Graphics Codex_ describes some of the easier ones to manipulate. In this
project, you'll work with an obscure standard format: the
[Geomview ASCII Object File Format](http://www.geomview.org/docs/html/OFF.html) (OFF). It has
the advantage of being really simple to generate...and G3D can already load it. Later you'll
work with OBJ and FBX files, which are more mainstream.
Rules for this Project
-------------------------------------------------
During this project, you are not permitted to directly invoke the following classes and methods
or look at their source code: `G3D::Welder`, `G3D::MeshBuilder`, `G3D::MeshAlg`, and
`G3D::ArticulatedModel::loadHeightfield`. You may read their documentation and reproduce their
functionality with your own implementations if you wish.
You are not permitted to leverage `G3D::ArticulatedModel`'s ability to directly load an image
as a heightfield when specified in a scene file, or to use `G3D::HeightfieldModel`.
You _may_ look at the G3D Procedural Geometry sample program. Beware that it uses different
data structures.
Specification
===============================================================================
Implement the following by extending the Cubes project. For all shapes, there should be no
colocated vertices. For the meshes, you may use arbitrary indexed convex-polygon faces that G3D
will automatically tessellate into triangles on load.
_This project's report requires you to plan your approach before you begin writing code, so
read the whole document before beginning implementation._
1. Create `data-files/model/cube.off`, an axis-aligned cube with unit edge lengths, centered at
the origin. You may make this file in a text editor directly, or write a procedure to
generate it.
2. Create a procedure (function or method) named `makeCylinder` that generates a file
`data-files/model/cylinder.off` given a radius and height. This file should be an indexed
convex-polygon mesh for a cylinder about the $y$-axis that is centered at the origin.
3. Create a user interface for invoking `makeCylinder`. Use number boxes for the parameters and
a button labelled "Generate" to actually launch the execution. Display a message on screen
while generating and then flush the
[`G3D::ArticulatedModel`](http://casual-effects.com/g3d/G3D10/build/manual/class_g3_d_1_1_articulated_model.html)
cache, and reload the current scene on completion.
4. Write a procedure that generates a regular heightfield in `data-files/model/heightfield.off`
with elevation given by the grayscale values of an image file. Use a file dialog to select
the file, a number box to scale the $x$ and $z$ integer coordinates by, and a number box to
set the scale that 1.0 corresponds to for height. Make a button labelled "Generate" that
launches the generator. Display a message on screen while generating and then flush the
`G3D::ArticulatedModel` cache, and reload the current scene on completion.
5. Write a procedure that generates a drinking glass from a contour by creating a solid of
revolution and saves it as `data-files/model/glass.off`. You may make a user interface for
the contour parameters, but are not required to. Make a number box that selects the
number of rotational slices and a button labelled "Generate" that launches the
generator. Display a message on screen while generating and then flush the
`G3D::ArticulatedModel` cache, and reload the current scene on completion.
1. The glass must have nonzero thickness for the bowl part that contains the liquid.
It cannot have any "exposed backfaces".
2. The glass must have a stem or thick base that is not penetrated by an inner well (i.e.,
you can't just double all surfaces).
3. The glass must be geometrically *closed* and *water tight* (i.e., it should topologically
be a sphere)
6. Create a visually interesting `data-files/scene/demo.Scene.Any` scene by combining the
elements from your program. Apply materials and lighting in the `.Scene.Any` file.
Report
==========================================================================
1. Describe the main algorithms of your program, using pseudocode or diagrams as appropriate.
2. Summarize your implementation design in no more than two paragraphs. Provide sufficient
information to aid future implementors seeking to reproduce your work. Why did you make the
program this way? What were your main iteration structures? What were the key
G3D routines that you used?
3. Demonstrate that your program is functioning correctly by showing clear pictures of each of
the shapes with the wireframe visible. For the heightfield, show the source image as well.
Examples are given below.
4. Show one or more images of your demo scene (without wireframe). Describe it in a single
paragraph, indicating both what it is and how you approached the construction. You must
feature a mesh generated by your program, but can augment the scene with other models if you
wish.
5. Answer these questions in at most three sentences each:
1. G3D uses some real-time rendering tricks to fake reflections of the sky, but if you fly
along the shore of my island, you'll see that the water doesn't actually reflect the
mountains. How could you simulate the image of the missing reflection in a plane of water
or a mirror using only nonreflective 3D models?
2. The underlying renderer converts all models into triangles, which you can see in the
wireframe renderings. Why are triangle meshes the preferred primitive compared to, say,
quadrilaterals, which would be more space efficient and are often easier for modeling?
3. The regular tessellation we used for heightfields is inefficient. It will allocate the
same number of triangles for flat portions of the heightfield that need few triangles as
for very hilly ones that need many to accurately represent the shape. Propose an
alternative algorithm for computing a more efficient triangular tessellation for a given
heightfield. Do not actually implement your algorithm. You may invent one on your own
or use external resources to discover existing algorithms.
6. Describe what you learned while working on this project. Distinguish software engineering,
algorithmic, and mathematical skills. Include the reading and ideas that you had to
implicitly work through even if not required.
7. *Workflow*
1. Make a table planning major tasks for this project and then recording your planned and
actual time on them, as show in Table [tbl:workflow]. Each task should be between 0.5 and
1.5 planned hours. Roll up smaller ones and break down longer ones. Break each task
directly corresponding to a deliverable into three versions: a complete first draft, a
minimum viable product, and the polished version. Infrastructure tasks that are necessary
precursors for others can remain as a single version.
2. How long did the project take you, from start to finish (excluding time spent reading,
including time writing the report)? Distinguish time to the minimum viable product
vs. polishing time. Report this as two numbers, in hours.
Task | Description |Planned Time(h)| Actual Time
---------------------|------------------------------------------------------------|--------------:|------------:
Report Draft | Formatted Markdeep document with placeholder text. | 0.50 | 0.25
`makeCylinder` Draft | Empty function with correct signature connected to GUI. | 0.25 | 0.10
`makeCylinder` MVP | `for` loops generating all correct vertices and possibly-buggy faces. | 0.25 | 0.50
Report MVP | Probably-correct answers and quick screenshots. | 0.50 | 0.50
`makeCylinder` Polish| 100% correct and add GUI for number of slices. | 0.25 | 0.30
Report Polish | Tweak table layout, rewrite question#5 answers from scratch, grammar and spelling pass, take more demo scene shots and Photoshop contrast on them. | 0.50 | 0.75
... | ... | ... | ...
[Table [tbl:workflow]: Sample project planning table.]
## Correctness Examples
![Cube](cube.jpg width=155 border=1) ![Cylinder](cylinder.jpg width=155 border=1) ![Heightfield](heightfield.jpg width=155 border=1) ![Drinking glass](wineglass.jpg width=155 border=1)
## Demo Examples
![Gothenburg, Sweden ([source](http://www.liedman.net/2014/06/25/sunshine/))](gothenburg.jpg height=100 border=1) ![Artificial terrain](island.jpg height=100 border=1) ![Table setting](table.jpg height=100 border=1)
Advice
=====================================================================================
## How to use this section
The advice sections of these projects are (intentionally) written in a way that won't make much
sense to you before you've started coding. I intend you to do the reading from the books, start
on the project, and then get stuck about 90 minutes in to the project.
How do you know when you're stuck? Easy: tasks start taking longer than you estimated. For the
first project, tasks probably took longer than expected because you were just getting accustomed
to the tools. But from now on, when something is taking a long time to implement or debug, it
is almost always because you're not really sure what the code is supposed to do.
When you're stuck--that is when you read the Advice section. The problem that you're stuck on
is usually a problem that everyone encounters when completing this project, including me. The
advice here will make sense in the context of that problem. It is a record of what I had to
figure out when completing it myself and what I've told students to help them on similar
projects.
If you have a problem and find something in the advice section that helps, you should feel good
about yourself. That is a sign that you're on the right track. You're following the same path
that people who successfully completed the project were on and hit the same roadblocks along
the way.
After reading the Advice section, you may still be unsure how to proceed with your code. Or,
none of the advice may seem relevant. In this case, you need to go back to the reading. Rarely
can anyone understand technical writing on the first pass. The second time through a chapter,
you'll spot all kinds of information that you then appreciate. For example, plan to reread the
C++ chapter of the Graphics Codex in the middle of every project.
Workflow
-------------------------------------------------------------------------------------
The _first_ task I took on for this project was writing the report. That's right...I started
with the _last_ piece of the document. I did that because writing the report helped me
to understand what I was tackling. It is easier to make images and objects if you know
how they'll be used.
Attempting to answer the questions before creating the implementation will also help you to
think about the project in the right way. I don't expect you to be able to answer them
beforehand, but trying to is very valuable.
Track the amount of code that you're writing versus my reference solution. If your solution
starts to get much longer than mine, consider whether an alternative design would reduce
the amount of code that you need to write (and debug, and later maintain...)
My solution contained about 160 source lines of code in `.cpp` and `.h` files. I measured this
by running Find in Files in Visual Studio with the search string "`;`" in the `source`
directory and then looking at the bottom of the results for the count. About 60 lines of that
were from the G3D Starter project after I'd stripped out unnecessary parts for this project.
The remaining 100 lines were my solution, including the GUI code and some debugging and testing
code.
I also created six `.Scene.Any` files for testing and rendering results and ten OFF files. For
every image that I produced and test that I ran, I retained a scene file that had the exact
object and camera positions. This allowed me to easily go back and reproduce the results.
My journal was about 300 lines long and included about 20 images. In addition to documenting
successes, every bug that I encountered was documented in the journal, including of course the
solution to fixing it. This helps me when I encounter similar bugs on later projects, or if
I'm handing off a project to another team member later.
When you make your planning schedule, ensure that you're working in iterative passes. Complete
all "draft" tasks before moving on to any "MVP" ones, and then complete those before "polish".
G3D APIs
-------------------------------------------------------------------------------------
To write text files, you can use [`G3D::TextOutput`](http://casual-effects.com/g3d/G3D10/build/manual/class_g3_d_1_1_text_output.html),
[`fprintf`](http://www.cplusplus.com/reference/cstdio/fprintf/), or
[`std::cout`](http://www.cplusplus.com/reference/iostream/cout/) to write to the
file. I prefer the `TextOutput` because it generalizes to indenting and has a matching
`TextInput` for parsing.
For convenience, directly add your user interface to the [`G3D::GApp::debugPane`](http://casual-effects.com/g3d/G3D10/build/manual/class_g3_d_1_1_g_app.html#a682d93c21b914a28f6baeb4b6270d1a6) instead of
making a new window. See the following:
- [`G3D::GuiPane`](http://casual-effects.com/g3d/G3D10/build/manual/class_g3_d_1_1_file_dialog.html#aa6faec311dce2ec6faa7ded11d654068)
- [`G3D::GApp::drawMessage`](http://casual-effects.com/g3d/G3D10/build/manual/class_g3_d_1_1_g_app.html#a1d796dc185b9528b900d936a8b537f8d)
- [`G3D::msgBox`](http://casual-effects.com/g3d/G3D10/build/manual/namespace_g3_d.html#afa98304f088797b023e37167c507373c)
- [`G3D::FileDialog::getFilename`](http://casual-effects.com/g3d/G3D10/build/manual/class_g3_d_1_1_file_dialog.html#aa6faec311dce2ec6faa7ded11d654068)
Use `G3D::GuiPane` to create your
[`G3D::GuiNumberBox`](http://casual-effects.com/g3d/G3D10/build/manual/class_g3_d_1_1_gui_number_box.html)es
and
[`G3D::GuiButton`](http://casual-effects.com/g3d/G3D10/build/manual/class_g3_d_1_1_gui_button.html)s.
You can use [`G3D::Image`](http://casual-effects.com/g3d/G3D10/build/manual/class_g3_d_1_1_image.html) to load the image for the heightfield.
Since this is your first time using G3D's GUI system, I've provided an
example of how to use it in Listing [gui]. The overall design is easy to use:
- Add controls to `G3D::GuiPane`s
- Pass pointers to the underlying data that you want the control to modify
- Use C++ lambda expressions for callbacks on buttons.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// inside App::makeGUI:
GuiPane* heightfieldPane = debugPane->addPane("Heightfield");
heightfieldPane->setNewChildSize(240);
heightfieldPane->addNumberBox("Max Y", &m_heightfieldYScale, "m",
GuiTheme::LOG_SLIDER, 0.0f, 100.0f)->setUnitsSize(30);
heightfieldPane->addNumberBox("XZ Scale", &m_heightfieldXZScale, "m/px",
GuiTheme::LOG_SLIDER, 0.001f, 10.0f)->setUnitsSize(30);
heightfieldPane->beginRow(); {
heightfieldPane->addTextBox("Input Image", &m_heightfieldSource)->setWidth(210);
heightfieldPane->addButton("...", [this]() {
FileDialog::getFilename(m_heightfieldSource, "png", false);
})->setWidth(30);
} heightfieldPane->endRow();
heightfieldPane->addButton("Generate", [this](){
shared_ptr
mug and a doughnut.
[Public Domain image by Lucas V. Barbosa](https://commons.wikimedia.org/w/index.php?curid=1236079)](morph.gif)
Consider the mesh of a torus (e.g., a doughnut). It has one hole...as does a coffee mug. This
means that we can transform one into the other, at least, as long as there are enough triangles
to have some flexibility. Take the doughnut (imagining that it is made of very stretchy rubber)
and pull out one side without ripping it. You now have a doughnut that is fat on one side. Push
in on the fat lobe without pushing one side through the other and there's a dent. That dent
becomes the bowl of the coffee mug.
This long digression brings us to an essential point for completing the project at hand: a
cube, a cylinder, and a drinking glass are all topologically equivalent to a sphere. _You can
produce a cube, a cylinder, and a drinking glass using the exact same code_. All that changes
is the number of vertical and angular slices, and the offsets (in this case, $y$ and radius) of
the vertices. If you're having trouble with the drinking glass code, consider that it is just
a generalization of the cylinder code (using the mesh from the heightfield.)
Debugging
------------------------------------------------------------------------------------
This is our first project with somewhat in-depth code, and presents a good opportunity
to discuss debugging strategies.
I liberally used [`G3D::debugAssert`](http://casual-effects.com/g3d/G3D10/build/manual/debug_assert_8h.html#adb3cef18983da8440d55b7557f83358c),
[`G3D::debugAssertM`](http://casual-effects.com/g3d/G3D10/build/manual/debug_assert_8h.html#af7e8ca29ffa4af20db0de5a93e1b81e9), and
[`G3D::alwaysAssertM`](http://casual-effects.com/g3d/G3D10/build/manual/debug_assert_8h.html#ac765dd93625b827ed4487eaa30a7c532) throughout
my code. I sought to programmatically verify every assumption made.
I tend to run my programs in optimized, release mode by default. It is faster, which is helpful
for optimizing workflow. As we start using larger data files and running more complex algorithms,
this will be essential for completing projects in a timely manner.
The caveat to running an optimized build is that as soon as something goes wrong, your first
response should be to switch to the debug build. That will enable G3D's assertions and your own.
Do this before you attempt to debug the program or reason about it.
Always debug on the simplest case that triggers the error. For example, don't generate a 1000-sided cylinder
and try to figure out what is wrong with it. Instead, use a three-sided cylinder. You can
inspect the output file directly at that size, and can single-step through your code if
necessary.
For the heightfield, a 2x2 image is a good place to start. Then try 4x3 (asymmetry will flush
bugs with swapping $x$ and $z$), and finally step up to a larger size.
I debugged the solid of revolution tested by first creating a triangular bipyramid, which is the simplest
solid of revolution. It contains five vertices and six triangular faces. Note that this does not test
the center strip case.
When debugging the cylinder and solid of revolution, I made one strip at a time (top, center,
and then bottom) and verified the 3D model for each before proceeding to the next. Otherwise
it would have been too hard to identify the source of the problem when something was generated
incorrectly.
Coding Style
------------------------------------------------------------------------------------
Now that you're moving code between projects, correct and useful documentation is very
important. Remember; you're not programming for the computer, and you're not programming for
me--you're programming for your future self and partners.
The weakest form of implementation documentation is a comment. (I'm not talking about interface
documentation, where comments are valuable.) Strong interfaces, clear method names and helper
methods, clear variable names, useful auxiliary classes, and consistent use of design patterns
are much better forms of documentation.
Sometimes a comment is justified because you need to explain _why_ you did something unexpected
in the implementation. But in general, most *comments are a sign that your code is poorly
written*. If the code is good enough, then it doesn't need many comments because it is obvious
what the interfaces are and how the implementations work.
Don't comment bad code. Rewrite it. Complex algorithms and fancy optimizations are exceptions
to this advice: there, you need to explain the tricky thing that was going on. But does the
problem really justify doing something tricky? Did you profile your code before and after the
change? Was the speedup worth it? I'd rather have code that is correct and robust than faster
than required.
Coding conventions become important when bringing together code from many sources. These
conventions include the use of capitalization and whitespace. It doesn't matter what
conventions you use so long as they are consistent across the codebase.
Since you're using a lot of G3D code, I recommend that you follow the G3D project's coding
conventions, which are largely the official Java conventions adapted to C++ syntax. You have
the full G3D source code available to you, which abundantly demonstrates the coding
conventions.
Gallery
==============================================================================================
For more inspiration and some impressive extensions to the base specification, look at some
examples of work created by my students for this project:
- [Williams College CS371 2016](https://www.cs.williams.edu/~morgan/cs371-f16/gallery/1-meshes/index.html)
- [Williams College CS371 2014](https://www.cs.williams.edu/~morgan/cs371-f14/gallery/1-Meshes/index.html)
- [Williams College CS371 2012](https://www.cs.williams.edu/~morgan/cs371-f12/gallery/1-Meshes/index.html)
- [Williams College CS371 2010](https://www.cs.williams.edu/~morgan/cs371-f10/gallery/1-Meshes/index.html)