Posts Tagged With ‘c++&8217


Center/Right Align a Widget in QTreeWidget

Qt comes with a QTreeWidget class which can be used to show data in a tree view. It can be used to show hierarchical data using either Model/View framework or by manually creating the hierarchy. The QTreeWidget supports multi-columns for each row and also allows editing of the individual cells. However, sometimes we need to present a QWidget in a cell to allow user to interact with data. For example, we might want a user to choose a Boolean value for a cell and instead of asking the user to type Yes/No or True/False, we can present the user with a checkbox. This eliminates human errors in type the values. Often, there is also a need to embed a QPushButton within a cell to allow user to run some action.

Let’s dig in a single example on how to embed a QWidget in a cell of a QTreeWidget.

// Create a tree widget with three columns.
QTreeWidget* treeWidget = new QTreeWidget();
treeWidget->setColumnCount(3);
QStringList columnNames;
columnNames << "Column 1" << "Column 2" << "Column 3";
treeWidget->setHeaderLabels(columnNames);

// Add a top level tree widget item.
QTreeWidgetItem* item = new QTreeWidgetItem();
treeWidget->addTopLevelItem(item);

// Add a check box to the second column.
QCheckBox* checkBox = new QCheckBox("Click Me!");
treeWidget->setItemWidget(item, 1, checkBox1);

// Add tree widget to the parent widgets layout.
this->setLayout(new QVBoxLayout());
this->setContentsMargins(0, 0, 0, 0);
this->layout()->addWidget(treeWidget);

The above code produces the output as shown in the below image. The check box is left aligned and QTreeWidget does not offer a way to center or right align it out of the box. A standard solution offered online is to inherit from QTreeWidgetItem and take control of painting the item directly.

QTreeWidget by default aligns inserted widget to the left.

Inheriting from QTreeWidgetItem is unnecessarily complicated and there is no need to do it. A simpler way is to put the checkbox in another widget and use a horizontal layout with stretch before and after the checkbox! This trick can be used to right align as well by omitting the stretch before the checkbox. Here is the code:

// Create a check box.
QCheckBox* checkBox = new QCheckBox("Click Me!");

// Put the check box in a wrapping widget with appropriate layout.
QWidget* checkBoxWrapper = new QWidget;
QHBoxLayout* layout = new QHBoxLayout();
layout->addStretch();
layout->addWidget(checkBox);
layout->addStretch();
layout->setContentsMargins(0, 0, 0, 0);
checkBoxWrapper->setLayout(layout);

// Add it to the tree widget.
mTreeWidget->setItemWidget(item, 1, checkBoxWrapper);

This code is going to produce the following output:

Center aligned widget by using a wrapping widget with a horizontal layout.

The complete Visual Studio 2019 solution for this demo can be downloaded here.


QWidgetLogger: A Qt logging device for SLogLib

SLogLib is a flexible logging library written in C++. It comes with three logging devices: console, file, and memory but often there is a need to show log directly on the user interface. I heavily use Qt for all projects requiring user interface, so I wrote a logging device for QWidget. It is available in SLogLib repository in the Contrib/QWidgetLogger folder.

Here is how to use the QWidgetLogger:

QWidgetLogger* logger = new QWidgetLogger(new HtmlFormatter(), "WidgetLogger");
SLogLib::addLoggingDevice(logger);

Once above code is added, any of the SLOGLIB_LOG_MSG_* logging macros will write to the QWidgetLogger as well as any other loggers added to SLogLib. QWidgetLogger internally uses SLogTextEdit class derived from QTextEdit. Instance of SLogTextEdit used by QWidgetLogger can be retrieved by QWidgetLogger::widget() function. This instance should be added to the UI to show the logging messages. QDockWidget is a good choice to show logging widget with QMainWindow.

SLogTextEdit sets a monospace 10 point font and it can be changed using style sheet. The color and style of the messages logged can also be changed using the Formatter. The HtmlFormatter used in the example above define different colors for different types of logging messages using HTML codes.

In Qt, UI elements can only be updated from the main thread but the logging messages might come from any thread. So SLogTextEdit checks if the message was posted from the main thread or some other worker thread. If the message was posted from a worker thread, SLogTextEdit emits a signal to only of its own private slot and updates itself in the slot. In Qt slots always run in the context of the main thread. This method works well but signal and slot mechanism is slow and update to widget lags while logging too many messages in a short period of time.


