Jump to content

Flatten polygon selection based on normals


bjlotus
Go to solution Solved by zipit,

Recommended Posts

Hi all,

 

I've been playing around with python inside c4d for the last couple days, just for the sake of learning something new (and open future creative possibilities, why not).

I've been able to manipulate splines and polygons, create objects from scratch and send modelling commands... now, I want to do this 'simple thing' and I'm a bit stuck: I want to be able to 'flatten' a polygon selection based on the normals, like when you drag the scale axis to 0% on z handle when modelling.

 

So far I've been able to move the selected polygons (or its points) based on the normal direction, but can't seem to get this one. Any hints will be greatly appreciated.

 

 

PS: I know there're a few plugins around, but I just want to know how it works internally and the math involved, so I can try and implement a modelling sequence.

 

Thanks!

Jon

Link to comment

If you want to flatten a group of polys, you need to define the plane you are flattening them against. That is pretty arbitrary; you could use a plane perpendicular to the average of all normals, or make a plane from three points, or whatever.

 

Then you use the plane as xz plane of a coordinate system, transform your points into that, and set the new transformed coordinates of each point to 0 (or to a fraction of its current value if you want a more gradual flattening). Finally, you transform the points back.

Link to comment

Hi @Cairyn, thanks for your reply.

I think I conceptually understand what you said, but I don't have a clue about how to implement it...

 

I wrote a little example as an exercise, where I create a tube, select a couple polygons and translate the selected polygons based on the average normals. After that, I'd like to flatten out the selection... At some point I calculated the average points to see if that could be where the flattening might be applied  (which is close to the modelling axis, when orientation is set to 'normals'), but I think I'd like to flatten where the selection starts, as shown in red on the following image:

 

bEV9eeW.png

 

I've also added a couple colored nulls  where the average normals (yellow) and average points (cyan) are, just for reference.

 

import c4d
from c4d import utils

