Clone orientation without a "Up Vector"
-
Hey guys,
In Cinema 4D, when setting a Cloner's mode to "Object" and "Distribution" mode to "Surface" (while leaving the "Up Vector" setting to "None"), the Cloner's clones are orientated in such a way that they follow the curvature of the geometry of the source Object while still seemingly being orientated in a "continuous flow", where they are not abruptly changing their orientation like they would if they were exclusively following the Object's normals in local/object space. In fact, their "continuous orientation" almost makes it seem that their orientation is also taking into account global/world space?
While I know this is a very basic setup, I am still going to include a screenshot to leave no room for interpretation as to what I am referring to:
My question is: in plain english, in the code of Cinema 4D's Cloner, how is this achieved? Is there a detection of adjacent polygons and then an averaging of normals?...or is an invisible Vector Field generated that dictates the orientation of the clones? As you can tell, I am quite lost and in need of guidance, as I am looking to loosely replicate some of the behavior of the Cloner object (in this specific mode and with these specific settings) within my plugin. While my approach will be more abstract, it still needs to get the fundamentals of this orientation logic correct.
Any insight would be greatly, greatly appreciated.
Thank you so much!
-
Hi @justinleduc,
Thanks for reaching out to us. Even though the setup might sound very basic, adding some extra visual information usually doesn't hurt and makes things easier!
Unfortunatelly, the problem you're pointing out to lies in the algorithmic field and relates to a non-public part of C4D, hence is Out of Scope for this forum:
We will neither reveal non-public functionalities of Cinema 4D's API, nor will we establish a direct line of communication with the developers of Cinema 4D.
With that said, there's a small comment on your question.
Your assumption about "continuous orientation" sounds a little too strong, at least from the mathematical point of view. You can already notice some visible discountinuities that you can see on your screenshot, e.g. on the figure's right hand:
I personally would not expect anything super smart happening under the hood with the "Up Vector" parameter set to None.The general suggestion would be to orient your objects in the tangent space. However, such formulation is quite ambiguous:
- Having tangential plane at the given point does not define the direction. Of course, you can come up with some clever approach here, for example, you could orient your objects along the direction of the steepest curvature at the given point, but such approach might be too expensive and just not worth it. The easier way would be to rely on the order of polygon vertices.
- Even the term "tangential plane" is already ambiguos, because there're several way how you can define it. For example, you can take a polygon normal as a normal for your tangential plane, or you can take into account any normal information (e.g. use vertex normal data at your point) and use this instead.
My personal suggestion would be to start with something simple (tangential plane from the polygon normals and direction that depends on polygon vertices order). Once you have this working, you can try making things more complex to improve the quality of your algorithm.
Cheers,
Ilia -
@i_mazlov Hey Ilia,
I sincerely can't thank you enough for leading me down the right path. Having a tangential plane from the polygon normals mixed in with a direction that depends on polygon vertices order is an approach that never occurred to me. I wish I had asked my question sooner, as I have spent the last 2 weeks exploring many different and more intricate approaches without any success, so you can only imagine how grateful I am for your assistance today.
After implementing your recommended approach, I noticed the following: while the clones (red Pyramid primitive objects as illustrated in the screenshot of my previous message) that are situated on the front-side and left-side facing polygons of my Object are oriented in the desired fashion (i.e. just like the Cloner object with a "Up Vector" set to "None"), the clones situated on the back-side and right-side facing polygons of my Object are oriented in the polar opposite direction of where I would like them to "point" towards (i.e. so the opposite of the desired direction). I presume that this is due to the fact that the normals values were calculated on the Object's local coordinates. For example, a polygon situated at the front of the Object (let's say, the middle chest part of the Object) will have its normals be
(0;0.1;-1)
, while a polygon situated on the complete opposite side of the Object (i.e. its middle back) will have its normals value be(0;0.1;1)
.As you can observe in the screenshot from my previous message, the clones (Pyramid primitives) are all facing upwards on the (for example) right thigh of the Object, whether they are in front, on the back, left or right side of said thigh. Same thing for the arms, where they are all uniformly pointing in either the right or left direction (discarding the wrists or elbows of course).
My question is: with the current approach of having a tangential plane from the polygon normals mixed in with a direction that depends on polygon vertices order, would it be possible to replicate the uniform direction of the clones as illustrated in the screenshot, regardless of the side they are facing on the Object?
I have attempted several things today, such as adjusting the vertex order based on normal direction, introducing a directional bias in world space and, lastly, using world space normals for consistency. All to say, nothing has worked so far. I kept questioning whether I was ever on the right path or if I was over-complicating things when the solution (which escapes me) might be much simpler.
Once again, thank you very, very much for your guidance and assistance. It has already been extremely valuable. I'm very grateful!
-
Hi @justinleduc ,
Great that the suggested aproach has already given you some initial results!
The effect you're describing is understandable and kind of expected. What you need here is to come up with some approach of choosing the direction that is robust to the polygon orientation.
The first thing that comes to mind when talking about polygon orientation is a cross product. Namely, you can calculate the cross product between the vector that lies in your tangent plane and the normal of the tangent plane. This would give you some vector that lies on your tangent plane and accounts for the polygon orientation. I did not check this approach on practise myself, but
on paperon whiteboard this solution looks viableCheers,
Ilia -
Hey everyone,
While @i_mazlov mentioned here the relevant keyword (tangent space), I think this topic is a bit in danger of getting too technical for the average Python user. Please also keep in mind that apart from us not being able to share implementation details, general algorithmic and math questions are out of scope of support. You will have to fill in the details yourself here.
What the cloner does, is relatively simple, it constructs a frame in a consistent manner for each polygon. When you have a polygon input that is constructed in a regular manner in the sense that all polygons organize their winding in the same manner, this will result in a sense of flow. But as soon as you spin a tri or quad in such mesh, the flow will be broken for that polygon (but you can of course still construct a frame aligned with that polygon, it will just point into a different direction). Primitives are usually highly regular in that regard, which is why this works well for them. Handwoven meshes tend not to have a consistent winding order on the other hand (which of the points of a polygon is its 'a' point).
For the general task of constructing frames, please see the Matrix manual. For your specific case, the base recipe would be quite simple, just construct a frame on point
a
and the edgesAB
andAC
for each polygon. This will result in consistently aligned frames for a mesh when its polygons have a consistent winding (which is usually not the case for handwoven meshes). To construct such consistently aligned frames for arbitrary meshes is a very hard problem to solve.Cheers,
FerdinandResult:
Code:
"""Demonstrates how to construct frames for each polygon in an object in a consistent manner. The script will create a pyramid for each polygon in the selected object. The pyramid will be aligned with the frame constructed from the edges AB and AC and the pyramid will be colored red. Must be run as a Script Manager script in Cinema 4D. """ import c4d, mxutils doc: c4d.documents.BaseDocument # The currently active document. op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`. def main() -> None: """Called by Cinema 4D when the script is being executed. """ if not isinstance(op, c4d.PolygonObject): raise ValueError("Please select a polygon object.") # Get all points for the selected object. points: list[c4d.Vector] = op.GetAllPoints() #For each polygon in the object, create a pyramid that is aligned with a frame constructed upon # the points, a, b, c. doc.StartUndo() cpoly: c4d.CPolygon for cpoly in op.GetAllPolygons(): # Get the points a, b, c for the current polygon and construct the midpoint for the polygon. # For quads, the dividing edge will always be AC. a: c4d.Vector = points[cpoly.a] b: c4d.Vector = points[cpoly.b] c: c4d.Vector = points[cpoly.c] midpoint: c4d.Vector = (a + c) * .5 # Now construct a frame based on the edges AB and AC. x: c4d.Vector = (b - a).GetNormalized() z: c4d.Vector = (c - a).GetNormalized() y: c4d.Vector = z.Cross(x).GetNormalized() # We need to recalculate z because AB might not be orthogonal to AC. z = x.Cross(y).GetNormalized() # Create a pyramid and set its properties. pyramid: c4d.BaseObject = mxutils.CheckType(c4d.BaseObject(c4d.Opyramid)) pyramid[c4d.PRIM_PYRAMID_LEN] = c4d.Vector(1, 3, 1) pyramid[c4d.ID_BASEOBJECT_USECOLOR] = c4d.ID_BASEOBJECT_USECOLOR_ALWAYS pyramid[c4d.ID_BASEOBJECT_COLOR] = c4d.Vector(1, 0, 0) # Align the pyramid so that it acts/looks like an arrow. We could of course do the same by # swizzling our frame vectors, but this is more readable. pyramid[c4d.PRIM_AXIS] = c4d.PRIM_AXIS_XP # Set the global matrix of the pyramid to the frame we constructed. pyramid.SetMg(c4d.Matrix(off=midpoint, v1=x, v2=y, v3=z)) pyramid.InsertUnder(op) doc.AddUndo(c4d.UNDOTYPE_NEW, pyramid) doc.EndUndo() c4d.EventAdd() if __name__ == '__main__': main()
-
First off, thank you so very kindly @ferdinand. I have been spending the first half of today experimenting with your code snippet (after translating it to C++) and I can attest to it working as expected, which was absolutely delightful. Thank you again for your assistance with this.
After further investigation and exploration, I must admit to feeling quite... silly (to put it nicely) and I must apologize for a healthy dose of profound foolishness on my side.
Somehow, I thought that the orientation of clones of a Cloner whose "Distribution" setting was set to "Surface" with its "Up Vector" setting set to "None" would be almost identical to the orientation of the voxels of a Volume Builder whose "Volume Type" was set to "Vector" while having its voxels orientation be influenced by a "Solid Layer" in a "Group Field" with the "Direction" turned on (and set to a specific vector direction,
Z+
in this case). Please see the screenshot below for a better understanding of what I mean:I have also attached the project file of the screenshot included above to this message, which you can find here: figure-vector-field-01.c4d
I feel silly, because... I tested @ferdinand 's approach on different meshes (like the Landscape Primitive) and the orientation of the Pyramids graciously provided by Ferdinand's code VS the orientation of the Voxels from the Volume Builder (with a Group Field and Solid Layer) was vastly different as expected (or rather, as I should have expected but didn't).
This now strays away from the original question and leads me consider making a new thread, which I shall do after I first exhaust every potential solution my skillset can offer. I will first dive back into the code generously offered by @ferdinand back in December (here), which should hopefully lead me in the right direction (pun unintended).
I thank you all once again for your very thorough, generous and invaluable assistance
-
Hey @justinleduc,
my bad, I somehow thought this thread was about Python. This changes of course a bit my interjection to not overcomplicate things. But a little bit simplicity does also not hurt in C++ .
If you want to go off into the deep end, compute smooth alignment fields for discrete geometry which hasn't the information baked into its topology, the very popular researcher Keenan Crane is a good stating point. He wrote multiple papers which might be relevant for you, as he has done extensive research in the area of alignment fields, distance maps, and unwrapping. Very popular is the paper Globally Optimal Direction Fields (2013) which might be exactly what you need.
Because this is at least not a fully solved, and therefore non-trivial, problem, you can also expect some nasty higher math. As hinted at before, we cannot help you with understanding these techniques/papers. But you are welcome to ask questions in General Talk when you are stuck to see if we have an idea (but there is no guarantee that we will answer or help you).
Cheers,
Ferdinand -
@ferdinand Oh wow! Thank you so much for letting me know about Keenan Crane, as I was not familiar with him. I am in awe of the exhaustive list of tools he and his lab have published.
The Globally Optimal Direction Fields paper seems indeed to be exactly what I am looking for. I was also surprised to see that I had already skimmed the Youtube video of the paper back in December.
Thank you so much @ferdinand for this invaluable piece of information. Time to dive in!
Cheers!