Added Multi-threading Support in SLogLib

Recently, I worked on a project which made heavy use of C++ threads. To use SLogLib in this project I added multi-threaded support in SLogLib with the help of c++ 11 std::mutex and std::lock_guard. Over the last few months multi-threaded support in SLogLib has been extensively tested and there are no known bugs.

All threading support is located in LoggingManager.cpp. The functions which modify internal state in LoggingManager are protected by std::mutex. There is a support for building call stack through the use of SLOGLIB_ADD_TO_CALLSTACK macro. In the latest build, there is a separate callstack for each thread.

Checkout the latest commit from https://github.com/saurabhg17/SLogLib.


String Selection Widget for Qt5

Some time back, I developed a data entry application in Qt5. One of the requirements was to let the user select a single string from a predefined list of string. I developed a custom widget called SStringSelector for this purpose. SStringSelector has two views: display and selection. The display view presents the currently selected string (blank if no string is selected), and a push button. To select a string, the user clicks on the button which presents the user with the selection dialog. The selection dialog consists of a list of string in an QListWidget and the user can select one of them by double-clicking a string. If the list of strings are long, the user can filter them using a filter QLineEdit present above the QListWidget.

SStringSelector is distributed as a part of QtUtils repository hosted on Github. The SStringSelector widget is really simple to use. Simple add the SStringSelector.h and SStringSelector.cpp files in your project and add an instance of SStringSelector in the layout of your app.

Below are some screenshots of the widget under Windows:

The SStringSelector Widget.
Selection Dialog of the SStringSelector Widget.
Filtering Strings in the Selection Dialog.

Color Picker Widget for Qt5

Qt5 support standard dialogs such as QFileDialog, QFontDialog, and QColorDialog, however, it does not provide a color picker to allow a user to pick a color. Recently, I need a color picker for one of my projects and I implemented a simple color picker widget.

SColorPicker is available from Github as a part of QtUtils repository. To use SColorPicker, add the header and cpp files directly in your project. Then, simply add an instance of SColorPicker in a layout. SColorPicker will appear as 16×16 pixels colored square in the layout. If you need a different size, change it in the SColorPicker's constructor. When a user double-clicks on the colored square, the system’s color dialog will appear allowing the user to choose a color. The selected color can be obtained from color() function or by connecting to colorPicked() signal.

Below are the screenshots of the SColorPicker_Demo and system color dialog present to the user on Windows 10 computer.


Search Box using QLineEdit

This week, at work I had to implement a search box for a software I am working on. The search box is to filter some data dynamically as user types a query. I wanted to show a clear (cross) icon at the right side of the search box so that user can clear the results instead of selecting the current query and deleting it manually. Lastly, for clarity I wanted to show a search icon on the left side of search box. The search box looks like this:

Screenshot of the Search box implemented using QLineEdit

After the user enters a query a clear icon appears on the right. The clear icon is in fact a button and clicking it will clear the current search.
Screenshot of the Search box with keywords implemented using QLineEdit

It is really easy to make this search box using QLineEdit. We need only the following three lines of code:

QLineEdit* _lineEdit = new QLineEdit();
_lineEdit->setClearButtonEnabled(true);
_lineEdit->addAction(":/resources/search.ico", QLineEdit::LeadingPosition);
_lineEdit->setPlaceHolderText("Search...");

// add _lineEdit to your widget

Line 2 enables the clear button which adds the clear action and cross icon to the right. Line 3 adds another action with a search icon to the left of the QLineEdit. We don’t listen to this action as it is merely decorative. Line 4 adds a placeholder text which is shown in the QLineEdit but is cleared as soon as user starts typing.

We only connect textChanged(const QString&)  signal which is emitted both when a user clicks on the cross icon and when he enters a search query.

 




Cross-platform high-resolution timer

Often there is a need to estimate the time it takes for a piece of code to run. This is useful not only for debugging but also for reporting the execution time of lengthy tasks to the user.

On Windows, QueryPerformanceFrequency() and QueryPerformanceCounter() can be used to determine the execution time of a code. QueryPerformanceFrequency() returns the frequency of the current performance counter in counts per second and QueryPerformanceCounter() returns a high resolution (<1µs) time stamp. Together they can be used to determine time it takes to run a piece of code is:

LARGE_INTEGER _frequency;
QueryPerformanceFrequency(&_frequency);

LARGE_INTEGER _start;
QueryPerformanceCounter(&_start);

// Code which takes a long time to run.

LARGE_INTEGER _stop;
QueryPerformanceCounter(&_stop);

double _intervalInSeconds = (_stop.QuadPart - _start.QuadPart) / _frequency.QuadPart;

On Linux, clock_gettime can be used to get a time interval with a resolution of nano-seconds. clock_gettime() requires two arguments: clockid_t and timespec structure. To build a timer, CLOCK_MONOTONIC is a good choice for clockid_t as the time is guaranteed to be monotonically increasing. timespec structure have two field: tv_sec (time in seconds) and tv_nsec (time in nanoseconds). Code to determine the time it takes to run a piece of code is:

struct timespec _start;
clock_gettime(CLOCK_MONOTONIC, &_start);

// Code which takes long time to run.

struct timespec _stop;
clock_gettime(CLOCK_MONOTONIC, &_stop);

double _intervalInseconds = (_stop.tv_sec + _stop.tv_nsec*1e-9) - (_start.tv_sec + _start.tv_nsec*1e-9);

I have written a simple class which can be user on both windows and Linux. It has the following interface:

class Timer
{
public:

    enum TimeUnit
    {
        TimeInSeconds = 1,
        TimeInMilliSeconds = 1000,
        TimeInMicroSeconds = 1000000
    };

public:

    Timer();
    ~Timer();

    // On Windows, returns true if high performance timer is available.
    // On Linux, always returns true.
    bool IsTimerAvailable();

    // Start the timer.
    void Start();

    // Stop the timer and return the time elapsed since the timer was started.
    double Stop(TimeUnit timeUnit = TimeInMilliSeconds);

    // Get the time elapsed since Start() was called.
    double TimeElapsedSinceStart(TimeUnit timeUnit = TimeInMilliSeconds);

    // Get the total time elapsed between Start() and Stop().
    double TotalTimeElasped(TimeUnit timeUnit = TimeInMilliSeconds);
};

You can download the code from the following links:
Timer.h
Timer.cpp
Timer_Unix.cpp
Timer.zip


SLogLib: An easy to use, fully customizable and extensible, cross-platform logging library

A long time ago when I was doing PhD I was implementing a very complex geometric algorithm for computing intersection of two triangular meshes. There were bugs in code which would trigger only in certain edge cases. Since it was a GUI program using std::cout was not an option. Initially I tried writing messages to a file but soon realized it was too tedious as code was spanned across several files and I had to manually insert file names, function names, line numbers for every logging message.

A quick search on Internet revealed many logging libraries. I tried couple of them (unfortunately I can’t remember their names now) but none of them allowed customization of the output. The libraries I came across could output to variety of devices, supported multi-threading and many other fancy features but it was not possible to change the way messages was reported to the user. This was very important to me because I wanted to format my messages in a particular way so that I can easily check how my code was crashing on edge cases.

So, I wrote the first version of SLogLib sometime in 2005. It was build on a single principle that user should be in complete control of how messages are written to devices. In order to do that, SLogLib wraps all information required for logging into a structure called Message and passes it to a Formatter. The Formatter converts the Message structure to a std::string which will be outputted to the device. The Formatter must be written by the user. However, to make it easier to start using SLogLib and illustrate how to write a Formatter few Formatters are included with SLogLib.

Over past decade SLogLib has been very useful to me for a variety of projects and I hope that other can find it useful as well. SLogLib is hosted on Github under MIT license. You can clone of fork it from here: https://github.com/saurabhg17/SLogLib.




Computing area of all facets in CGAL::Polyhedron_3

In this post I will show how to compute area of a facet in CGAL::Polyhedron_3.

ComputeFacetArea() Functor

ComputeFacetArea() is a thread-safe functor for computing the area of a given facet in a CGAL::Polyhedron_3. The facet must be a planar polygon with arbitrary number of sides. We need facet’s normal vector to compute it’s area. The facet normals must be be initialized using the ComputeFacetArea()’s constructor. The code for computing the facet normals is presented in this post: Computing normal of all facets in CGAL::Polyhedron_3.

#ifndef _SMESHLIB_OPERATIONS_COMPUTEFACETAREA_H_
#define _SMESHLIB_OPERATIONS_COMPUTEFACETAREA_H_

