aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatt Strapp <matt@mattstrapp.net>2021-12-17 13:17:58 -0600
committerMatt Strapp <matt@mattstrapp.net>2021-12-17 13:17:58 -0600
commite0a8f6b97c473d28af96f28ca6b46d41b91646a3 (patch)
tree8faef1da18b809c9585c11ba5917f1f8268cddf6
parentAdd README (diff)
parentClarify how to get eye point in Sky::ScreenPtHitsSky (diff)
downloadcsci4611-e0a8f6b97c473d28af96f28ca6b46d41b91646a3.tar
csci4611-e0a8f6b97c473d28af96f28ca6b46d41b91646a3.tar.gz
csci4611-e0a8f6b97c473d28af96f28ca6b46d41b91646a3.tar.bz2
csci4611-e0a8f6b97c473d28af96f28ca6b46d41b91646a3.tar.lz
csci4611-e0a8f6b97c473d28af96f28ca6b46d41b91646a3.tar.xz
csci4611-e0a8f6b97c473d28af96f28ca6b46d41b91646a3.tar.zst
csci4611-e0a8f6b97c473d28af96f28ca6b46d41b91646a3.zip
Merge branch 'support-code' of https://github.umn.edu/umn-csci-4611-f21/shared-upstream
-rw-r--r--dev/a6-harold/.gitignore2
-rw-r--r--dev/a6-harold/CMakeLists.txt209
-rw-r--r--dev/a6-harold/README.md0
-rw-r--r--dev/a6-harold/billboards.cc206
-rw-r--r--dev/a6-harold/billboards.h78
-rw-r--r--dev/a6-harold/cmake/DownloadHelper.txt.in26
-rw-r--r--dev/a6-harold/cmake/ExternalProjectDownloadBuildInstall.cmake98
-rw-r--r--dev/a6-harold/cmake/MessageMacros.cmake17
-rw-r--r--dev/a6-harold/cmake/UseOpenGL.cmake52
-rw-r--r--dev/a6-harold/config.h.in15
-rw-r--r--dev/a6-harold/data/toonDiffuse.pngbin0 -> 685 bytes
-rw-r--r--dev/a6-harold/data/toonSpecular.pngbin0 -> 654 bytes
-rw-r--r--dev/a6-harold/edge_mesh.cc235
-rw-r--r--dev/a6-harold/edge_mesh.h82
-rw-r--r--dev/a6-harold/ground.cc283
-rw-r--r--dev/a6-harold/ground.h63
-rw-r--r--dev/a6-harold/harold_app.cc278
-rw-r--r--dev/a6-harold/harold_app.h120
-rw-r--r--dev/a6-harold/main.cc10
-rw-r--r--dev/a6-harold/shaders/artsy.frag29
-rw-r--r--dev/a6-harold/shaders/artsy.vert17
-rw-r--r--dev/a6-harold/shaders/outline.frag7
-rw-r--r--dev/a6-harold/shaders/outline.vert23
-rw-r--r--dev/a6-harold/shaders/stroke2d.frag9
-rw-r--r--dev/a6-harold/shaders/stroke2d.vert7
-rw-r--r--dev/a6-harold/shaders/stroke3d.frag9
-rw-r--r--dev/a6-harold/shaders/stroke3d.vert10
-rw-r--r--dev/a6-harold/sky.cc79
-rw-r--r--dev/a6-harold/sky.h52
-rw-r--r--worksheets/a6_harold.md94
30 files changed, 2110 insertions, 0 deletions
diff --git a/dev/a6-harold/.gitignore b/dev/a6-harold/.gitignore
new file mode 100644
index 0000000..dd1a9a8
--- /dev/null
+++ b/dev/a6-harold/.gitignore
@@ -0,0 +1,2 @@
+config.h
+build
diff --git a/dev/a6-harold/CMakeLists.txt b/dev/a6-harold/CMakeLists.txt
new file mode 100644
index 0000000..83ca82d
--- /dev/null
+++ b/dev/a6-harold/CMakeLists.txt
@@ -0,0 +1,209 @@
+# Original Author(s) of this File:
+# Daniel Keefe, 2017, University of Minnesota
+#
+# Author(s) of Significant Updates/Modifications to the File:
+# ...
+
+
+
+# You are encouraged to copy this example, move it outside of the MinGfx directory, and use
+# it as a starting point for your project. When you do this, you'll have to edit the
+# following line as needed to point to the MinGfx install prefix used on your system.
+
+# !!!!!!!!!!!!! EDIT THE FOLLOWING LINE AS NEEDED !!!!!!!!!!!!!
+list(APPEND CMAKE_PREFIX_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../build/install ../..)
+
+
+
+#### BASIC PROJECT SETUP ####
+
+project(a6-harold)
+
+# Using 3.9 to get a modern version of FindOpenGL.cmake
+cmake_minimum_required (VERSION 3.9)
+
+# Dependencies that are auto-downloaded, built, and installed for you will go in the
+# directory pointed to by the CMAKE_INSTALL_PREFIX. It defaults to a location inside
+# the build directory.
+if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT OR "${CMAKE_INSTALL_PREFIX}" STREQUAL "")
+ set (CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/install" CACHE PATH "default install path" FORCE )
+endif()
+
+# Add to paths cmake uses to search for scripts, modules, and config packages
+list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake ${CMAKE_INSTALL_PREFIX})
+list(INSERT CMAKE_PREFIX_PATH 0 ${CMAKE_INSTALL_PREFIX})
+
+include(MessageMacros)
+h1("Building ${PROJECT_NAME}")
+h2("Configuring paths")
+
+message(STATUS "Module path: ${CMAKE_MODULE_PATH}")
+message(STATUS "Prefix path: ${CMAKE_PREFIX_PATH}")
+message(STATUS "Install prefix: ${CMAKE_INSTALL_PREFIX}")
+
+set(DATA_DIR_BUILD ${CMAKE_CURRENT_SOURCE_DIR}/data)
+set(DATA_DIR_INSTALL ${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}/data)
+
+message(STATUS "Data dir (in build tree): ${DATA_DIR_BUILD}")
+message(STATUS "Data dir (in install tree): ${DATA_DIR_INSTALL}")
+
+set(SHADERS_DIR_BUILD ${CMAKE_CURRENT_SOURCE_DIR}/shaders)
+set(SHADERS_DIR_INSTALL ${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}/shaders)
+
+message(STATUS "Shaders dir (in build tree): ${SHADERS_DIR_BUILD}")
+message(STATUS "Shaders dir (in install tree): ${SHADERS_DIR_INSTALL}")
+
+
+# Configure a header file to pass some of the CMake settings to the source code
+configure_file(
+ ${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
+ ${CMAKE_CURRENT_SOURCE_DIR}/config.h
+)
+
+#### SOURCE FOR THIS PROJECT ####
+h2("Configuring source files")
+
+set(SOURCEFILES
+ billboards.cc
+ harold_app.cc
+ edge_mesh.cc
+ ground.cc
+ main.cc
+ sky.cc
+)
+
+set(HEADERFILES
+ billboards.h
+ harold_app.h
+ config.h
+ edge_mesh.h
+ ground.h
+ sky.h
+)
+
+set(EXTRAFILES
+ config.h.in
+ README.md
+)
+
+set(SHADERFILES
+ shaders/artsy.vert
+ shaders/artsy.frag
+ shaders/outline.vert
+ shaders/outline.frag
+ shaders/stroke2d.vert
+ shaders/stroke2d.frag
+ shaders/stroke3d.vert
+ shaders/stroke3d.frag
+)
+
+set_source_files_properties(${EXTRAFILES} PROPERTIES HEADER_FILE_ONLY TRUE)
+set_source_files_properties(${SHADERFILES} PROPERTIES HEADER_FILE_ONLY TRUE)
+
+source_group("Shaders" FILES ${SHADERFILES})
+
+
+#### COMPILE OPTIONS ####
+
+h2("Configuring Compiler Options")
+
+
+
+message(STATUS "Building for " ${CMAKE_SYSTEM_NAME} ".")
+
+# Linux specific
+if (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+ add_definitions(-DLINUX)
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
+endif()
+
+
+# Apple specific
+if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+ add_definitions(-DOSX)
+
+ # RPATH settings, see https://cmake.org/Wiki/CMake_RPATH_handling
+ set(CMAKE_MACOSX_RPATH ON)
+
+ # use, i.e. don't skip the full RPATH for the build tree
+ SET(CMAKE_SKIP_BUILD_RPATH FALSE)
+
+ # when building, don't use the install RPATH already
+ # (but later on when installing)
+ SET(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
+
+ SET(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
+
+ # add the automatically determined parts of the RPATH
+ # which point to directories outside the build tree to the install RPATH
+ SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
+
+ # the RPATH to be used when installing, but only if it's not a system directory
+ LIST(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES "${CMAKE_INSTALL_PREFIX}/lib" isSystemDir)
+ IF("${isSystemDir}" STREQUAL "-1")
+ SET(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
+ ENDIF("${isSystemDir}" STREQUAL "-1")
+
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
+endif()
+
+
+# Windows specific
+if (WIN32)
+ add_definitions(-DWIN32)
+
+ if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
+ message(FATAL_ERROR
+ "You must use the 64 bit version of the compiler. Be sure to set the correct generator when configuring through CMake.")
+ endif()
+endif()
+
+
+
+
+#### DEFINE TARGET(S) ####
+
+h2("Defining Target(s)")
+
+add_executable(${PROJECT_NAME} ${SOURCEFILES} ${HEADERFILES} ${EXTRAFILES} ${SHADERFILES})
+
+
+
+#### FIND AND ADD DEPENDENCIES ####
+
+h2("Adding Dependencies")
+set(EXTERNAL_DIR external)
+
+
+# MinGfx (linked with an imported cmake target so no need to specify include dirs)
+# This will try to find MinGfxConfig.cmake, which should have been installed under
+# CMAKE_INSTALL_PREFIX/lib/cmake/MinGfx when you installed the MinGfx Toolkit.
+find_package(MinGfx REQUIRED)
+target_link_libraries(${PROJECT_NAME} PUBLIC MinGfx::MinGfx)
+
+
+# Add dependency on OpenGL
+include(UseOpenGL)
+UseOpenGL(${PROJECT_NAME} PUBLIC ${EXTERNAL_DIR})
+
+
+
+#### INSTALL TARGET(S) ####
+
+h2("Configuring Install Target")
+
+# The install locations are relative to the CMAKE_INSTALL_PREFIX variable
+install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin)
+
+install(
+ DIRECTORY data/
+ DESTINATION ${DATA_DIR_INSTALL}
+ OPTIONAL
+)
+
+install(
+ DIRECTORY shaders/
+ DESTINATION ${SHADERS_DIR_INSTALL}
+ OPTIONAL
+)
diff --git a/dev/a6-harold/README.md b/dev/a6-harold/README.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dev/a6-harold/README.md
diff --git a/dev/a6-harold/billboards.cc b/dev/a6-harold/billboards.cc
new file mode 100644
index 0000000..5ec508a
--- /dev/null
+++ b/dev/a6-harold/billboards.cc
@@ -0,0 +1,206 @@
+/** CSci-4611 Assignment 6: Harold
+ */
+
+#include "billboards.h"
+
+Billboards::Billboards() {
+
+}
+
+
+Billboards::~Billboards() {
+
+}
+
+
+void Billboards::Init(ShaderProgram *stroke3d_shaderprog) {
+ stroke3d_shaderprog_ = stroke3d_shaderprog;
+}
+
+
+/// Projects a 2D normalized screen point (e.g., the mouse position in normalized
+/// device coordinates) to a 3D point on a plane defined by an "origin", which can
+/// really be any point coincident with the plane, and the plane normal. Returns
+/// true if the screen point projects onto the plane and stores the result in
+/// plane_point. Returns false if the screen point does not project onto the plane.
+bool Billboards::ScreenPtHitsPlane(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const Point3 &plane_origin, const Vector3 &plane_normal,
+ const Point2 &normalized_screen_pt, Point3 *plane_point)
+{
+ Matrix4 camera_matrix = view_matrix.Inverse();
+ Point3 eye = camera_matrix.ColumnToPoint3(3);
+
+ Point3 pt3d = GfxMath::ScreenToNearPlane(view_matrix, proj_matrix, normalized_screen_pt);
+ Ray ray(eye, (pt3d - eye).ToUnit());
+ float t;
+ return ray.IntersectPlane(plane_origin, plane_normal, &t, plane_point);
+}
+
+
+/// Checks to see if a ray starting at the eye point and passing through 2D
+/// normalized screen point projects onto any of the billboards in the scene. If
+/// so, returns the id of the closest billboard intersected. If not, returns -1.
+int Billboards::IntersectBillboard(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const Point2 &normalized_screen_pt)
+{
+ Matrix4 camera_matrix = view_matrix.Inverse();
+ Point3 eye = camera_matrix.ColumnToPoint3(3);
+
+ int id = -1;
+ float best_i_time = -1.0;
+
+ Point3 pt3d = GfxMath::ScreenToNearPlane(view_matrix, proj_matrix, normalized_screen_pt);
+ Ray ray(eye, (pt3d - eye).ToUnit());
+
+ Point3 i_point;
+ float i_time;
+ int i_tri_id;
+ for (int i=0; i<billboards_.size(); i++) {
+ Matrix4 to_billboard_space = billboards_[i].transform.Inverse();
+ Ray ray_billboard_space = to_billboard_space * ray;
+ if ((ray_billboard_space.IntersectAABB(billboards_[i].bounding_box, &i_time)) &&
+ (ray_billboard_space.IntersectMesh(billboards_[i].mesh, &i_time, &i_point, &i_tri_id)) &&
+ ((best_i_time == -1.0) || (i_time < best_i_time))) {
+ id = i;
+ }
+ }
+
+ return id;
+}
+
+
+/// Adds a new stroke as a billboard by projecting it onto a plane parallel
+/// to the filmplane that intersects with the base point, which should lie
+/// on the ground.
+void Billboards::AddBillboardStroke(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const std::vector<Point2> &stroke2d, const Mesh &stroke2d_mesh,
+ const Color &stroke_color, Ground *ground_ptr)
+{
+ Matrix4 camera_matrix = view_matrix.Inverse();
+ Point3 eye = camera_matrix.ColumnToPoint3(3);
+
+ // Create a new billboard
+ Billboard b;
+ b.color = stroke_color;
+ b.transform = Matrix4();
+ b.mesh = stroke2d_mesh;
+
+ // find the base point for the billboard
+ ground_ptr->ScreenPtHitsGround(view_matrix, proj_matrix, stroke2d[0], &b.anchor_pt);
+ Vector3 norm = (eye - b.anchor_pt);
+ norm[1] = 0.0; // with 0 change in Y so the billboard does not tilt
+ norm.Normalize(); // convert to a unit vector
+
+ // project the stroke into 3D to lie on the projection plane
+ std::vector<Point3> verts;
+ for (int i=0; i<b.mesh.num_vertices(); i++) {
+ Point2 pscreen = Point2(b.mesh.read_vertex_data(i)[0], b.mesh.read_vertex_data(i)[1]);
+ Point3 pplane;
+ ScreenPtHitsPlane(view_matrix, proj_matrix, b.anchor_pt, norm, pscreen, &pplane);
+ verts.push_back(pplane);
+ }
+
+ Matrix4 to_canonical_billboard = Matrix4::Align(b.anchor_pt, Vector3::UnitY(), norm,
+ Point3::Origin(), Vector3::UnitY(), Vector3::UnitZ());
+
+ for (int i=0; i<verts.size(); i++) {
+ verts[i] = to_canonical_billboard * verts[i];
+ }
+
+ b.mesh.SetVertices(verts);
+
+ for (int i=0; i<b.mesh.num_vertices(); i++) {
+ b.bounding_box = b.bounding_box + AABB(b.mesh.read_vertex_data(i));
+ }
+ b.bounding_box.set_user_data((int)billboards_.size());
+
+ billboards_.push_back(b);
+}
+
+
+/// Edits an existing billboard by adding an additional stroke to it.
+void Billboards::AddToBillboard(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ int billboard_id, const Mesh &stroke2d_mesh,
+ const Color &stroke_color)
+{
+ Matrix4 camera_matrix = view_matrix.Inverse();
+ Point3 eye = camera_matrix.ColumnToPoint3(3);
+
+ // Really what this does is add a new billboard that lies in the same plane
+ // as the one we are "editing" and has the same anchor point so that they
+ // will both rotate the same way.
+ Billboard b;
+ b.color = stroke_color;
+ b.transform = Matrix4();
+ b.mesh = stroke2d_mesh;
+
+ b.anchor_pt = billboards_[billboard_id].anchor_pt;
+
+
+ Vector3 norm = (eye - b.anchor_pt);
+ norm[1] = 0.0; // with 0 change in Y so the billboard does not tilt
+ norm.Normalize(); // convert to a unit vector
+
+ // project the stroke into 3D to lie on the projection plane
+ std::vector<Point3> verts;
+ for (int i=0; i<b.mesh.num_vertices(); i++) {
+ Point2 pscreen = Point2(b.mesh.read_vertex_data(i)[0], b.mesh.read_vertex_data(i)[1]);
+ Point3 pplane;
+ ScreenPtHitsPlane(view_matrix, proj_matrix, b.anchor_pt, norm, pscreen, &pplane);
+ verts.push_back(pplane);
+ }
+
+ Matrix4 to_canonical_billboard = Matrix4::Align(b.anchor_pt, Vector3::UnitY(), norm,
+ Point3::Origin(), Vector3::UnitY(), Vector3::UnitZ());
+
+ for (int i=0; i<verts.size(); i++) {
+ verts[i] = to_canonical_billboard * verts[i];
+ }
+
+ b.mesh.SetVertices(verts);
+
+ for (int i=0; i<b.mesh.num_vertices(); i++) {
+ b.bounding_box = b.bounding_box + AABB(b.mesh.read_vertex_data(i));
+ }
+ b.bounding_box.set_user_data((int)billboards_.size());
+
+ billboards_.push_back(b);
+}
+
+
+void Billboards::UpdateBillboardRotations(const Point3 &current_eye_point) {
+ // Whenver the camera moves, we also need to rotate the billboards so
+ // that they always face the viewer.
+ for (int i=0; i<billboards_.size(); i++) {
+ // billboards are stored in a coordinate system where the base is
+ // at (0,0,0), rotation happens about the Y axis, and the billboard's
+ // normal is +Z.
+ Point3 a_p = Point3(0,0,0); // 'anchor point' in billboard coordinates
+ Vector3 a_v1 = Vector3::UnitY(); // 'up' in billboard coordinates
+ Vector3 a_v2 = Vector3::UnitZ(); // 'normal' in billboard coordinates
+
+ Point3 b_p = billboards_[i].anchor_pt; // 'anchor point' in world coordinates
+ Vector3 b_v1 = Vector3::UnitY(); // 'up' in world coordinates
+ Vector3 b_v2 = (current_eye_point - b_p); // 'normal' in world coordinates
+ b_v2[1] = 0.0; // force 0 change in Y so the billboard does not tilt
+ b_v2.Normalize(); // convert to a unit vector
+
+ billboards_[i].transform = Matrix4::Align(a_p,a_v1,a_v2, b_p,b_v1,b_v2);
+ }
+}
+
+
+void Billboards::Draw(const Matrix4 &view_matrix, const Matrix4 &proj_matrix) {
+ // Draw billboard meshes
+ stroke3d_shaderprog_->UseProgram();
+ stroke3d_shaderprog_->SetUniform("projectionMatrix", proj_matrix);
+ for (int i=0; i<billboards_.size(); i++) {
+ Matrix4 model_matrix = billboards_[i].transform;
+ Matrix4 modelview_matrix = view_matrix * model_matrix;
+ stroke3d_shaderprog_->SetUniform("modelViewMatrix", modelview_matrix);
+ stroke3d_shaderprog_->SetUniform("strokeColor", billboards_[i].color);
+ billboards_[i].mesh.Draw();
+ }
+ stroke3d_shaderprog_->StopProgram();
+}
+
diff --git a/dev/a6-harold/billboards.h b/dev/a6-harold/billboards.h
new file mode 100644
index 0000000..7de2799
--- /dev/null
+++ b/dev/a6-harold/billboards.h
@@ -0,0 +1,78 @@
+/** CSci-4611 Assignment 6: Harold
+ */
+
+#ifndef BILLBOARDS_H_
+#define BILLBOARDS_H_
+
+#include <mingfx.h>
+using namespace mingfx;
+
+#include "ground.h"
+
+/** Billboards are strokes planted in the ground that rotate automatically to
+ face the viewer. This class holds the entire collection of billboard strokes.
+ */
+class Billboards {
+public:
+ Billboards();
+ virtual ~Billboards();
+
+ void Init(ShaderProgram *stroke3d_shaderprog);
+
+ /// Projects a 2D normalized screen point (e.g., the mouse position in normalized
+ /// device coordinates) to a 3D point on a plane defined by an "origin", which can
+ /// really be any point coincident with the plane, and the plane normal. Returns
+ /// true if the screen point projects onto the plane and stores the result in
+ /// plane_point. Returns false if the screen point does not project onto the plane.
+ bool ScreenPtHitsPlane(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const Point3 &plane_origin, const Vector3 &plane_normal,
+ const Point2 &normalized_screen_pt, Point3 *plane_point);
+
+
+ /// Checks to see if a ray starting at the eye point and passing through 2D
+ /// normalized screen point projects onto any of the billboards in the scene. If
+ /// so, returns the id of the closest billboard intersected. If not, returns -1.
+ int IntersectBillboard(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const Point2 &normalized_screen_pt);
+
+
+ /// Adds a new stroke as a billboard by projecting it onto a plane parallel
+ /// to the filmplane that intersects with the anchor point, which should lie
+ /// on the ground.
+ void AddBillboardStroke(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const std::vector<Point2> &stroke2d, const Mesh &stroke2d_mesh,
+ const Color &stroke_color, Ground *ground_ptr);
+
+ /// Edits an existing billboard by adding an additional stroke to it.
+ void AddToBillboard(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ int billboard_id, const Mesh &stroke2d_mesh,
+ const Color &stroke_color);
+
+
+ /// Adjusts the transformation matrix used to draw each billboard so that the
+ /// billboard will face the camera. Needs to be called each time the camera
+ /// is moved within the scene.
+ void UpdateBillboardRotations(const Point3 &current_eye_point);
+
+
+ /// Draws all of the billboards
+ void Draw(const Matrix4 &view_matrix, const Matrix4 &proj_matrix);
+
+private:
+
+ // each billboard stores the following
+ struct Billboard {
+ Mesh mesh;
+ Color color;
+ Point3 anchor_pt;
+ AABB bounding_box;
+ Matrix4 transform;
+ };
+
+ // the array of all active billboards
+ std::vector<Billboard> billboards_;
+
+ ShaderProgram *stroke3d_shaderprog_;
+};
+
+#endif
diff --git a/dev/a6-harold/cmake/DownloadHelper.txt.in b/dev/a6-harold/cmake/DownloadHelper.txt.in
new file mode 100644
index 0000000..fb29bff
--- /dev/null
+++ b/dev/a6-harold/cmake/DownloadHelper.txt.in
@@ -0,0 +1,26 @@
+# This file is part of the MinGfx cmake build system.
+# See the main MinGfx/CMakeLists.txt file for authors, copyright, and license info.
+
+# This is a "helper" cmake project -- the only thing this project does is download
+# the external project. So, the configure, build, install, and test commands for
+# ExternalProject_Add() are intentionally set as NOPs.
+
+cmake_minimum_required (VERSION 3.9)
+
+project(@EXT_PROJECT_NAME@-download)
+
+include(ExternalProject)
+ExternalProject_Add(
+ @EXT_PROJECT_NAME@
+ SOURCE_DIR "@DOWNLOAD_DIR@/@EXT_PROJECT_NAME@/src"
+ BINARY_DIR "@DOWNLOAD_DIR@/@EXT_PROJECT_NAME@/download-helper"
+ @DOWNLOAD_OPTIONS@
+ CONFIGURE_COMMAND ""
+ BUILD_COMMAND ""
+ INSTALL_COMMAND ""
+ TEST_COMMAND ""
+ LOG_DOWNLOAD ON
+ GIT_PROGRESS 1
+)
+
+
diff --git a/dev/a6-harold/cmake/ExternalProjectDownloadBuildInstall.cmake b/dev/a6-harold/cmake/ExternalProjectDownloadBuildInstall.cmake
new file mode 100644
index 0000000..ce12d1d
--- /dev/null
+++ b/dev/a6-harold/cmake/ExternalProjectDownloadBuildInstall.cmake
@@ -0,0 +1,98 @@
+# This file is part of the MinGfx cmake build system.
+# See the main MinGfx/CMakeLists.txt file for authors, copyright, and license info.
+
+
+# Calling CMAKE_CURRENT_LIST_DIR inside a function returns the list dir of the calling script
+# but we want the list dir of this file in order to find the DownloadHelper.txt.in file, which
+# should be stored right next to this one. So, defining this variable outside the scope of the
+# functions below.
+set(DIR_OF_THIS_FILE ${CMAKE_CURRENT_LIST_DIR})
+
+
+
+# Usage:
+# ExternalProject_Download(
+# # This first argument is the name of the project to download. It is required:
+# glm
+#
+# # Additional arguments specify how to download the project using GIT, SVN, CVS, or URL.
+# # These can be any of the arguments used for the downloading step of the cmake builtin
+# # ExternalProject_Add command.
+# GIT_REPOSITORY "https://github.com/g-truc/glm.git"
+# GIT_TAG master
+# etc..
+# )
+function(ExternalProject_Download EXT_PROJECT_NAME DOWNLOAD_DIR)
+
+ include(MessageMacros)
+ h1("BEGIN EXTERNAL PROJECT DOWNLOAD (${EXT_PROJECT_NAME}).")
+
+ h2("Creating a download helper project for ${EXT_PROJECT_NAME}.")
+
+ set(DOWNLOAD_OPTIONS ${ARGN})
+ string (REGEX REPLACE "(^|[^\\\\]);" "\\1 " DOWNLOAD_OPTIONS "${DOWNLOAD_OPTIONS}")
+
+
+ file(MAKE_DIRECTORY ${DOWNLOAD_DIR}/${EXT_PROJECT_NAME})
+ configure_file(
+ ${DIR_OF_THIS_FILE}/DownloadHelper.txt.in
+ ${DOWNLOAD_DIR}/${EXT_PROJECT_NAME}/download-helper/CMakeLists.txt
+ )
+
+ h2("Generating build files for the ${EXT_PROJECT_NAME} download helper project.")
+ execute_process(COMMAND "${CMAKE_COMMAND}" -G "${CMAKE_GENERATOR}" . WORKING_DIRECTORY "${DOWNLOAD_DIR}/${EXT_PROJECT_NAME}/download-helper")
+
+ h2("Building the ${EXT_PROJECT_NAME} download helper project. (This actually performs the download and may take some time...)")
+ execute_process(COMMAND "${CMAKE_COMMAND}" --build . WORKING_DIRECTORY "${DOWNLOAD_DIR}/${EXT_PROJECT_NAME}/download-helper")
+
+ h2("Completed download of external project ${EXT_PROJECT_NAME}.")
+
+endfunction()
+
+
+# Usage:
+# ExternalProject_BuildAndInstallNow(
+# # This first argument is the name of the external project to download. It is required:
+# VRPN
+# # This second argument is the relative path from ${EXTERNAL_DIR_NAME}/projectname/ to the project's
+# # main CMakeLists.txt file:
+# src
+#
+# # Additional arguments are passed on as options to the cmake build file generator
+# -DVRPN_BUILD_DIRECTSHOW_VIDEO_SERVER=OFF
+# -DVRPN_BUILD_HID_GUI=OFF
+# etc..
+# )
+function(ExternalProject_BuildAndInstallNow EXT_PROJECT_NAME DOWNLOAD_DIR RELPATH_TO_CMAKELISTS)
+
+ include(MessageMacros)
+ h1("BEGIN EXTERNAL PROJECT BUILD AND INSTALL (${EXT_PROJECT_NAME}).")
+
+ # any extra args to the function are interpreted as arguments for the cmake config process
+ set(CMAKE_CONFIG_OPTIONS ${ARGN})
+
+ # always set the install prefix to be the same as for the main project
+ list(APPEND CMAKE_CONFIG_OPTIONS -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX})
+
+ #string (REGEX REPLACE "(^|[^\\\\]);" "\\1 " CMAKE_CONFIG_OPTIONS "${CMAKE_CONFIG_OPTIONS}")
+
+
+ set(SRC_DIR "${DOWNLOAD_DIR}/${EXT_PROJECT_NAME}/${RELPATH_TO_CMAKELISTS}")
+ set(BUILD_DIR "${CMAKE_BINARY_DIR}/external/${EXT_PROJECT_NAME}")
+
+ file(MAKE_DIRECTORY ${BUILD_DIR})
+
+ h2("Generating build files for external project ${EXT_PROJECT_NAME}.")
+ message(STATUS "Using source dir: ${SRC_DIR}")
+ message(STATUS "Using build dir: ${BUILD_DIR}")
+ message(STATUS "Config options: ${CMAKE_CONFIG_OPTIONS}")
+
+ execute_process(COMMAND "${CMAKE_COMMAND}" -G "${CMAKE_GENERATOR}" ${SRC_DIR} ${CMAKE_CONFIG_OPTIONS} WORKING_DIRECTORY ${BUILD_DIR})
+
+ h2("Building external project ${EXT_PROJECT_NAME}. (This may take some time...)")
+ execute_process(COMMAND "${CMAKE_COMMAND}" --build ${BUILD_DIR} --target install)
+
+ h2("Completed external build of ${EXT_PROJECT_NAME}.")
+
+endfunction()
+
diff --git a/dev/a6-harold/cmake/MessageMacros.cmake b/dev/a6-harold/cmake/MessageMacros.cmake
new file mode 100644
index 0000000..4628e5c
--- /dev/null
+++ b/dev/a6-harold/cmake/MessageMacros.cmake
@@ -0,0 +1,17 @@
+# This file is part of the MinVR cmake build system.
+# See the main MinVR/CMakeLists.txt file for authors, copyright, and license info.
+
+
+macro(h1 TITLE)
+ string(TOUPPER ${TITLE} TITLE)
+ message(STATUS "\n\n==== ${TITLE} ====")
+endmacro()
+
+macro(h2 TITLE)
+ message(STATUS "\n* ${TITLE}")
+endmacro()
+
+macro(h3 TITLE)
+ message(STATUS "- ${TITLE}")
+endmacro()
+
diff --git a/dev/a6-harold/cmake/UseOpenGL.cmake b/dev/a6-harold/cmake/UseOpenGL.cmake
new file mode 100644
index 0000000..2ec5ffb
--- /dev/null
+++ b/dev/a6-harold/cmake/UseOpenGL.cmake
@@ -0,0 +1,52 @@
+# This file is part of the MinGfx cmake build system.
+# See the main MinGfx/CMakeLists.txt file for authors, copyright, and license info.
+
+# Either finds a pre-installed version or complains.
+
+# Usage: In your CMakeLists.txt, somewhere after you define the target that depends
+# on the OpenGL library (typical with something like add_executable(${PROJECT_NAME} ...)
+# or add_library(${PROJECT_NAME} ...)), add the following two lines:
+
+# include(UseOpenGL)
+# UseOpenGL(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/external)
+
+# The second argument can be either PUBLIC, PRIVATE, or INTERFACE, following the keyword
+# usage described here:
+# https://cmake.org/cmake/help/latest/command/target_include_directories.html
+
+# The third argument is the directory to use for downloading the external project if
+# autobuild is used.
+
+
+
+macro(UseOpenGL YOUR_TARGET INTERFACE_PUBLIC_OR_PRIVATE DOWNLOAD_DIR)
+
+ message(STATUS "Searching for OpenGL...")
+
+ # Check to see if the library is already installed on the system
+ # CMake ships with FindOpenGL.cmake and in CMake 3.9+ it defines
+ # the imported targets OpenGL::GL and OpenGL::GLU. Using these is
+ # now the preferred way to link with OpenGL and all of its dependencies.
+ # See https://cmake.org/cmake/help/v3.9/module/FindOpenGL.html
+ find_package(OpenGL)
+
+ if (NOT ${OPENGL_FOUND})
+ message(FATAL_ERROR "OpenGL was not found on the system. MinGfx can auto-download and build many dependencies for you, but not OpenGL. It should come pre-installed on your system.")
+ endif()
+
+ message(STATUS "Ok: OpenGL Found.")
+ message(STATUS "OpenGL headers: ${OPENGL_INCLUDE_DIR}")
+ message(STATUS "OpenGL libs: ${OPENGL_LIBRARIES}")
+
+
+ message(STATUS "Linking target ${YOUR_TARGET} with ${INTERFACE_PUBLIC_OR_PRIVATE} dependency OpenGL::GL.")
+ target_link_libraries(${YOUR_TARGET} ${INTERFACE_PUBLIC_OR_PRIVATE} OpenGL::GL)
+
+ if (${OPENGL_GLU_FOUND})
+ message(STATUS "Linking target ${YOUR_TARGET} with ${INTERFACE_PUBLIC_OR_PRIVATE} dependency OpenGL::GLU.")
+ target_link_libraries(${YOUR_TARGET} ${INTERFACE_PUBLIC_OR_PRIVATE} OpenGL::GLU)
+ endif()
+
+ target_compile_definitions(${YOUR_TARGET} ${INTERFACE_PUBLIC_OR_PRIVATE} -DUSE_OPENGL)
+
+endmacro()
diff --git a/dev/a6-harold/config.h.in b/dev/a6-harold/config.h.in
new file mode 100644
index 0000000..9798f4c
--- /dev/null
+++ b/dev/a6-harold/config.h.in
@@ -0,0 +1,15 @@
+/** CSci-4611 Assignment 6: Harold
+ */
+
+
+// The file config.h.in is processed by cmake to produce config.h. This
+// replaces strings of the form "at"CMAKE_VARIABLE_NAME"at" with the value
+// of the corresponding cmake variable, allowing us to pass directory paths
+// and other information configured with cmake into our C++ code.
+
+
+#define DATA_DIR_BUILD "@DATA_DIR_BUILD@"
+#define DATA_DIR_INSTALL "@DATA_DIR_INSTALL@"
+
+#define SHADERS_DIR_BUILD "@SHADERS_DIR_BUILD@"
+#define SHADERS_DIR_INSTALL "@SHADERS_DIR_INSTALL@"
diff --git a/dev/a6-harold/data/toonDiffuse.png b/dev/a6-harold/data/toonDiffuse.png
new file mode 100644
index 0000000..d2e40d9
--- /dev/null
+++ b/dev/a6-harold/data/toonDiffuse.png
Binary files differ
diff --git a/dev/a6-harold/data/toonSpecular.png b/dev/a6-harold/data/toonSpecular.png
new file mode 100644
index 0000000..c13a32f
--- /dev/null
+++ b/dev/a6-harold/data/toonSpecular.png
Binary files differ
diff --git a/dev/a6-harold/edge_mesh.cc b/dev/a6-harold/edge_mesh.cc
new file mode 100644
index 0000000..1abe2e0
--- /dev/null
+++ b/dev/a6-harold/edge_mesh.cc
@@ -0,0 +1,235 @@
+/** CSci-4611 Assignment 5: Art Render
+ */
+
+#include "edge_mesh.h"
+
+EdgeMesh::EdgeMesh() : gpuDirty_(true) {
+
+}
+
+EdgeMesh::~EdgeMesh() {
+}
+
+
+
+void EdgeMesh::CreateFromMesh(const Mesh &mesh) {
+ verts_.clear();
+ norms_.clear();
+ indices_.clear();
+ leftNorms_.clear();
+ rightNorms_.clear();
+
+ // temp data holders for use while constructing the mesh in an indexed primitives mode
+ std::vector<Point3> vertices; // vertex positions
+ std::vector<Vector3> normals; // vertex normals
+ std::vector<Vector3> leftNormals, rightNormals; // normals of adj. faces
+ std::vector< std::vector<unsigned int> > triangles; // fin triangles
+
+ for (int t = 0; t < mesh.num_triangles(); t++) {
+ std::vector<unsigned int> tri = mesh.read_triangle_indices_data(t);
+ Point3 a = mesh.read_vertex_data(tri[0]);
+ Point3 b = mesh.read_vertex_data(tri[1]);
+ Point3 c = mesh.read_vertex_data(tri[2]);
+ Vector3 e1 = b-a;
+ e1.Normalize();
+ Vector3 e2 = c-a;
+ e2.Normalize();
+ Vector3 n = e1.Cross(e2);
+
+ //Vector3 n = ((b-a).cross(c-a)).to_unit();
+ //std::cout << n << std::endl;
+ addEdge(&vertices, &normals, &leftNormals, &rightNormals, &triangles, mesh, tri[0], tri[1], n);
+ addEdge(&vertices, &normals, &leftNormals, &rightNormals, &triangles, mesh, tri[1], tri[2], n);
+ addEdge(&vertices, &normals, &leftNormals, &rightNormals, &triangles, mesh, tri[2], tri[0], n);
+ }
+
+ // now transfer data to the member variable data holders
+ for (int i=0; i<vertices.size(); i++) {
+ verts_.push_back(vertices[i][0]);
+ verts_.push_back(vertices[i][1]);
+ verts_.push_back(vertices[i][2]);
+ norms_.push_back(normals[i][0]);
+ norms_.push_back(normals[i][1]);
+ norms_.push_back(normals[i][2]);
+ leftNorms_.push_back(leftNormals[i][0]);
+ leftNorms_.push_back(leftNormals[i][1]);
+ leftNorms_.push_back(leftNormals[i][2]);
+ rightNorms_.push_back(rightNormals[i][0]);
+ rightNorms_.push_back(rightNormals[i][1]);
+ rightNorms_.push_back(rightNormals[i][2]);
+ }
+
+ for (int i=0; i<triangles.size(); i++) {
+ indices_.push_back(triangles[i][0]);
+ indices_.push_back(triangles[i][1]);
+ indices_.push_back(triangles[i][2]);
+ }
+ gpuDirty_ = true;
+}
+
+
+void EdgeMesh::addEdge(std::vector<Point3> *vertices,
+ std::vector<Vector3> *normals,
+ std::vector<Vector3> *leftNormals,
+ std::vector<Vector3> *rightNormals,
+ std::vector< std::vector<unsigned int> > *triangles,
+ const Mesh &mesh, int v0, int v1, Vector3 n)
+{
+ // edgeMap contains the index of the first of four consecutive vertices
+ // that form the quad fin
+
+ EdgeMap::iterator it = edgeMap.find(std::make_pair(v1, v0));
+ if (it != edgeMap.end()) { // found
+ int v = it->second;
+ (*rightNormals)[v+0]
+ = (*rightNormals)[v+1]
+ = (*rightNormals)[v+2]
+ = (*rightNormals)[v+3]
+ = n;
+ }
+ else {
+ int v = (int)vertices->size();
+ edgeMap[std::make_pair(v0, v1)] = v;
+
+ vertices->push_back(mesh.read_vertex_data(v0));
+ vertices->push_back(mesh.read_vertex_data(v0));
+ vertices->push_back(mesh.read_vertex_data(v1));
+ vertices->push_back(mesh.read_vertex_data(v1));
+
+ normals->push_back(Vector3());
+ normals->push_back(mesh.read_normal_data(v0));
+ normals->push_back(Vector3());
+ normals->push_back(mesh.read_normal_data(v1));
+
+ leftNormals->push_back(n);
+ leftNormals->push_back(n);
+ leftNormals->push_back(n);
+ leftNormals->push_back(n);
+
+ rightNormals->push_back(-n);
+ rightNormals->push_back(-n);
+ rightNormals->push_back(-n);
+ rightNormals->push_back(-n);
+
+ std::vector<unsigned int> tri1;
+ tri1.push_back(v+0);
+ tri1.push_back(v+2);
+ tri1.push_back(v+3);
+ triangles->push_back(tri1);
+
+ std::vector<unsigned int> tri2;
+ tri2.push_back(v+0);
+ tri2.push_back(v+3);
+ tri2.push_back(v+1);
+ triangles->push_back(tri2);
+ }
+}
+
+
+
+void EdgeMesh::UpdateGPUMemory() {
+ if (gpuDirty_) {
+ // sanity check -- for each attribute that is added make sure the number
+ // of triangles is equal to the number of tris in the verts array.
+ if (norms_.size() / 3 != num_vertices()) {
+ std::cerr << "EdgeMesh::UpdateGPUMemory() -- warning: the number of per vertex normals in the mesh is not equal to the number vertices in the mesh. (N = " << norms_.size() / 3 << ", V = " << num_vertices() << ")" << std::endl;
+ }
+ if (leftNorms_.size() / 3 != num_vertices()) {
+ std::cerr << "EdgeMesh::UpdateGPUMemory() -- warning: the number of per vertex left-normals in the mesh is not equal to the number vertices in the mesh. (LN = " << leftNorms_.size() / 3 << ", V = " << num_vertices() << ")" << std::endl;
+ }
+ if (rightNorms_.size() / 3 != num_vertices()) {
+ std::cerr << "EdgeMesh::UpdateGPUMemory() -- warning: the number of per vertex right-normals in the mesh is not equal to the number vertices in the mesh. (RN = " << rightNorms_.size() / 3 << ", V = " << num_vertices() << ")" << std::endl;
+ }
+
+ GLsizeiptr totalMemSize = 0;
+
+ GLsizeiptr vertsMemSize = verts_.size() * sizeof(float);
+ GLsizeiptr vertsMemOffset = 0;
+ totalMemSize += vertsMemSize;
+
+ GLsizeiptr normsMemSize = norms_.size() * sizeof(float);
+ GLsizeiptr normsMemOffset = totalMemSize;
+ totalMemSize += normsMemSize;
+
+ GLsizeiptr leftNormsMemSize = leftNorms_.size() * sizeof(float);
+ GLsizeiptr leftNormsMemOffset = totalMemSize;
+ totalMemSize += leftNormsMemSize;
+
+ GLsizeiptr rightNormsMemSize = rightNorms_.size() * sizeof(float);
+ GLsizeiptr rightNormsMemOffset = totalMemSize;
+ totalMemSize += rightNormsMemSize;
+
+ glGenBuffers(1, &vertexBuffer_);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer_);
+ glBufferData(GL_ARRAY_BUFFER, totalMemSize, NULL, GL_STATIC_DRAW);
+
+ glBufferSubData(GL_ARRAY_BUFFER, vertsMemOffset, vertsMemSize, &verts_[0]);
+ glBufferSubData(GL_ARRAY_BUFFER, normsMemOffset, normsMemSize, &norms_[0]);
+ glBufferSubData(GL_ARRAY_BUFFER, leftNormsMemOffset, leftNormsMemSize, &leftNorms_[0]);
+ glBufferSubData(GL_ARRAY_BUFFER, rightNormsMemOffset, rightNormsMemSize, &rightNorms_[0]);
+
+ glGenVertexArrays(1, &vertexArray_);
+ glBindVertexArray(vertexArray_);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer_);
+
+ // attribute 0 = vertices (required)
+ int attribID = 0;
+ int nComponents = 3;
+ glEnableVertexAttribArray(attribID);
+ glVertexAttribPointer(attribID, nComponents, GL_FLOAT, GL_FALSE, nComponents*sizeof(GLfloat), (char*)0 + vertsMemOffset);
+
+ // attribute 1 = normals (required)
+ attribID = 1;
+ nComponents = 3;
+ glEnableVertexAttribArray(attribID);
+ glVertexAttribPointer(attribID, nComponents, GL_FLOAT, GL_TRUE, nComponents*sizeof(GLfloat), (char*)0 + normsMemOffset);
+
+ // attribute 2 = left normals (required)
+ attribID = 2;
+ nComponents = 3;
+ glEnableVertexAttribArray(attribID);
+ glVertexAttribPointer(attribID, nComponents, GL_FLOAT, GL_TRUE, nComponents*sizeof(GLfloat), (char*)0 + leftNormsMemOffset);
+
+ // attribute 3 = right normals (required)
+ attribID = 3;
+ nComponents = 3;
+ glEnableVertexAttribArray(attribID);
+ glVertexAttribPointer(attribID, nComponents, GL_FLOAT, GL_TRUE, nComponents*sizeof(GLfloat), (char*)0 + rightNormsMemOffset);
+
+ glBindVertexArray(0);
+
+
+ glGenBuffers(1, &elementBuffer_);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBuffer_);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices_.size() * sizeof(unsigned int), &indices_[0], GL_STATIC_DRAW);
+
+
+ gpuDirty_ = false;
+ }
+}
+
+
+void EdgeMesh::Draw() {
+ if (gpuDirty_) {
+ UpdateGPUMemory();
+ }
+
+ // set defaults to pass to shaders any for optional attribs
+ glVertexAttrib3f(1, 0.0, 0.0, 1.0); // normal = +Z
+ glVertexAttrib3f(2, 0.0, 0.0, 1.0); // left-normal = +Z
+ glVertexAttrib3f(3, 0.0, 0.0, 1.0); // right-normal = +Z
+
+ glBindVertexArray(vertexArray_);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBuffer_);
+ glDrawElements(GL_TRIANGLES, (GLsizei)indices_.size(), GL_UNSIGNED_INT, (void*)0);
+ glBindVertexArray(0);
+}
+
+
+int EdgeMesh::num_vertices() const {
+ return (int)verts_.size()/3;
+}
+
+int EdgeMesh::num_triangles() const {
+ return (int)indices_.size()/3;
+}
diff --git a/dev/a6-harold/edge_mesh.h b/dev/a6-harold/edge_mesh.h
new file mode 100644
index 0000000..5120d0b
--- /dev/null
+++ b/dev/a6-harold/edge_mesh.h
@@ -0,0 +1,82 @@
+/** CSci-4611 Assignment 5: Art Render
+ */
+
+#ifndef EDGE_MESH_H
+#define EDGE_MESH_H
+
+#include <mingfx.h>
+using namespace mingfx;
+
+#include <vector>
+
+
+/** This special kind of mesh stores two triangles that form a quadralateral
+ along each edge of an existing mesh. The quad initially has a width=0, but
+ when rendered, two of the vertices are extended along the surfaces normal
+ direction, which creates a "fin" that can be drawn as a thick border. This
+ can be used to create a silhouette edge renderer if the shader only extends
+ the edges that lie along a silhouette of the mesh.
+ */
+class EdgeMesh {
+public:
+ EdgeMesh();
+ virtual ~EdgeMesh();
+
+ /// Creates two triangles along each edge of the mesh passed in.
+ void CreateFromMesh(const Mesh &mesh);
+
+ /// Saves the mesh data to the GPU - must be called with InitOpenGL or Draw.
+ void UpdateGPUMemory();
+
+ /// Num vertices in the edge mesh
+ int num_vertices() const;
+
+ /// Num triangles in the edge mesh
+ int num_triangles() const;
+
+ /// Access to vertex position by vertex number
+ Point3 vertex(int vertexID) const;
+
+ /// Access to vertex normal by vertex number
+ Vector3 normal(int vertexID) const;
+
+ /// Access to vertex color by vertex number
+ Color color(int vertexID) const;
+
+ /// Access to vertex texture coords by vertex number
+ Point2 tex_coords(int textureUnit, int vertexID) const;
+
+
+ /// Draws the mesh assuming a shader has already been bound.
+ void Draw();
+
+private:
+
+ // Some routines and variables are needed internally to construct the edge
+ // mesh from a regular mesh.
+
+ typedef std::map<std::pair<int,int>,int> EdgeMap;
+ EdgeMap edgeMap;
+
+ void addEdge(std::vector<Point3> *vertices,
+ std::vector<Vector3> *normals,
+ std::vector<Vector3> *leftNormals,
+ std::vector<Vector3> *rightNormals,
+ std::vector< std::vector<unsigned int> > *triangles,
+ const Mesh &mesh, int v0, int v1, Vector3 n);
+
+ std::vector<float> verts_; // vertex positions
+ std::vector<float> norms_; // normals
+ std::vector<unsigned int> indices_; // indices
+ std::vector<float> leftNorms_; // normals of adjacent triangles
+ std::vector<float> rightNorms_;
+
+ GLuint vertexBuffer_;
+ GLuint vertexArray_;
+ GLuint elementBuffer_;
+
+ bool gpuDirty_;
+};
+
+#endif
+
diff --git a/dev/a6-harold/ground.cc b/dev/a6-harold/ground.cc
new file mode 100644
index 0000000..51d4d92
--- /dev/null
+++ b/dev/a6-harold/ground.cc
@@ -0,0 +1,283 @@
+/** CSci-4611 Assignment 6: Harold
+ */
+
+#include "ground.h"
+
+
+Ground::Ground() : diffuse_ramp_(GL_CLAMP_TO_EDGE),
+ specular_ramp_(GL_CLAMP_TO_EDGE), light_pos_(30,30,30)
+{
+
+}
+
+Ground::~Ground() {
+
+}
+
+Mesh* Ground::mesh_ptr() { return &ground_mesh_; }
+
+
+void Ground::Init(const std::vector<std::string> &search_path) {
+ // init ground geometry, a simple grid is used. if it is running too slow,
+ // you can turn down the resolution by decreasing nx and ny, but this will
+ // make the hills look more jaggy.
+ const int nx = 150;
+ const int ny = 150;
+ const float size = 100.0;
+ std::vector<Point3> verts;
+ std::vector<Vector3> norms;
+ for (int j = 0; j <= ny; j++) {
+ for (int i = 0; i <= nx; i++) {
+ float x = size*(float)j/nx - size/2.0f;
+ float y = size*(float)i/ny - size/2.0f;
+ verts.push_back(Point3(x, 0, y));
+ norms.push_back(Vector3(0,1,0));
+ }
+ }
+ std::vector<unsigned int> indices;
+ for (int j = 0; j < ny; j++) {
+ for (int i = 0; i < nx; i++) {
+ // L\ triangle
+ indices.push_back((i+0)+(j+0)*(nx+1));
+ indices.push_back((i+1)+(j+0)*(nx+1));
+ indices.push_back((i+0)+(j+1)*(nx+1));
+ // \7 triangle
+ indices.push_back((i+1)+(j+0)*(nx+1));
+ indices.push_back((i+1)+(j+1)*(nx+1));
+ indices.push_back((i+0)+(j+1)*(nx+1));
+ }
+ }
+ ground_mesh_.SetIndices(indices);
+ ground_mesh_.SetVertices(verts);
+ ground_mesh_.SetNormals(norms);
+ ground_mesh_.UpdateGPUMemory();
+ ground_edge_mesh_.CreateFromMesh(ground_mesh_);
+
+
+ // load textures and shaders
+ diffuse_ramp_.InitFromFile(Platform::FindFile("toonDiffuse.png", search_path));
+ specular_ramp_.InitFromFile(Platform::FindFile("toonSpecular.png", search_path));
+
+ artsy_shaderprog_.AddVertexShaderFromFile(Platform::FindFile("artsy.vert", search_path));
+ artsy_shaderprog_.AddFragmentShaderFromFile(Platform::FindFile("artsy.frag", search_path));
+ artsy_shaderprog_.LinkProgram();
+
+ outline_shaderprog_.AddVertexShaderFromFile(Platform::FindFile("outline.vert", search_path));
+ outline_shaderprog_.AddFragmentShaderFromFile(Platform::FindFile("outline.frag", search_path));
+ outline_shaderprog_.LinkProgram();
+}
+
+
+
+
+// Projects a 2D normalized screen point (e.g., the mouse position in normalized
+// device coordinates) to a 3D point on the ground. Returns true and sets ground_point
+// to be equal to the result if the conversion is successful. Returns false if
+// the screen point does not project onto the ground.
+bool Ground::ScreenPtHitsGround(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const Point2 &normalized_screen_pt, Point3 *ground_point)
+{
+ Matrix4 camera_matrix = view_matrix.Inverse();
+ Point3 eye = camera_matrix.ColumnToPoint3(3);
+
+ Point3 pt3d = GfxMath::ScreenToNearPlane(view_matrix, proj_matrix, normalized_screen_pt);
+ Ray ray(eye, (pt3d - eye).ToUnit());
+ float i_time;
+ int i_tri;
+ return ray.FastIntersectMesh(&ground_mesh_, &i_time, ground_point, &i_tri);
+}
+
+
+
+
+/** This implements the "h" term used in the equations described in section 4.5 of the
+ paper. Three arguments are needed:
+ 1. projection_plane_normal: We need to know where the projection plane is in 3-space
+ Since a plane can be defined by a point within the plane and a normal, we use
+ this normal together with the 3rd argument to the function to define the projection
+ plane described in the paper.
+ 2. silhouette_curve: As described in the paper, the silhouette curve is a 3D version
+ of the curve the user draws with the mouse. It is formed by projecting the
+ original 2D screen-space curve onto the 3D projection plane.
+ 3. closest_pt_in_plane: As described in the paper, this is the closest point within
+ the projection plane to the vertex of the mesh that we want to modify. In other
+ words, it is the perpendicular projection of the vertex we want to modify onto
+ the projection plane.
+ */
+float hfunc(const Vector3 projection_plane_normal, const std::vector<Point3> &silhouette_curve, const Point3 &closest_pt_in_plane) {
+ // define the y axis for a "plane space" coordinate system as a world space vector
+ Vector3 plane_y = Vector3(0,1,0);
+ // define the x axis for a "plane space" coordinate system as a world space vector
+ Vector3 plane_x = plane_y.Cross(projection_plane_normal).ToUnit();
+ // define the origin for a "plane space" coordinate system as the first point in the curve
+ Point3 origin = silhouette_curve[0];
+
+ // loop over line segments in the curve, find the one that lies over the point by
+ // comparing the "plane space" x value for the start and end of the line segment
+ // to the "plane space" x value for the closest point to the vertex that lies
+ // in the projection plane.
+ float x_target = (closest_pt_in_plane - origin).Dot(plane_x);
+ for (int i=1; i<silhouette_curve.size(); i++) {
+ float x_start = (silhouette_curve[(size_t)i-1] - origin).Dot(plane_x);
+ float x_end = (silhouette_curve[i] - origin).Dot(plane_x);
+ if ((x_start <= x_target) && (x_target <= x_end)) {
+ float alpha = (x_target - x_start) / (x_end - x_start);
+ float y_curve = silhouette_curve[(size_t)i-1][1] + alpha*(silhouette_curve[i][1] - silhouette_curve[(size_t)i-1][1]);
+ return y_curve - closest_pt_in_plane[1];
+ }
+ else if ((x_end <= x_target) && (x_target <= x_start)) {
+ float alpha = (x_target - x_end) / (x_start - x_end);
+ float y_curve = silhouette_curve[i][1] + alpha*(silhouette_curve[(size_t)i-1][1] - silhouette_curve[i][1]);
+ return y_curve - closest_pt_in_plane[1];
+ }
+ }
+
+ // here return 0 because the point does not lie under the curve.
+ return 0.0;
+}
+
+
+
+
+/// Modifies the vertices of the ground mesh to create a hill or valley based
+/// on the input stroke. The 2D path of the stroke on the screen is passed
+/// in, this is the centerline of the stroke mesh that is actually drawn on
+/// the screen while the user is drawing.
+void Ground::ReshapeGround(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const std::vector<Point2> &stroke2d)
+{
+ // TODO: Deform the 3D ground mesh according to the algorithm described in the
+ // Cohen et al. Harold paper.
+
+ // You might need the eye point and the look vector, these can be determined
+ // from the view matrix as follows:
+ Matrix4 camera_matrix = view_matrix.Inverse();
+ Point3 eye = camera_matrix.ColumnToPoint3(3);
+ Vector3 look = -camera_matrix.ColumnToVector3(2);
+
+
+
+ // There are 3 major steps to the algorithm, outlined here:
+
+ // 1. Define a plane to project the stroke onto. The first and last points
+ // of the stroke are guaranteed to project onto the ground plane. The plane
+ // should pass through these two points on the ground. The plane should also
+ // have a normal vector that points toward the camera and is parallel to the
+ // ground plane.
+
+
+
+
+
+ // 2. Project the 2D stroke into 3D so that it lies on the "projection plane"
+ // defined in step 1.
+
+
+
+
+
+ // 3. Loop through all of the vertices of the ground mesh, and adjust the
+ // height of each based on the equations in section 4.5 of the paper, also
+ // repeated in the assignment handout. The equations rely upon a function
+ // h(), and we have implemented that for you as hfunc() defined above in
+ // this file. The basic structure of the loop you will need is here:
+ std::vector<Point3> new_verts;
+ for (int i=0; i<ground_mesh_.num_vertices(); i++) {
+ Point3 P = ground_mesh_.read_vertex_data(i); // original vertex
+
+ // adjust P according to equations...
+
+
+
+
+
+ new_verts.push_back(P);
+ }
+ ground_mesh_.SetVertices(new_verts);
+ ground_mesh_.CalcPerVertexNormals();
+ ground_mesh_.UpdateGPUMemory();
+ ground_edge_mesh_.CreateFromMesh(ground_mesh_);
+}
+
+
+
+
+/// Draws the ground mesh with toon shading
+void Ground::Draw(const Matrix4 &view_matrix, const Matrix4 &proj_matrix, const Color &ground_color) {
+ // Lighting parameters
+ Color Ia(1.0f, 1.0f, 1.0f, 1.0f);
+ Color Id(1.0f, 1.0f, 1.0f, 1.0f);
+ Color Is(1.0f, 1.0f, 1.0f, 1.0f);
+
+ // Material parameters
+ Color ka = ground_color;
+ Color kd(0.4f, 0.4f, 0.4f, 1.0f);
+ Color ks(0.6f, 0.6f, 0.6f, 1.0f);
+ float s = 50.0f;
+
+ // Precompute matrices needed in the shader
+ Matrix4 model_matrix; // identity
+ Matrix4 modelview_matrix = view_matrix * model_matrix;
+ Matrix4 normal_matrix = modelview_matrix.Inverse().Transpose();
+ Point3 light_in_eye_space = view_matrix * light_pos_;
+
+ // Make sure the default option to only draw front facing triangles is set
+ glEnable(GL_CULL_FACE);
+
+
+ // Draw the ground using the artsy shader
+ artsy_shaderprog_.UseProgram();
+ artsy_shaderprog_.SetUniform("modelViewMatrix", modelview_matrix);
+ artsy_shaderprog_.SetUniform("normalMatrix", normal_matrix);
+ artsy_shaderprog_.SetUniform("projectionMatrix", proj_matrix);
+ artsy_shaderprog_.SetUniform("ka", ka);
+ artsy_shaderprog_.SetUniform("kd", kd);
+ artsy_shaderprog_.SetUniform("ks", ks);
+ artsy_shaderprog_.SetUniform("s", s);
+ artsy_shaderprog_.SetUniform("lightPosition", light_in_eye_space);
+ artsy_shaderprog_.SetUniform("Ia", Ia);
+ artsy_shaderprog_.SetUniform("Id", Id);
+ artsy_shaderprog_.SetUniform("Is", Is);
+ artsy_shaderprog_.BindTexture("diffuseRamp", diffuse_ramp_);
+ artsy_shaderprog_.BindTexture("specularRamp", specular_ramp_);
+ ground_mesh_.Draw();
+ artsy_shaderprog_.StopProgram();
+
+ // And, draw silhouette edges for the ground using the outline shader
+ glDisable(GL_CULL_FACE);
+ glEnable(GL_POLYGON_OFFSET_FILL);
+ glPolygonOffset(1,1);
+ static const float thickness = 0.2f;
+ outline_shaderprog_.UseProgram();
+ outline_shaderprog_.SetUniform("modelViewMatrix", modelview_matrix);
+ outline_shaderprog_.SetUniform("normalMatrix", normal_matrix);
+ outline_shaderprog_.SetUniform("projectionMatrix", proj_matrix);
+ outline_shaderprog_.SetUniform("thickness", thickness);
+ ground_edge_mesh_.Draw();
+ outline_shaderprog_.StopProgram();
+
+
+
+ // This can be useful for debugging, but it is extremely slow to draw.
+ // Before uncommenting this, it's recommended to turn down the resolution
+ // of the ground mesh by adjusting the nx and ny constants inside Init().
+ /**
+ // draw lines around each triangle
+ for (int t=0; t<ground_mesh_.num_triangles(); t++) {
+ std::vector<unsigned int> indices = ground_mesh_.triangle_vertices(t);
+ std::vector<Point3> loop;
+ loop.push_back(ground_mesh_.vertex(indices[0]));
+ loop.push_back(ground_mesh_.vertex(indices[1]));
+ loop.push_back(ground_mesh_.vertex(indices[2]));
+ qs_.DrawLines(model_matrix, view_matrix, proj_matrix, Color(0.7,0.7,0.7), loop, QuickShapes::LinesType::LINE_LOOP, 0.01);
+ }
+
+ // draw normals
+ for (int i=0; i<ground_mesh_.num_vertices(); i++) {
+ Point3 p1 = ground_mesh_.vertex(i);
+ Point3 p2 = p1 + 0.5*ground_mesh_.normal(i);
+ qs_.DrawLineSegment(model_matrix, view_matrix, proj_matrix, Color(0.7,0.7,0.7), p1, p2, 0.01);
+ }
+ **/
+}
+
diff --git a/dev/a6-harold/ground.h b/dev/a6-harold/ground.h
new file mode 100644
index 0000000..0cc18dd
--- /dev/null
+++ b/dev/a6-harold/ground.h
@@ -0,0 +1,63 @@
+/** CSci-4611 Assignment 6: Harold
+ */
+
+#ifndef GROUND_H_
+#define GROUND_H_
+
+#include <mingfx.h>
+using namespace mingfx;
+
+#include "edge_mesh.h"
+
+/** The ground is represented with a triangle mesh. 2D "screen space" strokes
+ are used to modify the vertices based on user input so that the user can create
+ a 3D landscape of hills and valleys.
+ */
+class Ground {
+public:
+ Ground();
+ virtual ~Ground();
+
+ /// Call from InitOpenGL() to initialize shaders, etc.
+ void Init(const std::vector<std::string> &search_path);
+
+ /// Projects a 2D normalized screen point (e.g., the mouse position in normalized
+ /// device coordinates) to a 3D point on the ground. Returns true and sets ground_point
+ /// to be equal to the result if the conversion is successful. Returns false if
+ /// the screen point does not project onto the ground.
+ bool ScreenPtHitsGround(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const Point2 &normalized_screen_pt, Point3 *ground_point);
+
+ /// Modifies the vertices of the ground mesh to create a hill or valley based
+ /// on the input stroke. The 2D path of the stroke on the screen is passed
+ /// in, this is the centerline of the stroke mesh that is actually drawn on
+ /// the screen while the user is drawing.
+ void ReshapeGround(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const std::vector<Point2> &stroke2d);
+
+ /// Draws the ground mesh with toon shading
+ void Draw(const Matrix4 &view_matrix, const Matrix4 &proj_matrix, const Color &ground_color);
+
+
+ Mesh* mesh_ptr();
+
+private:
+
+ // This is the actual triangle mesh for the ground
+ Mesh ground_mesh_;
+ // We also maintain a corresponding "edge mesh" in order to do the
+ // silhouette outlines like in assignment 5
+ EdgeMesh ground_edge_mesh_;
+
+ // The ground rendering is based on the artsy shader from assignment 5
+ ShaderProgram artsy_shaderprog_;
+ ShaderProgram outline_shaderprog_;
+ Texture2D diffuse_ramp_;
+ Texture2D specular_ramp_;
+ Point3 light_pos_;
+
+ // for debugging only
+ QuickShapes qs_;
+};
+
+#endif
diff --git a/dev/a6-harold/harold_app.cc b/dev/a6-harold/harold_app.cc
new file mode 100644
index 0000000..abf5c4d
--- /dev/null
+++ b/dev/a6-harold/harold_app.cc
@@ -0,0 +1,278 @@
+/** CSci-4611 Assignment 6: Harold
+ */
+
+#include "harold_app.h"
+#include "config.h"
+
+#include <iostream>
+#include <sstream>
+
+
+HaroldApp::HaroldApp() : GraphicsApp(1024,768, "Harold"),
+ drawing_state_(DRAWING_NONE),
+ sky_color_(1,1,1), ground_color_(0.25, 0, 0.25), crayon_color_(0.5,0,0.5)
+{
+ // Define a search path for finding data files (images and shaders)
+ search_path_.push_back(".");
+ search_path_.push_back("./data");
+ search_path_.push_back("./shaders");
+ search_path_.push_back(DATA_DIR_INSTALL);
+ search_path_.push_back(DATA_DIR_BUILD);
+ search_path_.push_back(SHADERS_DIR_INSTALL);
+ search_path_.push_back(SHADERS_DIR_BUILD);
+}
+
+
+HaroldApp::~HaroldApp() {
+}
+
+
+void HaroldApp::InitNanoGUI() {
+ // Setup the GUI window
+ nanogui::Window *window = new nanogui::Window(screen(), "Harold's Crayons");
+ window->setPosition(Eigen::Vector2i(10, 10));
+ window->setSize(Eigen::Vector2i(200,100));
+ window->setLayout(new nanogui::GroupLayout());
+
+ new nanogui::Label(window, "Crayon Color", "sans-bold");
+ auto cp1 = new nanogui::ColorPicker(window,
+ nanogui::Color((int)(255.0*crayon_color_[0]),
+ (int)(255.0*crayon_color_[1]),
+ (int)(255.0*crayon_color_[2]), 255)
+ );
+ cp1->setFixedSize({100, 20});
+ cp1->setFinalCallback([this](const nanogui::Color &c) {
+ crayon_color_ = Color(c.r(), c.g(), c.b(), c.w());
+ });
+
+ new nanogui::Label(window, "Sky Color", "sans-bold");
+ auto cp2 = new nanogui::ColorPicker(window,
+ nanogui::Color((int)(255.0*sky_color_[0]),
+ (int)(255.0*sky_color_[1]),
+ (int)(255.0*sky_color_[2]), 255)
+ );
+ cp2->setFixedSize({100, 20});
+ cp2->setFinalCallback([this](const nanogui::Color &c) {
+ sky_color_ = Color(c.r(), c.g(), c.b(), c.w());
+ });
+
+ new nanogui::Label(window, "Ground Color", "sans-bold");
+ auto cp3 = new nanogui::ColorPicker(window,
+ nanogui::Color((int)(255.0*ground_color_[0]),
+ (int)(255.0*ground_color_[1]),
+ (int)(255.0*ground_color_[2]), 255)
+ );
+ cp3->setFixedSize({100, 20});
+ cp3->setFinalCallback([this](const nanogui::Color &c) {
+ ground_color_ = Color(c.r(), c.g(), c.b(), c.w());
+ });
+
+ screen()->performLayout();
+}
+
+
+void HaroldApp::InitOpenGL() {
+ // Set up the camera in a good position to see the entire field
+ cam_.set_view_matrix(Matrix4::LookAt(Point3(0,2,10), Point3(0,2,0), Vector3(0,1,0)));
+ proj_matrix_ = Matrix4::Perspective(60, aspect_ratio(), 0.1f, 1600.0f);
+ glClearColor(0.7f, 0.7f, 0.7f, 1.0f);
+
+ stroke2d_shaderprog_.AddVertexShaderFromFile(Platform::FindFile("stroke2d.vert", search_path_));
+ stroke2d_shaderprog_.AddFragmentShaderFromFile(Platform::FindFile("stroke2d.frag", search_path_));
+ stroke2d_shaderprog_.LinkProgram();
+
+ stroke3d_shaderprog_.AddVertexShaderFromFile(Platform::FindFile("stroke3d.vert", search_path_));
+ stroke3d_shaderprog_.AddFragmentShaderFromFile(Platform::FindFile("stroke3d.frag", search_path_));
+ stroke3d_shaderprog_.LinkProgram();
+
+ ground_.Init(search_path_);
+ sky_.Init(&stroke3d_shaderprog_);
+ billboards_.Init(&stroke3d_shaderprog_);
+}
+
+
+
+void HaroldApp::AddToStroke(const Point2 &normalized_screen_pt) {
+ // the stroke2d_ array stores the raw 2D screen coordinates of the
+ // centerline of the stroke
+ stroke2d_.push_back(normalized_screen_pt);
+
+ // the mesh is a triangle strip that follows the centerline
+ // we need at least 2 samples before we can create triangles
+ if (stroke2d_.size() >= 2) {
+ const float half_stroke_width = 0.01f;
+ std::vector<Point3> verts;
+ std::vector<unsigned int> indices;
+ Point3 last_pt = Point3(stroke2d_[0][0], stroke2d_[0][1], 0);
+ Point3 pt = Point3(stroke2d_[1][0], stroke2d_[1][1], 0);
+ Vector3 tangent = (pt - last_pt).ToUnit();
+ Vector3 cotangent = tangent.Cross(Vector3::UnitZ());
+ verts.push_back(last_pt - half_stroke_width*cotangent);
+ verts.push_back(last_pt + half_stroke_width*cotangent);
+ for (int i=1; i<stroke2d_.size(); i++) {
+ pt = Point3(stroke2d_[i][0], stroke2d_[i][1], 0);
+ tangent = (pt - last_pt).ToUnit();
+ cotangent = tangent.Cross(Vector3::UnitZ());
+
+ verts.push_back(pt - half_stroke_width*cotangent);
+ verts.push_back(pt + half_stroke_width*cotangent);
+
+ indices.push_back((int)verts.size()-4);
+ indices.push_back((int)verts.size()-3);
+ indices.push_back((int)verts.size()-2);
+
+ indices.push_back((int)verts.size()-3);
+ indices.push_back((int)verts.size()-1);
+ indices.push_back((int)verts.size()-2);
+
+ last_pt = pt;
+ }
+ stroke2d_mesh_.SetVertices(verts);
+ stroke2d_mesh_.SetIndices(indices);
+ stroke2d_mesh_.UpdateGPUMemory();
+ }
+}
+
+
+
+// This function is called at the start of each new crayon stroke
+void HaroldApp::OnLeftMouseDown(const Point2 &mouse_in_pixels) {
+ // Add to the stroke_mesh_, which is a 2D triangle strip used to draw the user's
+ // crayon stroke on the screen.
+ Point2 mouse_in_ndc = PixelsToNormalizedDeviceCoords(mouse_in_pixels);
+ AddToStroke(mouse_in_ndc);
+
+
+ // Next, try to figure out what we are drawing based on where the stroke originated.
+ Point3 i_point;
+ edit_billboard_id_ = billboards_.IntersectBillboard(cam_.view_matrix(), proj_matrix_, mouse_in_ndc);
+ if (edit_billboard_id_ >= 0) {
+ // If the mouse starts on an existing billboard, then we are editing the billboard.
+ drawing_state_ = DrawingState::DRAWING_BILLBOARD_EDIT;
+ }
+ else if (ground_.ScreenPtHitsGround(cam_.view_matrix(), proj_matrix_, mouse_in_ndc, &i_point)) {
+ // If the mouse starts on the ground, then we could be about to edit the
+ // ground, OR we might be creating a new billboard. We won't know for sure
+ // until the user releases the mouse and we can check to see whether the
+ // stroke also ends on the ground.
+ drawing_state_ = DrawingState::DRAWING_GROUND_OR_BILLBOARD;
+ }
+ else {
+ // Otherwise, we must be drawing a stroke in the sky.
+ drawing_state_ = DrawingState::DRAWING_SKY;
+ }
+}
+
+
+// This function is called once each frame while the user is drawing with the crayon
+void HaroldApp::OnLeftMouseDrag(const Point2 &mouse_in_pixels, const Vector2 &delta_in_pixels) {
+ // Add to the stroke_mesh_, which is a 2D triangle strip used to draw the user's
+ // crayon stroke on the screen.
+ Point2 mouse_in_ndc = PixelsToNormalizedDeviceCoords(mouse_in_pixels);
+ AddToStroke(mouse_in_ndc);
+}
+
+
+// This function is called at the end of each stroke
+void HaroldApp::OnLeftMouseUp(const Point2 &mouse_in_pixels) {
+
+ // If we are in the temporary drawing_ground_or_billboard state, then we need
+ // to do a final check now to see if the stroke ended on the ground or not.
+ // If it did, then we interpret the stroke as drawing_ground. Otherwise, we
+ // treat it as creating a new billboard.
+ if (drawing_state_ == DrawingState::DRAWING_GROUND_OR_BILLBOARD) {
+ // The stroke was started on the ground, does it also end on the ground?
+ Point2 mouse_in_ndc = PixelsToNormalizedDeviceCoords(mouse_in_pixels);
+ Point3 i_point;
+ if (ground_.ScreenPtHitsGround(cam_.view_matrix(), proj_matrix_, mouse_in_ndc, &i_point)) {
+ drawing_state_ = DrawingState::DRAWING_GROUND;
+ }
+ else {
+ drawing_state_ = DrawingState::DRAWING_BILLBOARD;
+ }
+ }
+
+
+ // Now, the action to take in terms of what geometry to add or modify in
+ // the scene depends entirely on the drawing state:
+ if (drawing_state_ == DrawingState::DRAWING_SKY) {
+ sky_.AddSkyStroke(cam_.view_matrix(), proj_matrix_, stroke2d_mesh_, crayon_color_);
+ }
+ else if (drawing_state_ == DrawingState::DRAWING_BILLBOARD) {
+ billboards_.AddBillboardStroke(cam_.view_matrix(), proj_matrix_, stroke2d_, stroke2d_mesh_, crayon_color_, &ground_);
+ }
+ else if (drawing_state_ == DrawingState::DRAWING_BILLBOARD_EDIT) {
+ billboards_.AddToBillboard(cam_.view_matrix(), proj_matrix_, edit_billboard_id_, stroke2d_mesh_, crayon_color_);
+ }
+ else if (drawing_state_ == DrawingState::DRAWING_GROUND) {
+ if (stroke2d_.size() < 6) {
+ std::cout << "Stroke is too short, try again." << std::endl;
+ }
+ else {
+ ground_.ReshapeGround(cam_.view_matrix(), proj_matrix_, stroke2d_);
+ }
+ }
+
+
+ // Done with this stroke. Clear the 2d stroke and its mesh and reset the drawing state
+ stroke2d_.clear();
+ stroke2d_mesh_ = Mesh();
+ drawing_state_ = DrawingState::DRAWING_NONE;
+}
+
+
+// You can look around, like in minecraft, by dragging with the right mouse button.
+void HaroldApp::OnRightMouseDrag(const Point2 &mouse_in_pixels, const Vector2 &delta_in_pixels) {
+ Vector2 delta_in_ndc = PixelsToNormalizedDeviceCoords(delta_in_pixels);
+ cam_.OnMouseMove(delta_in_ndc);
+}
+
+
+void HaroldApp::UpdateSimulation(double dt) {
+ if (drawing_state_ == DrawingState::DRAWING_NONE) {
+ // When walking around using the arrow keys we need to adjust the height
+ // of the virtual camera when we walk up a hill. To do that, we shoot
+ // a ray straight down from the eye point to the ground, find the point
+ // of intersection on the ground, and then set the camera height to be
+ // 2.0 meters above this.
+ Ray ray(cam_.eye(), -Vector3::UnitY());
+ float i_time;
+ Point3 i_pt;
+ int i_tri;
+ if (ray.FastIntersectMesh(ground_.mesh_ptr(), &i_time, &i_pt, &i_tri)) {
+ float height = 2.0f + i_pt[1]; // 2 meters above the gound
+ cam_.UpdateHeight(height);
+ }
+ cam_.UpdateSimulation(dt, window());
+
+
+ // The billboards also need to be updated to face the current camera
+ billboards_.UpdateBillboardRotations(cam_.eye());
+ }
+}
+
+
+void HaroldApp::DrawUsingOpenGL() {
+ // Clear the screen using the current sky color
+ glClearColor(sky_color_[0], sky_color_[1], sky_color_[2], 1);
+
+ // Draw the sky strokes
+ sky_.Draw(cam_.view_matrix(), proj_matrix_);
+
+ // Draw the ground mesh
+ ground_.Draw(cam_.view_matrix(), proj_matrix_, ground_color_);
+
+ // Draw the billboards
+ billboards_.Draw(cam_.view_matrix(), proj_matrix_);
+
+
+ // If we are currently drawing (indicated by the stroke mesh containing >0
+ // triangles), then draw the 2D triangle strip mesh for the crayon stroke
+ if (stroke2d_mesh_.num_triangles() > 0) {
+ stroke2d_shaderprog_.UseProgram();
+ stroke2d_shaderprog_.SetUniform("strokeColor", crayon_color_);
+ stroke2d_mesh_.Draw();
+ stroke2d_shaderprog_.StopProgram();
+ }
+}
+
diff --git a/dev/a6-harold/harold_app.h b/dev/a6-harold/harold_app.h
new file mode 100644
index 0000000..801a49a
--- /dev/null
+++ b/dev/a6-harold/harold_app.h
@@ -0,0 +1,120 @@
+/** CSci-4611 Assignment 6: Harold
+ */
+
+#ifndef HAROLD_APP_H_
+#define HAROLD_APP_H_
+
+#include <mingfx.h>
+using namespace mingfx;
+
+#include "billboards.h"
+#include "edge_mesh.h"
+#include "ground.h"
+#include "sky.h"
+
+#include <string>
+#include <vector>
+
+
+/** Main application class for the Harold assignment, which is a partial
+ implementation of the Cohen et al. paper.
+ */
+class HaroldApp : public GraphicsApp {
+public:
+
+ HaroldApp();
+ virtual ~HaroldApp();
+
+ // These are used for drawing
+ void OnLeftMouseDown(const Point2 &pos);
+ void OnLeftMouseDrag(const Point2 &pos, const Vector2 &delta);
+ void OnLeftMouseUp(const Point2 &pos);
+
+ // These are used for walking around and looking (i.e., camera movement)
+ void OnRightMouseDrag(const Point2 &pos, const Vector2 &delta);
+ void UpdateSimulation(double dt);
+
+ // Adds the 2D point to stroke2d_, which holds the centerline of the user's
+ // drawn stroke. Also updates stroke2d_mesh_, which is a 2D triangle strip
+ // used to actually draw the stroke on the screen.
+ void AddToStroke(const Point2 &normalized_screen_pt);
+
+ void InitNanoGUI();
+ void InitOpenGL();
+
+ void DrawUsingOpenGL();
+
+private:
+
+ // user input from the mouse is interpreted differently based upon where the
+ // stroke originates and where it ends. we can model this as a simple state
+ // machine
+ enum DrawingState {
+ // left mouse button is up, not currently drawing
+ DRAWING_NONE,
+
+ // stroke started in the sky, currently adding a sky stroke
+ DRAWING_SKY,
+
+ // stroke started on a billboard, currently editing that billboard
+ DRAWING_BILLBOARD_EDIT,
+
+ // temporary state for between mouse down and mouse up -- here, we know
+ // the stroke started on the ground, but not yet sure if it will end on
+ // the ground or in the sky
+ DRAWING_GROUND_OR_BILLBOARD,
+
+ // stroke ended on the ground, currently editing the ground
+ DRAWING_GROUND,
+
+ // stroke ended in the sky, currently creating a new billboard
+ DRAWING_BILLBOARD,
+ };
+ DrawingState drawing_state_;
+
+ // the centerline of the stroke dragged out by the mouse on the screen
+ std::vector<Point2> stroke2d_;
+
+ // a 2d triangle strip mesh for drawing the stroke on the filmplane
+ Mesh stroke2d_mesh_;
+
+ // The ground mesh and functionality to draw and edit the ground.
+ Ground ground_;
+
+ // Sky strokes and functionality to add more strokes to the sky
+ Sky sky_;
+
+ // Billboard strokes and functionality to add more billboards, including
+ // editing them by adding on to an existing billboard
+ Billboards billboards_;
+
+ // when in the DRAWING_BILLBOARD_EDIT state, this holds the id of the
+ // billboard that is currently being edited.
+ int edit_billboard_id_;
+
+
+ // Stroke shaders do not use any lighting, just transform vertices and then
+ // render the triangle strip for the stroke in the appropriate color. The
+ // 2D shader program is used in this class. The 3D is created here so that
+ // there is only one copy for the whole application, but it is used both the
+ // sky and billboard classes.
+ ShaderProgram stroke2d_shaderprog_;
+ ShaderProgram stroke3d_shaderprog_;
+
+ // This implements something like the camera controls in minecraft. You can
+ // walk around using the arrow keys or ASWZ keys. You can also move your
+ // head to look around by dragging with the right mouse button. Internally,
+ // the camera creates a view matrix.
+ CraftCam cam_;
+ Matrix4 proj_matrix_;
+
+ // A list of paths to search for data files (images and shaders)
+ std::vector<std::string> search_path_;
+
+ // Current colors set by the GUI
+ Color sky_color_;
+ Color ground_color_;
+ Color crayon_color_;
+};
+
+#endif \ No newline at end of file
diff --git a/dev/a6-harold/main.cc b/dev/a6-harold/main.cc
new file mode 100644
index 0000000..b672b01
--- /dev/null
+++ b/dev/a6-harold/main.cc
@@ -0,0 +1,10 @@
+/** CSci-4611 Assignment 6: Harold
+ */
+
+#include "harold_app.h"
+
+int main(int argc, const char *argv[]) {
+ HaroldApp app;
+ app.Run();
+ return 0;
+}
diff --git a/dev/a6-harold/shaders/artsy.frag b/dev/a6-harold/shaders/artsy.frag
new file mode 100644
index 0000000..403009d
--- /dev/null
+++ b/dev/a6-harold/shaders/artsy.frag
@@ -0,0 +1,29 @@
+#version 330
+
+in vec3 Position;
+in vec3 Normal;
+
+out vec4 color;
+
+uniform vec3 lightPosition;
+uniform vec4 Ia, Id, Is;
+
+uniform vec4 ka, kd, ks;
+uniform float s;
+
+uniform sampler2D diffuseRamp;
+uniform sampler2D specularRamp;
+
+
+void main() {
+ vec3 l = normalize(lightPosition.xyz - Position);
+ vec3 n = normalize(Normal);
+ vec3 e = normalize(-Position);
+ vec3 h = normalize(e + l);
+ float diffuse = (dot(l,n) + 1)/2;
+ float specular = pow(max(dot(n,h),0), s);
+ color = ka*Ia
+ + kd*Id*texture(diffuseRamp, vec2(diffuse,0))
+ + ks*Is*texture(specularRamp, vec2(specular,0));
+ color.a = 1;
+}
diff --git a/dev/a6-harold/shaders/artsy.vert b/dev/a6-harold/shaders/artsy.vert
new file mode 100644
index 0000000..cd9b998
--- /dev/null
+++ b/dev/a6-harold/shaders/artsy.vert
@@ -0,0 +1,17 @@
+#version 330
+
+uniform mat4 modelViewMatrix;
+uniform mat4 normalMatrix;
+uniform mat4 projectionMatrix;
+
+layout(location = 0) in vec3 vertex;
+layout(location = 1) in vec3 normal;
+
+out vec3 Position;
+out vec3 Normal;
+
+void main() {
+ Position = (modelViewMatrix * vec4(vertex,1)).xyz;
+ Normal = normalize((normalMatrix * vec4(normal,0)).xyz);
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(vertex,1);
+}
diff --git a/dev/a6-harold/shaders/outline.frag b/dev/a6-harold/shaders/outline.frag
new file mode 100644
index 0000000..096721d
--- /dev/null
+++ b/dev/a6-harold/shaders/outline.frag
@@ -0,0 +1,7 @@
+#version 330
+
+out vec4 color;
+
+void main() {
+ color = vec4(0,0,0, 1);
+}
diff --git a/dev/a6-harold/shaders/outline.vert b/dev/a6-harold/shaders/outline.vert
new file mode 100644
index 0000000..a6073e5
--- /dev/null
+++ b/dev/a6-harold/shaders/outline.vert
@@ -0,0 +1,23 @@
+#version 330
+
+uniform mat4 modelViewMatrix;
+uniform mat4 normalMatrix;
+uniform mat4 projectionMatrix;
+uniform float thickness;
+
+layout(location = 0) in vec3 vertex;
+layout(location = 1) in vec3 normal;
+layout(location = 2) in vec3 leftNormal;
+layout(location = 3) in vec3 rightNormal;
+
+void main() {
+ vec3 p = (modelViewMatrix * vec4(vertex,1)).xyz;
+ vec3 e = -p;
+ vec3 nl = (normalMatrix * vec4(leftNormal,0)).xyz;
+ vec3 nr = (normalMatrix * vec4(rightNormal,0)).xyz;
+ vec3 v = vertex;
+ if (dot(e,nl) * dot(e,nr) < 0.0) {
+ v += thickness*normal;
+ }
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(v,1);
+}
diff --git a/dev/a6-harold/shaders/stroke2d.frag b/dev/a6-harold/shaders/stroke2d.frag
new file mode 100644
index 0000000..2dd7efa
--- /dev/null
+++ b/dev/a6-harold/shaders/stroke2d.frag
@@ -0,0 +1,9 @@
+#version 330
+
+uniform vec4 strokeColor;
+
+out vec4 color;
+
+void main() {
+ color = strokeColor;
+}
diff --git a/dev/a6-harold/shaders/stroke2d.vert b/dev/a6-harold/shaders/stroke2d.vert
new file mode 100644
index 0000000..e40fdec
--- /dev/null
+++ b/dev/a6-harold/shaders/stroke2d.vert
@@ -0,0 +1,7 @@
+#version 330
+
+layout(location = 0) in vec3 vertex;
+
+void main() {
+ gl_Position = vec4(vertex,1);
+}
diff --git a/dev/a6-harold/shaders/stroke3d.frag b/dev/a6-harold/shaders/stroke3d.frag
new file mode 100644
index 0000000..2dd7efa
--- /dev/null
+++ b/dev/a6-harold/shaders/stroke3d.frag
@@ -0,0 +1,9 @@
+#version 330
+
+uniform vec4 strokeColor;
+
+out vec4 color;
+
+void main() {
+ color = strokeColor;
+}
diff --git a/dev/a6-harold/shaders/stroke3d.vert b/dev/a6-harold/shaders/stroke3d.vert
new file mode 100644
index 0000000..d333c4f
--- /dev/null
+++ b/dev/a6-harold/shaders/stroke3d.vert
@@ -0,0 +1,10 @@
+#version 330
+
+uniform mat4 modelViewMatrix;
+uniform mat4 projectionMatrix;
+
+layout(location = 0) in vec3 vertex;
+
+void main() {
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(vertex,1);
+}
diff --git a/dev/a6-harold/sky.cc b/dev/a6-harold/sky.cc
new file mode 100644
index 0000000..bc08ca0
--- /dev/null
+++ b/dev/a6-harold/sky.cc
@@ -0,0 +1,79 @@
+/** CSci-4611 Assignment 6: Harold
+ */
+
+#include "sky.h"
+
+
+Sky::Sky() {
+
+}
+
+
+Sky::~Sky() {
+
+}
+
+void Sky::Init(ShaderProgram *stroke3d_shaderprog) {
+ stroke3d_shaderprog_ = stroke3d_shaderprog;
+}
+
+
+/// Projects a 2D normalized screen point (e.g., the mouse position in normalized
+/// device coordinates) to a 3D point on the "sky", which is really a huge sphere
+/// (radius = 1500) that the viewer is inside. This function should always return
+/// true since any screen point can successfully be projected onto the sphere.
+/// sky_point is set to the resulting 3D point. Note, this function only checks
+/// to see if the ray passing through the screen point intersects the sphere; it
+/// does not check to see if the ray hits the ground or anything else first.
+bool Sky::ScreenPtHitsSky(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const Point2 &normalized_screen_pt, Point3 *sky_point)
+{
+ // You will need the eye point, this can be
+ // determined from the view matrix as follows:
+ Matrix4 camera_matrix = view_matrix.Inverse();
+ Point3 eye = camera_matrix.ColumnToPoint3(3);
+
+ // TODO: Stitch together your worksheet implementation of this method
+ return true;
+}
+
+
+
+
+/// Creates a new sky stroke mesh by projecting each vertex of the 2D mesh
+/// onto the sky dome and saving the result as a new 3D mesh.
+void Sky::AddSkyStroke(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const Mesh &stroke2d_mesh, const Color &stroke_color)
+{
+ // TODO: Create a new SkyStroke and add it to the strokes_ array.
+
+
+
+
+
+
+
+
+
+
+}
+
+
+/// Draws all of the sky strokes
+void Sky::Draw(const Matrix4 &view_matrix, const Matrix4 &proj_matrix) {
+
+ // Precompute matrices needed in the shader
+ Matrix4 model_matrix; // identity
+ Matrix4 modelview_matrix = view_matrix * model_matrix;
+
+ // Draw sky meshes
+ stroke3d_shaderprog_->UseProgram();
+ stroke3d_shaderprog_->SetUniform("modelViewMatrix", modelview_matrix);
+ stroke3d_shaderprog_->SetUniform("projectionMatrix", proj_matrix);
+ for (int i=0; i<strokes_.size(); i++) {
+ stroke3d_shaderprog_->SetUniform("strokeColor", strokes_[i].color);
+ strokes_[i].mesh.Draw();
+ }
+ stroke3d_shaderprog_->StopProgram();
+}
+
diff --git a/dev/a6-harold/sky.h b/dev/a6-harold/sky.h
new file mode 100644
index 0000000..06fcca1
--- /dev/null
+++ b/dev/a6-harold/sky.h
@@ -0,0 +1,52 @@
+/** CSci-4611 Assignment 6: Harold
+ */
+
+#ifndef SKY_H_
+#define SKY_H_
+
+#include <mingfx.h>
+using namespace mingfx;
+
+/** Creates, holds, and draws all of the strokes on the sky.
+ */
+class Sky {
+public:
+ Sky();
+ virtual ~Sky();
+
+ void Init(ShaderProgram *stroke3d_shaderprog);
+
+ /// Projects a 2D normalized screen point (e.g., the mouse position in normalized
+ /// device coordinates) to a 3D point on the "sky", which is really a huge sphere
+ /// (radius = 1500) that the viewer is inside. This function should always return
+ /// true since any screen point can successfully be projected onto the sphere.
+ /// sky_point is set to the resulting 3D point. Note, this function only checks
+ /// to see if the ray passing through the screen point intersects the sphere; it
+ /// does not check to see if the ray hits the ground or anything else first.
+ bool ScreenPtHitsSky(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const Point2 &normalized_screen_pt, Point3 *sky_point);
+
+ /// Creates a new sky stroke mesh by projecting each vertex of the 2D mesh
+ /// onto the sky dome and saving the result as a new 3D mesh.
+ void AddSkyStroke(const Matrix4 &view_matrix, const Matrix4 &proj_matrix,
+ const Mesh &stroke2d_mesh, const Color &stroke_color);
+
+ /// Draws all of the sky strokes
+ void Draw(const Matrix4 &view_matrix, const Matrix4 &proj_matrix);
+
+private:
+
+ // Each stroke has a 3D mesh and a color
+ struct SkyStroke {
+ Mesh mesh;
+ Color color;
+ };
+
+ // To store a new stroke to draw, add it to this array
+ std::vector<SkyStroke> strokes_;
+
+
+ ShaderProgram *stroke3d_shaderprog_;
+};
+
+#endif
diff --git a/worksheets/a6_harold.md b/worksheets/a6_harold.md
new file mode 100644
index 0000000..573644d
--- /dev/null
+++ b/worksheets/a6_harold.md
@@ -0,0 +1,94 @@
+# Assignment 6 (Harold) Worksheet
+
+For this assignment, one of the key parts is to check what part of the virtual
+environment your mouse is currently touching - useful for determining what
+type of stroke should be drawn when the mouse is clicked and dragged.
+
+
+## Q1: Mouse-Sky Intersections (Part 1)
+
+From the handout, we know that the sky here is really just a giant sphere
+with a radius of 1500.0 units. In order to calculate where in the sky our
+mouse is pointing in the scene, we need to perform a *ray-sphere
+intersection* test. The ray starts at the eye location (camera position), and goes
+through the current mouse location on the near clipping plane. This ray can be
+traced to figure out where it intersects the sky sphere.
+
+Create a top-down diagram of the scene including the sky sphere, the camera,
+the mouse position, and the aforementioned ray from the eye through the mouse
+position.
+
+You can use the following images as inspiration for the shapes that you draw
+in your diagram (replace this image with your final diagram):
+
+![](./img/sky_camera_example.png)
+
+
+## Q2: Mouse-Sky Intersections (Part 2)
+
+Now, let's create the building blocks for the method `Sky::ScreenPtHitsSky()`,
+which tests to see where the ray from the eye through the mouse intersects the
+sky sphere! We're given the following information in this method:
+
+- Camera view matrix (`Matrix4 view_matrix`)
+- Camera projection matrix (`Matrix4 proj_matrix`)
+- `Point2` normalized device coordinates of mouse (`Point2 normalized_screen_pt`)
+ - Inclusive range [-1, 1]
+ - `Point2(-1, 1)` is the upper left corner, and `Point2(1, -1)` is the
+ lower right
+
+1. The info above actually gives us all we need to calculate the camera's position (also known as the eye position) in world space, but it may not be obvious at first how to do this. See if you can figure it out with a few hints below.
+```
+/* Hint 1: The view matrix transforms from one space to another, what are those spaces?
+ Hint 2: It is possible to calculate the inverse of a transformation matrix, and Matrix4 has a handy routine for this. As you would expect, the inverse of a transformation matrix will apply the opposite transformation.
+ */
+Point3 eye = /* --- Fill in your answer here --- */
+```
+
+2. Construct the mouse pointer location in world space. We consider the mouse
+ to be on the near clipping plane of the camera (this should sound familiar
+ from your drawing in Q1!). In order to grab this point, MinGfx has a handy
+ helper function called
+ [`GfxMath::ScreenToNearPlane`](https://ivlab.github.io/MinGfx/html/html/classmingfx_1_1_gfx_math.html#a2086a2f885f887fb53da8a5adb5860f0).
+ Use the MinGfx documentation at the link and the variables given above to
+ construct the world-space representation of the mouse location:
+
+```
+Point3 mouseIn3d = /* --- Fill in your answer here --- */
+```
+
+3. Create the ray from the eye through the world-space mouse location on the
+ near plane. Use MinGfx's builtin `Ray` class for this.
+
+```
+Ray eyeThroughMouse = /* --- Fill in your answer here --- */
+```
+
+4. Use the
+ [`Ray::IntersectSphere()`](https://ivlab.github.io/MinGfx/html/html/classmingfx_1_1_ray.html#a970c7dbc19167be625967fabfb39b4ff)
+ method to find the intersection point of the `eyeThroughMouse` ray and the
+ sky sphere. This method contains one bit of C++ syntax that you may not
+ have seen before - output parameters. The `Ray::IntersectSphere()` method
+ sets both `iTime` and `iPoint` this way. Usually, best practice here is to
+ declare a variable of the correct type before you call the method, then
+ pass in a *reference* to this variable. For example:
+
+```
+// Declare output parameter `x`
+float x;
+
+// Call someFunction with output parameter
+someFunction(&x);
+
+// x now has the value set by someFunction
+```
+
+ Using the variables declared from the previous steps, write a code snippet
+ that captures the return value of the sphere intersection test, as well as
+ the `t` value and the `point` where the ray intersects the sphere.
+
+```
+// Declare output parameters
+
+bool intersects = eyeThroughMouse.IntersectSphere(/* --- Fill parameters in --- */)
+```