Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    Accessing vector field data of Volume Builder or Volume object

    Cinema 4D SDK
    2024 python
    3
    8
    2.2k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • justinleducJ
      justinleduc
      last edited by

      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.

      Visualization of volume vectors within Cinema 4D

      Thank you very much in advance for any insight or guidance. I really appreciate it!

      Justin

      ferdinandF 1 Reply Last reply Reply Quote 0
      • ferdinandF
        ferdinand @justinleduc
        last edited by ferdinand

        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,
        Ferdinand

        Result:
        532c32ec-f11a-497a-90b3-9557695309c8-image.png
        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]
        

        MAXON SDK Specialist
        developers.maxon.net

        1 Reply Last reply Reply Quote 3
        • justinleducJ
          justinleduc
          last edited by justinleduc

          @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.

          ferdinandF 1 Reply Last reply Reply Quote 0
          • ferdinandF
            ferdinand @justinleduc
            last edited by

            Hey @justinleduc,

            as the Python interpreter tries to tell you, you capitalized the field x of Vec3 which does not exist. So it is maxon.Vec3.x and not maxon.Vec3.X. As a little warning, the maxon 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 or c4d.Matrix.

            Cheers,
            Ferdinand

            MAXON SDK Specialist
            developers.maxon.net

            1 Reply Last reply Reply Quote 1
            • justinleducJ
              justinleduc
              last edited by justinleduc

              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!

              ferdinandF 1 Reply Last reply Reply Quote 0
              • ferdinandF
                ferdinand @justinleduc
                last edited by

                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 on 2023.2.2 macOS when I wrote it. But when I try now on 2024.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

                MAXON SDK Specialist
                developers.maxon.net

                1 Reply Last reply Reply Quote 1
                • M
                  m_adam
                  last edited by

                  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.

                  MAXON SDK Specialist

                  Development Blog, MAXON Registered Developer

                  1 Reply Last reply Reply Quote 1
                  • justinleducJ
                    justinleduc
                    last edited by justinleduc

                    This post is deleted!
                    1 Reply Last reply Reply Quote 0
                    • First post
                      Last post