#include "PropertyMap.h"

namespace SMeshLib   {
namespace Operations {
;

// The ComputeFacetArea is a functor (delegate) to compute the area of a facet in CGAL::Polyhdeon_3.
// operator() is thread-safe.
// TPolyhedron is a type of CGAL::Polyhdeon_3.
// TFacetNormals is a property map associating a normal vector for each facet in the CGAL::Polyhdeon_3.
template<class TPolyhedron, class TFacetNormals>
struct ComputeFacetArea
{
public:
	
	// Redefine types from TPolyhedron for convenience.
	typedef typename TPolyhedron::Facet                                  Facet;
	typedef typename TPolyhedron::Traits::Vector_3                       Vector3;
	typedef typename TPolyhedron::Halfedge_around_facet_const_circulator HalfEdgeConstCirculator;
	
	// Return type of operator() required by QtConcurrent.
	typedef double result_type;
	
public:
	
	ComputeFacetArea(const TFacetNormals& facetNormals_)
		: facetNormals(facetNormals_)
	{}
	
	// Compute the area of a given facet.
	// The formula for computing facet area, which in general is a planar polygon, is from
	// http://softsurfer.com/Archive/algorithm_0101/algorithm_0101.htm
	inline double operator() (const Facet& f) const
	{
		std::vector<Vector3> _vectors;
		HalfEdgeConstCirculator s = f.facet_begin();
		HalfEdgeConstCirculator e = s;
		CGAL_For_all(s, e)
		{
			_vectors.push_back(s->vertex()->point() - CGAL::ORIGIN);
		}
		
		Vector3 _sumCrossProducts(CGAL::NULL_VECTOR);
		const size_t N = _vectors.size();
		for(size_t i=0 ; i<N ; ++i)
		{
			_sumCrossProducts = _sumCrossProducts + CGAL::cross_product(_vectors[i], _vectors[(i+1)%N]);
		}
		
		return fabs((facetNormals.value(&f) * _sumCrossProducts) / 2.0);
	}
	
public:
	
	// Facet normals are required for computing the area of a facet.
	const TFacetNormals& facetNormals;
};

};	// End namespace Operations.
};	// End namespace SMeshLib.

#endif // _SMESHLIB_OPERATIONS_COMPUTEFACETAREA_H_
Using ComputeFacetArea() Functor

Area of a facet f can be computed as double area = ComputeFacetArea(h);.

For most purposes, it is better to compute area of all facets once and cache them for later use. It is best to store the results in an associative container which associates the facet handle with the area. In the following example, I use PropertyMap which is a wrapper for std::set.

#include "ImportOBJ.h"
#include "ComputeFacetArea.h"
#include "ComputeFacetNormal.h"
#include "PropertyMap.h"
#include "CGAL/Simple_cartesian.h"
#include "CGAL/Polyhedron_items_3.h"
#include "CGAL/HalfedgeDS_list.h"
#include "CGAL/Polyhedron_3.h"

typedef CGAL::Simple_cartesian<double> Kernel;
typedef CGAL::Polyhedron_3<Kernel, 
	                       CGAL::Polyhedron_items_3, 
						   CGAL::HalfedgeDS_list> CgalPolyhedron;

// Compute the area of all facets in a CGAL::Polyhedron_3 and stores it in 
// facetAreas.
// TPolyhedron is a type of CGAL::Polyhedron_3.
// TFacetAreas is a property map which associates the facet area to facet in 
// the CGAL::Polyhdeon_3.
// TFacetNormals is a property map which associates a normal vector to facet in 
// the CGAL::Polyhdeon_3.
template<class TPolyhedron, class TFacetAreas, class TFacetNormals>
void computeFacetAreas(const TPolyhedron& polyhedron, TFacetAreas* facetAreas, const TFacetNormals& facetNormals)
{
	if(facetAreas == 0)
	{
		return;
	}
	
	typename TPolyhedron::Facet_const_iterator _begin = polyhedron.facets_begin();
	typename TPolyhedron::Facet_const_iterator _end   = polyhedron.facets_end();
	
	SMeshLib::Operations::ComputeFacetArea<TPolyhedron, TFacetNormals> _computeFacetArea(facetNormals);
	CGAL_For_all(_begin, _end)
	{
		facetAreas->setValue(&*_begin, _computeFacetArea(*_begin));
	}
}

