CalcFaceNormal() Creates Strange Results

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED

On 10/07/2012 at 11:53, xxxxxxxx wrote:

OK. Thanks.
I'm trying to implement your instructions. But I don't understand how to "loop through the 4 sides to get a neighbor poly".

This is what I've got so far.
But I don't think I'm doing that part correctly:

``````    BaseDocument *doc = GetActiveDocument();
BaseObject *obj = doc->GetActiveObject();
PolygonObject *pobj = ToPoly(obj);
LONG polycount = pobj->GetPolygonCount();
LONG pointcount = pobj->GetPointCount();
Vector *points = pobj->GetPointW();

Neighbor n;                                                       //Create an instance of the neighbor class
if( !n.Init(pointcount, vadr, polycount, NULL) )  return FALSE;   //initialize it to use the active object's points and polys

for (int i=0; i<polycount; i++)               //Loop once for each polygon
{
//Get the polygon's point index#'s and assign them to variables
LONG a,b,c,d;

PolyInfo *pli = n.GetPolyInfo(i);           //Get each polygon's information

LONG side;
for (side=0; side<4; side++)                //Loop 4 times to test all 4 sides of a polygon <----This is probably Wrong?
{
LONG connected = pli->face[side];         //Get the neighboring polygon's index#
if(connected != -1)                       //If there is actually a connected polygon...do this stuff
{
GePrint(LongToString(connected));
CPolygon poly = vadr[connected];      //Get the polygon from the index# we got earlier

//Get the polygon's point index#'s and assign them to variables
LONG polya=poly.a, polyb=poly.b, polyc=poly.c, polyd=poly.d;
GePrint("Point A= " + LongToString(polya) + "   " + "Point B= " + LongToString(polyb) + "     "  \+ "Point C= " + LongToString(polyc) + "   " + "Point D= " + LongToString(polyd));
}
}

}
``````

-ScottA

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED

On 10/07/2012 at 13:45, xxxxxxxx wrote:

No, you're pretty much right on track...

``````
BaseDocument *doc = GetActiveDocument();
BaseObject *obj = doc->GetActiveObject();
PolygonObject *pobj = ToPoly(obj);
LONG polycount = pobj->GetPolygonCount();
LONG pointcount = pobj->GetPointCount();
Vector *points = pobj->GetPointW();

Neighbor n;                                                    //Create an instance of the neighbor class
Bool reInit = TRUE;

for (int i=0; i<polycount; i++)                                //Loop once for each polygon
{
// if needed, (re)initialize the Neighbor class
if( reInit )
{
if( !n.Init(pointcount, vadr, polycount, NULL) )    //initialize it to use the active object's points and polys
return FALSE;
reInit = FALSE;
}

PolyInfo *pli = n.GetPolyInfo(i);                        //Get each polygon's information

LONG side;
for (side=0; side<4; side++)                            //Loop 4 times to test all 4 sides of a polygon
{
LONG nbrNdx = pli->face[side];                        //Get the neighboring polygon's index#
if(nbrNdx == NOTOK)
continue;                                        // if there's no neighbor, skip it

LONG pt1, pt2;
switch(side)
{
case 0:  // a->b
pt1 = vadr[i].b;   // set pt1 to b
pt2 = vadr[i].a;   // set pt2 to a (reversed sequence)
break;
case 1:  // b->c
pt1 = vadr[i].c;   // set pt1 to c
pt2 = vadr[i].b;   // set pt2 to b (reversed sequence)
break;
case 2:  // c->d
pt1 = vadr[i].d;   // set pt1 to d
pt2 = vadr[i].c;   // set pt2 to c (reversed sequence)
break;
case 3:  // d->a
pt1 = vadr[i].a;   // set pt1 to a
pt2 = vadr[i].d;   // set pt2 to d (reversed sequence)
break;
}

CPolygon nbrPoly = vadr[nbrNdx ];          //Get the neighbor poly
if( (nbrPoly.a == pt1 && nbrPoly.b == pt2) ||
(nbrPoly.b == pt1 && nbrPoly.c == pt2) ||
(nbrPoly.c == pt1 && nbrPoly.d == pt2) ||
(nbrPoly.d == pt1 && nbrPoly.a == pt2) )
{
GePrint("Poly: "+LongToString(nbrNdx )+" is NOT inverted (moving on to next side...)");
}
else
{
GePrint("Poly: "+LongToString(nbrNdx )+" is inverted and needs to be flipped");

// flip_poly(nbrNdx , vadr);   // fix up the poly indices, accounting for (potential) triangles

// note that if you adjust/flip it here, you likely need to call the neighbor
// Init() function again before calling GetPolyInfo() again, because the poly
// indices would be altered, so set the reInit flag.
reInit = TRUE;
}
}
}
``````