# -----------------------------------------------------------------------------------------------------------------------
# Main function
# -----------------------------------------------------------------------------------------------------------------------
def main():
    doc = c4d.documents.GetActiveDocument()

    #create an object and make it editable (use clone when using csto)
    obj = c4d.BaseObject(c4d.Otube)
    obj[c4d.PRIM_TUBE_IRAD] = 50  # inner rad
    obj[c4d.PRIM_TUBE_ORAD] = 100  # outer rad
    obj[c4d.PRIM_TUBE_HEIGHT] = 20  # height
    obj[c4d.PRIM_TUBE_SEG] = 12  # rotation segments
    obj[c4d.PRIM_TUBE_CSUB] = 1  # cap segments
    obj[c4d.PRIM_TUBE_HSUB] = 1  # height segments
    obj[c4d.PRIM_AXIS] = 2  # axis (2 = +Y)
    result = utils.SendModelingCommand(
        command=c4d.MCOMMAND_CURRENTSTATETOOBJECT,
        list=[obj.GetClone()],
        mode=c4d.MODELINGCOMMANDMODE_ALL,
        doc=doc)

    obj = result[0]

    # optimize the obj
    bc = c4d.BaseContainer()  # contain settings
    # set optimize (default c4d)
    bc.SetFloat(c4d.MDATA_OPTIMIZE_TOLERANCE, .01)
    bc.SetBool(c4d.MDATA_OPTIMIZE_POINTS, True)
    bc.SetBool(c4d.MDATA_OPTIMIZE_POLYGONS, True)
    bc.SetBool(c4d.MDATA_OPTIMIZE_UNUSEDPOINTS, True)

    result = c4d.utils.SendModelingCommand( # optimize, result returns Boolean
        command=c4d.MCOMMAND_OPTIMIZE,
        list=[obj],
        doc=doc,
        bc=bc)

    obj.Message(c4d.MSG_UPDATE)  # update object
    doc.InsertObject(obj)  # add to scene


    selected_polys = obj.GetPolygonS() #get selected polys/base select
    #select arbitrary polys
    selected_polys.Select(9)
    selected_polys.Select(13)

    selected_points = [] #to hold point vectors
    selected_points_indexes = [] #to hold points indexes
    selected_polygons_indexes = [] #to hold polygons indexes

    polys_count = obj.GetPolygonCount() #get number of polys
    polys_list = selected_polys.GetAll(polys_count) #list of poly indexes holding boolean values: 1: selected, 0: not selected

    points = obj.GetAllPoints()  # get all object points

    normals_avg = 0 #store average of normals


    #loop through polys, process if selected (true)
    for i in xrange(polys_count):
        if polys_list[i]:
            polygon = obj.GetPolygon(i)  #get polygon
            selected_polygons_indexes.append(i)
            a, b, c, d = points[polygon.a], points[polygon.b], points[polygon.c], points[polygon.d]
            #there could be shared points on neighbour polys, this checks they're not added multiple times
            if a not in selected_points:
                selected_points.append(a)
                selected_points_indexes.append(polygon.a)
            if b not in selected_points:
                selected_points.append(b)
                selected_points_indexes.append(polygon.b)
            if c not in selected_points:
                selected_points.append(c)
                selected_points_indexes.append(polygon.c)
            if d not in selected_points:
                selected_points.append(d)
                selected_points_indexes.append(polygon.d)

            #cross product
            cross = (b - a).Cross(d - a).GetNormalized()
            #add to avg
            normals_avg += cross

    #calculate avg
    normals_avg = normals_avg / selected_polys.GetCount()
    doc.InsertObject(createColorNull(normals_avg, "normals_avg", 255, 255, 0), parent=None)#display a yellow null where normals_avg is

    #translate points
    offset = 30  # arbitrary amount in cm
    for i in xrange(len(selected_points)):
        obj.SetPoint(selected_points_indexes[i], selected_points[i] + normals_avg * offset)


    #update selected points
    points = obj.GetAllPoints()
    points_avg = 0  # average pos of selected points
    selected_points = [] #reset
    for i in xrange(len(selected_polygons_indexes)):
        polygon = obj.GetPolygon(selected_polygons_indexes[i])  # get polygon
        a, b, c, d = points[polygon.a], points[polygon.b], points[polygon.c], points[polygon.d]
        if a not in selected_points:
            selected_points.append(a)
        if b not in selected_points:
            selected_points.append(b)
        if c not in selected_points:
            selected_points.append(c)
        if d not in selected_points:
            selected_points.append(d)
    print("sel points", selected_points)
    #calculate avg points
    for i in xrange(len(selected_points)):
        points_avg += selected_points[i]


    points_avg = points_avg / len(selected_points)
    doc.InsertObject(createColorNull(points_avg, "points_avg", 0, 255, 255), parent=None) #display a cyan null where points_avg is


    obj.Message(c4d.MSG_UPDATE)  # update object
    doc.Message(c4d.MSG_UPDATE)  # update doc
    c4d.EventAdd()  # refresh viewport

# -----------------------------------------------------------------------------------------------------------------------
# Creates a color null/point
# -----------------------------------------------------------------------------------------------------------------------
def createColorNull(pos, name, red, green, blue):
    null = c4d.BaseObject(c4d.Onull)
    if null is None:
        sys.exit('Could not create parent Null object!')
    null.SetName(name)
    null.SetRelPos(pos)
    null[c4d.NULLOBJECT_DISPLAY] = 1
    null[c4d.NULLOBJECT_RADIUS] = 3
    null[c4d.NULLOBJECT_ASPECTRATIO] = 1
    null[c4d.NULLOBJECT_ORIENTATION] = 1
    null[c4d.ID_BASEOBJECT_USECOLOR] = 2
    null[c4d.NULLOBJECT_ICONCOL] = True
    r = c4d.utils.RangeMap(value=red, mininput=0, maxinput=255, minoutput=0, maxoutput=1, clampval=True)
    g = c4d.utils.RangeMap(value=green, mininput=0, maxinput=255, minoutput=0, maxoutput=1, clampval=True)
    b = c4d.utils.RangeMap(value=blue, mininput=0, maxinput=255, minoutput=0, maxoutput=1, clampval=True)
    null[c4d.ID_BASEOBJECT_COLOR] = c4d.Vector(r, g, b)
    return null



# -----------------------------------------------------------------------------------------------------------------------
# Execute main()
# -----------------------------------------------------------------------------------------------------------------------
if __name__ == '__main__':
    main()

 

 

Any inputs on how should I continue to be able to flatten? Please keep in mind I'm kinda slow on this things 🙂

 

Thanks!

Jon

 

Link to comment
11 hours ago, bjlotus said:

I think I conceptually understand what you said, but I don't have a clue about how to implement it...

 

Hmm, I'm not sure how I can help you here (except programming the stuff for you, which I'm not gonna do as I don't have the time). From glancing at your code, I see that you know the basics already, including the cross product, so I gather you know the math behind it.

 

If you want to go on piecewise, you may want to think about your phrase "where the selection starts", which is a fairly difficult thing to find. Usually the center of the plane that you flatten against would be the average of the points involved, as that is easy to find. If you want anything else, you need to take into account that your object may be rotated arbitrarily, and that the points that you want to flatten may form some rotated structure within the object as well.

 

So, assuming you want to define "where the selection starts" through a bounding box, that bounding box may be axis parallel to the world, axis parallel to the object's local system, or it may be an optimal bounding box for the selected points (a BB with minimal volume, which is not easy to calculate). Or, actually, any other box containing your points.

 

You could use a bounding box whose direction is defined by the average normal. But there is no guarantee that the BB's lower boundary actually equals the red line in your screenshot - that is totally a consequence of your symmetric object and symmetric point selection.

 

...Delay the decision for a while and think about the transformation first. Use the average point center as zero coordinate for your flatten-plane and create the flatten-plane from that and the average normal. That way, you have at least a coordinate system to work with. Then, make a sample scene that includes some asymmetries and rotations so you are forced to set up the transformation correctly. Continue as described above. Once you have the flattening basically done, you can think about the bounding box and the placement of the flatten-plane again.

 

(Low hanging fruits, and stuff... 😉 )

Link to comment

Hi @Cairyn, thanks again for your input!

 

You're right about the flattening area position, I should use the average of the points and then mess with bounding boxes if needed, which of course can be tricky on odd shaped objects.

 

49 minutes ago, Cairyn said:

so I gather you know the math behind it.

 

Well, I'm a bit rusty when it comes to 3D to be honest. I'm not sure how I can create my flatten plane based on the avg normal and avg points... but rainy day ahead, so I'll be trying!

 

Thanks!

Jon

Link to comment
  • Solution

Hi @bjlotus, hi @Cairyn,

 

there is one problem with your normals calculation. You calculate just a vertex normal in the polygon and then declare it to be the polygon normal 😉 (your variable "cross"), i.e., you assume all your polygons to be coplanar. With today's high density meshes you can probably get away with that to a certain degree, but it would not hurt to actually calculate the mean vertex normal of a polygon, a.k.a., the polygon normal to have a precise result.

 

The problem with your "flattening" is that it is not one. Unless I have overread something here in the thread, the missing keyword would be a point-plane projection. You just translate all selected points by a fixed amount, which was if I understood that correctly only a work in progress, but obviously won't work. 

 

Things could also be written a bit more tidely and compact in a pythonic fashion, but that has very little impact on the performance and is mostly me being nitpicky 😉 I did attach a version of how I would do this at the end (there are of course many ways to do this, but maybe it will help you).

 

Cheers,

Ferdinand

 

"""'Flattens' the active polygon selection of a PolygonObject.

Projects the points which are part of the active polygon selection into the 
mean plane of the polygon selection.
"""

import c4d


def GetMean(collection):
    """Returns the mean value of collection. 

    In Python 3.4+ we could also use statistics.mean() instead.

    Args:
        collection (iterable): An iterable of types that support addition,
        whose product supports multiplication.

    Returns:
        any: The mean value of collection.
    """
    return sum(collection) * (1. / len(collection))


def GetPolygonNormal(cpoly, points):
    """Returns the mean of all vertex normals of a polygon.

    You could also use PolygonObject.CreatePhongNormals, in case you expect
    to always have a phong tag present and want to respect phong breaks. 

    Args:
        cpoly (c4d.Cpolygon): A polygon.
        points (list[c4d.vector]): All the points of the object.

    Returns:
        c4d.Vector: The polygon normal.
    """
    # The points in question.
    a, b, c, d = (points[cpoly.a], points[cpoly.b],
                  points[cpoly.c], points[cpoly.d])
    points = [a, b, c] if c == d else [a, b, c, d]
    step = len(points) - 1

    # We now could do some mathematical gymnastics to figure out just two
    # vertices we want to use to compute the normal of the two triangles in
    # the quad. But this would not only be harder to read, but also most
    # likely slower. So we are going to be 'lazy' and just compute all vertex
    # normals in the polygon and then compute the mean value for them.
    normals = []
    for i in range(step + 1):
        o = points[i - 1] if i > 0 else points[step]
        p = points[i]
        q = points[i + 1] if i < step else points[0]
        # The modulo operator is the cross product.
        normals.append(((p - q)) % (p - o))

    # Return the normalized (with the inversion operator) mean of them.
    return ~GetMean(normals)