// Compute the normal of all facets in a CGAL::Polyhedron_3 and stores it in facetNormals.
// TPolyhedron is a type of CGAL::Polyhedron_3.
// TFacetNormals is a property map which associates a normal vector to facet in the CGAL::Polyhdeon_3. 
template<class TPolyhedron, class TFacetNormals>
void computeFacetNormals(const TPolyhedron& polyhedron, TFacetNormals* facetNormals)
{
	if(facetNormals == 0)
	{
		return;
	}
	
	typename TPolyhedron::Facet_const_iterator _begin = polyhedron.facets_begin();
	typename TPolyhedron::Facet_const_iterator _end   = polyhedron.facets_end();

	SMeshLib::Operations::ComputeFacetNormal<TPolyhedron> _computeFacetNormal;
	CGAL_For_all(_begin, _end)
	{
		facetNormals->setValue(&*_begin, _computeFacetNormal(*_begin));
	}
}

void testComputeFacetArea()
{
	CgalPolyhedron _poly;
	SMeshLib::IO::importOBJ("Venus.obj", &_poly);
	
	typedef SMeshLib::PropertyMap<const CgalPolyhedron::Facet*, CgalPolyhedron::Traits::Vector_3> FacetNormalPM;
	FacetNormalPM _facetNormals;
	computeFacetNormals(_poly, &_facetNormals);
	
	typedef SMeshLib::PropertyMap<const CgalPolyhedron::Facet*, double> FacetAreaPM;
	FacetAreaPM _facetAreas;
	computeFacetAreas(_poly, &_facetAreas, _facetNormals);
}
Downloads

ImportOBJ.h
PropertyMap.h
ComputeFacetNormal.h
ComputeFacetArea.h
TestComputeFacetArea.cpp
Venus.obj
ComputeFacetArea.zip


Computing normal of all vertices in CGAL::Polyhedron_3

In this post I will show how to compute normal vector at a vector in CGAL::Polyhedron_3.

ComputeVertexNormal() Functor

ComputeVertexNormal() is a thread-safe functor for computing the normal vector at a given vertex in a CGAL::Polyhedron_3. The normal vector at a vertex is the average of the normal vectors of all facets incident on the vertex. The facet normals must be be initialized using the ComputeVertexNormal()’s constructor. The code for computing the facet normals is presented in this post: Computing normal of all facets in CGAL::Polyhedron_3.

#ifndef _SMESHLIB_OPERATIONS_COMPUTEVERTEXNORMAL_H_
#define _SMESHLIB_OPERATIONS_COMPUTEVERTEXNORMAL_H_

#include "PropertyMap.h"

namespace SMeshLib   {
namespace Operations {
;

// The ComputeVertexNormal is a functor (delegate) to compute the normal of a vertex in CGAL::Polyhdeon_3.
// operator() is thread-safe.
// TPolyhedron is a type of CGAL::Polyhdeon_3.
// TFacetNormals is a property map which associates a normal vector to each facet in the CGAL::Polyhdeon_3.
// The vertex normal is the average of all facet normals incident on it.
template<class TPolyhedron, class TFacetNormals>
struct ComputeVertexNormal
{
public:
	
	// Redefine types from TPoly for convenience.
	typedef typename TPolyhedron::Vertex                                  Vertex;
	typedef typename TPolyhedron::Facet                                   Facet;
	typedef typename TPolyhedron::Traits::Vector_3                        Vector3;
	typedef typename TPolyhedron::Halfedge_around_vertex_const_circulator HalfEdgeConstCirculator;
	
	// Return type of operator() required by QtConcurrent.
	typedef Vector3 result_type;
	
public:
	
	ComputeVertexNormal(const TFacetNormals& facetNormals_)
		: facetNormals(facetNormals_)
	{}
	
	// Compute normal of the given vertex.
	inline Vector3 operator() (const Vertex& v) const
	{
		Vector3                 n = CGAL::NULL_VECTOR;
		HalfEdgeConstCirculator s = v.vertex_begin();
		HalfEdgeConstCirculator e = s;
		CGAL_For_all(s, e)
		{
			// Border edge doesn't have facet and hence no normal.
			if(!s->is_border())
			{
				n = n + facetNormals.value(&*(s->facet()));
			}
		}
		return n/std::sqrt(n*n);
	}
	
public:
	
