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 @Havremunken,
no, there is no code example for what you are trying to do. There is the ActiveObject C++ Example, which is supposed to demonstrate branching but does IMHO a terrible job at it. When you want to look at some resources, I would recommend these (although none of them will cover your subject exactly):
Branching is how Cinema 4D realizes arbitrary entity relationships, i.e., a scene graph. By default, it seems a bit like Cinema 4D only implements hierarchical relationships via GeListNode
, but that is not true, as there is also the function GeListNode::GetBranchInfo
. So, while a node can have a parent, a list of children, and up to two siblings, it can also have a list of other relations which are explicitly not hierarchical.
A BaseDocument
is for example also a node, its previous and next siblings realize the list of loaded documents. The branches of a document realize all its content, the Obase
branch holds for example the GeListHead
(a special type of GeListNode
for the root of a branch) to which all top level objects of the scene are parented (which then hold their local hierarchies). The 'trick' is then that every node in a scene has such branches, an object has branches for its tags, tracks, and more; a material has branches for its shaders, tracks, and more, and so on.
Please see the links I have posted above for details.
Generally, at least without getting hacky, you cannot implement branches for nodes that you do not own. So, you can not add a foo branch to documents or objects in general, you can only add a foo branch to the specific MyFooObjectData
plugin you implement (or any other node type you implement). Since, it worked apparently quite well last time, I will just write some mock code of what you are trying to do (very high level and obviously untested and uncompiled).
// Plugin ID for the generic asset node and at the same time its branch ID. T is not only the prefix
// for tag types but also generic node types, e.g., Tbasedocument, Tpluginlayer, Tgelistnode, etc.
static const Int32 Tassetnode = 1030001;
// Realizes your asset manager just as before but this time it internalizes its asset data.
//
// Since a scene hook is a node and part of the scene graph, it can have children and branches.
// We therefore are going to establish an Tassetnode branch in the scene hook.
class AssetAccessManager : public SceneHookData
{
INSTANCEOF(AssetAccessManager, SceneHookData)
// The head of our custom asset branch.
private:
GeListHead* _assetHead;
public:
// Returns either an existing asset branch head for the AssetAccessManager node or creates a new one.
GeListHead* GetAssetHead()
{
iferr_scope_handler
{
return nullptr;
};
// We already established an asset head before.
if (_assetHead)
return _assetHead;
// Get the scene hook node for #this SceneHookData and try to find its asset branch.
const GeListNode* node = Get();
if (!node)
return nullptr;
Bool result = node->GetBranchInfo(
[_assetHead](const BranchInfo& info) -> maxon::Result<maxon::Bool>
{
// Check if the branch is an asset branch.
if (info.id != Tassetnode)
return false; // Continue searching/iterating.
// We found the asset branch, we can stop iterating.
_assetHead = info.head;
return true;
},
GETBRANCHINFO::NONE) iferr_return;
if (_assetHead)
return _assetHead;
// Create a new branch head for this asset manager's asset branch.
_assetHead = GeListHead::Alloc();
if (!_assetHead)
return nullptr;
// Attach the branch to the scene hook.
_assetHead->SetParent(node);
return _assetHead;
}
// Implements the branch info for your node, so that Cinema 4D knows about the asset branch.
// It is important to implement this, without it Cinema 4D will crash.
Int32 FlowTag::GetBranchInfo(GeListNode* node, BranchInfo* info, Int32 max, GETBRANCHINFO flags)
{
GeListHead* assetHead = GetAssetHead();
if (!assetHead)
return 0;
info[0].head = assetHead; // The head for the new branch.
info[0].id = Tassetnode; // The ID of the branch. This is usually the ID of the
// types of nodes to be found in the branch, e.g., Obase.
info[0].name = "myAssetData"_s; // A label for humans for the branch, make sure it is
// reasonably unique, there might be bad code out there
// which tries to find branches by name.
info[0].flags = BRANCHINFOFLAGS::NONE; // The flags for this branch, see docs for details.
return 1 // The number of branches we have defined.
}
// Realizes a custom function to add assets to the asset manager and with that to the scene.
BaseList2D* AddAsset(const Url& url)
{
// Instantiate an asset node, it fully encapsulates the loading and storage of asset data.
BaseList2D* asset = BaseList2D::Alloc(Tassetnode);
if (!asset)
return nullptr;
// Load the asset data from the URL, this is done via a custom message, just as discussed in
// example from the old thread.
asset->Message(MSG_LOAD_ASSET, (void*)&url);
// Add the asset to the asset branch. We realize here just a plain list, but we could also have
// complex hierarchies, or assets which have branches themselves.
GeListHead* assetHead = GetAssetHead();
if (!assetHead)
return nullptr;
asset->InsertUnderLast(assetHead);
return asset;
}
};
// Realizes the asset node.
//
// We realize here a plain NodeData, the base type of ObjectData, TagData, SceneHookData, etc. We
// could also realize a specialized node, but custom branches are one of the few cases where it can
// make sense to realize a plain node. But you could also realize a specialized node, e.g.,
// MyAssetObjectData. You can also store existing node types in your branches, e.g., it is totally
// valid to implement your own Obase (i.e., object) branch. Might be better in your asset case, I do
// not know. In that case you could only serialize the data an object does serialize, points,
// polygons, tags, tracks, etc., but would not have to implement your own node. Pulling up an asset
// would then also not work via a message but via a custom function, e.g., static BaseObject*
// LoadAsset(const Url& url).
//
// See also:
// SDK/plugins/example.main/source/object/objectdata_hyperfile.cpp
class MyAssetData : public NodeData
{
INSTANCEOF(AssetAccessManager, SceneHookData)
private:
// The asset data, this is just a mock, you would have to implement this. This example is extra
// stupid, because something like an Int32 is easily stored in the data container of a node. An
// option which always exists, and while slightly less performant than customizing reading and
// writing the node, it is also much easier to implement.
Int32 _assetData;
// Loads the asset data from a URL.
Bool LoadAsset(const Url& url)
{
_assetData = 42;
return true;
}
public:
// Handles messages for the asset node.
Bool Message(GeListNode* node, Int32 type, void* data)
{
switch (type)
{
case MSG_LOAD_ASSET:
{
// Load the asset data from the URL.
Url* url = reinterpret_cast<Url*>(data);
if (!url)
return false;
return LoadAsset(*url);
}
break;
}
return true;
}
// Serializes the asset node. What I am showing here should only be done when we have to serialize
// truly complex data, as for example a texture with meta data. For that specific case one could
// ask how sensible it is to blow up scene sizes by internalizing textures in the scene graph.
// Then one could end up with a compromise, where one stores only information in the scene about
// where a localized texture has been stored, i.e., we just reinvented texture files :D.
// When you implement serialization, you must always implement Read, Write, and CopyTo. See the
// NodeData manual and SDK for details, I am too lazy to even mock-up all of them here, I am only
// going to demo Read/Write.
// Writes all data of this node to the scene file that is not already stored in the node's data
// container.
Bool Write(GeListNode* node, HyperFile* hf)
{
if (!hf)
return false;
// Write the asset data to the scene file.
if (hf->WriteInt32(_assetData) == 0)
return false;
return true;
}
// Reads all data of this node from the scene file that is not already stored in the node's data
// container.
Bool Read(GeListNode* node, HyperFile* hf, Int32 level)
{
if (!hf)
return false;
// Read the asset data from the scene file.
if (hf->ReadInt32(&_assetData) == 0)
return false;
return true;
}
}
I hope this gets you going. I cannot stress enough that this is a sktech.This will quite likely not compile, but it demonstrates the core methods and principles. Feel free to come back with concrete code yours when you need help.
But I currently do not have the time to write a full example for this from scratch.
Cheeers,
Ferdinand
Cheers,
Ferdinand
Hey @BruceC,
So I think in this case, it could be safely assume the data is linear values?
Normally, there should be no assumptions, the data should just be what the color management says. But I now replicated your field driving colors setup, and I too stumbled upon something which I cannot explain, at least at the moment.
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 get these parameters just as any others. Please show us the code you have, as I am not going to write here a complex node example from scratch. The ramp itself is just a variadic port, and each knot in the ramp is then a port bundle of type com.redshift3d.redshift4c4d.portbundle.gradient
.
So, the node path for the input port of the knot position of the 0ths node would be:
// This node path goes from the root of the true node, i.e., the RS Ramp node itself.
const String path = "<com.redshift3d.redshift4c4d.nodes.core.rsramp.ramp/_0/position"_s;
Please note that there is absolutely no guarantee that each variadic port does gives its 0th child the ID "_0" and its n-th child the ID "_n-1". I just happen to know that this will be the case for a default RS Ramp node. In practice you will always have to get all children of the varidiadic port, and then select the desired child by index in that list.
Cheers,
Ferdinand
Hello @myosis,
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.
All the methods you list simply do not exist (neither in C++ nor in Python), see c4d.utils.Neighbor for the type overview.
I therefore must assume that you are using an AI, like, for example, ChatGPT, which hallucinated these methods. Please note that we reserve the right to refuse support when confronted with undisclosed AI gibberish, especially for beginner content. Always state when you used an AI to generate code. |
Something such as an edge does not exist concretely in our API and many other APIs, i.e., other than for points and polygons, there is no explicit data type for edges which would be stored. Edges are defined implicitly by CPolygon
. To filter a selection for edges of a certain length, you would have to convert edge indices to polygon and point indices and then measure the distance between the relevant points.
Cheers,
Ferdinand
"""Deselects all edges in the edge selection of the active object whose edge length exceeds
MAX_EDGE_LENGTH.
Must be run as a Script Manager script with an editable polygon object selected.
"""
import c4d
doc: c4d.documents.BaseDocument # The currently active document.
op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`.
MAX_EDGE_LENGTH: float = 25.0 # The maximum length of an edge before to be considered too long.
MAX_EDGE_LENGTH_SQUARED: float = MAX_EDGE_LENGTH ** 2 # The square of `MAX_EDGE_LENGTH`.
def main() -> None:
"""Called by Cinema 4D when the script is being executed.
"""
if not op or not op.IsInstanceOf(c4d.Opolygon):
raise ValueError("The selected object is not a polygon object.")
# Get the edge selection of the object and turn it into a list of selected edges indices. Also,
# get the points and polygons of the object.
selection: c4d.BaseSelect = op.GetEdgeS()
selectedEdges: list[int] = [i for i in range(op.GetEdgeCount()) if selection.IsSelected(i)]
points: list[c4d.Vector] = op.GetAllPoints()
polygons: list[c4d.CPolygon] = op.GetAllPolygons()
def getPointByIndex(poly: c4d.CPolygon, index: int) -> c4d.Vector:
"""Returns the point of the polygon at the given index.
CPolygon has no index access, so we fix that here.
"""
if index == 0:
return points[poly.a]
elif index == 1:
return points[poly.b]
elif index == 2:
return points[poly.c]
elif index == 3:
return points[poly.d]
# Iterate over the edges and find the one's that are longer than MAX_EDGE_LENGTH. An edge index
# is defined as:
#
# "The edges are indexed by 4 * polygon + edge where polygon is the polygon index and edge is
# the edge index between 0 and 3."
#
# So, we must revert that here, then measure the edge length, and collect all too long edges.
tooLongEdges: list[int] = []
for edgeIndex in selectedEdges:
polygonIndex: int = edgeIndex // 4
edgeInPolygonIndex: int = edgeIndex % 4
poly: c4d.CPolygon = polygons[polygonIndex]
pointA: c4d.Vector = getPointByIndex(poly, edgeInPolygonIndex)
pointB: c4d.Vector = getPointByIndex(poly, (edgeInPolygonIndex + 1) % 4)
# Getting the length of a vector is quite expensive, so we compare the squared lengths.
edgeLengthSq: float = (pointA - pointB).GetLengthSquared()
if edgeLengthSq > MAX_EDGE_LENGTH_SQUARED:
tooLongEdges.append(edgeIndex)
# Print the indices of the edges that are too long.
print("The following edges are too long:", tooLongEdges)
# Deselect all edges in the object's edge selection that are too long.
for edgeIndex in tooLongEdges:
selection.Deselect(edgeIndex)
# Push an update event to Cinema 4D to redraw the object.
c4d.EventAdd()
if __name__ == '__main__':
main()
Hey @d_schmidt,
Thank you for reaching out to us. That feature has existed since Pyro was introduced but at the same time it is also an internal feature, i.e., a feature of which we publish the components, but which we do not support in public usage. To realize a node which supports name additions, you must register it with the flag OBJECT_CUSTOM_NAME_ADDITION:
RegisterObjectPlugin(ID_MY_PLUGIN, GeLoadString(IDS_MY_PLUGIN), otherFlags | OBJECT_CUSTOM_NAME_ADDITION, MyPlugin::Alloc, "omyplugin"_s, nullptr, 0);
Reading or writing to the custom name is then realised via the node message MSG_GETCUSTOM_NAME_ADDITION. I am showing the write access in C++:
Bool MyPlugin::Message(GeListNode* node, Int32 type, void* data)
{
...
switch (type)
{
// Something, for example the Object Manager, wants to know the addition to the name. The
// message data is just an empty string. The plugin should return the addition to the name.
case MSG_GETCUSTOM_NAME_ADDITION:
{
*reinterpret_cast<String*>(data) = "Hello World"_s;
break;
}
default:
break;
}
...
}
And since nothing is safe from @m_adam , I just found out that he wrapped the reading part for Python (cannot see the writing part handled in Python). Since you cannot cast arbitrary data in Python, we must use a dictionary as the data container.
Cheers,
Ferdinand
Hey @BruceC,
I think I understand now what your problem is and what you mean by 'MODATA_COLOR is always linear'.
@BruceC said:
I thought MODATA_COLOR is in computational color space which is controlled by the settings in Scene Settings Project -> COLOR MANAGEMENT, that's why I thought I can get the MODATA_COLOR's color space by an API. However, the colors values I got from MODATA_COLOR remain the same no matter if "Linear Workflow" checkbox is ticked or not [...]
Does this mean that MODATA_COLOR's value won't be affected by any scene level color space settings? It seems the color I got from MODATA_COLOR is always linear no matter if "Linear Workflow" checkbox is ticked.
The major misconception seems to be here what changing color management settings does in Cinema 4D (pre-2025 at least). As pointed out in my first answer, changing these settings does not (always) entail scene colors to be converted:
@ferdinand said:
What might here be your implicit question, is that nothing happens when you switch a scene. I.e., you have a scene that has been created with Basic color management with linear workflow turned off, and you now turn linear workflow on. This just changes in which color space scene color data is interpreted, it does not convert scene colors. For OCIO later then we created the scene color conversion functionality, which among other places is accessible as
SceneColorConverter
in the API.
When you toggle a scene between linear workflow on and off, literally nothing will happen to all the color parameters in it. What will change, is how Cinema 4D will bring colors in and out of that scene, i.e., what color value is written when the user picks a color in sRGB-2.1 space in a color picker, how a texture is converted before rendering, and how the final render data is then converted into an output bitmap.
Existing colors stored in a scene, are then just reinterpreted as colors in that new color space. Reinterpreted Iin the sense as that when they or their byproducts must be converted into other color spaces for display or file output, they will be treated as colors of that newly set color space (although their numerical values have been defined for a different space before). I.e., they effectively become different colors. That is not the most sensible thing from a modern OCIO perspective, but that is how 'Basic' mode worked.
With 2024 we then introduced OCIO, which had the same behavior, changing to OCIO or the render space in OCIO would not automatically entail a scene color conversion. But we added the option to do that (the backend is the mentioned SceneColorConverter
). With 2025 this now has changed and all render space changes also always entail a value conversion.
So, this is first of all not bound to MODATA_COLOR
and would apply to all color values in a scene. And when the scene is in Basic + NO LWF mode and your render texture uses an sRGB-1.0 profile, you would have to convert that color from sRGB-2.1 to sRGB-1.0, when the scene is in OCIO mode, you would have to convert from render space to sRGB-1.0. And when then scene is in Basic + LWF mode, you would not have to do anything at all.
To my knowledge there is currently no exception in the MoGraph code which would always treat MODATA_COLOR
as linear (if anything there could be an extra transform applied, but I am not ware of any in that case). When you still encounter a mismatch between your render engine output and our viewport, please reach out to us. It would then probably easier if you just show me.
Cheers,
Ferdinand
Hello @mia-elisenberg,
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.
Thank you for reaching out to us. The SDK is currently unstaffed, and I cannot just answer your question 'from the hip' at home. We will assign your topic by the end of the week and answer it then or at the start of the next week.
Thank you for your understanding,
Ferdinand
Hello @chuanzhen,
Thank you for reaching out to us. That is not a tree view, at least not the tree view from Python you are probably thinking of. This is a CUSTOMGUI_ITEMTREE with its matching data type ItemTreeData. This GUI is not exposed in Python and also not really exposed in C++ (because ItemTreeNode
is missing there). So, while you could add that element in your description, you could not really interact with it in Python (at all) or C++ (to a lesser extent here).
I have fully exposing and documenting this GUI in C++ on my backlog, maybe we could then think about exposing it in Python too. But there is a lot of type complexity behind it, which would mean a lot working for porting, since this is a Cinema API type. So, this likely will never be ported to Python.
Cheers,
Ferdinand
Hello @BruceC,
Thank you for providing more details. I have a better sense now of what you are trying to do, I am however still not sure about some details. Let me first state some facts and then ask you two questions at the end.
I have noticed that the new R2025 changed the color management system settings under Scene Settings -> Project -> COLOR MANAGEMENT.
Yes, that is true. As stated above, the legacy "Basic" color management has been removed with 2025. Cinema now always uses OCIO. What has also changed with 2025.0.0 is the color picker.
in both R2025 and R2024, it seems all possible options use linear color space for calculations
That is not entirely true, see the list I made in my first posting. In Cinema 4D versions before 2025.0.0, when you had "Basic" color management enabled and linear workflow disabled, your computational space was sRGB 2.1. But since LWF was the default in Basic mode for a long time, it is very unlikely to encounter that in the wild.
In R2024, I always found "The Application uses Linear Space for Calculations." in Basic COLOR MANAGEMENT regardless of the settings. In R2025, I found different information (e.g. "Calculations happen in linear sRGB space.", "Calculations happen in ACES2065-1 space.")
I am not sure where you found that information, but it is not correct (please let me know when the user docs publish such incorrect information). 2024 supported both OCIO and Basic color management, but even if we go back to for example S26 where only Basic existed, Cinema 4D could use a non-linear computational color space when the user did disable linear workflow.
In 2025, the OCIO render space can be technically anything since the whole purpose of OCIO is that the user can configure the three principal color spaces (render, display, view transform). ACES20265-1 is also not a working space (more or less OCIO/ACES slang for a computational space) but an input space, i.e., it is meant to bring in texture data. Setting it as the working space is quite unusual.
As already stated above, Cinema 4D is and always has used the default ACES working space ACEScg as its OCIO render space profile (fancy way of saying all data is assumed to be ACEScg when computations happen). As also stated above, there are some minor exceptions to this such as NodeData::Init where colors are interpreted as sRGB 2.2 (or more concretely - Cinema 4D will apply an sRGB-2.2->render-space transform to all colors set in NodeData::Init). See ACES: Encodings for details on the purpose of ACES color spaces.
But I don't know if the MODATA_COLOR data got above will always in linear color space. And I couldn't enumerate all possible COLOR MANAGEMENT settings to check. So I'd like to find out if there is a way to get the color space of the MODATA_COLOR data, so the correct color space could be set in the customized texture object.
MODATA_COLOR
will be in the color space I listed in my first posting under point 3. And as I said, there is no way "to find out" in what color space a color is, as this is not explicit metadata attached to a color. It would also contradict the idea of linear workflow and OCIO of having a unified computational color space.
I would also recommend having a look at:
Okay, now let's get to the questions. While I have a bit more concrete idea of what you are trying to do, there are still some question marks for me. You seem to try to write MoGraph color data into a 'customized texture object'.
Let's unfold a few more things here. Modern color management divides an application into at least three color spaces, an input space (that is how textures and color choices from the user come in), a computational space (render space in OCIO) to which all these input colors are converted to for rendering, and finally an output space, i.e., the color profile used by bitmap outputs of the app.
The in- and output spaces are other than the computational space usually not unified. I.e., the user could use both an old sRGB 2.1 PNG, a sRGB 1.0 HDR, and a fancy PSD in ACES2065-1 in his or her project. The same goes for the output space, the user could render the same document to different output spaces.
So, when you have some scene data, which is in the computational space of the scene, and you want to write that data into a texture, you will have to transform that color to the color space of the texture. When you are using cinema::BaseBitmap
as your bitmap type, there are dedicated color profiles attached to the bitmap for input (image), render, display, and view transform under which the bitmap shall be used.
In the end I am also guessing here a bit, since this heavily depends on how you render engine implements bitmaps, color management, and its rendering pipeline.
Cheers,
Fredinand
PS: Check the OCIO manual for color conversions. When you want to define your procedural texture in a fancy color space like for example 'ACES2065-1', you will need an ICC profile of that space. Not all OCIO/ACES color spaces are representable as ICC profiles (no clue if that applies here).
Hello @BruceC,
Thank you for reaching out to us. I am struggling with your question, since it is missing the version of Cinema 4D you are targeting and I find some points you are making a bit suprising.
Since 2025, Cinema 4D is permanently using OCIO as a color management system. The default render space profile is ACEScg which is a linear color space. Before in 2024 and earlier, when still "basic" color management was around, the default was also "linear workflow", i.e., the computational space was linear sRGB then.
All computations happen in the computational color space (render space in OCIO). There are some isolated exceptions to this, NodeData::Init is a big one in 2025, but the general rule applies. In the context of MoGraph exceptions can also apply, for example when you source your particle colors from a vertex color tag (which does not yet have a color profile), you can run into inconsistencies.
There is no function with which you could retrieve the color space of a color, as this would counter the idea of linear workflow/OCIO. All computations happen in the computational color space (LF), the render space (OCIO). The computational color spaces are:
I also checked an older 2024.2 version, and I could not spot anything out of the ordinary there with MODATA_COLOR
(I just printed some particle color values using different color management settings).
What might here be your implicit question, is that nothing happens when you switch a scene. I.e., you have a scene that has been created with Basic color management with linear workflow turned off, and you now turn linear workflow on. This just changes in which color space scene color data is interpreted, it does not convert scene colors. For OCIO later then we created the scene color conversion functionality, which among other places is accessible as SceneColorConverter in the API. But as a render engine you should usually not have to run this, as this will alter the physical data of a document, i.e., is something usually only the user should do. What also might trip you, is that you are misusing our colour picker (which is by default picking colors in sRGB 2.1).
But I am getting very speculative here. Please provide the Cinema 4D version you are targeting, and a code example and concrete scenario where your problems occur.
Cheers,
Ferdinand