...you'll have to create the flip_poly() routine (or just do it right there)... just note that you have to look for and handle triangle indices correctly.

EDIT: fixed multiple typos in the above :).

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED

On 10/07/2012 at 13:57, xxxxxxxx wrote:

..now, the issue you'll have with the above code is...

You're basically looking at each polygon, and checking to see if it's neighbors need to be flipped.  The problem is, you really need to start with a single polygon (or at least one per connected-group of polygons) and make sure every other polygon in the mesh matches it's orientation.

In other words, with just a single for(i=0; i<polycount; i++) loop, the first poly (i=0) might face one direction and the second (or 300th) one might face the opposite direction, so you might end up flipping various polys multiple times (if they happened to be neighbors of opposite-facing polys).

To resolve this issue, you'd need to keep track of which polys were tested (whether flipped or not) and which ones neighbors have already been tested and skip them when appropriate (reference my ConnectedPolyGroup() class example code in that other thread ... you could basically strip out the GroupID stuff from that code and insert the above (as appropriate) into the ProcessConnectedPolys() routine and mostly be done).

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED

On 10/07/2012 at 16:15, xxxxxxxx wrote:

Thanks a lot Giblet.
But it's reporting polygons as being reversed when they aren't.
I'm just using it on a cube with one polygon reversed to test it out. And it reports more than one polygon reversed.

At this point I don't really care about efficiency yet. Just accuracy.
So it's ok if it checks the same polygons multiple times(unless that's what's producing the errors).
Are you saying that I'd have to use your class to fix that problem?

-ScottA

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED

On 10/07/2012 at 20:08, xxxxxxxx wrote:

Yes and... yes.

If you're just using that one loop, you will get errors - depending on which of the 6 polygons (of the cube) is flipped.  Basically, if the flipped polygon gets tested - before it's found/fixed as a neighbor - it will cause it's 4 neighbors to be flipped (the wrong way) and then as others are tested, they'll end up flipping some of those back.

So yes... you'd need to do something like that example class does to track which ones have been processed and which have had their neighbors processed.  As stated, that example code would be one solution - exactly as it's already set up.

In other words, my cautionary note above was NOT about efficiency, it was about the accuracy (or lack thereof) of just doing your one loop code.

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED

On 10/07/2012 at 20:37, xxxxxxxx wrote:

...ok, I got bored again :).  Here you go:

``````
class NormAlign
{
private:
LONG        m_numVerts;
LONG        m_numPolys;
LONG        m_numMappedPolys;
UVWTag        *m_uvTag;
CPolygon    *m_pPolys;
UCHAR        *m_pPolyProcessed;
BaseSelect    *m_pSeedPolys;
BaseSelect    *m_pCheckPolys;

void FlipPoly(CPolygon *pPoly, LONG polyNdx);
Bool OrientPolyNeighbors(CPolygon *pPoly, PolyInfo *pPolyInfo);
Bool ProccessConnectedPolys(void);
public:
Bool AlignNormals(PolygonObject *pPolyObj);

NormAlign(void);
~NormAlign(void);
};

NormAlign::NormAlign(void)
{
m_uvTag = NULL;
m_pSeedPolys = NULL;
m_pCheckPolys = NULL;
m_pPolyProcessed = NULL;
}

NormAlign::~NormAlign(void)
{
BaseSelect::Free(m_pSeedPolys);
BaseSelect::Free(m_pCheckPolys);
bDelete(m_pPolyProcessed);
}