def ProjectOnPlane(p, q, normal):
    """Projects p into the plane defined by q and normal.

    Args:
        p (c4d.Vector): The point to project.
        q (c4d.Vector): A point in the plane.
        normal (c4d.Vector): The normal of the plane (expected to be a unit 
         vector).

    Returns:
        c4d.Vector: The projected point.
    """
    # The distance from p to the plane.
    distance = (p - q) * normal
    # Return p minus that distance.
    return p - normal * distance


def FlattenPolygonSelection(node):
    """'Flattens' the active polygon selection of a PolygonObject.

    Projects the points which are part of the active polygon selection into the 
    mean plane of the polygon selection.

    Args:
        node (c4d.PolygonObject): The polygon node.

    Returns:
        bool: If the operation has been carried out or not.

    Raises:
        TypeError: When node is not a c4d.PolygonObject.
    """
    if not isinstance(op, c4d.PolygonObject):
        raise TypeError("Expected a PolygonObject for 'node'.")

    # Get the point, polygons and polygon selection of the node.
    points = node.GetAllPoints()
    polygons = node.GetAllPolygons()
    polygonCount = len(polygons)
    baseSelect = node.GetPolygonS()
    # This is a list of booleans, e.g., for a PolygonObject with three
    # polygons and the first and third polygon being selected, it would be
    # [True, False, True].
    polygonSelection = baseSelect.GetAll(polygonCount)
    # The selected polygons and the points which are part of these polygons.
    selectedPolygonIds = [i for i, v in enumerate(polygonSelection) if v]
    selectedPolygons = [polygons[i] for i in selectedPolygonIds]
    selectedPointIds = list({p for cpoly in selectedPolygons
                             for p in [cpoly.a, cpoly.b, cpoly.c, cpoly.d]})
    selectedPoints = [points[i] for i in selectedPointIds]

    # There is nothing to do for us here.
    if not polygonCount or not selectedPolygons:
        return False

    # The polygon normals, the mean normal and the mean point. The mean point
    # and the mean normal define the plane we have to project into. Your
    # image implied picking the bottom plane of the bounding box of the
    # selected vertices as the origin of the plane, you would have to do that
    # yourself. Not that hard to do, but I wanted to keep things short ;)
    polygonNormals = [GetPolygonNormal(polygons[pid], points)
                      for pid in selectedPolygonIds]
    meanNormal = ~GetMean(polygonNormals)
    meanPoint = GetMean(selectedPoints)

    # Project all the selected points.
    for pid in selectedPointIds:
        points[pid] = ProjectOnPlane(points[pid], meanPoint, meanNormal)

    # Create an undo, write the points back into the polygon node and tell
    # it that we did modify it.
    doc.StartUndo()
    doc.AddUndo(c4d.UNDOTYPE_CHANGE, node)
    node.SetAllPoints(points)
    doc.EndUndo()
    node.Message(c4d.MSG_UPDATE)

    # Things went without any major hiccups :)
    return True


def main():
    """Entry point.
    """
    if FlattenPolygonSelection(op):
        c4d.EventAdd()


if __name__ == '__main__':
    main()

 

Link to comment

Hi @zipit,

 

First of all, thank you so much for your reply and the super detailed comments on your code, it really helps me understand a few things (and hopefully to many other beginners as well). Your script does -almost- exactly what I was trying to do... and more importantly, the comments helps to grasp the hows and whys very clearly. Again, really appreciate your input, thanks 😄

 

18 hours ago, zipit said:

there is one problem with your normals calculation. You calculate just a vertex normal in the polygon and then declare it to be the polygon normal 😉 (your variable "cross"), i.e., you assume all your polygons to be coplanar. With today's high density meshes you can probably get away with that to a certain degree, but it would not hurt to actually calculate the mean vertex normal of a polygon, a.k.a., the polygon normal to have a precise result.

 

