Place multiple objects on spline and align
-
I want to place multiple objects on fixed distances and align these objectsto the spline.
For a straight spline this is ok, but for a circle things go wrong.Here is the script:
from typing import Optional import c4d import random from c4d.utils import SplineHelp, SplineLengthData doc: c4d.documents.BaseDocument # The active document op: Optional[c4d.BaseObject] # The active object, None if unselected def PlaceCube(Pspline, PsplineLength, PxPosition) -> c4d.Vector: # calculate x position on spline, using given x position of the object to be placed splineOffset = PxPosition / PsplineLength # must be between 0 and 1 point = Pspline.GetSplinePoint(splineOffset, segment=0) print ("splineOffset, PxPosition, point: ", splineOffset, PxPosition, point) return point def main() -> None: sh = SplineHelp() #Add an instance of the spline helper class sh.InitSpline(op) #Dont forget to initialize it to the spline splineLength = sh.GetSplineLength() rXPos = 0 for i in range(5): cone1 = c4d.BaseObject(c4d.Ocone) cone1[c4d.PRIM_CONE_BRAD] = 100 xn = rXPos + cone1[c4d.PRIM_CONE_BRAD]/2 point = PlaceCube(op, splineLength, xn) # align to spline objmtx = cone1.GetMg() #Get the object's global matrix objmtx.off = point #Set the matrix positon to the point array splineOffset = xn / splineLength # must be between 0 and 1 normal = sh.GetNormal(splineOffset) #Get the normal values where the object currently is tangent = sh.GetTangent(splineOffset) #Get the tangent values where the object is ? objmtx.v3 = normal #Align the Z axis with the normal vector objmtx.v1 = tangent #Up vector to stabilize it ? cone1.SetMg(objmtx) #Update the object's position along the spline doc.InsertObject(cone1) rXPos = rXPos + cone1[c4d.PRIM_CONE_BRAD] + 100 c4d.EventAdd() if __name__ == '__main__': main()
-
Hello @pim,
Thank you for reaching out to us. While I mostly understand your problem, the problem description 'for a circle things go wrong' is quite ambiguous. So I had to interpret here bit.
There are some problems with your code, primarily that you did not set the y/v2/j component of the frame you assigned to your cone objects, likely producing non-orthogonal matrices which then cause problems further down the road. See the code example at the end for details.
But you also seem to be unaware of the quality of a normal returned by
c4d.utils.SplineHelp.GetNormal
, or generally the fact that the naive definition of a curve normal is often not what humans would consider useful (see Fig. I). The orientation of a normal at a point on a curve is defined by its curvature, causing the normals to change direction and length with the curvature of the curve. At the inflection points of a curve, the normals are technically null. This collides with the human notion of an inside and outside of a curve and that normals should face into consistent direction.
Fig. I: Top - The orientation and length of a normal of point on a curve depends on its curvature. At inflection points (red) the curvature of the spline is zero, and so is the normal. Bottom - Normals for a curve as humans would consider it useful. There is a clear sense of direction, an inside and an outside, and all normals are of equal length.The solution to this is parallel transport, a broader mathematical concept to maintain the orientation of a vector at multiple locations on a surface or curve that qualifies as "the same" for a local observer.
SplineHelp.GetNormal
yields mathematical normals whileSplineHelp.GetMatrix
yields transported frames/matrices for a spline. This and alternative algorithms have been discussed before here. Below I have provided a Python Programming tag to display the normals returned bySplineHelp
to get a better idea of what the type does (Fig. II).Fig II: The video shows the Python Programming tag drawing the two different notions of spline normals. When displaying the mathematical normals for the flower object, we can see the change of curvature affecting the normals.
SplineHelp
does interpolate between mathemical normals when told to do so, but computes them in a a bit unusal way - the normal is defined only by the current and next vertex instead of the previous, current, and next vertex. Note that parallel transport is always a workaround because what humans consider to be intuitively correct is not clearly and mathematically defined in this context. There are stronger algorithms than simple PTF as for example Rotation Minimizing Frames, but given a weird enough spline, they will also fail. See spline_normals.c4d for the scene file used in this video.Cheers,
FerdinandCode:
import c4d def main() -> None: """ """ if not op: raise RuntimeError("No object selected.") splineHelp = c4d.utils.SplineHelp() splineHelp.InitSplineWithUpVector(op, c4d.Vector(0, 1, 0)) splineLength: float = splineHelp.GetSplineLength() offset: float = 100 count: int = 5 for i in range(count): cone = c4d.BaseObject(c4d.Ocone) cone[c4d.PRIM_CONE_BRAD] = 100 # Not quite sure what this was supposed to do. # xn = rXPos + cone1[c4d.PRIM_CONE_BRAD]/2 # Compute the real offset in the spline. t = (i * offset) / splineLength # You ended up using SplineObject.GetSplinePoint instead of your SplineHelp 'sh' to get the # offset. The problem with SplineObject.GetSplinePoint is that it operates in object space. # You probably also want to operate with real offsets. off = splineHelp.GetPosition(t, realoffset=True) v3 = ~splineHelp.GetNormal(t, realoffset=True) v1 = ~splineHelp.GetTangent(t, realoffset=True) # Since this is a new object, getting its global matrix does not make too much sense, # it will always be equal to the identity matrix, i.e., c4d.Matrix(). # objmtx = cone1.GetMg() #Get the object's global matrix # You did not set the v2 component of your 'objmtx', causing its old value to linger. Which # then very likely resulted in a non-orthogonal matrix. Once such non-orthogonal matrix is # being set as an object matrix, Cinema 4D will silently orthogonalize it. But Cinema 4D # does not know that v1 and v3 are the 'good' components in this case, and v2 the one to # update, and instead will bring the matrix just into some orthogonal form. mg = c4d.Matrix(off=off, v1=v1, v2=~(v3 % v1), v3=v3) # But this would be frame for the mathematical normal at this point, which is likely not # what you want. So you just want to call 'GetMatrix' instead. mg = splineHelp.GetMatrix(t, realoffset=True) cone.SetMg(mg) doc.InsertObject(cone) # Related to the xn thing, I am once again not quite sure what this was supposed to do. # rXPos = rXPos + cone1[c4d.PRIM_CONE_BRAD] + 100 c4d.EventAdd() if __name__ == '__main__': main()
-
Hi Ferdinand,
Thanks for the extensive answer.
I have to look into it in more detail.Sorry for the unclear code,
nx was meant to place the object on the spline with a distance between the cones.
Perhaps it is easier to make an example script that places 5 cones divided and aligned over the circle spline.Something like this.
Your code now gives me this. The cones are not aligned to the spline.
If you could provide an example for placing the cones as in figure 1, that would help me very much.
-
Hey @pim,
I am not quite sure what constitutes as 'my code' in this context. But if you mean the code listing I posted above, effectively using
SplineHelp.GetMatrix
, and you want the cones to be differently oriented, you will have to swizzle the matrix to your liking. As explained in the Matrix Manual, a matrix is just a way to express a coordinate system over three axis and an offset. So when you have an orthogonal matrix, you can simply switch its v1(x), v2(y), and v3(z) axis in any manner you like to reorient the object along the principal planes. You can also invert axis to flip things, but then you must make sure that the handed-ness of the matrix remains correct, which simply means that you cannot only flip one axis but must always flip two axes at a time.Cheers,
FerdinandResult
Codeimport c4d def main() -> None: """ """ if not op: raise RuntimeError("No object selected.") splineHelp = c4d.utils.SplineHelp() splineHelp.InitSplineWithUpVector(op, c4d.Vector(0, 1, 0)) splineLength: float = splineHelp.GetSplineLength() offset: float = 100 count: int = 5 for i in range(count): cone = c4d.BaseObject(c4d.Ocone) cone[c4d.PRIM_CONE_BRAD] = 100 t = (i * offset) / splineLength mg = splineHelp.GetMatrix(t, realoffset=True) # Swizzle things into the form we need. temp = mg.v2 mg.v2 = mg.v3 mg.v3 = temp cone.SetMg(mg) doc.InsertObject(cone) c4d.EventAdd() if __name__ == '__main__': main()
-
And once again, thank you!
-