Bool NormAlign::AlignNormals(PolygonObject *pPolyObj)
{
m_numVerts = pPolyObj->GetPointCount();
m_numPolys = pPolyObj->GetPolygonCount();
m_pPolys = pPolyObj->GetPolygonW();

m_uvTag = (UVWTag * )pPolyObj->GetTag(Tuvw, 0);

m_pSeedPolys = BaseSelect::Alloc();
m_pCheckPolys = BaseSelect::Alloc();
if( !m_pSeedPolys || !m_pCheckPolys )    return false;

// track which polys have been processed.
m_pPolyProcessed = bNew UCHAR[m_numPolys];
if( !m_pPolyProcessed )                    return false;
ClearMem(m_pPolyProcessed, m_numPolys * sizeof(UCHAR), 0);

LONG polyNdx;
Bool done = false;
for(polyNdx=0; polyNdx<m_numPolys; polyNdx++)
{
if( m_pPolyProcessed[polyNdx] )    continue;

m_pSeedPolys->Select(polyNdx);                            // add this one to the list as a new seed poly
m_pPolyProcessed[polyNdx] = 1;                            // mark this one off...
m_numMappedPolys++;

done = this->ProccessConnectedPolys();                    // check orientation of all neighbors

if( done )    break;
}

// done with these...
BaseSelect::Free(m_pSeedPolys);
BaseSelect::Free(m_pCheckPolys);
bDelete(m_pPolyProcessed);
return true;
}

void NormAlign::FlipPoly(CPolygon *pPoly, LONG polyNdx)
{
CPolygon flippedpoly = *pPoly;

// flip the poly orientation (face normal) by re-ordering the point indices
if( pPoly->c != pPoly->d )
{
pPoly->b = flippedpoly.d;
pPoly->d = flippedpoly.b;
}
else
{
// Triangle
pPoly->b = flippedpoly.c;
pPoly->c = flippedpoly.b;
pPoly->d = flippedpoly.b;
}

// also adjust the uv-mapping, if any
if( m_uvTag )
{
if( pUVHndl )
{
UVWStruct olduvw, newuvw;

m_uvTag->Get(pUVHndl, polyNdx, olduvw);

newuvw.a = olduvw.a;
if( pPoly->c != pPoly->d )
{
newuvw.b = olduvw.d;
newuvw.c = olduvw.c;
newuvw.d = olduvw.b;
}
else
{
// Triangle
newuvw.b = olduvw.c;
newuvw.c = olduvw.b;
newuvw.d = olduvw.b;
}
m_uvTag->Set(pUVHndl, polyNdx, newuvw);
}
}
}

Bool NormAlign::OrientPolyNeighbors(CPolygon *pPoly, PolyInfo *pPolyInfo)
{
Bool bFlipped = false;
LONG side;
for(side=0; side<4; side++)
{
LONG nbrNdx = pPolyInfo->face[side];
if( nbrNdx == NOTOK )                continue;    // skip any sides that don't exist
if( m_pPolyProcessed[nbrNdx] )        continue;    // only checking neighbors not already checked

LONG pt1, pt2;
switch(side)
{
case 0:  // a->b
pt1 = pPoly->b;   // set pt1 to b
pt2 = pPoly->a;   // set pt2 to a (reversed sequence)
break;
case 1:  // b->c
pt1 = pPoly->c;   // set pt1 to c
pt2 = pPoly->b;   // set pt2 to b (reversed sequence)
break;
case 2:  // c->d
pt1 = pPoly->d;   // set pt1 to d
pt2 = pPoly->c;   // set pt2 to c (reversed sequence)
break;
case 3:  // d->a
pt1 = pPoly->a;   // set pt1 to a
pt2 = pPoly->d;   // set pt2 to d (reversed sequence)
break;
}

CPolygon *pNbrPoly = &m_pPolys[nbrNdx];          //Get the neighbor poly

if( (pNbrPoly->a == pt1 && pNbrPoly->b == pt2) ||
(pNbrPoly->b == pt1 && pNbrPoly->c == pt2) ||
(pNbrPoly->c == pt1 && pNbrPoly->d == pt2) ||
(pNbrPoly->d == pt1 && pNbrPoly->a == pt2) )
{
//GePrint("Poly: "+LongToString(nbrNdx )+" is NOT inverted (moving on to next side...)");
}
else
{
//            GePrint("Poly: "+LongToString(nbrNdx)+" is inverted and needs to be flipped...");

// fix up the poly indices, accounting for (potential) triangles
this->FlipPoly(pNbrPoly, nbrNdx);

// note that if you adjust/flip it here, you likely need to call the neighbor
// Init() function again before calling GetPolyInfo() again, because the poly
// indices would be altered, so set the reInit flag.
bFlipped = true;
}
m_pSeedPolys->Select(nbrNdx);                // add this one to the list as a new seed poly (to check it's neighbors in the next pass)
m_pPolyProcessed[nbrNdx] = 1;                // mark this one off...
m_numMappedPolys++;
}

return bFlipped;
}