I'm calculating the mean (i think) using the "normals_avg"  variable. I add each cross product inside the for loop and then divide by the amount of polys selected. This was a quick example so I just calculated normals the way I remembered when I used to play with other software years ago. I'm not even checking if the polys are a triangle hehe... your GetPolygonNormal function is certainly more elegant. I compared the results and, while not always exactly the same, they are pretty close.

 

18 hours ago, zipit said:

The problem with your "flattening" is that it is not one. Unless I have overread something here in the thread, the missing keyword would be a point-plane projection. You just translate all selected points by a fixed amount, which was if I understood that correctly only a work in progress, but obviously won't work. 

 

Yeap, no flattening on the code I posted; what I was trying to do was: given a selection, first translate it by a fixed amount and then flatten it. The flatten part was what I didn't know how to do and what @Cairyn was kindly trying to explain.

 

18 hours ago, zipit said:

Things could also be written a bit more tidely and compact in a pythonic fashion, but that has very little impact on the performance and is mostly me being nitpicky 😉 I did attach a version of how I would do this at the end (there are of course many ways to do this, but maybe it will help you).

 

Sure thing! I know it's a bit messy and that there're probably some extra assignments, or things I should rewrite as functions; Being just an example I was just throwing lines testing what happens. I'm just starting to explore python's possibilities inside c4d (never used python before), so your version does definitely helps in many ways (even about how to keep things tidy!)

 

Once again, thank you. Have a great day!

Jon

Link to comment

Hi @bjlotus,

 

6 hours ago, bjlotus said:

I'm calculating the mean (i think) using the "normals_avg"  variable. I add each cross product inside the for loop and then divide by the amount of polys selected. This was a quick example so I just calculated normals the way I remembered when I used to play with other software years ago. I'm not even checking if the polys are a triangle hehe... your GetPolygonNormal function is certainly more elegant. I compared the results and, while not always exactly the same, they are pretty close.

 

This was not me be pedantic about terminology and you are computing the mean and the normals. I was just pointing out that you were just computing the normal for one of the planes in a polygon - unless I am overlooking something here. But for quads there are two, one for each triangle in the quad. Below is an illustration of a quad which I did manually triangulate for illustration purposes. You can see the vertex normals in white (which are each equal to the normal of the triangle/plane the lie in). And two plane normals in black, one for each triangle. The actual polygon normal is then the arithmetic mean of both plane normals.

 

image.thumb.png.d0241603f21b37fd7f8ebbdaa1bac6f5.png

 

So when you just pick one vertex in a quad to calculate the polygon normal, i.e., simply say one of the white normals is the polygon normal, then you assume that both tris of the quad lie in the same plane, i.e., that the quad is coplanar. Which of course can happen, but there is no guarantee. Quads are today usually quite close to being coplanar (and not that comically deformed as my example), but you will still introduce a little bit of imprecision by assuming all quads to be coplanar. 

 

Cheers,

Ferdinand

Link to comment
17 hours ago, zipit said:

Hi @bjlotus,

 

 

This was not me be pedantic about terminology and you are computing the mean and the normals. I was just pointing out that you were just computing the normal for one of the planes in a polygon - unless I am overlooking something here. But for quads there are two, one for each triangle in the quad. Below is an illustration of a quad which I did manually triangulate for illustration purposes. You can see the vertex normals in white (which are each equal to the normal of the triangle/plane the lie in). And two plane normals in black, one for each triangle. The actual polygon normal is then the arithmetic mean of both plane normals.

 

image.thumb.png.d0241603f21b37fd7f8ebbdaa1bac6f5.png

 

So when you just pick one vertex in a quad to calculate the polygon normal, i.e., simply say one of the white normals is the polygon normal, then you assume that both tris of the quad lie in the same plane, i.e., that the quad is coplanar. Which of course can happen, but there is no guarantee. Quads are today usually very close to being coplanar (and not that comically deformed as my example), but you will still introduce a little bit of imprecision by assuming all quads to be coplanar. 

 

Cheers,

Ferdinand

 

Hi @zipit!

oh, now I totally understand what you mean! (pun intended 😄)

Thanks so much for the clarification; that would explain the tiny differences when I compared the results using your function. 

 

PS: it didn't sound pedantic at all!

 

Best,

Jon

 

Link to comment
×
×
  • Create New...

Copyright Core 4D © 2023 Powered by Invision Community