Accessing vector field data of Volume Builder or Volume object
-
Hey guys!
Digging the new forum!
To get right to it: I am looking to have a Python script that can read the vector values of a Volume Builder (or Volume object).
By "vector values", I am specifically referring to the "lines" in the screenshot I've attached to this message (see below), which are a representation of the spatial distribution of the position and directional values of the vectors of a Volume. So, in short, the position and directional values of each vector within the vector field are the values I care most about.
My question this: is this possible? I've looked at all of the Volume-related Python examples on the Github and scoured both the Python and C++ docs to find a method that would allow me to do so, but unfortunately, after a myriad of testing, I came out empty-handed.
Thank you very much in advance for any insight or guidance. I really appreciate it!
Justin
-
Hey @justinleduc,
Thank you for reaching out to us. Your question is a bit contradictory, as you seem to mix up the terms volume and field as if they were interchangeable which then leads to other ambiguities.
- Volume: This is just an alias for voxel grid. This structure is by definition discrete, i.e., you can enumerate all values in that structure.
- Field: A field is just an alias for a function
f(x, y, z)
and it is therefore by definition continous/smooth. One cannot enumerate all values in it just as one cannot enumerate all rational numbers between 0 and 1.
So, when you talk about fields and show us a screenshot which contains a field object, and then ask for enumerating "all the lines" in that screenshot that is a bit ambiguous. Because if these vectors were generated by a field, all you could do is emulate that UI representation of that field, but a field does not have a finite set of values one could iterate over.
But I think field is just communicative noise here, and what you want to do, is sample a volume object which has been set to volume type vector. You can do that in Python but unlike in C++, we do not have
GridIteratorInterface
, so we must do some legwork ourselves.Please share your code and scene examples in the future to make it easier for us to understand what you are doing and what you want to do. Find my answer below.
Cheers,
FerdinandResult:
Code:"""Demonstrates how to iterate over all cells of a volume in Python. Must be run as Script Manager script with a Volume Builder object selected which has been set to Volume Type: Vector. It will then print the vector values of all voxels. Since we do not have the type GridIteratorInterface in Python, we must do some computations ourself here. We also lack fancier feature like voxel level folding in Python offered by GridIteratorInterface in C++. """ import c4d import maxon import itertools op: c4d.BaseObject # The active object. def GetCenteredIndices(count: int) -> tuple[int, int]: """Yields indices for #count voxel cells centered on their index origin. So, 2 will yield [-1, 1], 3 will yield [-1, 0, 1], 4 will yield [-2, -1, 1, 2], 5 will yield [-2, -1, 0, 1, 2], and so on. """ if count % 2 == 0: half: int = int(count / 2) return [n for n in range(-half, 0)] + [n for n in range(1, half + 1)] else: half: int = int((count - 1) / 2) return [n for n in range(-half, 0)] + [0] + [n for n in range(1, half + 1)] def main(): """ """ # Validate the input. if not op or not op.IsInstanceOf(c4d.Ovolumebuilder): raise TypeError("op is not a c4d.Ovolumebuilder.") cache: c4d.modules.volume.VolumeObject | None = op.GetCache() hasVolumeCache: bool = cache and cache.IsInstanceOf(c4d.Ovolume) if not hasVolumeCache: raise RuntimeError("Object has no first level volume cache.") # Get the volume and ensure it has the grid type we/you expect it to have. volume: maxon.VolumeRef = cache.GetVolume() if volume.GetGridType() != c4d.GRIDTYPE_VECTOR32: raise RuntimeError("Object is not of matching vector32 grid type.") # Initialize a grid accessor so that we can access the volume data. In C++ we have # GridIteratorInterface which makes it much easier to iterate over all active cells # in a volume. Here we must get a bit creative ourselves. access: maxon.GridAccessorRef = maxon.GridAccessorInterface.Create( maxon.Vector32) access.Init(volume) # Get the transform of the volume. A volume, a voxel grid, is always world transform aligned. # That means it cannot be rotated, but it can be scaled (expressing the cell size) and have an # offset. In Python we also cannot use VolumeInterface.GetWorldBoundingBox() because while # the function has been wrapped, its return type has not (genius move I know :D). We use # GetActiveVoxelDim() instead, which gives us the shape of the volume, e.g., (5*2*3) or # (12*12*12) which in conjunction with the cell size then tells us the bounding box. transform: maxon.Matrix = volume.GetGridTransform() cellSize: maxon.Vector = transform.sqmat.GetLength() halfCellSize: c4d.Vector = c4d.Vector(cellSize.x * .5, cellSize.y * .5, cellSize.z * .5) shape: maxon.IntVector32 = volume.GetActiveVoxelDim() print(f"{transform = }\n{cellSize = }\n{shape = }\n") # Iterate over all cell indices in the volume. itertools.product is just a fancy way of doing # nested loops. for x, y, z in itertools.product(GetCenteredIndices(shape.x), GetCenteredIndices(shape.y), GetCenteredIndices(shape.z)): # Get the point in world coordinates for the cell index (x, y, z) and then its value. # In C++ we could just multiply our index by the volume #transform, here we must do some # legwork ourselves because most of the arithmetic types of the maxon API have not been # wrapped in Python. # #p is the local lower boundary point coordinate of the cell index (x, y, z) and #q that # point in global space which also has been shifted towards the cell origin. p: c4d.Vector = c4d.Vector(cellSize.x * x, cellSize.y * y, cellSize.z * z) q: c4d.Vector = c4d.Vector(cache.GetMg().off) + p + halfCellSize # Volumes are usually hollow, so most cells will hold the null value; we just step over them. value: maxon.Vector32 = access.GetValue(q) if value.IsNullValue(): continue print(f"{q.x:>6}, {q.y:>6}, {q.z:>6} = {value}") if __name__ == '__main__': main()
Here is also a section from some C++ code examples which I never got to ship which might be helpful here:
/// @brief Accesses the important metadata associated with a volume as for example its name, /// data type, transform, or bounding box. /// @param[in] volume The volume to access the metadata for. static maxon::Result<void> GetVolumeMetadata(const maxon::Volume& volume) { iferr_scope; // Get the purpose denoting label of the Pyro grid, e.g., "density", "color", or "velocity". const maxon::String gridName = volume.GetGridName(); // Get the storage convention of the grid, e.g., FOG or SDF, and the data type which is being // stored in the grid. Currently, the only grid types that are being used are ::FLOAT and // ::VECTOR32 regardless of the precision mode the simulation scene is in. const GRIDCLASS gridClass = volume.GetGridClass(); const GRIDTYPE gridDataType = volume.GetGridType(); // Get the matrix transforming from grid index space to world space. Due to volume grids always // being aligned with the standard basis orientation, the world axis orientation, this transform // will always express the standard orientation. The scale vector of the square matrix of this // transform expresses the size of a voxel in world space. const maxon::Matrix gridTransform = volume.GetGridTransform(); // Get the shape of the whole grid, e.g., (5, 5, 5) for a grid with five cells on each axis. // Other than #GetWorldBoundingBox() and despite its name, this method will consider inactive // voxels in its return value. It will always return the shape of the whole grid. const maxon::IntVector32 gridShape = volume.GetActiveVoxelDim(); // Get the index range of the active cells for the grid. E.g., a grid with the shape (5, 5, 5), // the origin (0, 0, 0), and all cells active would return [(-2, -2, -2), (2, 2, 2)]. But the // grid could also return [(-1, -1, -1), (1, 1, 1)] when no cells outside that range would be // active. const maxon::Range<Vector> indexRange = volume.GetWorldBoundingBox(); // Get the cell size of a voxel in world space. const Vector cellSize = gridTransform.sqmat.GetScale(); // Get the world space minimum and maximum of the active volume. const maxon::Range<Vector> boundingBox (gridTransform * indexRange.GetMin(), gridTransform * indexRange.GetMax()); // Compute the size of the total and the active cells volume of the grid. const maxon::Vector totalVolumeSize (cellSize * Vector(gridShape)); const maxon::Vector activeVolumeSize (boundingBox.GetDimension()); // Log the metadata to the console of Cinema 4D. ApplicationOutput( "name: @, gridClass: @, gridDataType: @\n" "shape: @, indexRange: @\n" "gridTransform: @\n" "boundingBox: @, cellSize: @\n" "totalVolumeSize: @, activeVolumeSize: @", gridName, gridClass, gridDataType, gridShape, indexRange, gridTransform, boundingBox, cellSize, totalVolumeSize, activeVolumeSize); return maxon::OK; } //! [GetVolumeMetadata] //! [GetVolumeData] /// @brief /// @tparam T /// @param volume /// @param result /// @return template<typename T> maxon::Result<void> GetVolumeData(const maxon::Volume& volume, maxon::BaseArray<T>& result) { iferr_scope; using GridIteratorType = maxon::GridIteratorRef<T, maxon::ITERATORTYPE::ON>; GridIteratorType iterator = GridIteratorType::Create() iferr_return; iterator.Init(volume) iferr_return; const maxon::Matrix gridTransform = volume.GetGridTransform(); maxon::String msg; // Iterate over the first 100 cells yielded by the iterator and add each 10th value to the result. int i = 0; int j = 0; for (; iterator.IsNotAtEnd() && ++i < 100 && ++j; iterator.StepNext()) { // Get the voxel index of the current cell and compute its world space position. const maxon::IntVector32 index = iterator.GetCoords(); const Vector worldPosition = gridTransform * maxon::Vector64(index); // Get the depth in the voxel tree of the current cell; but Pyro volumes do not use any tiling. // #level will therefore always be of value #::LEAF and every active cell in the volume will be // visited by the iterator. const maxon::TREEVOXELLEVEL level = iterator.GetVoxelLevel(); // Get the value stored in the current cell, const T value = iterator.GetValue(); if (j % 10 == 0) { msg += FormatString("[@] @ (@): @\n", level, index, worldPosition, value); result.Append(value) iferr_return; } } // Log the collected data to the console of Cinema 4D. ApplicationOutput(msg); return maxon::OK; } //! [GetVolumeData]
-
@ferdinand , I simply cannot thank you enough for providing such phenomenal support to my post. I have been pre-occupied full-time by this problem since I posted it 4 days ago, learning lots about Principal Direction Curvature and, as you pointed out, discrete mathematics. I am still a complete novice in these sectors, so needless to say, while I've been fortunate enough to make minuscule progress, I've been struggling quite a lot.
I want to thank you very kindly for correcting me on the differences between a Field and Volume. I always value the terminology I employ when asking for support, so not only am I thankful for your correction, but I'm also thankful that you still understood what I was trying to convey. Regardless; duly noted!
Above all though, I'm terribly grateful for the comments in the code you provided me with. I simply couldn't have asked for better support. Last week, I discovered the existence of C4D's Python SDK, and now I'm already digging deep in the nitty gritty (or at least it feels like it).
All that said, I feel almost guilty to report that the Python code you have provided me with throws an error in the console window. I've followed exactly what you have done (i.e. script executed from the Script Manager while the Volume Builder, which contains a Cube, is set to "Vector" and is selected), and this is the error I am getting:
Traceback (most recent call last): File "C:\Users\{removed}\AppData\Roaming\MAXON\Maxon Cinema 4D 2024_A5DBFF93\library\scripts\read-vector-data_004.py", line 89, in <module> main() File "C:\Users\{removed}\AppData\Roaming\MAXON\Maxon Cinema 4D 2024_A5DBFF93\library\scripts\read-vector-data_004.py", line 63, in main print(f"{transform = }\n{cellSize = }\n{shape = }\n") ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Program Files\Maxon Cinema 4D 2024\resource\modules\python\libs\python311\maxon\data.py", line 287, in __repr__ return f'<maxon.{self.__class__.__name__} object at {hex(id(self))} with the data: {self}>' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Program Files\Maxon Cinema 4D 2024\resource\modules\python\libs\python311\maxon\vec.py", line 46, in __str__ return f"X:{self.X}, Y:{self.y}, Z:{self.z}" ^^^^^^ AttributeError: 'Vector64' object has no attribute 'X'. Did you mean: 'x'?
For context, I am using Cinema 4D 2024.1.0 on Windows 11. Is there something I am doing wrong? I've tried Googling the error and looking at the SDK documentation, but I couldn't exactly pinpoint the cause.
Again, thanks a billion times for your extensive reply. Once I'm able to get this script to work, I'll get a lot of mileage out of it.
Cheers.
-
Hey @justinleduc,
as the Python interpreter tries to tell you, you capitalized the field
x
ofVec3
which does not exist. So it ismaxon.Vec3.x
and notmaxon.Vec3.X
. As a little warning, themaxon
API arithmetic types (Float
,Int
,Vec2
,Vec3
,Vector
,Mat3
,SqrMat3
,Matrix
, ...) are all only wrapped shallowly in Python (see here for a list of them).So, you can print them and do simple stuff like adding or multiplying them but doing any sophisticated math with them is often not possible as vectors lack then matrix multiplication support or even simple things like the dot product. You can for example have a look at maxon.Vec3 to get a sense. In Python it is often better to convert such values to classic API types such as
c4d.Vector
orc4d.Matrix
.Cheers,
Ferdinand -
Hey @ferdinand,
Again, thank you for your support. I very much appreciate it.
The source of my confusion stems from the fact that this error, which is coming from the file (as stated in the Python interpreter) "C:\Program Files\Maxon Cinema 4D 2024\resource\modules\python\libs\python311\maxon\vec.py", was obviously not written by me. So, if I understand you correctly, I should change the writing permissions of the "vec.py" file (as it currently doesn't allow for any modifications), change "self.X" to "self.x" and then save said file? (edit: it appears that this has resolved the issue. thank you!)
Lastly, unrelated to this error, and I'm sorry if this has already been clarified in your messages above and I was too much of a novice to understand it, but I was wondering if the code you provided me with is able to read the direction value of each cell? For example, if I was to apply a Field object to my vector-type Volume Builder to alter the direction of each cell (such as illustrated in the screenshot of my first message, where I applied a Group Field with a Solid Layer that sets a direction bias of 1 on the Z-axis), could I read the resulting direction from that same Python script?
Thank you very much once again for your time and assistance!
-
Hey @justinleduc,
The source of my confusion stems from the fact that this error, which is coming from the file ...
Good point I only read the error message very hastily, overlooking that error is raised in
vec.py
, I thought that was your code. But yes, you could fix it like you said. I ran my script on2023.2.2
macOS when I wrote it. But when I try now on2024.1
and our internal beta (both Win) I can reproduce your problem. This seems to be a regression.You can fix this yourself if you want to - or just remove the print statement in my code which triggers the problem (but then you might also run into problems with the later print statements).
@m_adam are you aware of that?
Cheers,
Ferdinand -
Hi correct I made a typo in the string representation of a Vec3, this is going to be fixed in an upcoming release.
Cheers,
Maxime. -
This post is deleted!