Navigation

    • Register
    • Login
    • Search
    1. Home
    2. ferdinand
    ferdinand

    ferdinand

    @ferdinand

    534
    Reputation
    1911
    Posts
    615
    Profile views
    7
    Followers
    0
    Following
    Joined Last Online

    • Profile
    • More
      • Following
      • Followers
      • Topics
      • Posts
      • Best
      • Groups
    ferdinand Follow

    Best posts made by ferdinand

    RE: Welcome Mr. Hoppe

    Hi,

    thanks for the kind words both from Maxon and the community. I am looking forward to my upcoming adventures with the SDK Team and Cinema community.

    Cheers,
    Ferdinand

    posted in Maxon Announcements •
    RE: Modified Pop Up Menu

    Hi,

    as @Cairyn said the problem is unreachable code. I also just saw now that you did assign the same ID to all your buttons in your CreateLayout(). Ressource and dialog element IDs should be unique. I would generally recommend to define your dialogs using a resource, but here is an example on how to do it in code.

    BUTTON_BASE_ID = 1000
    BUTTON_NAMES = ["Button1", "Button2", "Button3", "Button4", "Button5"]
    BUTTON_DATA = {BUTTON_BASE_ID + i: name for i, name in enumerate(BUTTON_NAMES)}
    
    class MyDialog(gui.GeDialog):
    
        def CreateLayout(self):
            """
            """
            self.GroupBegin(id=1013, flags=c4d.BFH_SCALEFIT, cols=5, rows=4)
            for element_id, element_name in BUTTON_DATA.items():
                self.AddButton(element_id, c4d.BFV_MASK, initw=100, 
                               name=element_name)
            self.GroupEnd()
            return True
    
        def Command(self, id, msg):
            """
            """
            if id == BUTTON_BASE_ID:
                print "First button has been clicked"
            elif id == BUTTON_BASE_ID + 1:
                print "Second button has been clicked"
            # ...
            if id in BUTTON_DATA.keys(): # or just if id in BUTTON_DATA
                self.Close()
            return True
    
    posted in Cinema 4D SDK •
    RE: Reading proper decimal values on lower numbers?

    Hi,

    that your script is not working has not anything to do with pseudo decimals, but the fact that you are treating numbers as strings (which is generally a bad idea) in a not very careful manner. When you truncate the string representation of a number which is represented in scientific notation (with an exponent), then you also truncate that exponent and therefor change the value of the number.

    To truncate a float you can either take the floor of my_float * 10 ** digits and then divide by 10 ** digits again or use the keyword round.

    data = [0.03659665587738824,
            0.00018878623163019122,
            1.1076812650509394e-03,
            1.3882258325566638e-06]
    
    for n in data:
        rounded = round(n, 4)
        floored = int(n * 10000) / 10000
        print(n, rounded, floored)
    
    0.03659665587738824 0.0366 0.0365
    0.00018878623163019122 0.0002 0.0001
    0.0011076812650509394 0.0011 0.0011
    1.3882258325566637e-06 0.0 0.0
    [Finished in 0.1s]
    

    Cheers
    zipit

    posted in General Talk •
    RE: GetAllTextures from materials only

    Hi,

    sorry for all the confusion. You have to pass actual instances of objects. The following code does what you want (and this time I actually tried it myself ;)).

    import c4d
    
    def main():
        """
        """
        bc = doc.GetAllTextures(ar=doc.GetMaterials())
        for cid, value in bc:
            print cid, value
    
    if __name__=='__main__':
       main()
    

    Cheers,
    zipit

    posted in Cinema 4D SDK •
    RE: Object materials won't show up in final render

    Hi,

    you use GetActiveDocument() in a NodeData environment. You cannot do this, since nodes are also executed when their document is not the active document (while rendering for example - documents get cloned for rendering).

    Cheers
    zipit

    posted in Cinema 4D SDK •
    RE: More Examples for GeUserArea?

    Hi,

    you have to invoke AddUserArea and then attach an instance of your implemented type to it. Something like this:

    my_user_area = MyUserAreaType()
    self.AddUserArea(1000,*other_arguments)
    self.AttachUserArea(my_user_area, 1000)
    

    I have attached an example which does some things you are trying to do (rows of things, highlighting stuff, etc.). The gadget is meant to display a list of boolean values and the code is over five years old. I had a rather funny idea of what good Python should look like then and my attempts of documentation were also rather questionable. I just wrapped the gadget into a quick example dialog you could run as a script. I did not maintain the code, so there might be newer and better ways to do things now.

    Also a warning: GUI stuff is usually a lot of work and very little reward IMHO.

    Cheers
    zipit

    import c4d
    import math
    import random
    
    from c4d import gui
    
    # Pattern Gadget
    IDC_SELECTLOOP_CELLSIZE = [32, 32]
    IDC_SELECTLOOP_GADGET_MINW = 400
    IDC_SELECTLOOP_GADGET_MINH = 32
    
    class ExampleDialog(gui.GeDialog):
        """
        """
        def CreateLayout(self):
            """
            """
    
            self.Pattern = c4d.BaseContainer()
            for i in range(10):
                self.Pattern[i] = random.choice([True, False])
            self.PatternSize = len(self.Pattern)
    
            self.gadget = Patterngadget(host=self)
            self.AddUserArea(1000, c4d.BFH_FIT, 400, 32)
            self.AttachUserArea(self.gadget, 1000)
            return True
    
    class Patterngadget(gui.GeUserArea):
        """
        A gui gadget to modify and display boolean patterns.
        """
    
        def __init__(self, host):
            """
            :param host: The hosting BaseToolData instance
            """
            self.Host = host
            self.BorderWidth = None
            self.CellPerColumn = None
            self.CellWidht = IDC_SELECTLOOP_CELLSIZE[0]
            self.CellHeight = IDC_SELECTLOOP_CELLSIZE[1]
            self.Columns = None
            self.Height = None
            self.Width = None
            self.MinHeight = IDC_SELECTLOOP_GADGET_MINH
            self.MinWidht = IDC_SELECTLOOP_GADGET_MINW
            self.MouseX = None
            self.MouseY = None
    
        """------------------------------------------------------------------------
            Overridden methods
            --------------------------------------------------------------------"""
    
        def Init(self):
            """
            Init the gadget.
            :return : Bool
            """
            self._get_colors()
            return True
    
        def GetMinSize(self):
            """
            Resize the gadget
            :return : int, int
            """
            return int(self.MinWidht), int(self.MinHeight)
    
        def Sized(self, w, h):
            """
            Get the gadgets height and width
            """
            self.Height, self.Width = int(h), int(w)
            self._fit_gadget()
    
        def Message(self, msg, result):
            """
            Fetch and store mouse over events
            :return : bool
            """
            if msg.GetId() == c4d.BFM_GETCURSORINFO:
                base = self.Local2Screen()
                if base:
                    self.MouseX = msg.GetLong(c4d.BFM_DRAG_SCREENX) - base['x']
                    self.MouseY = msg.GetLong(c4d.BFM_DRAG_SCREENY) - base['y']
                    self.Redraw()
                    self.SetTimer(1000)
            return gui.GeUserArea.Message(self, msg, result)
    
        def InputEvent(self, msg):
            """
            Fetch and store mouse clicks
            :return : bool
            """
            if not isinstance(msg, c4d.BaseContainer):
                return True
            if msg.GetLong(c4d.BFM_INPUT_DEVICE) == c4d.BFM_INPUT_MOUSE:
                if msg.GetLong(c4d.BFM_INPUT_CHANNEL) == c4d.BFM_INPUT_MOUSELEFT:
                    base = self.Local2Global()
                    if base:
                        x = msg.GetLong(c4d.BFM_INPUT_X) - base['x']
                        y = msg.GetLong(c4d.BFM_INPUT_Y) - base['y']
                        pid = self._get_id(x, y)
                        if pid <= self.Host.PatternSize:
                            self.Host.Pattern[pid] = not self.Host.Pattern[pid]
                            self.Redraw()
            return True
    
        def Timer(self, msg):
            """
            Timer loop to catch OnMouseExit
            """
            base = self.Local2Global()
            bc = c4d.BaseContainer()
            res = gui.GetInputState(c4d.BFM_INPUT_MOUSE,
                                    c4d.BFM_INPUT_MOUSELEFT, bc)
            mx = bc.GetLong(c4d.BFM_INPUT_X) - base['x']
            my = bc.GetLong(c4d.BFM_INPUT_Y) - base['y']
            if res:
                if not (mx >= 0 and mx <= self.Width and
                        my >= 0 and my <= self.Height):
                    self.SetTimer(0)
                    self.Redraw()
    
        def DrawMsg(self, x1, y1, x2, y2, msg):
            """
            Draws the gadget
            """
            # double buffering
            self.OffScreenOn(x1, y1, x2, y2)
            # background & border
            self.DrawSetPen(self.ColBackground)
            self.DrawRectangle(x1, y1, x2, y2)
            if self.BorderWidth:
                self.DrawBorder(c4d.BORDER_THIN_IN, x1, y1,
                                self.BorderWidth + 2, y2 - 1)
            # draw pattern
            for pid, state in self.Host.Pattern:
                x, y = self._get_rect(pid)
                self._draw_cell(x, y, state, self._is_focus(x, y))
    
        """------------------------------------------------------------------------
            Public methods
            --------------------------------------------------------------------"""
    
        def Update(self, cid=None):
            """
            Update the gadget.
            :param cid: A pattern id to toggle.
            """
            if cid and cid < self.Host.PatternSize:
                self.Host.Pattern[cid] = not self.Host.Pattern[cid]
            self._fit_gadget()
            self.Redraw()
    
        """------------------------------------------------------------------------
            Private methods
            --------------------------------------------------------------------"""
    
        def _get_colors(self, force=False):
            """
            Set the drawing colors.
            :return : Bool
            """
            self.ColScale = 1.0 / 255.0
            if self.IsEnabled() or force:
                self.ColBackground = self._get_color_vector(c4d.COLOR_BG)
                self.ColCellActive = c4d.GetViewColor(
                    c4d.VIEWCOLOR_ACTIVEPOINT) * 0.9
                self.ColCellFocus = self._get_color_vector(c4d.COLOR_BGFOCUS)
                self.ColCellInactive = self._get_color_vector(c4d.COLOR_BGEDIT)
                self.ColEdgeDark = self._get_color_vector(c4d.COLOR_EDGEDK)
                self.ColEdgeLight = self._get_color_vector(c4d.COLOR_EDGELT)
            else:
                self.ColBackground = self._get_color_vector(c4d.COLOR_BG)
                self.ColCellActive = self._get_color_vector(c4d.COLOR_BG)
                self.ColCellFocus = self._get_color_vector(c4d.COLOR_BG)
                self.ColCellInactive = self._get_color_vector(c4d.COLOR_BG)
                self.ColEdgeDark = self._get_color_vector(c4d.COLOR_EDGEDK)
                self.ColEdgeLight = self._get_color_vector(c4d.COLOR_EDGELT)
            return True
    
        def _get_cell_pen(self, state, _is_focus):
            """
            Get the color for cell depending on its state.
            :param state   : The state
            :param _is_focus : If the cell is hoovered.
            :return        : c4d.Vector()
            """
            if state:
                pen = self.ColCellActive
            else:
                pen = self.ColCellInactive
            if self.IsEnabled() and _is_focus:
                return (pen + c4d.Vector(2)) * 1/3
            else:
                return pen
    
        def _draw_cell(self, x, y, state, _is_focus):
            """
            Draws a gadget cell.
            :param x:       local x
            :param y:       local y
            :param state:   On/Off
            :param _is_focus: MouseOver state
            """
            # left and top bright border
            self.DrawSetPen(self.ColEdgeLight)
            self.DrawLine(x, y, x + self.CellWidht, y)
            self.DrawLine(x, y, x, y + self.CellHeight)
            # bottom and right dark border
            self.DrawSetPen(self.ColEdgeDark)
            self.DrawLine(x, y + self.CellHeight - 1, x +
                          self.CellWidht - 1, y + self.CellHeight - 1)
            self.DrawLine(x + self.CellWidht - 1, y, x +
                          self.CellWidht - 1, y + self.CellHeight - 1)
            # cell content
            self.DrawSetPen(self._get_cell_pen(state, _is_focus))
            self.DrawRectangle(x + 1, y + 1, x + self.CellWidht -
                               2, y + self.CellHeight - 2)
    
        def _get_rect(self, pid, offset=1):
            """
            Get the drawing rect for an array id.
            :param pid    : the pattern id
            :param offset : the pixel border offset
            :return       : int, int
            """
            pid = int(pid)
            col = pid / self.CellPerColumn
            head = pid % self.CellPerColumn
            return self.CellWidht * head + offset, self.CellHeight * col + offset
    
        def _get_id(self, x, y):
            """
            Get the array id for a coord within the gadget.
            :param x : local x
            :param y : local y
            :return  : int
            """
            col = (y - 1) / self.CellHeight
            head = (x - 1) / self.CellWidht
            return col * self.CellPerColumn + head
    
        def _is_focus(self, x, y):
            """
            Test if the cell coords are under the cursor.
            :param x : local x
            :param y : local y
            :return  : bool
            """
            if (self.MouseX >= x and self.MouseX <= x + self.CellWidht and
                    self.MouseY >= y and self.MouseY <= y + self.CellHeight):
                self.MouseX = c4d.NOTOK
                self.MouseY = c4d.NOTOK
                return True
            else:
                return False
    
        def _fit_gadget(self):
            """
            Fit the gadget size to the the array
            """
            oldHeight = self.MinHeight
            self.CellPerColumn = int((self.Width - 2) / self.CellWidht)
            self.Columns = math.ceil(
                self.Host.PatternSize / self.CellPerColumn) + 1
            self.MinHeight = int(IDC_SELECTLOOP_GADGET_MINH * self.Columns) + 3
            self.MinWidht = int(IDC_SELECTLOOP_GADGET_MINW)
            self.BorderWidth = self.CellWidht * self.CellPerColumn
            if oldHeight != self.MinHeight:
                self.LayoutChanged()
    
        def _get_color_vector(self, cid):
            """
            Get a color vector from a color ID.
            :param cid : The color ID
            :return    : c4d.Vector()
            """
            dic = self.GetColorRGB(cid)
            if dic:
                return c4d.Vector(float(dic['r']) * self.ColScale,
                                  float(dic['g']) * self.ColScale,
                                  float(dic['b']) * self.ColScale)
            else:
                return c4d.Vector()
    
    if __name__ == "__main__":
        dlg = ExampleDialog()
        dlg.Open(c4d.DLG_TYPE_ASYNC, defaultw=400, defaulth=400)
    
    posted in Cinema 4D SDK •
    RE: Python Generator Mimicking Cloner That Can Modify Individual Cloner Parameters (Except PSR)?

    Hi @bentraje,

    thank you for reaching out to us. As @Cairyn already said, there is little which prevents you from writing your own cloner. Depending on the complexity of the cloner, this can however be quite complex. Just interpolating between two positions or placing objects in a grid array is not too hard. But recreating all the complexity of a MoGraph cloner will be. It is also not necessary. Here are two more straight forward solutions:

    1. As I said in the last thread (the one you did link), MoGraph is perfectly capable of driving arbitrary parameters of an object. I overlooked in the old thread that you did ask what I meant with ""drive the attributes of an object or its material"", so the file cube_bend_mograph.c4d shows you how you can drive the strength parameter of a bend object with MoGraph. The difference I then made was that while you cannot drive such attributes directly via the particle (arrays), but you can let MoGraphs interpolation system take care of it. Please keep in mind that I am everything but an expert for MoGraph. There are probably more elegant solutions for all this, but all this is out of scope for this forum. Please contact technical support or refer to Cineversity for questions about MoGraph.
    2. One problem of MoGraph is that driving more than one arbitrary parameter of an object is a bit tedious. Since MoGraph's sole principle is interpolation and it can only interpolate on a single axis (between parameters), you will then have to start interpolating between two cloners to drive a second, third, fourth, etc. parameter. Here it would make sense to write a custom solution to have less complicated setups. Rather than rewriting a cloner, it would be however more sensible to just modify the cache of a cloner. This will only work when the cloner is in the "Instance Mode" "Instance", as otherwise the cache will only contain the instance wrappers (a bit confusing the naming here ^^). You could do this all inside a GVO, including building the cloner and its cache, but I did provide here a solution which relies on linking a cloner whose cache one wishes to modify. The idea is then simple, walk over all bend objects in the cache and modify their bend strength. You can find a scene file and the code below.

    Cheers,
    Ferdinand

    file: cube_bend_python.c4d
    code:

    """Example for modifying the cache of a node and returning it as the output
    of a generator.
    
    This specific example drives the bend strength of bend objects contained in
    a Mograph cloner object. The example is designed for a Python generator 
    object with a specific set of user data values. Please use the provided c4d
    file if possible.
    
    Note:
        This example makes use of the function `CacheIterator()` for cache 
        iteration which has been proposed on other threads for the task of walking
        a cache, looking for specific nodes. One can pass in one or multiple type
        symbols for the node types to be retrieved from the cache. I did not 
        unpack the topic of caches here any further.
    
        We are aware that robust cache walking can be a complex subject and 
        already did discuss adding such functionality to the SDK toolset in the
        future, but for now users have to do that on their own.
    
    As discussed in:
        plugincafe.maxon.net/topic/13275/
    """
    
    import c4d
    
    # The cookie cutter cache iterator template, can be treated as a black-box,
    # as it has little to do with the threads subject.
    def CacheIterator(op, types=None):
        """An iterator for the elements of a BaseObject cache.
    
        Handles both "normal" and deformed caches and has the capability to 
        filter by node type.
    
        Args:
            op (c4d.BaseObject): The node to walk the cache for.
            types (Union[list, tuple, int, None], optional): A collection of type
             IDs from one of which a yielded node has to be derived from. Will
             yield all node types if None. Defaults to None.
    
        Yields:
            c4d.BaseObject: A cache element of op.
    
        Raises:
            TypeError: On argument type violations.
        """
        if not isinstance(op, c4d.BaseObject):
            msg = "Expected a BaseObject or derived class, got: {0}"
            raise TypeError(msg.format(op.__class__.__name__))
    
        if isinstance(types, int):
            types = (types, )
        if not isinstance(types, (tuple, list, type(None))):
            msg = "Expected a tuple, list or None, got: {0}"
            raise TypeError(msg.format(types.__class__.__name__))
    
        # Try to retrieve the deformed cache of op.
        temp = op.GetDeformCache()
        if temp is not None:
            for obj in CacheIterator(temp, types):
                yield obj
    
        # Try to retrieve the cache of op.
        temp = op.GetCache()
        if temp is not None:
            for obj in CacheIterator(temp, types):
                yield obj
    
        # If op is not a control object.
        if not op.GetBit(c4d.BIT_CONTROLOBJECT):
            # Yield op if it is derived from one of the passed type symbols.
            if types is None or any([op.IsInstanceOf(t) for t in types]):
                yield op
    
        # Walk the hierarchy of the cache.
        temp = op.GetDown()
        while temp:
            for obj in CacheIterator(temp, types):
                yield obj
            temp = temp.GetNext()
    
    
    def main():
        """
        """
        # The user data.
        node = op[c4d.ID_USERDATA, 1]
        angle = op[c4d.ID_USERDATA, 2]
        fieldList = op[c4d.ID_USERDATA, 3]
    
        # Lazy parameter validation ;)
        if None in (node, angle, fieldList):
            raise AttributeError("Non-existent or non-populated user data.")
    
        # Get the cache of the node and clone it (so that we have ownership).
        cache = node.GetDeformCache() or node.GetCache()
        if cache is None:
            return c4d.BaseObject(c4d.Onull)
        clone = cache.GetClone(c4d.COPYFLAGS_NONE)
    
        # Iterate over all bend objects in the cache ...
        for bend in CacheIterator(clone, c4d.Obend):
            # ..., sample the field list for the bend object position, ...
            fieldInput = c4d.modules.mograph.FieldInput([bend.GetMg().off], 1)
            fieldOutput = fieldList.SampleListSimple(op, fieldInput,
                                                     c4d.FIELDSAMPLE_FLAG_VALUE)
            if (not isinstance(fieldOutput, c4d.modules.mograph.FieldOutput) or
                    fieldOutput.GetCount() < 1):
                raise RuntimeError("Error sampling field input.")
            # ... and set the bend strength with that field weight as a multiple 
            # of the angle defined in the user data.
            bend[c4d.DEFORMOBJECT_STRENGTH] = angle * fieldOutput.GetValue(0)
    
        # Return the clone's cache.
        return clone
    
    posted in Cinema 4D SDK •
    RE: Implementing a watermark on render

    Hello @shetal,

    thank you for reaching out to us. The reformulation of your question and the conformance with the forum guidelines on tagging is also much appreciated.

    About your question: As stated in the forum guidelines, we cannot provide full solutions for questions, but provide answers for specific questions. Which is why I will not show here any example code, the first step would have to be made by you. I will instead roughly line out the purpose of and workflow around VideoPostData, which I assume is what you are looking for anyway.

    VideoPostData is derived from NodeData, the base class to implement a node for a classic API scene graph. Node means here 'something that lives inside a document and is an addressable entity', examples for such nodes are materials, shaders, objects, tags, ..., and as such 'video post' node. As mentioned in its class description, VideoPostData is a versatile plugin interface which can be used to intervene a rendering process in multiple ways. The most tangible place for VideoPostData in the app is the render settings where video post plugins can be added as effects for a rendering process as shown below with the built-in water mark video post node.

    12dc3981-c9af-4e3b-80a3-a1e5dabc2a42-image.png

    VideoPostData is an effect, meaning that you cannot use it to invoke a rendering process and on its own it also cannot forcibly add itself to a rendering and must be included manually with the RenderData, the render settings of a rendering. However, a user could make a render setting which includes such watermark effect part of his or her default render settings. One could also implement another plugin interface, SceneHookData, to automatically add such effect to every active document. We would not encourage that though, as this could be confusing or infuriating for users. Finally, such VideoPostData plugin would be visible by default like all NodeData plugins, i.e., it would appear as something in menus that the user can add and interact with. To prevent this if desired, one would have to register the plugin with the flag PLUGINFLAG_HIDE suppress it from popping up in the 'Effect ...' button menu. I cannot tell you with certainty if it is possible to hide programmatically added effect nodes from the users view in the effect list of a render settings. There are some flags which can be used to hide instances of nodes, but I would have to test myself if this also applies in this list, it is more likely that this will not be possible.

    To implement a VideoPostData plugin interface, one can override multiple methods and take different approaches, the most commonly used one is to override VideoPostData::Execute(Link) which will be called multiple times for each rendered frame. The method follows a flag/message logic which is commonly used in Cinema 4D's classic API, where one gets passed in a flag which signalizes in which context the method is being called. Here the context is at which state of the rendering this call is being made, and the chain is:

    • VIDEOPOSTCALL::FRAMESEQUENCE - Series of images starts.
    • VIDEOPOSTCALL::FRAME - Image render starts.
    • VIDEOPOSTCALL::SUBFRAME - Sub-frame starts.
    • VIDEOPOSTCALL::RENDER - Render precalculation.
    • VIDEOPOSTCALL::INNER - Render precalculation.
    • VIDEOPOSTCALL::INNER - Immediately after rendering.
    • VIDEOPOSTCALL::RENDER - Immediately after shader cleanup.
    • VIDEOPOSTCALL::SUBFRAME - Sub-frame rendering done.
    • VIDEOPOSTCALL::FRAME - Frame rendering done.
    • VIDEOPOSTCALL::FRAMESEQUENCE - Complete rendering process finished.

    These flags are accompanied by information if the flags denotes the opening or closing of that 'step' in the rendering process. A developer often then restricts its plugin functionality to a certain flag. I.e., in your case you only want to execute some code when the closing VIDEOPOSTCALL::FRAME is being passed, i.e., after a single frame and all its sub-frames have been rendered. Execute() also passes in a pointer to a VideoPostStruct(Link) which carries information about the ongoing rendering. One of its fields is render, a pointer to a Render(Link). This data structure represents a rendering with multiple buffers and provides the method GetBuffer() which returns a pointer to VPBuffer buffer. In your case you would want to retrieve the RGBA buffer for the rendering by requesting the VPBUFFER_RGBA buffer (Link) with GetBuffer().

    This buffer is then finally the pixel buffer, the bitmap data you want to modify. The buffer is being read and written in a line wise fashion with VPBuffer::GetLine() and ::SetLine(). Here you would have to superimpose your watermark information onto the frame. I would do this in a shader like fashion, i.e., write a function which I can query for a texture coordinate for every pixel/fragment in every line and it will then return an RBGA value which I could then combine with the RGBA information which is in the buffer at that coordinate. The details on that depend on what you want to do, e.g.,

    • Should the watermark be tiled across the frame or just live in a 'corner'?
    • Should it contain alpha information?
    • Can the user influence it, or is it just a png file on disk?
    • etc...

    and the answers to that are mostly algorithmic and not directly connected to our API which limits the amount of support we can provide for them. If this all sounds very confusing to you, it might be helpful to look at our video post examples I did post in the previous thread, e.g., vpreconstructimage.cpp, as this will probably make things less daunting.

    If you decide that you do not want to take this route for technical or complexity reasons, you could write a SceneHookData plugin which listens via NodeData::Message for MSG_MULTI_RENDERNOTIFICATION(Link), a message family which is being sent in the context of a rendering. There you would have to evaluate the start field in the RenderNotificationData(Link) accompanying the message, to determine if the call is for the start or end of a rendering. Then you could grab the rendering output file(s) on disk with the help of the render settings from disk and 'manually' superimpose your watermark information. This will come with the drawback that you might have to deal with compressed video files like mpeg or Avi and all the image formats. Some complexity in that can be hidden away with our BaseBitmap type I did mention in my last posting, but not all of it. There is also the fact that you might run into problems when this plugin runs on a render server, where you cannot easily obtain write or even read access to files of the render output.

    I hope this give you some desired guidance,
    Ferdinand

    posted in Cinema 4D SDK •
    RE: API for new behavior of opnening Windows in Layout

    Hello @holgerbiebrach,

    please excuse the wait. So, this is possible in Python and quite easy to do. This new behavior is just the old dialog folding which has been reworked a little bit. I have provided a simple example at the end of the posting. There is one problem regarding title bars which is sort of an obstacle for plugin developers which want to distribute their plugins, it is explained in the example below.

    I hope this helps and cheers,
    Ferdinand

    The result:
    3453535.gif
    The code:

    """Example for a command plugin with a foldable dialog as provided with the
    Asset Browser or Coordinate Manger in Cinema 4D R25.
    
    The core of this is just the old GeDialog folding mechanic which has been
    changed slightly with R25 as it will now also hide the title bar of a folded
    dialog, i.e., the dialog will be hidden completely.
    
    The structure shown here mimics relatively closely what the Coordinate Manger
    does. There is however one caveat: Even our internal implementations do not
    hide the title bar of a dialog when unfolded. Instead, this is done via 
    layouts, i.e., by clicking onto the ≡ icon of the dialog and unchecking the
    "Show Window Title" option and then saving such layout. If you would want
    to provide a plugin which exactly mimics one of the folding managers, you
    would have to either ask your users to take these steps or provide a layout.
    
    Which is not ideal, but I currently do not see a sane way to hide the title
    bar of a dialog. What you could do, is open the dialog as an async popup which 
    would hide the title bar. But that would also remove the ability to dock the 
    dialog. You could then invoke `GeDialog.AddGadegt(c4d.DIALOG_PIN, SOME_ID)`to 
    manually add a pin back to your dialog, so that you can dock it. But that is 
    not how it is done internally by us, as we simply rely on layouts for that.
    """
    
    import c4d
    
    
    class ExampleDialog (c4d.gui.GeDialog):
        """Example dialog that does nothing.
    
        The dialog itself has nothing to do with the implementation of the
        folding.
        """
        ID_GADGETS_START = 1000
        ID_GADGET_GROUP = 0
        ID_GADGET_LABEL = 1
        ID_GADGET_TEXT = 2
    
        GADGET_STRIDE = 10
        GADEGT_COUNT = 5
    
        def CreateLayout(self) -> bool:
            """Creates dummy gadgets.
            """
            self.SetTitle("ExampleDialog")
            flags = c4d.BFH_SCALEFIT
    
            for i in range(self.GADEGT_COUNT):
                gid = self.ID_GADGETS_START + i * self.GADGET_STRIDE
                name = f"Item {i}"
    
                self.GroupBegin(gid + self.ID_GADGET_GROUP, flags, cols=2)
                self.GroupBorderSpace(5, 5, 5, 5)
                self.GroupSpace(2, 2)
                self.AddStaticText(gid + self.ID_GADGET_LABEL, flags, name=name)
                self.AddEditText(gid + self.ID_GADGET_TEXT, flags)
                self.GroupEnd()
            return True
    
    
    class FoldingManagerCommand (c4d.plugins.CommandData):
        """Provides the implementation for a command with a foldable dialog.
        """
        ID_PLUGIN = 1058525
        REF_DIALOG = None
    
        @property
        def Dialog(self) -> ExampleDialog:
            """Returns a class bound ExampleDialog instance.
            """
            if FoldingManagerCommand.REF_DIALOG is None:
                FoldingManagerCommand.REF_DIALOG = ExampleDialog()
    
            return FoldingManagerCommand.REF_DIALOG
    
        def Execute(self, doc: c4d.documents.BaseDocument) -> bool:
            """Folds or unfolds the dialog.
    
            The core of the folding logic as employed by the Asset Browser
            or the Coordinate manager in R25.
            """
            # Get the class bound dialog reference.
            dlg = self.Dialog
            # Fold the dialog, i.e., hide it if it is open and unfolded. In C++
            # you would also want to test for the dialog being visible with
            # GeDialog::IsVisible, but we cannot do that in Python.
            if dlg.IsOpen() and not dlg.GetFolding():
                dlg.SetFolding(True)
            # Open or unfold the dialog. The trick here is that calling
            # GeDialog::Open will also unfold the dialog.
            else:
                dlg.Open(c4d.DLG_TYPE_ASYNC, FoldingManagerCommand.ID_PLUGIN)
    
            return True
    
        def RestoreLayout(self, secret: any) -> bool:
            """Restores the dialog on layout changes.
            """
            return self.Dialog.Restore(FoldingManagerCommand.ID_PLUGIN, secret)
    
        def GetState(self, doc: c4d.documents.BaseDocument) -> int:
            """Sets the command icon state of the plugin.
    
            This is not required, but makes it a bit nicer, as it will indicate
            in the command icon when the dialog is folded and when not.
            """
            dlg = self.Dialog
            result = c4d.CMD_ENABLED
            if dlg.IsOpen() and not dlg.GetFolding():
                result |= c4d.CMD_VALUE
    
            return result
    
    
    def RegisterFoldingManagerCommand() -> bool:
        """Registers the example.
        """
        return c4d.plugins.RegisterCommandPlugin(
            id=FoldingManagerCommand.ID_PLUGIN,
            str="FoldingManagerCommand",
            info=c4d.PLUGINFLAG_SMALLNODE,
            icon=None,
            help="FoldingManagerCommand",
            dat=FoldingManagerCommand())
    
    
    if __name__ == '__main__':
        if not RegisterFoldingManagerCommand():
            raise RuntimeError(
                f"Failed to register {FoldingManagerCommand} plugin.")
    
    posted in Cinema 4D SDK •
    Discovering Channel Identifiers of a Substance Shader

    Dear community,

    The following code example demonstrates how to discover the channel identifiers of the "Channel" parameter of a Substance shader, so that the channel can be changed programmatically for a substance asset unknown at the time of writing the script.

    9b05b0e3-39d3-471b-855f-4c0e1e3dcdea-image.png

    This question reached us via mail, but since answering it requires no confidential data, we are sharing the solution here. The "trick" is to traverse the description of the shader, as these identifiers depend on the substance.

    Cheers,
    Ferdinand

    The result (the example script will randomly select a channel, but with the data provided, channels can also be selected by their name or a substring match as for example "diffuse"):

    substance_channels.gif

    The code:

    """Example for discovering the channels of a Substance shader.
    
    The solution is a bit hacky by traversing the description of the shader but should work.
    """
    
    import c4d
    import random
    
    def GetSubstanceChannels(shader: c4d.BaseShader) -> dict[int:str]:
        """Returns all channels of the substance loaded into #shader as a dictionary of id-label pairs.
        """
        if not isinstance(shader, c4d.BaseShader) or (shader.GetType() != c4d.Xsubstance):
            raise TypeError(f"{shader} is not a substance shader.")
    
        # Get the data for the "Channel" dropdown element from the description of the shader.
        description = shader.GetDescription(c4d.DESCFLAGS_DESC_NONE)
        channelData = description.GetParameter(c4d.SUBSTANCESHADER_CHANNEL)
    
        # Get the elements in the drop down menu.
        elements = channelData[c4d.DESC_CYCLE]
        if not isinstance(elements, c4d.BaseContainer):
            raise RuntimeError(f"Could not access Channel parameter description in {shader}.")
        
        # Pack the data into a dictionary and return it.
        return {id: label for id, label in elements}
    
    def main(doc: c4d.documents.BaseDocument):
        """
        """
        # Get the active material.
        material = doc.GetActiveMaterial()
        if not isinstance(material, c4d.BaseMaterial):
            raise RuntimeError("Please select a material.")
    
        # Get the substance shader loaded into the color channel of the material.
        shader = material[c4d.MATERIAL_COLOR_SHADER]
        channelData = GetSubstanceChannels(shader)
    
        for id, label in channelData.items():
            print (f"id: {id}, label: {label}")
    
        # To select a specific channel, one would have to do a string comparison here to find keywords as
        # "Color" or "Metal" in the channel label. I am just randomly selecting a channel instead.
        channelId = random.choice(tuple(channelData.keys()))
        channelLabel = channelData[channelId]
        print (f"Setting substance to channel '{channelLabel}({channelId})'")
        shader[c4d.SUBSTANCESHADER_CHANNEL] = channelId
    
        c4d.EventAdd()
    
    if __name__=='__main__':
        main(doc)
    
    posted in Cinema 4D SDK •

    Latest posts made by ferdinand

    RE: Script: Connect + Delete all subdivision surface objects

    Hello @derekheisler,

    Welcome to the Plugin Café forum and the Cinema 4D development community, it is great to have you with us!

    Getting Started

    Before creating your next postings, we would recommend making yourself accustomed with our Forum and Support Guidelines, as they line out details about the Maxon SDK Group support procedures. Of special importance are:

    • Support Procedures: Scope of Support: Lines out the things we will do and what we will not do.
    • Support Procedures: Confidential Data: Most questions should be accompanied by code but code cannot always be shared publicly. This section explains how to share code confidentially with Maxon.
    • Forum Structure and Features: Lines out how the forum works.
    • Structure of a Question: Lines out how to ask a good technical question. It is not mandatory to follow this exactly, but you follow the idea of keeping things short and mentioning your primary question in a clear manner.

    About your First Question

    Please excuse the slight delay and thank you for the community answer provided by @HerrMay.

    The core misconception in your code lies in the line objs = doc.GetObjects(), as it does not get a list of all objects in doc, but only the root level objects. Users are expected to do scene graph traversal on their own at the moment, there is no method which would do it for you. There are also other problems with your code, as for example trying to instantiate a BaseThread or using CallCommand (not a real problem but not recommended in most cases).

    There have been also multiple similar threads in the past:

    • How do you collapse complex dependencies in order?: This is about implementing a connect and delete.
    • How to traverse a GeListNode Tree?: This is about hierarchical traversal, i.e., getting all descendants and siblings for a node. There is also something that I would call branch traversal, it is much more complex and we have talked about it for example here.
    • CAD Normal Tag flipped: Entirely different topic, but could give you an idea on how to realize a simple GUI for this.

    Finde below a simple example.

    Cheers,
    Ferdinand

    Result:
    collapse.gif

    Code:

    """Demonstrates how to collapse all SDS objects in a scene.
    
    One could also make this more fancy with a GUI and undo steps, I did not do this here.
    """
    
    import c4d
    
    doc: c4d.documents.BaseDocument # The active document.
    
    def IterNodes(node: c4d.GeListNode) -> c4d.GeListNode:
        """Yields all descendants and next-siblings of `node` in a semi-iterative fashion.
        """
        if node is None:
            return
    
        while node:
            yield node
    
            # The recursion moving down.
            for descendant in IterNodes(node.GetDown()):
                yield descendant
    
            # The iteration in one level.
            node = node.GetNext()
    
    def Collapse(objects: list[c4d.BaseObject]) -> None:
        """Collapses all items in #objects as individual root nodes into singular objects.
    
        This function mimics the behaviour of the builtin (but unexposed) "Connect & Delete" command 
        by first running the "CSTO" and then "JOIN" command. With setups complex enough, this can still
        fail due to the non-existent dependency graph of the classic API (when one does CSTO things in
        the wrong order). In 99.9% of the cases this will not be the case, but one should get the
        inputs with #GETACTIVEOBJECTFLAGS_SELECTIONORDER as I did below to give the user more control.
        (or alternatively do not batch operate).
        """
        if len(objects) < 1:
            raise RuntimeError()
        doc: c4d.documents.BaseDocument = objects[0].GetDocument()
        doc.StartUndo()
    
        # CSTO all local hierarchies in #objects and replace these root nodes with their collapsed
        # counter parts.
        result = c4d.utils.SendModelingCommand(c4d.MCOMMAND_CURRENTSTATETOOBJECT, objects, 
            c4d.MODELINGCOMMANDMODE_ALL, c4d.BaseContainer(), doc, c4d.MODELINGCOMMANDFLAGS_NONE)
    
        if not result or len(result) != len(objects):
            raise RuntimeError()
    
        for old, new in zip(objects, result):
            parent, pred = old.GetUp(), old.GetPred()
            doc.AddUndo(c4d.UNDOTYPE_DELETEOBJ, old)
            old.Remove()
            doc.InsertObject(new, parent, pred)
            doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, new)
    
        # Join the CSTO results root by root object, and then replace the CSTO results with the final
        # collapsed result. JOIN is a bit weird when it comes to transforms, so we must store the 
        # transform of the to be joined object, then zero it out, and finally apply it to the joined 
        # result again.
        for obj in result:
            mg: c4d.Matrix = obj.GetMg()
            obj.SetMg(c4d.Matrix())
    
            joined = c4d.utils.SendModelingCommand(c4d.MCOMMAND_JOIN, [obj], 
                c4d.MODELINGCOMMANDMODE_ALL, c4d.BaseContainer(), doc, c4d.MODELINGCOMMANDFLAGS_NONE)
            
            if not joined:
                raise RuntimeError()
            
            parent, pred = obj.GetUp(), obj.GetPred()
            doc.AddUndo(c4d.UNDOTYPE_DELETEOBJ, obj)
            obj.Remove()
    
            new: c4d.BaseObject = joined[0]
            new.SetMg(mg)
            doc.InsertObject(new, parent, pred)
            doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, new)
    
        doc.EndUndo()
        c4d.EventAdd()
    
    def main() -> None:
        """Runs the example.
        """
        # Find all SDS objects in the scene.
        objectList: list[c4d.BaseObject] = [
            n for n in IterNodes(doc.GetFirstObject()) if n.CheckType(c4d.Osds)]
    
        # Collapse all SDS objects we have found.
        for obj in objectList:
            # We have to check if #obj is still a valid reference because we could have already 
            # collapsed the object away in a prior iteration; sds-objects can live inside sds-objects.
            if not obj.IsAlive():
                continue
            # Collapse the object.
            Collapse([obj])
    
        c4d.EventAdd()
    
    if __name__ == "__main__":
        main()
    
    posted in Cinema 4D SDK •
    RE: How to dynamically hide parameters

    Hello @kng_ito,

    Thank you for reaching out to us. Modifying the description of a node via NodeData.GetDDescription should only be done when NodeData.GetDEnabling is not sufficient. To hide a parameter, the field c4d.DESC_HIDE must be set to True. Find a simple example below.

    Cheers,
    Ferdinand

    Result:
    hide_param.gif

    File: pc14487.zip

    Code:

    // The description defintion of the tag Texample.
    CONTAINER Texample
    {
      NAME Texample;
      INCLUDE Texpression;
    
      // The main "Tag" tab of the tag.
      GROUP ID_TAGPROPERTIES
      {
        // The element which is being hidden (or not).
        REAL ID_HIDDEN_ELEMENT { MIN 0.0; MAX 100.0; UNIT METER; STEP 0.001; CUSTOMGUI REALSLIDER; }
        // The parameter based on which the hidden element is shown or not.
        BOOL ID_HIDE_CONDITION {}
      }
    }
    
    
    """Implements a tag which dynamically hides and shows a parameter.
    """
    import c4d
    import typing
    
    class ExampleTagData(c4d.plugins.TagData):
        """Implements a tag which dynamically hides and shows a parameter.
        """
        # The plugin ID for the hook.
        ID_PLUGIN: int = 1060794
    
        @classmethod
        def Register(cls) -> None:
            """Registers the plugin hook.
            """
            if not c4d.plugins.RegisterTagPlugin(
                    id=cls.ID_PLUGIN,
                    str="Example Tag",
                    info=c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE,
                    g=cls, 
                    description="texample",
                    icon=c4d.bitmaps.InitResourceBitmap(c4d.Tdisplay)):
                print(f"Warning: Failed to register '{cls}' tag plugin.")
            
        def Init(self, node: c4d.GeListNode) -> bool:
            """Called to initialize a tag instance.
    
            Args:
                node: The BaseTag instance representing this plugin object.
            """
            self.InitAttr(node, float, c4d.ID_HIDDEN_ELEMENT)
            self.InitAttr(node, bool, c4d.ID_HIDE_CONDITION)
    
            node[c4d.ID_HIDDEN_ELEMENT] = 50.0
            node[c4d.ID_HIDE_CONDITION] = False
    
            return True
    
        def Execute(self, tag: c4d.BaseTag, doc: c4d.documents.BaseDocument, op: c4d.BaseObject,
                    bt: c4d.threading.BaseThread, priority: int, flags: int) -> int:
            """Called when expressions are evaluated to let a tag modify a scene.
    
            Not used in this case.
            """
            return c4d.EXECUTIONRESULT_OK
    
        def GetDDescription(self, node: c4d.GeListNode, description: c4d.Description, 
                            flags: int) -> typing.Union[bool, tuple[bool, int]]:
            """Called by Cinema 4D when the description of a node is being evaluated to let the node
            dynamically modify its own description.
            """
            # Bail when the description cannot be not fully loaded.
            if not description.LoadDescription(node.GetType()):
                return False, flags
    
            # Define the ID of the parameter we want to modify and get the ID of the parameter which is
            # currently to be evaluated.
            paramId: c4d.DescID = c4d.DescID(c4d.DescLevel(c4d.ID_HIDDEN_ELEMENT, c4d.DTYPE_REAL, 0))
            evalId: c4d.DescID = description.GetSingleDescID()
    
            # Bail when there is a to be evaluated parameter and our parameter is not part of or equal
            # to the evaluated parameter.
            if (evalId and not paramId.IsPartOf(evalId)):
                return True, flags
    
            # Get the description data container instance (GetParameter>I<) for the parameter we want to
            # modify. All changes made to the container will be directly reflected on the node. 
            paramData: c4d.BaseContainer = description.GetParameterI(paramId)
            if paramData is None:
                return True, flags
    
            # Set the hidden state of the parameter ID_HIDDEN_ELEMENT based on the value of the 
            # parameter ID_HIDE_CONDITION.
            paramData[c4d.DESC_HIDE] = node[c4d.ID_HIDE_CONDITION]
    
            return True, flags | c4d.DESCFLAGS_DESC_LOADED
    
    
    if __name__ == "__main__":
        ExampleTagData.Register()
    
    
    posted in Cinema 4D SDK •
    2023.2.0 SDK Release

    Dear Cinema 4D Community,

    On March the 29th, 2023, Maxon Computer released Cinema 4D 2023.2.0. Alongside this release, a new Cinema 4D SDK and SDK documentation have been released, reflecting the API changes for 2023.2.0. For an overview of the features of Cinema 4D 2023.2, please refer to the spring release announcement.

    Note that with 2023.2, the entry points for our online documentation have changed; please update your bookmarks.

    • https://developers.maxon.net/docs/cpp: Points to the latest Cinema 4D C++ API documentation.
    • https://developers.maxon.net/docs/py: Points to the latest Cinema 4D Python API documentation.
    • https://developers.maxon.net/docs/cw: Points to the latest Cineware API documentation (still S22).

    For an overview of the SDK 2023.2 features and the URL changes, please refer to the Cinema 4D 2023.2.0 Release posting on our blog. For in detail change notes, please refer to the Python and C++ change notes.

    Happy rendering and coding,
    the Maxon SDK Team

    posted in Maxon Announcements •
    RE: How can I add an offset to the global matrix of an object that is rotated?

    Hello @HerrMay,

    Is the multiplication of a matrix with a vector (mg.off = mg * p) essentially the same what mg * c4d.utils.MatrixMove(vec) does?

    No, mg * p multiplies a matrix by a vector which will yield the transformed vector p'. mg * c4d.utils.MatrixMove(vec) on the other hand multiplies two matrices, yielding the combined transform of them, a new matrix.

    What would be equivalent to mg * p is mg * c4d.utils.MatrixMove(p) * c4d.Vetcor(0, 0, 0). Please note that the whole subject is also out of scope of support as declared in the forum guidelines. I understand that the subject is a common barrier for the more on the artist-sided leaning users of ours, and I help where I can, but we cannot teach you vector math, linear algebra, or however you want to label this subject.

    Find below a short code example.

    Cheers,
    Ferdinand

    Output:

    M = Matrix(v1: (1, 0, 0); 
               v2: (0, 1, 0); 
               v3: (0, 0, 1); 
               off: (0, 100, 0))
    p = Vector(100, 0, 0)
    
    M * p = Vector(100, 100, 0)
    M * c4d.utils.MatrixMove(c4d.Vector(100, 0, 0)) = Matrix(v1: (1, 0, 0); 
                                                             v2: (0, 1, 0); 
                                                             v3: (0, 0, 1); 
                                                             off: (100, 100, 0))
    M * c4d.Vector(100, 0, 0) = Vector(100, 100, 0)
    N * c4d.Vector(  0, 0, 0) = Vector(100, 100, 0)
    M * c4d.utils.MatrixMove(p) * c4d.Vector(0, 0, 0) = Vector(100, 100, 0)
    

    Code:

    import c4d
    
    # Let us assume #M to be the global matrix/transform of some object #op. View it as a tool to 
    # transform a point in global space into the local coordinate system of #op.
    M: c4d.Matrix = c4d.Matrix(off=c4d.Vector(0, 100, 0))
    
    # The matrix #M now has this form:
    #
    #   | v1     1    0    0 |   # The "x-axis" of the coordinate system defined by #M.
    #   | v2     0    1    0 |   # The "y-axis" of the coordinate system defined by #M.
    #   | v3     0    0    1 |   # The "z-axis" of the coordinate system defined by #M.
    #   ---------------------- 
    #   | off    0  100    0 |   # The origin of the coordinate system defined by #M.
    #
    # I.e., #M has the standard orientation and scale and only translates all things by 100 units
    # on the y-axis. So, the object #op would have the Euler angles (0°, 0°, 0°), the scale (1, 1, 1) 
    # and the position (0, 100, 0) in world coordinates.
    
    # Define a vector #p to transform and print both #M and #p.
    p: c4d.Vector = c4d.Vector(100, 0, 0)
    print(f"{M = }")
    print(f"{p = }")
    
    # Transforming a point #p by a matrix #M will yield a point #q that is in the same relation to #M
    # as #p is to the global frame.
    q: c4d.Vector = M * p
    
    # #q will be the vector (100, 100, 0) because , (100, 100, 0) and #M are the same relation as #p
    # and the identity matrix are, a.k.a., the world frame. In a less technical way, (0, 100, 0) is the
    # origin of the coordinate system defined by #M. And to express #p in a manner as if #M would be
    # its coordinate system, we have to add (0, 100, 0), because that is the new origin. For orientation
    # and scale it works more or less the same, I would recommend having a look at the Matrix manual or
    # Wikipedia article on the subject.
    print (f"{M * p = }")
    
    # We can construct new transforms in many ways (again, Matrix Manual :)), one of them is by 
    # combining multiple transforms via matrix multiplication.
    
    # We "add" the translation transform (100, 0, 0) to #M. Note that matrix multiplication is not 
    # commutative, i.e., "M * N = N * M" does not always hold true. In this case it would because only
    # translations are involved.
    N: c4d.Matrix = M * c4d.utils.MatrixMove(c4d.Vector(100, 0, 0))
    print(f"{M * c4d.utils.MatrixMove(c4d.Vector(100, 0, 0)) = }")
    
    # To get the same point as #q when multiplying a point #r with #N, we must pick the null-vector
    # because the origin of #N is already at where M * (100, 0, 0) is.
    print (f"{M * c4d.Vector(100, 0, 0) = }")
    print (f"{N * c4d.Vector(  0, 0, 0) = }")
    
    # We can also do it in one operation:
    print (f"{M * c4d.utils.MatrixMove(p) * c4d.Vector(0, 0, 0) = }")
    
    posted in Cinema 4D SDK •
    RE: Is it possible to intercept rendering to modify the document that is about to render?

    Hello @HerrMay,

    Thank you for reaching out to us.

    Is it possible to intercept rendering to modify the document that is about to render?

    All in all, yes, it is possible, but it is not recommended. MSG_MULTI_RENDERNOTIFICATION is accompanied by the render document in its message data. But modifying the render document is something one does at one's own risk at the moment. For public users, this document is meant to be read only.

    With that being said, nothing prevents you from modifying the document. But when the rendering is an editor rendering, the passed document will be the active document, i.e., you will change the active document. Click the video link below to see what I mean.

    modify_render_doc.mp4

    In Python this is even more problematic, as one cannot distinguish between renderings which clone the document and one's which do not.

    Cheers,
    Ferdiand

    Code:

    """Demonstrates a Python Programming tag which inserts sphere objects into the documents of upcoming
    renderings.
    
    WHAT IS SHOWN HERE IS NOT AN INTENDED USE OF MSG_MULTI_RENDERNOTIFICATION. The passed document is meant
    to be treated as read only for public API users. Modify at your own risk.
    """
    import c4d
    
    doc: c4d.documents.BaseDocument # The document evaluating this tag
    
    def main() -> None: pass
    
    def message(mid: int, data: object) -> bool:
        """Called by Cinema 4D to convey messages to the tag.
        """
        # There is an upcoming rendering.
        if mid == c4d.MSG_MULTI_RENDERNOTIFICATION and data["start"]:
            # Get the render document.
            doc: c4d.documents.BaseDocument = data["doc"]
    
            # Insert a sphere.
            sphere: c4d.BaseObject = c4d.BaseObject(c4d.Osphere)
            if not sphere:
                raise MemoryError()
    
            doc.InsertObject(sphere)
            
        return True
    
    posted in Cinema 4D SDK •
    RE: How can I add an offset to the global matrix of an object that is rotated?

    Hello @HerrMay,

    Thank you for reaching out to us. Yeah, transforms can be a tricky subject, I would recommend having a look at the Matrix Manual as it does explain some fundamentals.

    Your question was quite clear, but there is always some ambiguity with transform questions unless one uses a super mathematical language. But I think I got what you meant and have provided an example below.

    Cheers,
    Ferdinand

    Result:

    e177a34a-97fc-4b85-aa47-5219dc4cbb95-image.png
    The input object is selected, in blue an object translated by (0, 500, 0) units in the local coordinate system of the selected object.

    Code:

    """Demonstrates translating an object in its own local coordinate system.
    
    Must be run as a Script Manager script with an object selected.
    
    Related:
        https://developers.maxon.net/docs/Cinema4DPythonSDK/html/manuals/data_algorithms/classic_api/matrix.html
    """
    
    import c4d
    import typing
    
    doc: c4d.documents.BaseDocument  # The active document
    op: typing.Optional[c4d.BaseObject]  # The active object, None if unselected
    
    def main() -> None:
        """
        """
        if not op:
            raise RuntimeError("Please select an object.")
    
        # Clone the selected object and insert the clone into the document, so that we have this out of 
        # the way.
        clone: c4d.BaseObject = op.GetClone(c4d.COPYFLAGS_NO_HIERARCHY)
        if not clone:
            raise MemoryError("Could not clone object.")
    
        clone.SetName(f"{op.GetName()} (Clone)")
        clone[c4d.ID_BASEOBJECT_USECOLOR] = c4d.ID_BASEOBJECT_USECOLOR_ALWAYS
        clone[c4d.ID_BASEOBJECT_COLOR] = c4d.Vector(0, 0, 1)
        doc.InsertObject(clone)
    
        # The vector with which we want to translate something.
        p: c4d.Vector = c4d.Vector(0, 500, 0)
    
        # Translate an object inside its own local coordinate system.
        #
        # Doing this means simply multiplying our translation vector #p with the global matrix of the 
        # object and setting the result as the new offset of the global matrix. Remember, multiplying a 
        # point #p with the matrix #M will yield a global point #q that is in the same relation to #M as
        # #p is to the global frame. I.e., in this case it will yield a vector #q that is in the same 
        # relation with #mg as the vector (0, 500, 0) is with the world frame.
        mg: c4d.Matrix = op.GetMg()
        mg.off = mg * p
        clone.SetMg(mg)
    
        c4d.EventAdd()
    
    if __name__ == '__main__':
        main()
    
    posted in Cinema 4D SDK •
    RE: Is it possible to build a TreeView from a user database?

    Hello @jpheneger,

    Thank you for reaching out to us. I would recommend having a look at the Asset API Handbook as it provides examples for most basic to intermediate Asset API related tasks.

    • You can search in the user preferences repository, as it is the union of (allmost) all repositories, but when you want to optimize things, especially when you want to filter out all assets that are not located in your custom database, you should construct a repository for your user data base alone.
    • In Asset API: Asset Metadata I showed traversal by category, but that is usually not what you want to do, as this is potentially ambiguous.
    • If you want to search for all asset types, all asset instances, or all versions of an asset in a repository, you must simply null the respective Id. I would recommend having a look at the three asset search examples in Asset API: Asset Databases.

    If I know the top level category, is it possible to traverse the repository from that point and build up a TreeView to represent those assets?

    Assets are not intrinsically organized in a hierarchy so that one could traverse them naturally in that manner. Instead, the Asset API is simply a database where all items have a category field, and categories are assets themselves and therefore also have such field. If you want a tree data structure for your assets, you will first have to search for all assets in your repo that are of the asset type category which also do not have a parent category, i.e., are "root level" categories. From there on you can then branch out, by seraching for other assets being parented to these assets. The Asset API: Asset Metadata category examples should get you started on how things work. When you are only after a selection window for assets, i.e., an Asset Browser popup, using maxon::AssetManagerInterface::OpenPopup is probably going to be easier than doing things from scratch.

    Cheers,
    Ferdinand

    posted in Cinema 4D SDK •
    RE: Export fbx of only selected objects

    Hello @Aleksey,

    I am glad to hear that you found your solution. And as lined out above, we are of course aware that ChatGPT is also an excellent learning tool, and it is therefore in our interest to allow it. But there is also the other side to it, and that is why we are a bit undecided on how we will deal with it in the future.

    Cheers,
    Ferdinand

    posted in Cinema 4D SDK •
    RE: Crash when apply a Tag plugin.

    Hello @Dunhou,

    Thank you for reaching out to us and thank you at @HerrMay for providing what I would think is the correct answer.

    At least on my machine, nothing is crashing here, it is just that the description of the tag is malformed on line five.

    6ff23335-b6c4-4d15-9ef4-47f3a84ca4b2-image.png

    In this case, it is such a severe error, that Cinema 4D fails to load the description at all because it is cyclically referencing itself. The keyword INCLUDE in a resource allows us to include another resource in that resource. And when we try to reference the resource, we are defining, Cinema 4D is understandably confused. For less severe descriptions errors, one usually is able to then lick OK a few times to get Cinema 4D to load what it does understand. But that does not work here, and one is caught in an endless loop of self-reference.

    In practice, INCLUDE is usually used to load in the base description of something. So, when we take the simple description of the opydoublecircle example plugin which only defines one parameter named PYCIRCLEOBJECT_RAD,

    CONTAINER Opydoublecircle
    {
    	NAME Opydoublecircle;
    	INCLUDE Obase; // Loads 
    
    	GROUP ID_OBJECTPROPERTIES
    	{
    		REAL PYCIRCLEOBJECT_RAD { UNIT METER; MIN 0.0; }
    	}
    	INCLUDE Osplineprimitive;
    }
    

    we can see that Obase loaded the Basic and Coordinates tabs into that object description that every object must have. And Osplineprimitive loaded in the parameters which are shared by all spline primitives, as for example Plane, Reverse, etc.

    c2a597a9-df44-439e-aa0c-62908933dbeb-image.png

    When we remove Osplineprimitive, all these parameters will not be added to our object.

    CONTAINER Opydoublecircle
    {
    	NAME Opydoublecircle;
    	INCLUDE Obase; // Loads 
    
    	GROUP ID_OBJECTPROPERTIES
    	{
    		REAL PYCIRCLEOBJECT_RAD { UNIT METER; MIN 0.0; }
    	}
    }
    

    7a26bad0-db17-4ad5-ba7e-0de7490bc743-image.png

    What slightly complicates this, is that (allmost) all NodeData derived plugin types must include at least their base description, e.g., Obase for objects, Tbase for tags, Mbase for materials, Xbase for shaders, and so on.

    In some cases speccializations are allowed, as for example including Texpression instead of Tbase. Texpression extends Tbase and while Tbase is used for generic tags which usually do not modify the scene, e.g., the "Display" tag Tdisplay is based on Tbase:

    CONTAINER Tdisplay
    {
    	NAME Tdisplay;
    	INCLUDE Tbase;
    
    	GROUP ID_TAGPROPERTIES
    	{
    		COLUMNS 2;
    		BOOL DISPLAYTAG_AFFECT_DISPLAYMODE { }
    		LONG DISPLAYTAG_SDISPLAYMODE
    		{
    

    Texpression is usually used by tags which do modify a scene in some shape or form, e.g, the "Look at Camera" tag Tlookatcamera:

    CONTAINER Tlookatcamera
    {
    	NAME Tlookatcamera;
    	INCLUDE Texpression;
    
    	GROUP ID_TAGPROPERTIES
    	{
    		BOOL LOOKATCAMERA_PITCH { }
    	}
    }
    

    The difference is then simply that such tags whill have the expression paramaters in their 'Basic' tab.

    a91e446d-4b0d-4fcc-a0a4-72179491d19c-image.png

    As said before, I already have updating the GUI manuals on my desk, it will be one of the next things I do, as they are factually non-existent for Python and a bit outdated and assumptious regarding what users will easily understand for C++. But I would still recommend Python users having a look at the C++ docs, as the information provided there is directly transferable to Python.

    • Resource File Manual: Provides an overview over the different resource types.
    • Description Resources Manual: Provides an overview of the form of resource files you are writing here.

    As lined out in other threads, greping the Cinema 4D resource folder, e.g., C:\Program Files\Maxon\2023.1.3\resource\ is a good way to gather knowledge on your own right now. I like using Agent Ransack for doing things like this, but any grep-like tool, including these built into text editors, will do:

    afde649f-398b-439c-b596-12ffc6b12967-image.png

    Cheers,
    Ferdinand

    posted in Cinema 4D SDK •
    RE: How to be sure if an object is a generator/modifier?

    Hello @HerrMay,

    Thank you for reaching out to us. Well, what c4d.BaseList2D.GetInfo() is returning are the plugin flags the plugin has been registered with, e.g., c4d.OBJECT_GENERATOR. What one then considers "bullet proof" is a bit of a question of taste.

    c4d.BaseList2D.GetInfo returns the plugin flags mask without any filtering, i.e., it is in this sense bullet proof, and in the sense that it will correctly identify plugins that exhibit the behavior that comes with for example with OBJECT_GENERATOR. What you or the common user associate with these terms might be rightfully something different, and in the end these things are just that: flags. So nothing prevents a developer from registering something as a OBJECT_GENERATOR and then writing something that behaves very differently. But things like Olight and Ocamera rightfully do not classify as generators because they have no cache, the core characteristic of a generator, as also indicated by the documentation:

    OBJECT_GENERATOR: Generator object. Produces a polygonal or spline representation on its own. (E.g. primitive cube.)

    Noteworthy is in this context that SplineObject instances are generators because they generate their LineObject cache. the Modelling - Geometry Model Python code examples expand a bit more on that concept.

    306a3d9d-1b55-4788-bbcc-afde27d29a13-image.png

    As an alternative, you could test for objects being a PointObject instance, as all things which are not generators in your world view should be a point object (again, it is a bit ambiguous what you would consider a generator and what not). You can also check if something is a BaseObject but not a PointObject and has no cache to decide if it is something like Olight. What is a modifier or not in this interpretative/analytical sense is probably impossible, as there is already room for interpretation for the native objects of Cinema 4D, deformers and MoGraph effectors are modifiers, field objects are not. This likely extends to many plugin objects which might have modifier qualities but do not necessarily flag themselves as such with OBJECT_MODIFIER.

    Regarding ID_BASEOBJECT_GENERATOR_FLAG, all objects have this parameter, even the ones which do not display it. So, calling C4DAtom.GetParameter with the flag DESCFLAGS_GET_NO_GEDATADEFAULTVALUE will not yield None for objects which seem to not have this parameter. But you can inspect the description of an object to find out, see the code example at the end.

    Cheers,
    Ferdinand

    Result:

    Cube.1(Polygon) cannot be enabled or disabled.
    Group(Group) enabled-state is: 0.
    Linear Field(Linear Field) enabled-state is: 1.
    Camera(Camera) cannot be enabled or disabled.
    Light(Light) enabled-state is: 1.
    

    Code:

    """Demonstrates how to check if a BaseObject has the green enabled check-mark or not.
    
    Will print out the state for the selected object when run.
    """
    
    import c4d
    import typing
    
    doc: c4d.documents.BaseDocument  # The active document
    op: typing.Optional[c4d.BaseObject]  # The active object, None if unselected
    
    
    def main() -> None:
        """Runs the example.
        """
        if not op:
            return
    
        # Something like this will not work, because all BaseObject types carry this parameter, it is 
        # just that some of them are hiding it. So, we will never retrieve None here.
        state: typing.Optional[bool] = op.GetParameter(
            c4d.ID_BASEOBJECT_GENERATOR_FLAG, c4d.DESCFLAGS_GET_NO_GEDATADEFAULTVALUE)
        
        # But we can look up if this parameter is hidden in the description of the object.
        description: c4d.Description = op.GetDescription(c4d.DESCFLAGS_DESC_NONE)
        param: typing.Optional[c4d.BaseContainer] = description.GetParameter(
            c4d.DescID(c4d.DescLevel(c4d.ID_BASEOBJECT_GENERATOR_FLAG, c4d.DTYPE_BOOL, 0)))
        if not param:
            raise RuntimeError("Could not retrieve parameter container.")
    
        isHidden: bool = param[c4d.DESC_HIDE]
    
        if isHidden:
            print (f"{op.GetName()}({op.GetTypeName()}) cannot be enabled or disabled.")
        else:
            # We could also reuse #state here, but the fancier GetParameter() access is not required.
            print (f"{op.GetName()}({op.GetTypeName()}) enabled-state is: "
                   f"{op[c4d.ID_BASEOBJECT_GENERATOR_FLAG]}.")
    
    
    if __name__ == '__main__':
        main()
    
    posted in Cinema 4D SDK •