	const TFacetNormals& facetNormals;
};

};	// End namespace Operations.
};	// End namespace SMeshLib.

#endif // _SMESHLIB_OPERATIONS_COMPUTEVERTEXNORMAL_H_
Using ComputeVertexNormal() Functor

Normal vector at a vertex v can be computed as Vector3 normal = ComputeVertexNormal(f); .

For most purposes, it is better to compute area of all facets once and cache them for later use. It is best to store the results in an associative container which associates the facet handle with the area. In the following example, I use PropertyMap which is a wrapper for std::set.

#include "ImportOBJ.h"
#include "ComputeFacetNormal.h"
#include "ComputeVertexNormal.h"
#include "PropertyMap.h"
#include "CGAL/Simple_cartesian.h"
#include <CGAL/Polyhedron_items_3.h>
#include "CGAL/HalfedgeDS_list.h"
#include "CGAL/Polyhedron_3.h"

typedef CGAL::Simple_cartesian<double> Kernel;
typedef CGAL::Polyhedron_3<Kernel, 
	                       CGAL::Polyhedron_items_3, 
						   CGAL::HalfedgeDS_list> CgalPolyhedron;

// Compute the normal of all vertices in a CGAL::Polyhedron_3 and stores it in 
// vertexNormals.
// TPolyhedron is a type of CGAL::Polyhedron_3.
// TVertexNormals is a property map which associates a normal vector to vertex 
// in the CGAL::Polyhdeon_3.
// TFacetNormals is a property map which associates a normal vector to facet 
// in the CGAL::Polyhdeon_3.
template<class TPolyhedron, class TVertexNormals, class TFacetNormals>
void computeVertexNormals(const TPolyhedron& polyhedron, TVertexNormals* vertexNormals, const TFacetNormals& facetNormals)
{
	if(vertexNormals == 0)
	{
		return;
	}
	
	typename TPolyhedron::Vertex_const_iterator _begin = polyhedron.vertices_begin();
	typename TPolyhedron::Vertex_const_iterator _end   = polyhedron.vertices_end();
	
	SMeshLib::Operations::ComputeVertexNormal<TPolyhedron, TFacetNormals> _computeVertexNormal(facetNormals);
	CGAL_For_all(_begin, _end)
	{
		vertexNormals->setValue(&*_begin, _computeVertexNormal(*_begin));
	}
}

// Compute the normal of all facets in a CGAL::Polyhedron_3 and stores it in facetNormals.
// TPolyhedron is a type of CGAL::Polyhedron_3.
// TFacetNormals is a property map which associates a normal vector to facet in the CGAL::Polyhdeon_3. 
template<class TPolyhedron, class TFacetNormals>
void computeFacetNormals(const TPolyhedron& polyhedron, TFacetNormals* facetNormals)
{
	if(facetNormals == 0)
	{
		return;
	}
	
	typename TPolyhedron::Facet_const_iterator _begin = polyhedron.facets_begin();
	typename TPolyhedron::Facet_const_iterator _end   = polyhedron.facets_end();

	SMeshLib::Operations::ComputeFacetNormal<TPolyhedron> _computeFacetNormal;
	CGAL_For_all(_begin, _end)
	{
		facetNormals->setValue(&*_begin, _computeFacetNormal(*_begin));
	}
}

void testComputeVertexNormal()
{
	CgalPolyhedron _poly;
	SMeshLib::IO::importOBJ("Venus.obj", &_poly);
	
	typedef SMeshLib::PropertyMap<const CgalPolyhedron::Facet*, CgalPolyhedron::Traits::Vector_3> FacetNormalPM;
	FacetNormalPM _facetNormals;
	computeFacetNormals(_poly, &_facetNormals);
	
	typedef SMeshLib::PropertyMap<const CgalPolyhedron::Vertex*, CgalPolyhedron::Traits::Vector_3> VertexNormalPM;
	VertexNormalPM _vertexNormals;
	computeVertexNormals(_poly, &_vertexNormals, _facetNormals);
}
Downloads

ImportOBJ.h
PropertyMap.h
ComputeFacetNormal.h
ComputeVertexNormal.h
TestComputeVertexNormal.cpp
Venus.obj
ComputeVertexNormal.zip