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
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
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:
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.")
Dear Community,
this question reached us via email-support in the context of C++, but I thought the answer might be interesting for other users too.
The underlying question in this case was how to project points from object or world space into the texture space of an object with UV data. I am showing here deliberately an approach that can be followed both in C++ and Python, so that all users can benefit from this. In C++ one has also the option of using VolumeData and its methods VolumeData::GetUvw
or VolumeData::ProjectPoint
but must then either implement a volume shader (as otherwise the volume data attached to the ChannelData
passed to ShaderData::Output
will be nullptr
), or use VolumeData:: AttachVolumeDataFake
to access ::ProjectPoint
. There is however no inherent necessity to take this shader bound route as shown by the example.
Cheers,
Ferdinand
The script has created a texture with red pixels for the intersection points of the rays cast from each vertex of the spline towards the origin of the polygon object. The script also created the null object rays to visualize the rays which have been cast.
raycast_texture.c4d : The scene file.
You must save the script to disk before running it, as the script infers from the script location the place to save the generated texture to.
"""Demonstrates how to project points from world or object space to UV space.
This script assumes that the user has selected a polygon object and a spline object in the order
mentioned. The script projects the points of the spline object onto the polygon object and creates
a texture from the UV coordinates of the projected points. The texture is then applied to the
polygon object.
The script uses the `GeRayCollider` class to find the intersection of rays cast from the points of
the spline object to the polygon object. The UV coordinates of the intersection points are then
calculated using the `HairLibrary` class. In the C++ API, one should use maxon::
GeometryUtilsInterface::CalculatePolygonPointST() instead.
Finally, using GeRayCollider is only an example for projecting points onto the mesh. In practice,
any other method can be used as long as it provides points that lie in the plane(s) of a polygon.
The meat of the example is in the `main()` function. The other functions are just fluff.
"""
import os
import c4d
import mxutils
import uuid
from mxutils import CheckType
doc: c4d.documents.BaseDocument # The currently active document.
op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`.
def CreateTexture(points: list[c4d.Vector], path: str, resolution: int = 1000) -> None:
"""Creates a texture from the given `points` and saves it to the given `path`.
Parameters:
path (str): The path to save the texture to.
points (list[c4d.Vector]): The points to create the texture from.
"""
# Check the input values for validity.
if os.path.exists(path):
raise FileExistsError(f"File already exists at path: {path}")
if not path.endswith(".png"):
raise ValueError("The path must end with '.png'.")
# Create a drawing canvas to draw the points on.
canvas: c4d.bitmaps.GeClipMap = CheckType(c4d.bitmaps.GeClipMap())
if not canvas.Init(resolution, resolution, 24):
raise MemoryError("Failed to initialize GeClipMap.")
# Fill the canvas with white.
canvas.BeginDraw()
canvas.SetColor(255, 255, 255)
canvas.FillRect(0, 0, resolution, resolution)
# Draw the points on the canvas.
canvas.SetColor(255, 0, 0)
for p in points:
x: int = int(p.x * resolution)
y: int = int(p.y * resolution)
x0: int = max(0, x - 1)
y0: int = max(0, y - 1)
x1: int = min(resolution, x + 1)
y1: int = min(resolution, y + 1)
canvas.FillRect(x0, y0, x1, y1)
canvas.EndDraw()
# Save the canvas to the given path.
bitmap: c4d.bitmaps.BaseBitmap = CheckType(canvas.GetBitmap())
bitmap.Save(path, c4d.FILTER_PNG)
c4d.bitmaps.ShowBitmap(bitmap)
def ApplyTexture(obj: c4d.BaseObject, path: str) -> None:
"""Applies the texture at the given `path` to the given `obj`.
"""
CheckType(obj, c4d.BaseObject)
# Check the input values for validity.
if not os.path.exists(path):
raise FileNotFoundError(f"File does not exist at path: {path}")
# Create a material and apply the texture to it.
material: c4d.BaseMaterial = CheckType(c4d.BaseMaterial(c4d.Mmaterial), c4d.BaseMaterial)
obj.GetDocument().InsertMaterial(material)
shader: c4d.BaseShader = CheckType(c4d.BaseShader(c4d.Xbitmap), c4d.BaseShader)
shader[c4d.BITMAPSHADER_FILENAME] = path
material.InsertShader(shader)
material[c4d.MATERIAL_COLOR_SHADER] = shader
material[c4d.MATERIAL_PREVIEWSIZE] = c4d.MATERIAL_PREVIEWSIZE_1024
# Apply the material to the object.
tag: c4d.TextureTag = CheckType(obj.MakeTag(c4d.Ttexture))
tag[c4d.TEXTURETAG_PROJECTION] = c4d.TEXTURETAG_PROJECTION_UVW
tag[c4d.TEXTURETAG_MATERIAL] = material
def CreateDebugRays(spline: c4d.SplineObject, p: c4d.Vector) -> None:
"""Adds spline objects to the document to visualize the rays from the given `p` to the points of
the given `spline`.
"""
doc: c4d.documents.BaseDocument = CheckType(spline.GetDocument(), c4d.documents.BaseDocument)
rays: c4d.BaseObject = c4d.BaseObject(c4d.Onull)
rays.SetName("Rays")
doc.InsertObject(rays)
for q in spline.GetAllPoints():
ray: c4d.SplineObject = c4d.SplineObject(2, c4d.SPLINETYPE_LINEAR)
ray.SetPoint(0, p)
ray.SetPoint(1, q * spline.GetMg())
ray.Message(c4d.MSG_UPDATE)
ray.InsertUnder(rays)
def main() -> None:
"""Carries out the main logic of the script.
"""
# Check the object selection for being meaningful input.
selected: list[c4d.BaseObject] = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER)
if (len(selected) != 2 or not selected[0].CheckType(c4d.Opolygon) or
not selected[1].CheckType(c4d.Ospline)):
raise ValueError("Please select a polygon object and a spline object.")
polygonObject, splineObject = selected
# Get the uvw tag, the points, and the polygons of the polygon object.
uvwTag: c4d.UvwTag = mxutils.CheckType(polygonObject.GetTag(c4d.Tuvw))
points: list[c4d.Vector] = [polygonObject.GetMg() * p for p in polygonObject.GetAllPoints()]
polys: list[c4d.CPolygon] = polygonObject.GetAllPolygons()
# We are casting here in a dumb manner towards the center of the polygon object. In practice,
# one should cast rays towards the plane of the polygon object. Or even better, use another
# method to project the points onto the polygon object, as GeRayCollider is not the most
# efficient thing in the world.
rayTarget: c4d.Vector = polygonObject.GetMg().off
CreateDebugRays(splineObject, rayTarget)
# Initialize the GeRayCollider to find the intersection of rays cast from the points of the
# spline object to the polygon object.
collider: c4d.utils.GeRayCollider = c4d.utils.GeRayCollider()
if not collider.Init(polygonObject):
raise MemoryError("Failed to initialize GeRayCollider.")
# Init our output list and iterate over the points of the spline object.
uvPoints: list[c4d.Vector] = []
for p in splineObject.GetAllPoints():
# Transform the point from object to world space (q) and then to the polygon object's space
# (ro). Our ray direction always points towards the center of the polygon object.
q: c4d.Vector = splineObject.GetMg() * p
ro: c4d.Vector = ~polygonObject.GetMg() * q
rd: c4d.Vector = rayTarget - ro
# Cast the ray and check if it intersects with the polygon object.
if not collider.Intersect(ro, rd, 1E6) or collider.GetIntersectionCount() < 1:
continue
# Get the hit position and the polygon ID of the intersection.
hit: dict = collider.GetNearestIntersection()
pos: c4d.Vector = mxutils.CheckType(hit.get("hitpos", None), c4d.Vector)
pid: int = mxutils.CheckType(hit.get("face_id", None), int)
# One mistake would be now to use the barycentric coordinates that are in the intersection
# data, as Cinema uses an optimized algorithm to interpolate in a quad and not the standard
# cartesian-barycentric conversion. In Python these polygon weights are only exposed in a
# bit weird place, the hair library. In C++ these barycentric coordinates make sense because
# there exist methods to convert them to weights. In Python the barycentric coordinates are
# pretty much useless as we do not have such a conversion function here.
# Compute the weights s, t for the intersection point in the polygon.
s, t = c4d.modules.hair.HairLibrary().GetPolyPointST(
pos, points[polys[pid].a], points[polys[pid].b],
points[polys[pid].c], points[polys[pid].d], True)
# Get the uv polygon and bilinearly interpolate the coordinates using the weights. It would
# be better to use the more low-level variable tag data access functions in VariableTag
# than UvwTag.GetSlow() in a real-world scenario.
uvw: list[c4d.Vector] = list(uvwTag.GetSlow(pid).values())
t0: c4d.Vector = c4d.utils.MixVec(uvw[0], uvw[1], s)
t1: c4d.Vector = c4d.utils.MixVec(uvw[3], uvw[2], s)
uv: c4d.Vector = c4d.utils.MixVec(t0, t1, t)
# Append the UV coordinates to the output list.
uvPoints.append(uv)
# Write the UV coordinates to a texture and apply it to the polygon object.
path: str = os.path.join(os.path.dirname(__file__), f"image-{uuid.uuid4()}.png")
CreateTexture(uvPoints, path, resolution=1024)
ApplyTexture(polygonObject, path)
c4d.EventAdd()
if __name__ == '__main__':
main()
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
Dear community,
We will have to touch multiple parts of developers.maxon.net
on the 18.01.2024 and 19.01.2024 22.01.2024. This will result in outages of our documentation and the forum these days. I will try to keep the outage times to a minimum and it will certainly not span the whole two days. But especially one task I will do on Friday might take hours to complete and I can only do that on a forum which is in maintenance mode.
Please make sure to download a recent offline documentation in case you plan to do extended development work the next two days. As a result, forum support might also be delayed on these days.
Cheers,
Ferdinand
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
Dear development community,
On September the 10th, 2024, Maxon Computer released Cinema 4D 2025.0.0. For an overview of the new features of Cinema 4D 2025.0, please refer to the release announcement. Alongside this release, a new Cinema 4D SDK and SDK documentation have been released, reflecting the API changes for 2025.0.0. The major changes are:
cinema
namespace has been introduced which contains all the entities which were formerly in the anonymous global namespace known as the Classic API. Plugin authors must adopt their code to this new API, although the changes are not nearly as extensive as for 2024. See the 2025 migration guide for details. Code examples and documentation have been updated to now refer to a Cinema API.c4d
package remains the home for all formerly Classic and now Cinema API entities.Head to our download section for the newest SDK downloads, or the C++ and Python API change notes for an in detail overview of the changes.
We discovered late in the cycle bugs in the Asset API code examples and OCIO code in the Python SDK. Which is why the publication of the Python SDK and GitHub code examples has been postponed until these bugs are fixed. They should be ready latest by Friday the 13th of September. But the Python online documentation is accessible and error free (to our knowledge).
We had to make some last minute changes to the C++ SDK regarding OCIO code examples. Only the extended C++ SDK contains these changes. The application provided
sdk.zip
will catch up with the next release of Cinema 4D.
Happy rendering and coding,
the Maxon SDK Team
Cloudflare unfortunately still does interfere with our server cache. And you might have to refresh your cache manually.
When you are not automatically redirected to the new versions, and also do not see 2024.5 in the version selector, please press
CTRL + F5
or pressCTRL
and click on the reload icon of your browser anywhere ondevelopers.maxon.net/docs/
to refresh your cache. You only have to do this once and it will apply to all documentations at once. Otherwise your cache will automatically update latest by 19/07/2024 00:00.
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
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
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)
Hello @markeee,
Welcome to the Maxon developers forum and its community, it is great to have you with us!
Before creating your next postings, we would recommend making yourself accustomed with our forum and support procedures. You did not do anything wrong, we point all new users to these rules.
It is strongly recommended to read the first two topics carefully, especially the section Support Procedures: Asking Questions.
I really do not want to demotivate you in your first posting, but what you are trying is not trivial and this is one of the cases where when you have to ask, it is probably not for you. The Place tool is not exposed in the public API and so is not the general Rigid Body Dynamics API. Writing something like the place tool yourself is a very hard task.
What is relatively easy, is just to create an RBD setup, and then animate that for X frames, in the hopes that it until then has settled.
Cheers,
Ferdinand
"""Creates a little RBD setup to place an object on top of another object.
I am using here a plane as the object to settle on, but it could also be a non-planar object. Must
be run as a Script Manager script and will place/create a Platonic, as if it had fallen on the world
grid and settled on the origin of it.
Since this must run a simulation, the script will run a while (and block the UI in the mean time).
One could make this nicer with threading and statusbar spam, but I did not :)
"""
import c4d
import mxutils
doc: c4d.documents.BaseDocument # The currently active document.
op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`.
def main() -> None:
"""Called by Cinema 4D when the script is being executed.
"""
# Generate a platonic object and a plane object, scale up the plane, move the platonic 500 units
# up on the y-axis, and finally create a little RBD setup.
platonic: c4d.BaseObject = mxutils.CheckType(c4d.BaseObject(c4d.Oplatonic))
plane: c4d.BaseObject = mxutils.CheckType(c4d.BaseObject(c4d.Oplane))
plane[c4d.PRIM_PLANE_WIDTH] = 5000
plane[c4d.PRIM_PLANE_HEIGHT] = 5000
platonic.SetMg(c4d.Matrix(off=c4d.Vector(0, 500, 0)))
mxutils.CheckType(plane.MakeTag(c4d.Tcollider))
rbd: c4d.BaseTag = mxutils.CheckType(platonic.MakeTag(c4d.Trigidbody))
rbd[c4d.RIGIDBODY_PBD_CUSTOM_INITIAL_VELOCITY] = True
rbd[c4d.RIGIDBODY_PBD_CUSTOM_INITIAL_ANGULAR_VELOCITY] = True
rbd[c4d.RIGIDBODY_PBD_INITIAL_LINEAR_VELOCITY] = c4d.Vector(-.1)
# Create a dummy document, insert our little rig, and execute the passes for 100 frames, in the
# hopes that the platonic has settled until then.
temp: c4d.documents.BaseDocument = mxutils.CheckType(c4d.documents.BaseDocument())
temp.InsertObject(plane)
temp.InsertObject(platonic)
fps: int = temp.GetFps()
for i in range(100):
temp.SetTime(c4d.BaseTime(i, fps))
if not temp.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE):
raise RuntimeError(f"Failed to execute pass for {c4d.BaseTime(i, fps)} for {temp}.")
# Get the matrix from the settled object. We can zero out the x/z pos, so that the objects sits
# dead-center in the world grid.
mg: c4d.Matrix = platonic.GetMg()
mg.off = c4d.Vector(0, mg.off.y, 0)
# Create a new platonic (with the same settings) and insert into the active document and set our
# computed matrix.
result: c4d.BaseObject = mxutils.CheckType(c4d.BaseObject(c4d.Oplatonic))
result.SetMg(mg)
doc.InsertObject(result)
# Cinema 4D will garbage collect #temp and its content on its own, but we can call flush to make
# it a bit cleaner.
temp.Flush()
# Push an update event.
c4d.EventAdd()
if __name__ == '__main__':
main()
Hey @freeze,
yes, clonerObj
would be in your case the object which holds the generated MoGraph particle output in its cache. But generally speaking, you should not reach into caches. Sometimes it is necessary, but caches (be it generator or deform caches) are a dynamically generated part of the scene graph you usually should not interact with.
Just getting the matrices of the elements in the cache is fine (to print them or do something similar harmless). Establishing pointers into caches or relying on the fact that there is an established cache is not fine (note that my code checks for the cache actually being populated).
I still do not really understand what you want to do, but it strikes me as being on the 'you should really not do that' side. As I said in the beginning, MoGraph is a particle system and you are only supposed to interact with the abstract particle data.
I have the feeling you want to interact with the MoGraph particles as if they were tangible objects in the scene graph. This is simply not supported/intended.
Cheers,
Ferdinand
Hey @freeze,
I am still not quite sure how you mean that. MoGraph is a particle system under the hood, i.e., you will not find the actual cloned objects in it unless you reach into the cache of the cloner. The particle arrays is all there exists, e.g., MODATA_MATRIX
for the transform of the particles. When you want to retrieve the matrix of the input object, you must get that.
Cheers,
Ferdinand
import c4d
import mxutils
doc: c4d.documents.BaseDocument # The currently active document.
op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`.
def main() -> None:
"""Called by Cinema 4D when the script is being executed.
"""
moData: c4d.modules.mograph.MoData | None = c4d.modules.mograph.GeGetMoData(op) if op else None
if not moData:
raise RuntimeError("Please select an object that holds MoGraph particle data.")
print ("Particles:\n")
# Iterate over the (local) particle matrices for this MoGraph data. Local means here in relation
# to the MoGraph generator, i.e., cloner.
for i, ml in enumerate(moData.GetArray(c4d.MODATA_MATRIX)):
print(f"{i = }, {ml = }")
print ("\nInputs:\n")
# Iterate over the child objects of the cloner, i.e., the clone sources.
for node in op.GetChildren():
print (f"{node.GetName() = }, {node.GetMg() = }")
print ("\nCache:\n")
# Look into the cache of the cloner which might contain the flattened particle data (when in
# Instance or Render-Instance mode). When the cloner is in Multi-Instance mode, the cache will
# not be (fully) populated.
cache: c4d.BaseObject | None = op.GetCache()
if cache:
for node in mxutils.IterateTree(cache):
print (f"{node.GetName() = }, {node.GetMg() = }")
if __name__ == '__main__':
main()
Hey @freeze,
Thank you for reaching out to us. I do not understand your question, specifically this part:
the matrixItemsCnt always be 60 ,full of the instances in this case ,same as the Matrix I got.Any way can I just get the clones shown as the random result?
Could you please reiterate what you want to achieve or what you would consider incorrect/surprising about your results?
Cheers,
Ferdinand
Hey @BigRoy,
Thank you for reaching out to us. What Python is trying to tell you, is that the data type at this parameter is not wrapped for the Python API. Not every data type known to the Cinema 4D API, and there are probably hundreds when you count plugins, is exposed to Python.
The data types in the core Cinema API (i.e., things in what the Python API calls c4d
) are mostly wrapped for Python (but there are also exceptions as for example ItemTreeData where the Python API has no clue what to do with such paramater). Redshift is for example a prime offender and has introduced many data types which are inaccessible in Python (at least as a whole).
What you can however often do, when the author of the data type implemented it, is channel access. See here and here for examples. Since you do not show us what description_id
is, I can only speculate here. My guess would be something from the "lower" section of the user data, as these types are all not wrapped (usually):
I doubt that this is a regression, as this error is an intended feature of our API. It is more likely that you are now iterating over different user data than you did in 2023 and 2024. When you think differently, you would have to provide us a bit more information, specifically the data type and the scene file in which you are trying to iterate on some user data. I.e., just print out your description_id
as this is a DescID, which will also contain type information.
E.g.,
for descid, _ in op.GetUserDataContainer():
# You could also use C4Datom.GetParameter as this is a bit more elegant and does not need a try/catch:
try:
value = obj[descid]
except:
# Will print something like ((700, 5, 0), (1, 19, 0)) where the second triple is the actual parameter
# (desc-level) and a desc level is composed as (ID, TYPE, CREATOR). I.e., the data type would be
# 19 here, which is the value of `DTYPE_REAL`, i.e., a float parameter. When the data type integer
# is a value in the millions, e.g., (1, 1234567, 0)), this is plugin data type from a third party. These
# data types cannot be wrapped in Python (only component access works).
print (descid)
So, long story short: The error is an intended feature. And when there would be a regression (which strikes me as quite unlikely), your code was always dagerous, as it assumes that you can read all parameter data types in Python (which you cannot). For details we would need the data type of the failing parameter and a scene file (when it is one of the native data types which are wrapped).
Cheers,
Ferdinand
Hello @uogygiuol,
Welcome to the Maxon developers forum and its community, it is great to have you with us!
Before creating your next postings, we would recommend making yourself accustomed with our forum and support procedures. You did not do anything wrong, we point all new users to these rules.
It is strongly recommended to read the first two topics carefully, especially the section Support Procedures: Asking Questions.
You can search for objects with c4d.documents.BaseDocument.SearchObject.
doc: c4d.documents.BaseDocument = c4d.documents.GetActiveDocument()
target: c4d.BaseObject | None = doc.SearchObject("Eberhardt")
if target is None:
raise RuntimeError("Could not find target.")
Apart from the fact that names are a poor way to identify objects, you can also not find multiple objects with the same name in this manner. With release 2025 we added abstracted scene iteration.
doc: c4d.documents.BaseDocument = c4d.documents.GetActiveDocument()
op: c4d.BaseObject = doc.GetFirstObject()
# Walk the obejct tree and look for objects with a given name.
for node in mxutils.IterateTree(op, True, True, True):
if node.GetName() == "Eberhardt":
pass
# There is also abstract scene iteration now, here we look for any node named "Eberhardt" in #doc.
for node in mxutils.RecurseGraph(doc, yieldBranches=True, yieldHierarchy=True):
if node.GetName() == "Eberhardt":
pass
# Or for objects and tags named "Eberhardt". Check the docs on #RecurseGraph, there are a lot of
# options for finetuning the scene traversal.
for node in mxutils.RecurseGraph(doc, yieldBranches=True, yieldHierarchy=True, nodeFilter=[c4d.Obase, c4d.Tbase]):
if node.GetName() == "Eberhardt":
pass
Last but not least, what you are doing in your example code, recursion (at least unbound recursion) should be avoided, as this can quite easily lead to stack overflows. See this thread for how to do scene traversal manually.
Cheers,
Ferdinand
Hello @AG_Vivimagic,
Welcome to the Maxon developers forum and its community, it is great to have you with us!
Before creating your next postings, we would recommend making yourself accustomed with our forum and support procedures. You did not do anything wrong, we point all new users to these rules.
It is strongly recommended to read the first two topics carefully, especially the section Support Procedures: Asking Questions.
First of all, your script strikes me as sourced from or at least written with the help of a chat bot. Please disclose when you use AI, otherwise we might refuse to help you. Please also have a look at Support Procedures: Asking Questions, 'I am getting the render settings to align and execute however it is not updating the UI. It need to look like this !' is not a proper question.
I am assuming that you want to have similar setup of video post effects in your render data, as if when you choose the Preview Hardware Renderer manually. To achieve that, you must manually curate the effects. You should also avoid using raw integer values when setting parameters, as this leads to confusing code and easily to errors. newRenderData.SetBit(8) #make it selected
does for example not do what you think it does. The value 8 is BIT_ENABLEPAINT
, but you probably mean here BIT_ACTIVE
(2).
Cheers,
Ferdinand
import c4d
doc: c4d.documents.BaseDocument # The currently active document.
op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`.
def main() -> None:
"""Called by Cinema 4D when the script is being executed.
"""
# Set the document FPS.
doc[c4d.DOCUMENT_FPS] = 25
# Create a new render data instance with the values you wanted.
rdata: c4d.documents.RenderData = c4d.documents.RenderData()
rdata.SetName("Playblast")
rdata[c4d.RDATA_RENDERENGINE] = c4d.RDATA_RENDERENGINE_PREVIEWHARDWARE
rdata[c4d.RDATA_XRES_VIRTUAL] = 1920
rdata[c4d.RDATA_YRES_VIRTUAL] = 1080
rdata[c4d.RDATA_FRAMERATE] = 25
rdata[c4d.RDATA_FRAMESEQUENCE] = c4d.RDATA_FRAMESEQUENCE_ALLFRAMES
rdata[c4d.RDATA_FORMAT] = c4d.FILTER_MOVIE
# We must now curate the video post effects for this render data. This is probably a bit overkill,
# we could also just delete all of them, since we know that the Preview Renderer only uses MBL which
# cannot be removed. But there could be a third party effect which supports the preview renderer
# and which is present by default, so we do it the proper way by checking each effect for being
# compatible with the selected render engine.
effect: c4d.documents.BaseVideoPost = rdata.GetFirstVideoPost()
while effect:
nxt: c4d.documents.BaseVideoPost = effect.GetNext()
if not effect.RenderEngineCheck(c4d.RDATA_RENDERENGINE_PREVIEWHARDWARE):
effect.Remove()
effect = nxt
# Add the native RDATA_RENDERENGINE_PREVIEWHARDWARE effect.
previewEffect: c4d.documents.BaseVideoPost = c4d.documents.BaseVideoPost(
c4d.RDATA_RENDERENGINE_PREVIEWHARDWARE)
rdata.InsertVideoPostLast(previewEffect)
# Insert the render data and do the other things you wanted to do.
doc.InsertRenderDataLast(rdata)
doc.SetActiveRenderData(rdata)
rdata.SetBit(c4d.BIT_ACTIVE)
c4d.CallCommand(12161)
if __name__ == '__main__':
main()
Hey @BruceC,
Thank you for the added information, but I will still need that step by step bug report. Without it, I will not touch a debugger or a scene file. There are a plethora of reasons why this could go wrong. I am still very much in the dark at what you are looking exactly (e.g., are you using our Picture Viewer to view render results, or do you use your render engines one. What are your rendering and how, etc., pp.).
As I told you, applying a manual transform is not intended. It might be what you have to do as a work-around for older versions which we won't (hot)fix anymore, but there is no default answer here for you. But for all of that I first need a reliable bug/issue report which I can reproduce, and then other than you look at with a debug version of Cinema 4D. As I said, feel free to use your render engine, and please be precise in the reproduction steps. If you want to, you can also use our beta program bug tracker instead (since you are an MRD).
Cheers,
Ferdinand
Hey @Cankar001,
Good to hear that you found your solution! One minor thing - you should avoid ApplicationOutput
in production code, as it leads to console spam which we want to avoid in Cinema 4D. Using it in test code is fine. See Debug and Output Functions for alternatives.
An even better way to do what you did in your code would be to use error handling. E.g., your code could look like this:
// Your original function, I turned this into a function using our error system, indicated by the
// Result<T> return type.
static maxon::Result<maxon::Float> GetCurrentUnitScale(const BaseDocument* const document)
{
// The error scope handler for this function, i.e., all error returns exit through this handler.
iferr_scope;
// When there is no document, we return an error. When printed, this will then print the error
// message and the source code location, e.g., myfile.cpp:123.
if (!document)
return maxon::NullptrError(MAXON_SOURCE_LOCATION, "Invalid document pointer."_s);
// Your code goes here.
// ...
return 1.0f;
}
// A function calling this function which does use error handling itself.
static maxon::Result<void> Foo(const BaseDocument* const document)
{
iferr_scope;
// Call the function and return, i.e., propagate the error upwards when there is one.
const maxon::Float unitScale = GetCurrentUnitScale(document) iferr_return;
// voids in the Result<T> return type are returned with maxon::OK.
return maxon::OK;
}
// A function calling this function which does not use error handling itself, i.e., the error
// must terminate here.
static bool Bar(const BaseDocument* const document)
{
// Here we use a manual scope handler to terminate the error chain. You have often to do this in
// Cinema API (e.g., ObjectData::GetVirtualObjects), as methods there are not error handled
// opposed to the Maxon API.
iferr_scope_handler
{
// Print a debug message with the passed in error #err and the name of this function. And
// force a debugger to halt when some condition is met.
WarningOutput("@ failed with error: @"_s, MAXON_FUNCTIONNAME, err);
if (someErrorCondition)
DebugStop();
return false;
};
// Call the function (we still have to handle the error with an iferr_return), and then let it
// terminate in our manual scope handler.
const maxon::Float unitScale = GetCurrentUnitScale(document) iferr_return;
return true;
}
Cheers,
Ferdinand