Get MODATA_SIZE of Mograph Particles to Build Stacks of Geometry
-
Hi,
I tried to get the sizes of clones with
MoData.GetArray(c4d.MODATA_SIZE)
, but it returns None where the other IDs (e.g.c4d.MODATA_MATRIX
) works fine.Is this an issue or am I doing something wrong? Any advice would be appreciated.
-
Hello @kng_ito,
Thank you for reaching out to us.
MOGRAPH_SIZE
is only used by the MoGraph Matrix object. If you want the scale of particles, it is encoded in the matrices. The size of particles, i.e., the absolute size values of the geometry attached to a particle, is not accessible, as MoGraph is just a particle system which does not store that information. Find an example at the end of this post.PS: Please post code. While we appreciate screenshots, it is not fun to copy code from a screenshot.
Cheers,
FerdinandEdit: I forogot that there is also Matrix.GetScale, so that you do not have to access the components yourself. But it should not make too much of a difference, unless you have 100,000 or more MoGraph particles, then you should use the builtin method to optimize performance.
The result:
The code:""" """ import c4d import typing from c4d.modules import mograph op: typing.Optional[c4d.BaseObject] def main(): """ """ if op is None: return moData: mograph.MoData = mograph.GeGetMoData(op) if moData is None: return matrices: list[c4d.Matrix] = moData.GetArray(c4d.MODATA_MATRIX) # A matrix stores three transforms: offset, orientation, and scale (there is actually a fourth # transform, skew, stored in a matrix, but it is mostly irrelevant). The scale is encoded in the # length of the three frame components v1, v2, and v3. I would recommend having a look at the # matrix Manual in the Python documentation when things seem unclear. # # So, if one wants a list of nice scale vectors, one could grab them like this. scales: list[c4d.Vector] = [ c4d.Vector(m.v1.GetLength(), m.v2.GetLength(), m.v3.GetLength()) for m in matrices] print (f"{matrices = }") print (f"{scales = }") if __name__=='__main__': main()
-
Hi @ferdinand,
Thank you for your quick answer. That makes sence.
I am not sure I should continue in this thread or should open a new one, but I would like to ask about the way to access the actual size of clones.I am trying to create a system of stacking cubes whose scale animates, and I need to get the size of each clone to do so. Currently, the size of each clone is obtained by caching the cloner, but this causes processing delays and animation problems.
The preview:
https://drive.google.com/file/d/1G91Hx__2xZ_ZJhK3EJJEtF82sCc38Z9f/view?usp=sharingThe scene file:
StackingCubes_ProcessingDelayIssue.c4dThe Python Effector code:
import c4d def main(): moData = c4d.modules.mograph.GeGetMoData(op) marr = moData.GetArray(c4d.MODATA_MATRIX) cloner = moData.GetGenerator() cached = cloner.GetCache() cubes = cached.GetChildren() pos_y = 0 for i, cube in enumerate(cubes): height = cube.GetRad().y*2 * cube.GetMg().GetScale().y pos_y += height / 2 marr[i].off = c4d.Vector(0, pos_y, 0) pos_y += height / 2 moData.SetArray(c4d.MODATA_MATRIX, marr, False) return True
Is there any way to get the size of clones without processing delay?
Any advice or examples similar to this topic would be appreciated.
Thank you. -
Hey @kng_ito,
Thank you for your reply. I think it could go both ways, your follow-up question could be a new thread, or a remain here. Because it is Monday, and I am still a bit weekend-lazy, I opted for staying here and adjusting the thread title a bit.
About your code and what you want to do: There are already commercial plugin solutions which solve that stacking thingy. If you want more features, it can get a bit involved, but nothing too hard IMHO. I however opted here for a very naive solution in my answer, which does what your question demanded: stacking cubes generators along the y-axis with no orientation randomization allowed except for the stacking-axis (y).
Your code had the right idea, and you were almost there, but you did two things incorrectly:
- The children of a cloner do not correspond 1:1 to the particle geometry, and you must retrieve for each particle its indexed clone geometry so that you can do your offset computations.
- Unless I am a bit Monday-brain-dead, the math in your loop does not quite pan out for me too. You want to offset each particle by half its height plus the offset, and then increment the offset by the full particle height (at least when the bounding box origin is equal to geometry object origin assumption holds true).
As hinted at, there are also other problems with this, most notably that your code always assumes the origin of the particle geometry to lie on the origin of its bounding box (which is true for most generators, but usually not true for hand-made geometry), and the inability of this effector to compute the height of an arbitrarily rotated geometry. I did not tackle both problems in my answer, as they would have bloated the example and are also technically out of scope of support, at least the last problem. If you want to implement that and struggle with it, I would have to ask you to open new threads for these questions.
Cheers,
FerdinandThe file: stacking_cubes.c4d
The result:
The code:
"""Simple example for an effector that 'stacks' clones by their particle geometry size. This can get quite complicated when supposed to be done with arbitrary particle transforms where particles are rotated in odd angles. This is just a naïve variant stacking along the y-axis, assuming all particles to have an orientation of (0, y, 0), i.e., only allowing for rotations around the stacking axis. Your code had basically the correct idea, multiply the particle scale by the actual geometry size, but had some problems in the details. """ import c4d import math import typing from c4d.modules import mograph op: c4d.BaseObject # The Python effector object. # The geometry associated with a Mograph particle is stored as a floating point value, to retrieve # the index of the cloner child associated with that floating point number, this formula must be # used. CloneOffset2Index: typing.Callable[[float, int], int] = lambda offset, count: ( int(math.floor(c4d.utils.ClampValue(offset * count, 0, count - 1))) ) def main() -> None: """Implements the 'stacking' effector. """ # Get Mograph data, the particle matrices, and the particle clone offsets. moData: mograph.MoData = mograph.GeGetMoData(op) cloneMatrices: list[c4d.Matrix] = moData.GetArray(c4d.MODATA_MATRIX) cloneOffsets: list[float] = moData.GetArray(c4d.MODATA_CLONE) if len(cloneMatrices) != len(cloneOffsets): raise IndexError("Clone matrix and index arrays are not of equal length.") # Get the cloner and its children, the to be cloned geometry. cloner: c4d.BaseObject = moData.GetGenerator() cloneGeometries: list[c4d.BaseObject] = cloner.GetChildren() cloneGeometriesCount: int = len(cloneGeometries) # Convert cloneOffsets to integer values. cloneIndices[n] will now contain the child index # for cloneMatrices[n], i.e., cloneGeometries[cloneIndices[n]] is the geometry for the particle # n with the matrix cloneMatrices[n]. cloneIndices: list[int] = [ CloneOffset2Index(offset, cloneGeometriesCount) for offset in cloneOffsets] # Apart from not properly dealing with the clone geometry, your code had also some math problems # in the loop. # The offset vector for the clones, so that we can collect the 'stacking' information. offset: c4d.Vector = c4d.Vector() # Iterate over all clones as clone matrix, clone geometry index pairs. for cloneMatrix, cloneIndex in zip(cloneMatrices, cloneIndices): # Get the y-scale of the particle and half the size of the geometry bounding box along # the y-axis and construct a vector with it, representing half the height of the actual # particle geometry. particleScaleY: float = cloneMatrix.GetScale().y geometryRadiusY: float = cloneGeometries[cloneIndex].GetRad().y localOffset: c4d.Vector = c4d.Vector(0, particleScaleY * geometryRadiusY, 0) # Offset the clone by half its height on top of the current offset, so that it sits on top # of whatever was below it. And after that, increment the offset by twice that value, as we # want to respect the full particle in the offset. cloneMatrix.off = offset + localOffset offset += localOffset * 2 # What we did here in the last step is a very lack-lustre implementation, as it assumes the # origin of a particle geometry to always be located on the arithmetic mean of its bounding # box minima/maxima, i.e, that it always sits "in the middle". This assumption holds true # for things like primitive generators, but will quickly fail on "real world" geometry which # can place its origin almost anywhere. To make this effector more robust, you will have to # evaluate the delta between the origin of the geometry in world coordinates and the bounding # box origin (BaseObject.GetMp()) in world coordinates and respect that in your calculations. # I left this here out because I wanted the example to be A. simple, and because B. we cannot # provide full solutions. # What has also not been respected in this script, is the orientation of particles, you could # for example want to stack cubes which are "balancing" on one of their edges, this here only # works for staccking geometry on the top and bottom faces of their bounding boxes. moData.SetArray(c4d.MODATA_MATRIX, cloneMatrices, False) return True
-
Hi @ferdinand,
I expected there to be a way to know which particle referred to which original geometry, but I didn't know how, so thanks. I should have looked more closely at the documentation. Moreover, I didn't think it could handle blended cloners.
Anyway, thank you very much. Problem solved.
-
Hey,
I've been trying to unpick this project to achieve the result I need, but not getting anywhere.
I'm trying to get the x and y size of individual clones, specifically text objects, so I can create a rectangle spline that moves form letter to letter (or word to word) and draws a box around each letter or word. Driven by an index number slider in xpresso.
Any ideas? -
Managed to get it working with the help of ChatGPT!
GetCloneSize.c4d -
Celebration was a bit premature. Doesn't work with text (MoText) - any ideas? Chat GTP seems flummoxed by this...
import c4d import math import typing from c4d.modules import mograph def get_clone_info(op, index: int) -> typing.Tuple[c4d.Vector, c4d.Vector]: """Gets the world coordinates and bounding box size of the specified clone index.""" moData: mograph.MoData = mograph.GeGetMoData(op) if moData is None: raise ValueError("MoData is None. Ensure this is a MoGraph object.") cloneMatrices: typing.List[c4d.Matrix] = moData.GetArray(c4d.MODATA_MATRIX) cloneOffsets: typing.List[float] = moData.GetArray(c4d.MODATA_CLONE) if len(cloneMatrices) != len(cloneOffsets): raise IndexError("Clone matrix and index arrays are not of equal length.") # Ensure the index is within the valid range if index < 0 or index >= len(cloneMatrices): raise IndexError(f"Index {index} is out of range for the number of clones {len(cloneMatrices)}.") cloneMatrix: c4d.Matrix = cloneMatrices[index] worldPosition: c4d.Vector = cloneMatrix.off # Get the bounding box size of the clone geometry boundingBoxSize: c4d.Vector = c4d.Vector(0, 0, 0) if op.GetTypeName() == "Text": # For Text objects, calculate the bounding box size of each letter textObject = op textObject.Update() bbox = textObject.GetBBox() if bbox: min_point = bbox["min"] max_point = bbox["max"] min_point = min_point * cloneMatrix max_point = max_point * cloneMatrix boundingBoxSize = max_point - min_point print(f"Index: {index}, World Position: {worldPosition}, Bounding Box Size: {boundingBoxSize}") return worldPosition, boundingBoxSize def main(): # Get the index from the user data slider index = op[c4d.ID_USERDATA, 1] # Adjust the ID to match your user data field try: worldPosition, boundingBoxSize = get_clone_info(op, index) # Store the results in user data fields or custom data fields op[c4d.ID_USERDATA, 2] = worldPosition # Adjust the ID to match your user data field for world position op[c4d.ID_USERDATA, 3] = boundingBoxSize # Adjust the ID to match your user data field for bounding box size except Exception as e: print(f"Error: {e}") op[c4d.ID_USERDATA, 2] = c4d.Vector(0, 0, 0) op[c4d.ID_USERDATA, 3] = c4d.Vector(0, 0, 0)
-
Hi @PMenich ,
We insistently ask you to create a separate thread with a dedicated title name, code example and specific question or issue description, if your topic diverges from the original one, especially if the original one is two years old. You're welcome to refer this thread, e.g. by posting a link.
Please also pay extra attention to the following parts of our Support Procedures:
We cannot provide support for the side-effects of unexperienced users using AI models to write code.
We provide code examples but we do not provide full solutions or design applications.
We cannot debug your code for you and instead provide answers to specific problems.
Additionally we kindly ask you to consolidate your sequential messages in a single posting for better readability and human perception.
Cheers,
Ilia@edit @ferdinand: Removed new user note as @PMenich is not a new user.
-
wow. smackdown.
Well, in the spirit of sharing, I managed to figure it out (with the help of AI) which I will attach here for any future users who might like to take a look.Pete
(C4D user since V6)
-
Hey @PMenich,
Ilia did not intend to "smack down" you here. We must establish a scope of support as this is general practice in all information systems. When you go into your local library and ask them if they can give you a list of the latest Netflix shows, they will also turn you away. This must be done as one is otherwise overrun with information and support requests. We also all have our normal development duties and on top have to run the documentation and its infrastructure. So, forum support can only take up a tiny fraction of our time.
Ilia was a bit formal here, but he politely and precisely pointed out what you did wrong. You should start a new thread when you have questions and not hijack old threads. We also cannot provide support for "this does not work", especially in the context of generative AI. It is all line out in our support proecdues. Ilia was probably just a bit in a hurry here as we are internally at a threshold in our development cycle, so things might sound a bit formal.
When there are still open question, please open a new thread, provide a meaningful problem description and question (and generally follow our support procedures). I generally would say we are quite forthcoming with our development support and Ilia's precise answers can be hardly called a 'smack-down'.
Cheers,
Ferdinand -
fairs. I thought this was a normal forum for sharing ideas and solving problems/challenges.
Didn't really think about it being a support thing.
My bad -
No worries, everything is fine, that is what we are here for, to clear up things. When you have questions I encourage you to open a new thread so that we can see what we can do. And this is also an open forum, so you can ask questions to the community if you want to. But unless pointed out specifically otherwise, we assume things to be support requests.
Cheers,
Ferdinand