Bool NormAlign::ProccessConnectedPolys(void)
{
Neighbor    nbr;
Bool reInit = true;

// NOTE: the way this code is set up, GetCount() will return a value of 1, when called from AlignNormals()...
//       once inside here, it's copied to the CheckPolys selection and cleared.  Then OrientPolyNeighbors()
//       repopulates the list by adding each (any) neighbor poly that was checked to the SeedPolys list, to
//       serve as a new 'seed' polys to be checked in the next pass.
//
//       Even though this new method (using a BaseSelect to list the seed polys to be checked) requires 2
//       loops to decode, it's faster than the old way of looping through every poly every pass.
while( m_pSeedPolys->GetCount() )
{
//        GePrint("m_pSeedPolys->GetCount() : "+LongToString(m_pSeedPolys->GetCount()));

m_pSeedPolys->CopyTo(m_pCheckPolys);                            // set up the CheckPolys selection to loop through
m_pSeedPolys->DeselectAll();                                    // then clear out the SeedPolys selection for the next pass...

LONG polyNdx, seg = 0, smin, smax;
#if API_VERSION < 13000
while( m_pCheckPolys->GetRange(seg++,&smin,&smax) )
#else
while( m_pCheckPolys->GetRange(seg++,MAXLONGl,&smin,&smax) )
#endif
{
for(polyNdx=smin; polyNdx<=smax; polyNdx++)
{
// if needed, (re)initialize the Neighbor class
if( reInit )
{
nbr.Flush();
if( !nbr.Init(m_numVerts, m_pPolys, m_numPolys, NULL) )
return true;    // failure state, but return that we're "done"
reInit = false;
}

// (re)Orient this polygon's neighbors (as needed)...
reInit = this->OrientPolyNeighbors(&m_pPolys[polyNdx], nbr.GetPolyInfo(polyNdx));

if( m_numMappedPolys == m_numPolys )
{
return true;
}
}
}
}
if( m_numMappedPolys == m_numPolys )
return true;
return false;
}
``````

I haven't compiled or tested that, but I think it should work.

EDIT: it's now been tested and I added the poly flipping code...

Notes:

1. You can call it like so...

``````
NormAlign na;
na.AlignNormals(op);
op->Message(MSG_UPDATE);
``````

2. also note that if the first polygon (at index 0) is the one that's facing the wrong directio, the above code will flip ALL the polygons to match it (ie. the wrong direction).

3. finally, while this was an interesting programming exercise, the Cinema 4D "Align Normals" command is apparently doing something more efficient (faster), so I'd still recommend not re-inventing the wheel.

EDIT #2:

Since it sounds like you want to use this, I broke it into more digestible chunks and re-worked the code a bit (for example, it turns out the the tracking array m_pNeighborsAdded[] was not actually needed, etc.)

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED

On 10/07/2012 at 22:33, xxxxxxxx wrote:

This is insanely useful stuff Giblet.
The built-in command may be faster. But it doesn't tell you which polygons were reversed. It just changes them without telling you which ones it changed.
So this is VERY useful.

I'm using it in a command data plugin. And it seems to be working well so far.
This is also the very first time I've used a custom class within a C++ C4D plugin project too.
Is there any benefit to calling to the class like you posted. Opposed to calling it like this?:

``````    NormAlign *myalign = gNew NormAlign;
myalign->AlignNormals(pobj);
``````

The SDK says that we should use gNew when possible.
And I don't know which way I should do it. Your way... or the way I posted.
Or is it just a matter of whatever style we prefer to use?

-ScottA

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED

On 10/07/2012 at 22:45, xxxxxxxx wrote:

re: using an instance of a class vs. allocating a pointer to one

If you just use an instance, when that instance goes out of scope (at the end of the routine that created it), the instance cleans itself up and goes away (it's Destructer is called).

If you allocate a pointer to one, memory is allocated for the class, so you have to be sure to call gDelete() when you're done with it to free up that memory.

If you need to pass it to some other function, it's generally more convenient to allocate a pointer to one, so you can just pass the pointer around, but if you're going to just use it (and be done with it) in the same function, it's typically more convenient to just declare an instance within that routine.

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED

On 11/07/2012 at 00:46, xxxxxxxx wrote:

>Originally posted by xxxxxxxx

This is also the very first time I've used a custom class within a C++ C4D plugin project too.

Really? But it's so useful, not only in C++, but also in Python. For example, this is a class I use to compute Face and Vertex normals and store information while doing certain operations in a tool-plugin.

``````// Copyright (C) 2012 Niklas Rosenstein
//
// This program is free software: you can redistribute it and/or modify
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
/** This class computes the normals of a PolygonObject's faces and vertices. It
also contains buffers for information that was calculated at the time
the RockgenNormalCache object was passed to RockifyObject(). **/
class RockgenNormalCache {

public:

LONG facecount;
LONG vertexcount;
Matrix* facenormals;
Matrix* vertexnormals;

// Information filled by RockifyObject()
Bool arith_ok;
LONG* arith_count;
Vector* arith_vectorsum;
Real* arith_influence;
Vector* final_vertices;

RockgenNormalCache()
: facecount(0), vertexcount(0), facenormals(NULL), vertexnormals(NULL),
arith_vectorsum(NULL), arith_count(NULL), arith_influence(NULL),
arith_ok(FALSE) {
}

RockgenNormalCache(PolygonObject* op) {
Init(op);
}

~RockgenNormalCache() {
FreeCache();
}

Bool IsInit() const {
return facenormals && vertexnormals && arith_vectorsum &&
arith_count && arith_influence;
}

Bool Init(PolygonObject* op) {
#ifdef DEBUG
char buffer[200];
GePrint("RockgenNormalCache::Init()");
#endif

facenormals = NULL;
vertexnormals = NULL;
arith_ok = FALSE;
arith_vectorsum = NULL;
arith_count = NULL;
arith_influence = NULL;
final_vertices = NULL;

facecount = op->GetPolygonCount();
vertexcount = op->GetPointCount();

Vector p1, p2, p3, p4, e13, e24, mid;
const Vector* op_vertices = op->GetPointR();
const CPolygon* op_faces = op->GetPolygonR();
Neighbor nbinfo;
if (!nbinfo.Init(vertexcount, op_faces, facecount, NULL)) {
#ifdef DEBUG
GePrint("Neighbor information could not be built.");
#endif
return FALSE;
}

// Allocate memory for face- and vertex-normals
facenormals = (Matrix* ) malloc(sizeof(Matrix) * facecount);
if (!facenormals) {
#ifdef DEBUG
GePrint(" Could not allocate facenormals.");
#endif
goto allocfail;
}
vertexnormals = (Matrix* ) malloc(sizeof(Matrix) * vertexcount);
if (!vertexnormals) {
#ifdef DEBUG
GePrint(" Could not allocate ppoints vertices.");
#endif
goto allocfail;
}

arith_vectorsum = (Vector* ) malloc(sizeof(Vector) * vertexcount);
if (!arith_vectorsum) {
#ifdef DEBUG
GePrint(" Could not allocate arith_vectorsum.");
#endif
goto allocfail;
}
arith_count = (LONG* ) malloc(sizeof(LONG) * vertexcount);
if (!arith_count) {
#ifdef DEBUG
GePrint(" Could not allocate arith_count.");
#endif
goto allocfail;
}
arith_influence = (Real* ) malloc(sizeof(Real) * vertexcount);
if (!arith_influence) {
#ifdef DEBUG
GePrint(" Could not allocate arith_influence.");
#endif
goto allocfail;
}
final_vertices = (Vector* ) malloc(sizeof(Vector) * vertexcount);
if (!final_vertices) {
#ifdef DEBUG
GePrint(" Could not allocate final_vertices.");
#endif
goto allocfail;
}

goto continue_;

allocfail:
nbinfo.Flush();
FreeCache();
return FALSE;

continue_:
Matrix* c_facenormal = facenormals;
const CPolygon* c_op_face = op_faces;

#ifdef DEBUG
GePrint(" Computing face normals.");
#endif

// Compute face Normals
for (int i=0; i < facecount; i++, c_op_face++, c_facenormal++) {
p1 = op_vertices[c_op_face->a];
p2 = op_vertices[c_op_face->b];
p3 = op_vertices[c_op_face->c];
p4 = op_vertices[c_op_face->d];

c_facenormal->v1 = p3 - p1;
c_facenormal->v2 = p4 - p2;
c_facenormal->v1.Normalize();
c_facenormal->v2.Normalize();

// Compute face midpoint
c_facenormal->off = p1 + p2 + p3;
if (c_op_face->c != c_op_face->d) {
c_facenormal->off = (c_facenormal->off + p4) / 4.0;
}
else {
c_facenormal->off /= 3.0;
}

// Compute face normal
c_facenormal->v3 = c_facenormal->v1.Cross(c_facenormal->v2);
c_facenormal->v3.Normalize();
}

LONG nbinfo_polyc, *nbinfo_polyv;
const Vector* c_op_vertex = op_vertices;
Matrix* c_vertexnormal = vertexnormals;
Matrix* t_facenormal;
Vector v1, v2, v3;

#ifdef DEBUG
GePrint(" Computing vertex normals.");
#endif

// Compute vertex normals
for (int i=0; i < vertexcount; i++, c_vertexnormal++, c_op_vertex++) {
nbinfo.GetPointPolys(i, &nbinfo_polyv, &nbinfo_polyc);
v1 = v2 = v3 = 0;

for (int j=0; j < nbinfo_polyc; j++) {
t_facenormal = &facenormals[nbinfo_polyv[j]];
v1 += t_facenormal->v1;
v2 += t_facenormal->v2;
v3 += t_facenormal->v3;
}

c_vertexnormal->off = *c_op_vertex;
c_vertexnormal->v1 = v1;
c_vertexnormal->v2 = v2;
c_vertexnormal->v3 = v3;
c_vertexnormal->Normalize();
}

#ifdef DEBUG
GePrint("RockgenNormalCache::Init() ended");
#endif

nbinfo.Flush();
return TRUE;
}

void FreeCache() {
#ifdef DEBUG
GePrint("RockgenNormalCache::FreeCache()");
#endif
if (facenormals) {
free(facenormals);
facenormals = NULL;
}
if (vertexnormals) {
free(vertexnormals);
vertexnormals = NULL;
}
if (arith_vectorsum) {
free(arith_vectorsum);
arith_vectorsum = NULL;
}
if (arith_count) {
free(arith_count);
arith_count = NULL;
}
if (arith_influence) {
free(arith_influence);
arith_influence = NULL;
}
if (final_vertices) {
free(final_vertices);
final_vertices = NULL;
}
}

};
``````

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED

On 11/07/2012 at 04:59, xxxxxxxx wrote:

Scott, I updated my latest code listing above, if you're interested.

THE POST BELOW IS MORE THAN 5 YEARS OLD. RELATED SUPPORT INFORMATION MIGHT BE OUTDATED OR DEPRECATED

On 11/07/2012 at 07:32, xxxxxxxx wrote:

Thanks a ton Giblet.