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
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)
Hi,
a bilinear interpolation is quite straight forward. If you have the quadrilateral Q with the the points,
c---d
| |
a---b
then the bilinear interpolation is just,
ab = lerp(a, b, t0)
cd = lerp(c, d, t0)
res = lerp(ab, cd, t1)
where t0, t1
are the interpolation offset(s), i.e. the texture coordinates in your case (the ordering/orientation of the quad is obviously not set in stone). I am not quite sure what you do when rendering normals, but when you render a color gradient, in a value noise for example, you actually want to avoid linear interpolation, because it will give you these ugly star-patterns. So you might need something like a bi-quadratic, bi-cubic or bi-cosine interpolation, i.e. pre-interpolate your interpolation offsets.
If I am not overlooking something, this should also work for triangles when you treat them as quasi-quadrilaterals like Cinema does in its polygon type.
Cheers,
zipit
Hi @bentraje,
thank you for reaching out to us. As @Cairyn already said, there is little which prevents you from writing your own cloner. Depending on the complexity of the cloner, this can however be quite complex. Just interpolating between two positions or placing objects in a grid array is not too hard. But recreating all the complexity of a MoGraph cloner will be. It is also not necessary. Here are two more straight forward solutions:
Cheers,
Ferdinand
file: cube_bend_python.c4d
code:
"""Example for modifying the cache of a node and returning it as the output
of a generator.
This specific example drives the bend strength of bend objects contained in
a Mograph cloner object. The example is designed for a Python generator
object with a specific set of user data values. Please use the provided c4d
file if possible.
Note:
This example makes use of the function `CacheIterator()` for cache
iteration which has been proposed on other threads for the task of walking
a cache, looking for specific nodes. One can pass in one or multiple type
symbols for the node types to be retrieved from the cache. I did not
unpack the topic of caches here any further.
We are aware that robust cache walking can be a complex subject and
already did discuss adding such functionality to the SDK toolset in the
future, but for now users have to do that on their own.
As discussed in:
plugincafe.maxon.net/topic/13275/
"""
import c4d
# The cookie cutter cache iterator template, can be treated as a black-box,
# as it has little to do with the threads subject.
def CacheIterator(op, types=None):
"""An iterator for the elements of a BaseObject cache.
Handles both "normal" and deformed caches and has the capability to
filter by node type.
Args:
op (c4d.BaseObject): The node to walk the cache for.
types (Union[list, tuple, int, None], optional): A collection of type
IDs from one of which a yielded node has to be derived from. Will
yield all node types if None. Defaults to None.
Yields:
c4d.BaseObject: A cache element of op.
Raises:
TypeError: On argument type violations.
"""
if not isinstance(op, c4d.BaseObject):
msg = "Expected a BaseObject or derived class, got: {0}"
raise TypeError(msg.format(op.__class__.__name__))
if isinstance(types, int):
types = (types, )
if not isinstance(types, (tuple, list, type(None))):
msg = "Expected a tuple, list or None, got: {0}"
raise TypeError(msg.format(types.__class__.__name__))
# Try to retrieve the deformed cache of op.
temp = op.GetDeformCache()
if temp is not None:
for obj in CacheIterator(temp, types):
yield obj
# Try to retrieve the cache of op.
temp = op.GetCache()
if temp is not None:
for obj in CacheIterator(temp, types):
yield obj
# If op is not a control object.
if not op.GetBit(c4d.BIT_CONTROLOBJECT):
# Yield op if it is derived from one of the passed type symbols.
if types is None or any([op.IsInstanceOf(t) for t in types]):
yield op
# Walk the hierarchy of the cache.
temp = op.GetDown()
while temp:
for obj in CacheIterator(temp, types):
yield obj
temp = temp.GetNext()
def main():
"""
"""
# The user data.
node = op[c4d.ID_USERDATA, 1]
angle = op[c4d.ID_USERDATA, 2]
fieldList = op[c4d.ID_USERDATA, 3]
# Lazy parameter validation ;)
if None in (node, angle, fieldList):
raise AttributeError("Non-existent or non-populated user data.")
# Get the cache of the node and clone it (so that we have ownership).
cache = node.GetDeformCache() or node.GetCache()
if cache is None:
return c4d.BaseObject(c4d.Onull)
clone = cache.GetClone(c4d.COPYFLAGS_NONE)
# Iterate over all bend objects in the cache ...
for bend in CacheIterator(clone, c4d.Obend):
# ..., sample the field list for the bend object position, ...
fieldInput = c4d.modules.mograph.FieldInput([bend.GetMg().off], 1)
fieldOutput = fieldList.SampleListSimple(op, fieldInput,
c4d.FIELDSAMPLE_FLAG_VALUE)
if (not isinstance(fieldOutput, c4d.modules.mograph.FieldOutput) or
fieldOutput.GetCount() < 1):
raise RuntimeError("Error sampling field input.")
# ... and set the bend strength with that field weight as a multiple
# of the angle defined in the user data.
bend[c4d.DEFORMOBJECT_STRENGTH] = angle * fieldOutput.GetValue(0)
# Return the clone's cache.
return clone
Hello @shetal,
thank you for reaching out to us. The reformulation of your question and the conformance with the forum guidelines on tagging is also much appreciated.
About your question: As stated in the forum guidelines, we cannot provide full solutions for questions, but provide answers for specific questions. Which is why I will not show here any example code, the first step would have to be made by you. I will instead roughly line out the purpose of and workflow around VideoPostData
, which I assume is what you are looking for anyway.
VideoPostData is derived from NodeData, the base class to implement a node for a classic API scene graph. Node means here 'something that lives inside a document and is an addressable entity', examples for such nodes are materials, shaders, objects, tags, ..., and as such 'video post' node. As mentioned in its class description, VideoPostData is a versatile plugin interface which can be used to intervene a rendering process in multiple ways. The most tangible place for VideoPostData in the app is the render settings where video post plugins can be added as effects for a rendering process as shown below with the built-in water mark video post node.
VideoPostData is an effect, meaning that you cannot use it to invoke a rendering process and on its own it also cannot forcibly add itself to a rendering and must be included manually with the RenderData
, the render settings of a rendering. However, a user could make a render setting which includes such watermark effect part of his or her default render settings. One could also implement another plugin interface, SceneHookData
, to automatically add such effect to every active document. We would not encourage that though, as this could be confusing or infuriating for users. Finally, such VideoPostData
plugin would be visible by default like all NodeData
plugins, i.e., it would appear as something in menus that the user can add and interact with. To prevent this if desired, one would have to register the plugin with the flag PLUGINFLAG_HIDE
suppress it from popping up in the 'Effect ...' button menu. I cannot tell you with certainty if it is possible to hide programmatically added effect nodes from the users view in the effect list of a render settings. There are some flags which can be used to hide instances of nodes, but I would have to test myself if this also applies in this list, it is more likely that this will not be possible.
To implement a VideoPostData plugin interface, one can override multiple methods and take different approaches, the most commonly used one is to override VideoPostData::Execute
(Link) which will be called multiple times for each rendered frame. The method follows a flag/message logic which is commonly used in Cinema 4D's classic API, where one gets passed in a flag which signalizes in which context the method is being called. Here the context is at which state of the rendering this call is being made, and the chain is:
VIDEOPOSTCALL::FRAMESEQUENCE
- Series of images starts.VIDEOPOSTCALL::FRAME
- Image render starts.VIDEOPOSTCALL::SUBFRAME
- Sub-frame starts.VIDEOPOSTCALL::RENDER
- Render precalculation.VIDEOPOSTCALL::INNER
- Render precalculation.VIDEOPOSTCALL::INNER
- Immediately after rendering.VIDEOPOSTCALL::RENDER
- Immediately after shader cleanup.VIDEOPOSTCALL::SUBFRAME
- Sub-frame rendering done.VIDEOPOSTCALL::FRAME
- Frame rendering done.VIDEOPOSTCALL::FRAMESEQUENCE
- Complete rendering process finished.These flags are accompanied by information if the flags denotes the opening or closing of that 'step' in the rendering process. A developer often then restricts its plugin functionality to a certain flag. I.e., in your case you only want to execute some code when the closing VIDEOPOSTCALL::FRAME
is being passed, i.e., after a single frame and all its sub-frames have been rendered. Execute() also passes in a pointer to a VideoPostStruct
(Link) which carries information about the ongoing rendering. One of its fields is render
, a pointer to a Render
(Link). This data structure represents a rendering with multiple buffers and provides the method GetBuffer()
which returns a pointer to VPBuffer
buffer. In your case you would want to retrieve the RGBA buffer for the rendering by requesting the VPBUFFER_RGBA
buffer (Link) with GetBuffer().
This buffer is then finally the pixel buffer, the bitmap data you want to modify. The buffer is being read and written in a line wise fashion with VPBuffer::GetLine()
and ::SetLine()
. Here you would have to superimpose your watermark information onto the frame. I would do this in a shader like fashion, i.e., write a function which I can query for a texture coordinate for every pixel/fragment in every line and it will then return an RBGA value which I could then combine with the RGBA information which is in the buffer at that coordinate. The details on that depend on what you want to do, e.g.,
and the answers to that are mostly algorithmic and not directly connected to our API which limits the amount of support we can provide for them. If this all sounds very confusing to you, it might be helpful to look at our video post examples I did post in the previous thread, e.g., vpreconstructimage.cpp, as this will probably make things less daunting.
If you decide that you do not want to take this route for technical or complexity reasons, you could write a SceneHookData
plugin which listens via NodeData::Message
for MSG_MULTI_RENDERNOTIFICATION
(Link), a message family which is being sent in the context of a rendering. There you would have to evaluate the start
field in the RenderNotificationData
(Link) accompanying the message, to determine if the call is for the start or end of a rendering. Then you could grab the rendering output file(s) on disk with the help of the render settings from disk and 'manually' superimpose your watermark information. This will come with the drawback that you might have to deal with compressed video files like mpeg or Avi and all the image formats. Some complexity in that can be hidden away with our BaseBitmap
type I did mention in my last posting, but not all of it. There is also the fact that you might run into problems when this plugin runs on a render server, where you cannot easily obtain write or even read access to files of the render output.
I hope this give you some desired guidance,
Ferdinand
Dear community,
The following code example demonstrates how to discover the channel identifiers of the "Channel" parameter of a Substance shader, so that the channel can be changed programmatically for a substance asset unknown at the time of writing the script.
This question reached us via mail, but since answering it requires no confidential data, we are sharing the solution here. The "trick" is to traverse the description of the shader, as these identifiers depend on the substance.
Cheers,
Ferdinand
The result (the example script will randomly select a channel, but with the data provided, channels can also be selected by their name or a substring match as for example "diffuse"):
The code:
"""Example for discovering the channels of a Substance shader.
The solution is a bit hacky by traversing the description of the shader but should work.
"""
import c4d
import random
def GetSubstanceChannels(shader: c4d.BaseShader) -> dict[int:str]:
"""Returns all channels of the substance loaded into #shader as a dictionary of id-label pairs.
"""
if not isinstance(shader, c4d.BaseShader) or (shader.GetType() != c4d.Xsubstance):
raise TypeError(f"{shader} is not a substance shader.")
# Get the data for the "Channel" dropdown element from the description of the shader.
description = shader.GetDescription(c4d.DESCFLAGS_DESC_NONE)
channelData = description.GetParameter(c4d.SUBSTANCESHADER_CHANNEL)
# Get the elements in the drop down menu.
elements = channelData[c4d.DESC_CYCLE]
if not isinstance(elements, c4d.BaseContainer):
raise RuntimeError(f"Could not access Channel parameter description in {shader}.")
# Pack the data into a dictionary and return it.
return {id: label for id, label in elements}
def main(doc: c4d.documents.BaseDocument):
"""
"""
# Get the active material.
material = doc.GetActiveMaterial()
if not isinstance(material, c4d.BaseMaterial):
raise RuntimeError("Please select a material.")
# Get the substance shader loaded into the color channel of the material.
shader = material[c4d.MATERIAL_COLOR_SHADER]
channelData = GetSubstanceChannels(shader)
for id, label in channelData.items():
print (f"id: {id}, label: {label}")
# To select a specific channel, one would have to do a string comparison here to find keywords as
# "Color" or "Metal" in the channel label. I am just randomly selecting a channel instead.
channelId = random.choice(tuple(channelData.keys()))
channelLabel = channelData[channelId]
print (f"Setting substance to channel '{channelLabel}({channelId})'")
shader[c4d.SUBSTANCESHADER_CHANNEL] = channelId
c4d.EventAdd()
if __name__=='__main__':
main(doc)
Hello @joel,
Thank you for reaching out to us. Yeah, the GUI examples for Python are in a bit rough state. I would recommend having a look at the C++ Docs, as they cover more ground.
There are in principal two ways to define GUIs in Cinema 4D, dialogs and descriptions (see the C++ Manual for details). Dialogs are primarily used for things like CommandData
plugins, i.e., when you need a separate window. NodeData
plugins (an object, material, shader, tag, etc.) use description resources to define their GUIs and are displayed in the Attribute Manger.
I should really find the time to write a new GUI manual both for C++ and Python, we are aware that it is urgent, but I never got to it yet, because it will be quite some undertaking. But I have written a little example for your specific case, in the hopes that it will help you. It goes over some basic and more advanced techniques:
CommandData
plugin and a GeDialog
together.GeDialog.CreateLayout
works.Cheers,
Ferdinand
The result:
The code:
"""Implements a CommandData plugin with dialog with a dynamic GUI, using multiple CUSTOMGUI_LINKBOX
gadgets.
Save this file as "someName.pyp" and target Cinema 4D's plugin directory list to the directory
containing the file "someName.pyp". The plugin will appear as "Dialog Manager Command" in the
"Extensions" menu.
"""
import c4d
import typing
class MyDialog(c4d.gui.GeDialog):
"""Implements a dialog with link box gadgets which can be dynamically added and removed at
runtime.
This also demonstrates how one can put an abstraction layer / data model (or however one wants
to call such thing) on top of a couple of gadgets, here the link box GUIs.
"""
# The gadget IDs of the dialog.
# The three groups.
ID_GRP_MAIN: int = 1000
ID_GRP_ELEMENTS: int = 1001
ID_GRP_BUTTONS: int = 1002
# The three buttons at the bottom.
ID_BTN_ADD: int = 2000
ID_BTN_REMOVE: int = 2001
ID_BTN_PRINT: int = 2002
# The dynamic elements. They start at 3000 and then go NAME, LINK, NAME, LINK, ...
ID_ELEMENTS_START: int = 3000
ID_ELEMENT_NAME: int = 0
ID_ELEMENT_LINK: int = 1
# A default layout flag for GUI gadgets and a default gadget spacing.
DEFAULT_FLAGS: int = c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT
DEFAULT_SPACE: tuple[int] = (5, 5, 5, 5)
# A settings container for a LinkBoxGui instance, these are all default settings, so we could
# pass the empty BaseContainer instead with the same effect. But here you can tweak the settings
# of a custom GUI. Since we want all link boxes to look same, this is done as a class constant.
LINKBOX_SETTINGS: c4d.BaseContainer = c4d.BaseContainer()
LINKBOX_SETTINGS.SetBool(c4d.LINKBOX_HIDE_ICON, False)
LINKBOX_SETTINGS.SetBool(c4d.LINKBOX_LAYERMODE, False)
LINKBOX_SETTINGS.SetBool(c4d.LINKBOX_NO_PICKER, False)
LINKBOX_SETTINGS.SetBool(c4d.LINKBOX_NODE_MODE, False)
def __init__(self, items: list[c4d.BaseList2D] = []) -> None:
"""Initializes a MyDialog instance.
Args:
items (list[c4d.BaseList2D]): The items to init the dialog with.
"""
super().__init__()
self._items: list[c4d.BaseList2D] = [] # The items linked in the dialog.
self._doc: typing.Optional[c4d.documents.BaseDocument] = None # The document of the dialog.
self._hasCreateLayout: bool = False # If CrateLayout() has run for the dialog or not.
# Bind the dialog to the passed items.
self.Items = items
# Our data model, we expose _items as a property, so that we can read and write items from
# the outside. For basic type gadgets, e.g., string, bool, int, float, etc., there are
# convenience methods attached to GeDialog like Get/SetString. But there is no GetLink() method.
# So one must do one of two things:
#
# 1. Store all custom GUI gadgets in a list and manually interact with them.
# 2. Put a little abstraction layer on top of things as I did here.
#
# Calling myDialogInstance.Items will always yield all items in the order as shown in the GUI,
# and calling my myDialogInstance.Items = [a, b, c] will then show them items [a, b, c] in three
# link boxes in the dialog. No method is really intrinsically better, but I prefer it like this.
@property
def Items(self) -> list[c4d.BaseList2D]:
"""gets all items linked in the link boxes.
"""
return self._items
@Items.setter
def Items(self, value: list[c4d.BaseList2D]) -> None:
"""Sets all items linked in link boxes.
"""
if not isinstance(value, list):
raise TypeError(f"Items: {value}")
# Set the items and get the associated document from the first item.
self._items = value
self._doc = value[0].GetDocument() if len(self._items) > 0 else None
# Update the GUI when this setter is being called after CreateLayout() has already run.
if self._hasCreateLayout:
self.PopulateDynamicGroup(isUpdate=True)
def InitValues(self) -> bool:
"""Called by Cinema 4D once CreateLayout() has ran.
Not needed in this case.
"""
return super().InitValues()
def CreateLayout(self) -> bool:
"""Called once by Cinema 4D when a dialog opens to populate the dialog with gadgets.
But one is not bound to adding only items from this method, a dialog can be repopulated
dynamically.
"""
self._hasCreateLayout = True
self.SetTitle("Dialog Manager Command")
# The outmost layout group of the dialog. It has one column and we will only place other
# groups in it. Items are placed like this:
#
# Main {
# a,
# b,
# c,
# ...
# }
#
self.GroupBegin(id=self.ID_GRP_MAIN, flags=c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, cols=1)
# Set the group spacing of ID_GRP_MAIN to (5, 5, 5, 5)
self.GroupBorderSpace(*self.DEFAULT_SPACE)
# An layout group inside #ID_GRP_MAIN, it has two columns and we will place pairs of
# labels and link boxes in it. The layout is now:
#
# Main {
# Elements {
# a, b,
# c, d,
# ... }
# b,
# c,
# ...
# }
#
self.GroupBegin(id=self.ID_GRP_ELEMENTS, flags=c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, cols=2)
# Set the group spacing of ID_GRP_ELEMENTS to (5, 5, 5, 5).
self.GroupBorderSpace(*self.DEFAULT_SPACE)
# Call our PopulateDynamicGroup() method, here with isUpdate=False, so that group
# ID_GRP_ELEMENTS won't be flushed the first time its is being built. Doing this is the same
# as moving all the code from PopulateDynamicGroup() to the line below.
self.PopulateDynamicGroup(isUpdate=False)
self.GroupEnd() # ID_GRP_ELEMENTS
# A second layout group inside ID_GRP_MAIN, its has three columns and will place our buttons
# in it. The layout is now:
#
# Main {
# Elements {
# a, b,
# c, d,
# ... }
# Buttons {
# a, b, c,
# e, f, g,
# ...},
# c,
# ...
# }
#
self.GroupBegin(id=self.ID_GRP_BUTTONS, flags=c4d.BFH_SCALEFIT, cols=3)
self.GroupBorderSpace(*self.DEFAULT_SPACE)
# The three buttons.
self.AddButton(id=self.ID_BTN_ADD, flags=c4d.BFH_SCALEFIT, name="Add Item")
self.AddButton(id=self.ID_BTN_REMOVE, flags=c4d.BFH_SCALEFIT, name="Remove Last Item")
self.AddButton(id=self.ID_BTN_PRINT, flags=c4d.BFH_SCALEFIT, name="Print Items")
self.GroupEnd() # ID_GRP_BUTTONS
self.GroupEnd() # ID_GRP_MAIN
return super().CreateLayout()
def PopulateDynamicGroup(self, isUpdate: bool = False):
"""Builds the dynamic part of the GUI.
This is a custom method that is not a member of GeDialog.
Args:
isUpdate (bool, optional): If this is an GUI update event. Defaults to False.
Raises:
MemoryError: On gadget allocation failure.
RuntimeError: On linking objects failure.
"""
# When this is an update event, i.e., the group #ID_GRP_ELEMENTS has been populated before,
# flush the items in the group and set the gadget insertion pointer of the this dialog to
# the start of #ID_GRP_ELEMENTS. Everything else done in CreateLayout(), the groups, the
# buttons, the spacings, remains intact.
if isUpdate:
self.LayoutFlushGroup(self.ID_GRP_ELEMENTS)
# For each item in self._items ...
for i, item in enumerate(self.Items):
# Define the current starting id: 3000, 3002, 3004, 3006, ...
offset: int = self.ID_ELEMENTS_START + (i * 2)
# Add a static text element containing the class name of #item or "Empty" when the
# item is None.
self.AddStaticText(id=offset + self.ID_ELEMENT_NAME,
flags=c4d.BFH_LEFT,
name=item.__class__.__name__ if item else "Empty")
# Add a link box GUI, a custom GUI is added by its gadget ID, its plugin ID, here
# CUSTOMGUI_LINKBOX, and additionally a settings container, here the constant
# self.LINKBOX_SETTINGS.
gui: c4d.gui.LinkBoxGui = self.AddCustomGui(
id=offset + self.ID_ELEMENT_LINK,
pluginid=c4d.CUSTOMGUI_LINKBOX,
name="",
flags=c4d.BFH_SCALEFIT,
minw=0,
minh=0,
customdata=self.LINKBOX_SETTINGS)
if not isinstance(gui, c4d.gui.LinkBoxGui):
raise MemoryError("Could not allocate custom GUI.")
# When item is not a BaseList2D, i.e., None, we do not have to set the link.
if not isinstance(item, c4d.BaseList2D):
continue
# Otherwise try to link #item in the link box GUI.
if not gui.SetLink(item):
raise RuntimeError("Failed to set node link from data.")
if isUpdate:
self.LayoutChanged(self.ID_GRP_ELEMENTS)
def AddEmptyItem(self) -> None:
"""Adds a new empty item to the data model and updates the GUI.
This is a custom method that is not a member of GeDialog.
"""
self._items.append(None)
self.PopulateDynamicGroup(isUpdate=True)
def RemoveLastItem(self) -> None:
"""Removes the last item from the data model and updates the GUI.
This is a custom method that is not a member of GeDialog.
"""
if len(self._items) > 0:
self._items.pop()
self.PopulateDynamicGroup(isUpdate=True)
def UpdateItem(self, cid: int):
"""Updates an item in list of link boxes.
This is a custom method that is not a member of GeDialog.
Args:
cid (int): The gadget ID for which this event was fired (guaranteed to be a link box
GUI gadget ID unless I screwed up somewhere :D).
"""
# The index of the link box and therefore index in self._items, e.g., the 0, 1, 2, 3, ...
# link box GUI / item.
index: int = int((cid - self.ID_ELEMENTS_START) * 0.5)
# Get the LinkBoxGui associated with the ID #cid.
gui: c4d.gui.LinkBoxGui = self.FindCustomGui(id=cid, pluginid=c4d.CUSTOMGUI_LINKBOX)
if not isinstance(gui, c4d.gui.LinkBoxGui):
raise RuntimeError(f"Could not access link box GUI for gadget id: {cid}")
# Retrieve the item in the link box gui. This can return None, but in this case we are
# okay with that, as we actually want to reflect in our data model self._items when
# link box is empty. The second argument to GetLink() is a type filter. We pass here
# #Tbaselist2d to indicate that we are interested in anything that is a BaseList2D. When
# would pass Obase (any object), and the user linked a material, the method would return
# None. If we would pass Ocube, only cube objects would be retrieved.
item: typing.Optional[c4d.BaseList2D] = gui.GetLink(self._doc, c4d.Tbaselist2d)
# Write the item into our data model and update the GUI.
self.Items[index] = item
self.PopulateDynamicGroup(isUpdate=True)
def PrintItems(self) -> None:
"""Prints all items held by the dialog to the console.
This is a custom method that is not a member of GeDialog.
"""
for item in self.Items:
print(item)
def Command(self, cid: int, msg: c4d.BaseContainer) -> bool:
"""Called by Cinema 4D when the user interacts with a gadget.
Args:
cid (int): The id of the gadget which has been interacted with.
msg (c4d.BaseContainer): The command data, not used here.
Returns:
bool: Success of the command.
"""
# You could also put a lot of logic into this method, but for an example it might be better
# to separate out the actual logic into methods to make things more clear.
# The "Add Item" button has been clicked.
if cid == self.ID_BTN_ADD:
self.AddEmptyItem()
# The "Remove Item" button has been clicked.
elif cid == self.ID_BTN_REMOVE:
self.RemoveLastItem()
# The "Print Items" button has been clicked.
elif cid == self.ID_BTN_PRINT:
self.PrintItems()
# One of the link boxes has received an interaction.
elif (cid >= self.ID_ELEMENTS_START and (cid - self.ID_ELEMENTS_START) % 2 == self.ID_ELEMENT_LINK):
self.UpdateItem(cid)
return super().Command(cid, msg)
def CoreMessage(self, cid: int, msg: c4d.BaseContainer) -> bool:
"""Called by Cinema 4D when a core event occurs.
You could use this to automatically update the dialog when the selection state in the
document has changed. I did not flesh this one out.
"""
# When "something" has happened in the, e.g., the selection has changed ...
# if cid == c4d.EVMSG_CHANGE:
# items: list[c4d.BaseList2D] = (
# self._doc.GetSelection() + self._doc.GetActiveMaterials() if self._doc else [])
# newItems: list[c4d.BaseList2D] = list(n for n in items if n not in self._items)
# self.Items = self.Items + newItems
return super().CoreMessage(cid, msg)
class DialogManagerCommand (c4d.plugins.CommandData):
"""Provides an implementation for a command data plugin with a foldable dialog.
This will appear as the entry "Dialog Manager Command" in the extensions menu.
"""
ID_PLUGIN: int = 1060264 # The plugin ID of the command plugin.
REF_DIALOG: typing.Optional[MyDialog] = None # The dialog hosted by the plugin.
def GetDialog(self, doc: typing.Optional[c4d.documents.BaseDocument] = None) -> MyDialog:
"""Returns a class bound MyDialog instance.
Args:
doc (typing.Optional[c4d.documents.BaseDocument], optional): The active document.
Defaults to None.
This is a custom method that is not a member of CommandData.
"""
# Get the union of all selected objects, tags, and materials in #doc or define the empty
# list when doc is None. Doing it in this form is necessary, because GetState() will call
# this method before Execute() and we only want to populate the dialog when the user invokes
# the command.
items: list[c4d.BaseList2D] = doc.GetSelection() + doc.GetActiveMaterials() if doc else []
# Instantiate a new dialog when there is none.
if self.REF_DIALOG is None:
self.REF_DIALOG = MyDialog(items)
# Update the dialog state when the current document selection state is different. This will
# kick in when the user selects items, opens the dialog, closes the dialog, and changes the
# selection. This very much a question of what you want, and one could omit doing this or
# do it differently.
elif doc is not None and self.REF_DIALOG.Items != items:
self.REF_DIALOG.Items = items
# Return the dialog instance.
return self.REF_DIALOG
def Execute(self, doc: c4d.documents.BaseDocument) -> bool:
"""Folds or unfolds the dialog.
"""
# Get the dialog bound to this command data plugin type.
dlg: MyDialog = self.GetDialog(doc)
# Fold the dialog, i.e., hide it if it is open and unfolded.
if dlg.IsOpen() and not dlg.GetFolding():
dlg.SetFolding(True)
# Open or unfold the dialog.
else:
dlg.Open(c4d.DLG_TYPE_ASYNC, self.ID_PLUGIN)
return True
def RestoreLayout(self, secret: any) -> bool:
"""Restores the dialog on layout changes.
"""
return self.GetDialog().Restore(self.ID_PLUGIN, secret)
def GetState(self, doc: c4d.documents.BaseDocument) -> int:
"""Sets the command icon state of the plugin.
With this you can tint the command icon blue when the dialog is open or grey it out when
some condition is not met (not done here). You could for example disable the plugin when
there is nothing selected in a scene, when document is not in polygon editing mode, etc.
"""
# The icon is never greyed out, the button can always be clicked.
result: int = c4d.CMD_ENABLED
# Tint the icon blue when the dialog is already open.
dlg: MyDialog = self.GetDialog()
if dlg.IsOpen() and not dlg.GetFolding():
result |= c4d.CMD_VALUE
return result
def RegisterDialogManagerCommand() -> bool:
"""Registers the example.
"""
# Load one of the builtin icons of Cinema 4D as the icon of the plugin, you can browse the
# builtin icons under:
# https://developers.maxon.net/docs/py/2023_2/modules/c4d.bitmaps/RESOURCEIMAGE.html
bitmap: c4d.bitmaps.BaseBitmap = c4d.bitmaps.InitResourceBitmap(c4d.Tdisplay)
# Register the plugin.
return c4d.plugins.RegisterCommandPlugin(
id=DialogManagerCommand.ID_PLUGIN,
str="Dialog Manager Command",
info=c4d.PLUGINFLAG_SMALLNODE,
icon=bitmap,
help="Opens a dialog with scene element link boxes in it.",
dat=DialogManagerCommand())
# Called by Cinema 4D when this plugin module is loaded.
if __name__ == '__main__':
if not RegisterDialogManagerCommand():
raise RuntimeError(
f"Failed to register {DialogManagerCommand} plugin.")
Dear Community,
This question reached us via mail and since answering it does not require confidential information and since I thought others find this interesting too, I am sharing my answer here. The question was How to add enum values dynamically to a node attribute that uses an Enum GUI?".
Find my answer below.
Cheers,
Ferdinand
Result:
Code:
#include "c4d_basematerial.h"
#include "maxon/datadescription_nodes.h"
#include "maxon/graph.h"
#include "maxon/graph_helper.h"
#include "maxon/nodesgraph.h"
#include "maxon/nodesystem.h"
namespace maxon
{
/// @brief Demonstrates how to modify the enum values of an attribute.
/// @details One place where one could do this is inside the #::InstantiateImpl method of a
/// #NodeTemplateInterface one is implementing so that every new node is being populated with
/// enum values of our liking. But as demonstrated by the example below, nothing prevents us
/// from doing the same thing at runtime on a graph.
Result<void> AddEnumValues(BaseDocument* doc)
{
iferr_scope;
// Get the active material's node graph and start a transaction.
NodeMaterial* const material = static_cast<NodeMaterial*>(doc->GetActiveMaterial());
CheckArgument(material);
nodes::NodesGraphModelRef graph = material->GetGraph(GetActiveNodeSpaceId()) iferr_return;
GraphTransaction transaction = graph.BeginTransaction() iferr_return;
{
// Get the mutable root for #graph. This way is not only shorter than first getting the
// mutable node system for #graph and then its mutable root, but also the only way that
// actually works here. We can do this because starting a transaction on a graph model also
// implies modifying the node system. So, we do not have to call NodeSystem::BeginModification
// in this case.
nodes::MutableNode root = nodes::ToMutableNode(graph.GetRoot()) iferr_return;
// Iterate over all children of the root to get hold of nodes which have our port.
for (auto node : root.GetChildren())
{
// Attempt to get hold of our enum port.
nodes::MutablePort enumPort = node.GetInputs().FindPort(
Id("in@eSp1K8T8GNcuPwKSds8Lvs")) iferr_return;
// We could also add a non-existing port here with MutableNode::AddPort
if (!enumPort || !enumPort.IsValid())
continue;
// NodeTemplateInterface::InstantiateImpl code would start here.
// Set the data type and label of the port, doing this is obviously optional.
enumPort.SetType<String>() iferr_return;
enumPort.SetValue(NODE::BASE::NAME, "My Enumeration"_s) iferr_return;
// Build the enum data and write it into the port.
BaseArray<Tuple<Id, Data>> entries;
for (const Int32 i : {1, 2, 3, 4, 5})
{
const String data = FormatString("Item @", i);
const Id id = Id::Create(label) iferr_return;
entries.Append(Tuple<Id, Data>(id, data)) iferr_return;
}
DataDictionary enumPortData;
enumPortData.Set(DESCRIPTION::DATA::BASE::ENUM, entries) iferr_return;
enumPort.SetValue(nodes::PortDescriptionData, std::move(enumPortData)) iferr_return;
// And set the default value of the port.
enumPort.SetDefaultValue("Item 1"_s) iferr_return;
}
// Commit the transaction and with it the node system modification.
} transaction.Commit() iferr_return;
return OK;
}
}
Hello @blkmsk,
Thank you for reaching out to us. There are multiple mistakes in your code, but instead of writing a lengthy answer, I decided to write a code example which we will ship with the next release of Cinema 4D.
Your main problem was not that you did not know how to get or set and port value, but that you did not understand the structure of a graph. And while it has been somewhat explained in our other examples, we lacked an example which dissected the topic in more detail. Which is why I wrote this example.
Cheers,
Ferdinand
#coding: utf-8
"""Demonstrates the inner structure of node graphs and how to find entities in them.
Run this script in the Script Manager on a document which contains at least one Redshift node
material with at least one "Texture" node in the graph. The script will print out the URL of
each texture node and print the GraphNode tree for each material in the document.
Topics:
* The difference between node and asset IDs.
* The inner structure of a graph.
* Reading the value of a port (both for connected and unconnected ports).
Entities:
* PrintGraphTree: Prints a given graph as a GraphNode tree to the console.
* main: Runs the main example.
"""
__author__ = "Ferdinand Hoppe"
__copyright__ = "Copyright (C) 2023 MAXON Computer GmbH"
__date__ = "16/11/2023"
__license__ = "Apache-2.0 License"
__version__ = "2024.?.?"
import c4d
import maxon
import typing
doc: c4d.documents.BaseDocument # The active document.
def PrintGraphTree(graph: maxon.GraphModelRef) -> None:
"""Prints a given graph as a GraphNode tree to the console.
This can be helpful to understand the structure of a graph. Note that this will print the true
data of a graph, which will include all ports (most of them are usually hidden in the Node
Editor) and also hidden graph entities.
"""
kindMap: dict[int, str] = {
maxon.NODE_KIND.NODE: "NODE",
maxon.NODE_KIND.INPORT: "INPORT",
maxon.NODE_KIND.INPUTS: "INPUTS",
maxon.NODE_KIND.NONE: "NONE",
maxon.NODE_KIND.OUTPORT: "OUTPORT",
maxon.NODE_KIND.OUTPUTS: "OUTPUTS",
}
if graph.IsNullValue():
raise RuntimeError("Invalid graph.")
def iterTree(node: maxon.GraphNode,
depth: int = 0) -> typing.Iterator[tuple[int, maxon.GraphNode]]:
"""Yields all descendants of #node and their hexarchy depth, including #node itself.
This is one way to iterate over a node graph. But when one does not require the hierarchy
as we do here, one should use GraphNode.GetInnerNodes() as demonstrated in main() below.
"""
yield (node, depth)
for child in node.GetChildren():
for item in iterTree(child, depth + 1):
yield item
root: maxon.GraphNode = graph.GetRoot()
for node, depth in iterTree(root):
print (f"{' ' * depth} + '{node.GetId()}' [{kindMap.get(node.GetKind(), 'UNKNOWN KIND')}]"+
("" if not node.GetId().IsEmpty() else f"(Root Node)"))
def main():
"""Runs the main example.
"""
# For each #material in the currently active document ...
for material in doc.GetMaterials():
# ... get its node material reference and step over all materials which do not have a
# Redshift graph or where the graph is malformed.
nodeMaterial: c4d.NodeMaterial = material.GetNodeMaterialReference()
if not nodeMaterial.HasSpace("com.redshift3d.redshift4c4d.class.nodespace"):
continue
graph: maxon.GraphModelRef = nodeMaterial.GetGraph(
"com.redshift3d.redshift4c4d.class.nodespace")
if graph.IsNullValue():
raise RuntimeError("Found malformed empty graph associated with node space.")
# Get the root of the graph, the node which contains all other nodes. Since we only want
# to read information here, we do not need a graph transaction. But for all write operations
# we would have to start a transaction on #graph.
root: maxon.GraphNode = graph.GetRoot()
# Iterate over all nodes in the graph, i.e., unpack things like nodes nested in groups.
# With the mask argument we could also include ports in this iteration.
for node in root.GetInnerNodes(mask=maxon.NODE_KIND.NODE, includeThis=False):
# There is a difference between the asset ID of a node and its ID. The asset ID is
# the identifier of the node template asset from which a node has been instantiated.
# It is more or less the node type identifier. When we have three RS Texture nodes
# in a graph they will all have the same asset ID. But their node ID on the other hand
# will always be unique to a node.
assetId: maxon.Id = node.GetValue("net.maxon.node.attribute.assetid")[0]
nodeId: maxon.Id = node.GetId()
# Step over everything that is not a Texture node, we could also check here for a node
# id, in case we want to target a specific node instance.
if assetId != maxon.Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler"):
continue
# Now we got hold of a Texture node in a graph. To access a port value on this node,
# for example the value of the Filename.Path port, we must get hold of the port entity.
# Get the Filename input port of the Texture node.
filenameInPort: maxon.GraphNode = node.GetInputs().FindChild(
"com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0")
# But #filenameInPort is a port bundle - that is how ports are called which are composed
# of multiple (static) child ports. The port has the data type "Filename" with which we
# cannot do much on its own. Just as in the node editor, we must use its
# nested child ports to get and set values, in this case the "path".
pathPort: maxon.GraphNode = filenameInPort.FindChild("path")
# Another way to get hold of the path port would be using a node path, where we define
# an identifier which addresses the port node directly from the root of the graph.
alsoPathPort: maxon.GraphNode = graph.GetNode(
node.GetPath() + ">com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0\path")
# The underlying information is here the nature of the type GraphNode. The word "node"
# in it does not refer to a node in a graph but to a node in a tree. All entities in a
# graph - nodes, ports, and things that are not obvious from the outside - are organized
# in a tree of GraphNode instances. Each GraphNode instance has then a NODEKIND which
# expresses the purpose of that node, examples for node kinds are:
#
# * NODE_KIND.NODE : A GraphNode that represents a tangible node in a graph. The
# Nodes API calls this a 'true node'.
# * NODE_KIND.INPORT : A GraphNode that represents a input port in a graph.
# * NODE_KIND.OUTPORT: A GraphNode that represents a output port in a graph.
#
# For our example the GraphNode tree of a material graph could look like this:
# + "" // The root node of the graph, it is
# | is the container that holds all
# | entities in the graph. It has the
# | empty ID as its ID.
# |---+ "texturesampler@bbRMv5CAGpPtJFHs5n43CV" // An RS Texture node in it.
# | |---+ ">" // A child node of the texture that
# | | holds all of its input ports, it has
# | | a special NODE_KIND and is the
# | | underlying node of what we accessed
# | | with the .GetInputs() call above.
# | |---+ "...nodes.core.texturesampler" // The Filename input port of a Texture
# | | node. It is a port bundle and
# | | therefore has child ports attached to
# | | it. This node and all of its children
# | | are of type NODE_KIND.INPORT.
# | |---+ "path" // The nested port for the Filename.Path.
# |---+ "standardmaterial@FTf3YdB7AxEn1JKQL2$W" // An RS Standard Material node in the
# | | graph.
# | |---+ ">" // Its input ports.
# | | | ...
# | |
# | |---+ "<" // Its output ports.
# | | ...
# |
# |---+ ... // Another node in the graph.
# On this entity we can now read and write values. In 2023 an earlier we would have used
# Get/SetDefaultValue for this, in 2024 and onwards these methods have been deprecated
# and we use GetValue now instead.
url: str | None = pathPort.GetValue("effectivevalue")
print (f"{pathPort}: {url}")
# Print the GraphNode tree for the current #graph.
PrintGraphTree(graph)
if __name__ == "__main__":
main()
Hey @justinleduc,
Thank you for reaching out to us. Your question is a bit contradictory, as you seem to mix up the terms volume and field as if they were interchangeable which then leads to other ambiguities.
f(x, y, z)
and it is therefore by definition continous/smooth. One cannot enumerate all values in it just as one cannot enumerate all rational numbers between 0 and 1.So, when you talk about fields and show us a screenshot which contains a field object, and then ask for enumerating "all the lines" in that screenshot that is a bit ambiguous. Because if these vectors were generated by a field, all you could do is emulate that UI representation of that field, but a field does not have a finite set of values one could iterate over.
But I think field is just communicative noise here, and what you want to do, is sample a volume object which has been set to volume type vector. You can do that in Python but unlike in C++, we do not have GridIteratorInterface
, so we must do some legwork ourselves.
Please share your code and scene examples in the future to make it easier for us to understand what you are doing and what you want to do. Find my answer below.
Cheers,
Ferdinand
Result:
Code:
"""Demonstrates how to iterate over all cells of a volume in Python.
Must be run as Script Manager script with a Volume Builder object selected which has been set to
Volume Type: Vector. It will then print the vector values of all voxels. Since we do not have the
type GridIteratorInterface in Python, we must do some computations ourself here. We also lack
fancier feature like voxel level folding in Python offered by GridIteratorInterface in C++.
"""
import c4d
import maxon
import itertools
op: c4d.BaseObject # The active object.
def GetCenteredIndices(count: int) -> tuple[int, int]:
"""Yields indices for #count voxel cells centered on their index origin.
So, 2 will yield [-1, 1], 3 will yield [-1, 0, 1], 4 will yield [-2, -1, 1, 2], 5 will yield
[-2, -1, 0, 1, 2], and so on.
"""
if count % 2 == 0:
half: int = int(count / 2)
return [n for n in range(-half, 0)] + [n for n in range(1, half + 1)]
else:
half: int = int((count - 1) / 2)
return [n for n in range(-half, 0)] + [0] + [n for n in range(1, half + 1)]
def main():
"""
"""
# Validate the input.
if not op or not op.IsInstanceOf(c4d.Ovolumebuilder):
raise TypeError("op is not a c4d.Ovolumebuilder.")
cache: c4d.modules.volume.VolumeObject | None = op.GetCache()
hasVolumeCache: bool = cache and cache.IsInstanceOf(c4d.Ovolume)
if not hasVolumeCache:
raise RuntimeError("Object has no first level volume cache.")
# Get the volume and ensure it has the grid type we/you expect it to have.
volume: maxon.VolumeRef = cache.GetVolume()
if volume.GetGridType() != c4d.GRIDTYPE_VECTOR32:
raise RuntimeError("Object is not of matching vector32 grid type.")
# Initialize a grid accessor so that we can access the volume data. In C++ we have
# GridIteratorInterface which makes it much easier to iterate over all active cells
# in a volume. Here we must get a bit creative ourselves.
access: maxon.GridAccessorRef = maxon.GridAccessorInterface.Create(
maxon.Vector32)
access.Init(volume)
# Get the transform of the volume. A volume, a voxel grid, is always world transform aligned.
# That means it cannot be rotated, but it can be scaled (expressing the cell size) and have an
# offset. In Python we also cannot use VolumeInterface.GetWorldBoundingBox() because while
# the function has been wrapped, its return type has not (genius move I know :D). We use
# GetActiveVoxelDim() instead, which gives us the shape of the volume, e.g., (5*2*3) or
# (12*12*12) which in conjunction with the cell size then tells us the bounding box.
transform: maxon.Matrix = volume.GetGridTransform()
cellSize: maxon.Vector = transform.sqmat.GetLength()
halfCellSize: c4d.Vector = c4d.Vector(cellSize.x * .5, cellSize.y * .5, cellSize.z * .5)
shape: maxon.IntVector32 = volume.GetActiveVoxelDim()
print(f"{transform = }\n{cellSize = }\n{shape = }\n")
# Iterate over all cell indices in the volume. itertools.product is just a fancy way of doing
# nested loops.
for x, y, z in itertools.product(GetCenteredIndices(shape.x),
GetCenteredIndices(shape.y),
GetCenteredIndices(shape.z)):
# Get the point in world coordinates for the cell index (x, y, z) and then its value.
# In C++ we could just multiply our index by the volume #transform, here we must do some
# legwork ourselves because most of the arithmetic types of the maxon API have not been
# wrapped in Python.
# #p is the local lower boundary point coordinate of the cell index (x, y, z) and #q that
# point in global space which also has been shifted towards the cell origin.
p: c4d.Vector = c4d.Vector(cellSize.x * x, cellSize.y * y, cellSize.z * z)
q: c4d.Vector = c4d.Vector(cache.GetMg().off) + p + halfCellSize
# Volumes are usually hollow, so most cells will hold the null value; we just step over them.
value: maxon.Vector32 = access.GetValue(q)
if value.IsNullValue():
continue
print(f"{q.x:>6}, {q.y:>6}, {q.z:>6} = {value}")
if __name__ == '__main__':
main()
Here is also a section from some C++ code examples which I never got to ship which might be helpful here:
/// @brief Accesses the important metadata associated with a volume as for example its name,
/// data type, transform, or bounding box.
/// @param[in] volume The volume to access the metadata for.
static maxon::Result<void> GetVolumeMetadata(const maxon::Volume& volume)
{
iferr_scope;
// Get the purpose denoting label of the Pyro grid, e.g., "density", "color", or "velocity".
const maxon::String gridName = volume.GetGridName();
// Get the storage convention of the grid, e.g., FOG or SDF, and the data type which is being
// stored in the grid. Currently, the only grid types that are being used are ::FLOAT and
// ::VECTOR32 regardless of the precision mode the simulation scene is in.
const GRIDCLASS gridClass = volume.GetGridClass();
const GRIDTYPE gridDataType = volume.GetGridType();
// Get the matrix transforming from grid index space to world space. Due to volume grids always
// being aligned with the standard basis orientation, the world axis orientation, this transform
// will always express the standard orientation. The scale vector of the square matrix of this
// transform expresses the size of a voxel in world space.
const maxon::Matrix gridTransform = volume.GetGridTransform();
// Get the shape of the whole grid, e.g., (5, 5, 5) for a grid with five cells on each axis.
// Other than #GetWorldBoundingBox() and despite its name, this method will consider inactive
// voxels in its return value. It will always return the shape of the whole grid.
const maxon::IntVector32 gridShape = volume.GetActiveVoxelDim();
// Get the index range of the active cells for the grid. E.g., a grid with the shape (5, 5, 5),
// the origin (0, 0, 0), and all cells active would return [(-2, -2, -2), (2, 2, 2)]. But the
// grid could also return [(-1, -1, -1), (1, 1, 1)] when no cells outside that range would be
// active.
const maxon::Range<Vector> indexRange = volume.GetWorldBoundingBox();
// Get the cell size of a voxel in world space.
const Vector cellSize = gridTransform.sqmat.GetScale();
// Get the world space minimum and maximum of the active volume.
const maxon::Range<Vector> boundingBox (gridTransform * indexRange.GetMin(),
gridTransform * indexRange.GetMax());
// Compute the size of the total and the active cells volume of the grid.
const maxon::Vector totalVolumeSize (cellSize * Vector(gridShape));
const maxon::Vector activeVolumeSize (boundingBox.GetDimension());
// Log the metadata to the console of Cinema 4D.
ApplicationOutput(
"name: @, gridClass: @, gridDataType: @\n"
"shape: @, indexRange: @\n"
"gridTransform: @\n"
"boundingBox: @, cellSize: @\n"
"totalVolumeSize: @, activeVolumeSize: @",
gridName, gridClass, gridDataType, gridShape, indexRange, gridTransform, boundingBox, cellSize,
totalVolumeSize, activeVolumeSize);
return maxon::OK;
}
//! [GetVolumeMetadata]
//! [GetVolumeData]
/// @brief
/// @tparam T
/// @param volume
/// @param result
/// @return
template<typename T>
maxon::Result<void> GetVolumeData(const maxon::Volume& volume, maxon::BaseArray<T>& result)
{
iferr_scope;
using GridIteratorType = maxon::GridIteratorRef<T, maxon::ITERATORTYPE::ON>;
GridIteratorType iterator = GridIteratorType::Create() iferr_return;
iterator.Init(volume) iferr_return;
const maxon::Matrix gridTransform = volume.GetGridTransform();
maxon::String msg;
// Iterate over the first 100 cells yielded by the iterator and add each 10th value to the result.
int i = 0; int j = 0;
for (; iterator.IsNotAtEnd() && ++i < 100 && ++j; iterator.StepNext())
{
// Get the voxel index of the current cell and compute its world space position.
const maxon::IntVector32 index = iterator.GetCoords();
const Vector worldPosition = gridTransform * maxon::Vector64(index);
// Get the depth in the voxel tree of the current cell; but Pyro volumes do not use any tiling.
// #level will therefore always be of value #::LEAF and every active cell in the volume will be
// visited by the iterator.
const maxon::TREEVOXELLEVEL level = iterator.GetVoxelLevel();
// Get the value stored in the current cell,
const T value = iterator.GetValue();
if (j % 10 == 0)
{
msg += FormatString("[@] @ (@): @\n", level, index, worldPosition, value);
result.Append(value) iferr_return;
}
}
// Log the collected data to the console of Cinema 4D.
ApplicationOutput(msg);
return maxon::OK;
}
//! [GetVolumeData]
Hi,
I cannot reproduce this neither. The interesting question would be what does print random.seed
for you and is this reproduceable on your end?
My suspicion would be that someone or something treated random.seed()
like a property instead of like a function, which then led to - with all the "Python functions are first class objects" thing - random.seed
being an integer. Something like this:
>>> import random
>>> print(random.seed)
<bound method Random.seed of <random.Random object at 0x103b62218>>
>>> random.seed(12345)
>>> random.seed = 12345 # 'accidently' treating it like a property
>>> print(random.seed)
12345
>>> random.seed(12345)
Traceback (most recent call last):
File "<string>", line 1, in <module>
TypeError: 'int' object is not callable
Cheers
zipit
Dear community,
a support request alerted us to the fact that the C++ code example Create a Redshift Material does not correctly display textures of the material in the viewport when applied to an object until the material is edited at least once by the user.
This is caused by a limitation of node materials and the Nodes API in general which does not fair well for graphs which are not attached to a document (e.g., a material not inserted into a document). I have updated the C++ example to a variant which deals with this issue by moving inserting the material ahead. This requires then more verbose error handling. I also added a more verbose undo handling, as this is non-trivial in this case.
The corresponding Python code example did this already correctly (mostly by accident). I updated it too, and modernized things like using CreateEmptyGraph
in favour of CreateDefaultGraph
and SetPortValue
in favour of SetDefaultValue
. The Python example now also demonstrates how to consolidate an undo operation for creating a node material.
The examples will be shipped in a future documentation update. Find a preview of both updated examples below.
Cheers,
Ferdinand
#include "c4d.h"
#include "c4d_basedocument.h"
#include "c4d_baselist.h"
#include "c4d_basematerial.h"
#include "c4d_tools.h"
#include "lib_description.h"
#include "lib_noise.h"
#include "maxon/apibase.h"
#include "maxon/graph.h"
#include "maxon/nodesgraph.h"
#include "maxon/graph_helper.h"
#include "maxon/node_undo.h"
//! [create_redshift_material]
/// Creates a small node setup of four nodes for a Redshift node material and inserts the material
/// into the passed document.
///
/// The example uses two textures delivered as assets with Cinema 4D so that they can be linked in
/// shaders. This can be ignored when locally provided textures should be used instead.
///
/// @param[in, out] doc The document to insert the node material into.
maxon::Result<void> CreateRedshiftMaterial(BaseDocument* doc)
{
// --- Error handling ----------------------------------------------------------------------------
// We must declare our node material here, so that the error handler below can access it.
NodeMaterial* material;
Bool didInsertMaterial = false;
// This is the error handler for the scope of this function. It is necessary and the exit
// point for code which returns errors like for example `iferr_return` or a return statement.
// What is not necessary, is how explict we are about this. We could replace the whole handler
// with an `iferr_scope;`, but would then give up the special handling we do below.
//
// We use the error handler here to gracefully unwind the document state when an error
// occurred while we tried to build the material graph. The primary thing we do, is remove
// the material #material from the passed document #doc. This error handler is also special
// in that it demonstrates how to raise new errors while handling errors.
iferr_scope_handler -> maxon::Error
{
// We declare a few errors in advance to make our code a bit cleaner (and a tiny bit
// less performant). We need these when errors occur while handling the error #err
// which is passed to this error scope.
maxon::AggregatedError aggError { MAXON_SOURCE_LOCATION };
maxon::IllegalStateError undoError { MAXON_SOURCE_LOCATION,
"Could not unroll or finalize undo stack upon error handling."_s };
maxon::UnexpectedError critError { MAXON_SOURCE_LOCATION,
"Critical error while aggregating errors."_s };
// There is a valid document and material and we inserted the material. We try to
// unwind the document state before returning the error.
if (doc && material && didInsertMaterial)
{
// We try to add an undo item for removing the material, on success, we try
// to remove the material and close the undo.
Bool isUndoError = !doc->AddUndo(UNDOTYPE::DELETEOBJ, material);
if (!isUndoError)
{
material->Remove();
isUndoError = !doc->EndUndo();
}
// An error occurred while handing the undo, we attempt to use an AggregatedError, a
// container which can hold multiple errors, to return both the original error #err which
// has been passed into this scope, as well as #undoError to indicate that while handling
// #err another error occurred.
if (isUndoError)
{
// AggregatedError::AddError is itself of type Result<>, i.e., can fail. We use here
// iferr (Result<T>) to return #critError in such cases.
iferr (aggError.AddError(maxon::Error(err)))
return critError;
iferr (aggError.AddError(undoError))
return critError;
// Building the aggregated error succeeded we return it instead of #err.
return aggError;
}
}
// We return the original error as there was either no document handling to do, or there was
// no error in doing so. The special thing is here that we must wrap the #err in an Error
// because #err is an error pointer and our precise error handling with error aggregation
// requires us to return a error reference instead (as indicated by the return type of this
// handler).
return maxon::Error(err);
};
// --- Logic starts here -------------------------------------------------------------------------
// This main thread check is not strictly necessary in all cases, but when #doc is a loaded
// document, e.g., the active document, then we are bound by threading restrictions and must not
// modify such document off-main-thread.
if (!GeIsMainThreadAndNoDrawThread())
return maxon::IllegalStateError(MAXON_SOURCE_LOCATION, "Must run on main thread."_s);
if (!doc)
return maxon::NullptrError(MAXON_SOURCE_LOCATION, "Invalid document pointer."_s);
// We start an undo item. This example is written with the goal to wrap the whole operation
// into a singular undo operation. All the manual undo-handling in the function can be omitted but
// depending on the context, we might then end up with more than one undo step to revert to
// the previous document state.
if (!doc->StartUndo())
return maxon::IllegalStateError(
MAXON_SOURCE_LOCATION, "Could not start undo item for document."_s);
// The asset URL of the "rust" and "sketch" texture assets delivered with Cinema 4D. See the
// Asset API Handbook in the Cinema 4D C++ documentation for more information on the Asset AP.
const maxon::Url rustTextureUrl { "asset:///file_edb3eb584c0d905c"_s };
const maxon::Url sketchTextureUrl { "asset:///file_3b194acc5a745a2c"_s };
// Create a NodeMaterial by instantiating a Mmaterial BaseMaterial and casting it to a
// NodeMaterial. This could also be done in two steps (first allocating the Mmaterial and
// then casting it) or by casting an already existing Mmaterial into a NodeMaterial.
material = static_cast<NodeMaterial*>(BaseMaterial::Alloc(Mmaterial));
if (!material)
return maxon::OutOfMemoryError(MAXON_SOURCE_LOCATION, "Could not allocate node material."_s);
// Define the ID of the Redshift node space and add an empty graph.
const maxon::LiteralId redshiftId("com.redshift3d.redshift4c4d.class.nodespace");
material->CreateEmptyGraph(redshiftId) iferr_return;
const maxon::nodes::NodesGraphModelRef& graph = material->GetGraph(redshiftId) iferr_return;
// Insert the material into the passed document. When we do not do this before the transaction,
// textures will not be properly reflected in the viewport unless we do a dummy commit at the
// end. It is also quite important that we do this after we called CreateEmptyGraph or
// CreateDefaultGraph as these will otherwise add an undo item (without us being able to
// consolidate this into our undo).
doc->InsertMaterial(material);
didInsertMaterial = true;
if (!doc->AddUndo(UNDOTYPE::NEWOBJ, material))
return maxon::IllegalStateError(
MAXON_SOURCE_LOCATION, "Could not add undo for adding material to document."_s);
// The ids of the nodes which are required to build the graph. These ids can be discovered in the
// Asset Browser with the #-button in the "Info Area" of the Asset Browser.
const maxon::Id outputNodeTypeId("com.redshift3d.redshift4c4d.node.output");
const maxon::Id materialNodeTypeId("com.redshift3d.redshift4c4d.nodes.core.standardmaterial");
const maxon::Id colormixNodeTypeId("com.redshift3d.redshift4c4d.nodes.core.rscolormix");
const maxon::Id noiseNodeTypeId("com.redshift3d.redshift4c4d.nodes.core.maxonnoise");
const maxon::Id textureNodeTypeId("com.redshift3d.redshift4c4d.nodes.core.texturesampler");
// Define a settings dictionary for the transaction we start below. Doing this is not necessary,
// as the userData argument to BeginTransaction is optional. The here used attribute and enum
// requires the nodespace.framework and `node_undo.h` to be included with the module. We
// instruct the graph to not open a new undo operation for this transaction and instead add items
// to the ongoing undo operation of the document.
maxon::DataDictionary userData;
userData.Set(maxon::nodes::UndoMode, maxon::nodes::UNDO_MODE::ADD) iferr_return;
// Start modifying the graph by starting a graph transaction. Transactions in the Nodes API work
// similar to transactions in databases and implement an all-or-nothing model. When an error occurs
// within a transaction, all already carried out operations will not be applied to the graph. The
// modifications to a graph are only applied when the transaction is committed. The scope
// delimiters { and } used here between BeginTransaction() and transaction.Commit() are not
// technically required, but they make a transaction more readable.
maxon::GraphTransaction transaction = graph.BeginTransaction(userData) iferr_return;
{
// Add the new nodes to the graph which are required for the setup. Passing the empty ID as
// the first argument will let Cinema 4D choose the node IDs, which is often the best option
// when newly created nodes must not be referenced by their ID later.
maxon::GraphNode outNode = graph.AddChild(maxon::Id(), outputNodeTypeId) iferr_return;
maxon::GraphNode materialNode = graph.AddChild(maxon::Id(), materialNodeTypeId) iferr_return;
maxon::GraphNode colormixNode = graph.AddChild(maxon::Id(), colormixNodeTypeId) iferr_return;
maxon::GraphNode noiseNode = graph.AddChild(maxon::Id(), noiseNodeTypeId) iferr_return;
maxon::GraphNode rustTexNode = graph.AddChild(maxon::Id(), textureNodeTypeId) iferr_return;
maxon::GraphNode sketchTexNode = graph.AddChild(maxon::Id(), textureNodeTypeId) iferr_return;
// The port IDs which are used here can be discovered with the "IDs" option "Preferences/Node
// Editor" option enabled. The selection info overlay in the bottom left corner of the Node
// Editor will now show both the IDs of the selected node or port.
// Get the "Out Color" out-port of the "Standard Material" node and the "Surface" in-port of
// from the "Output" node in this graph.
maxon::GraphNode outcolorPortMaterialNode = materialNode.GetOutputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.nodes.core.standardmaterial.outcolor")) iferr_return;
maxon::GraphNode surfacePortOutNode = outNode.GetInputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.node.output.surface")) iferr_return;
// Connect the "Out Color" port to the "Surface" port. The connection order of an out-port
// connecting to an in-port is mandatory.
outcolorPortMaterialNode.Connect(surfacePortOutNode) iferr_return;
// Connect the "outColor" out-port of the the "Texture" node for the rust texture to the
// "Input 1" in-port of the "Color Mix" node.
maxon::GraphNode outcolorPortRustTexNode = rustTexNode.GetOutputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler.outcolor")) iferr_return;
maxon::GraphNode input1PortColormixNode = colormixNode.GetInputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.nodes.core.rscolormix.input1")) iferr_return;
outcolorPortRustTexNode.Connect(input1PortColormixNode) iferr_return;
// Connect the output port of the sketch "Texture" node to the "Input 2" in-port of the
// "Color Mix" node.
maxon::GraphNode outcolorPortSketchTexNode = sketchTexNode.GetOutputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler.outcolor")) iferr_return;
maxon::GraphNode input2PortColormixNode = colormixNode.GetInputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.nodes.core.rscolormix.input2")) iferr_return;
outcolorPortSketchTexNode.Connect(input2PortColormixNode) iferr_return;
// Connect the output port of the "Color Mix" node to the "Base > Color" in-port of the "Standard
// Material" node.
maxon::GraphNode outcolorPortColormixNode = colormixNode.GetOutputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.nodes.core.rscolormix.outcolor")) iferr_return;
maxon::GraphNode basecolorPortMaterialNode = materialNode.GetInputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.nodes.core.standardmaterial.base_color")) iferr_return;
outcolorPortColormixNode.Connect(basecolorPortMaterialNode) iferr_return;
// Connect the output port of the "Maxon Noise" node both to the "Mix Amount" in-port of
// the "Color Mix" node and the in-port "Reflection > Roughness" of the "Standard Material" node.
maxon::GraphNode outcolorPortNoiseNode = noiseNode.GetOutputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.nodes.core.maxonnoise.outcolor")) iferr_return;
maxon::GraphNode amountPortColormixNode = colormixNode.GetInputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.nodes.core.rscolormix.mixamount")) iferr_return;
maxon::GraphNode roughnessPortMaterialNode = materialNode.GetInputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.nodes.core.standardmaterial.refl_roughness")) iferr_return;
outcolorPortNoiseNode.Connect(amountPortColormixNode) iferr_return;
outcolorPortNoiseNode.Connect(roughnessPortMaterialNode) iferr_return;
// Set the noise type of the "Maxon Noise" node to "Wavy Turbulence"
// Setting a value of a port without a connection is done by setting the default value of a
// port. What is done here is the equivalent to a user changing the value of a noise type of
// the noise node to "Wavy Turbulence" in the Attribute Manager of Cinema 4D.
maxon::GraphNode noisetypePortNoiseNode = noiseNode.GetInputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.nodes.core.maxonnoise.noise_type")) iferr_return;
noisetypePortNoiseNode.SetPortValue(NOISE_WAVY_TURB) iferr_return;
// Set the texture paths of both texture nodes to the texture asset URLs defined above.
// Here we encounter something new, a port which has ports itself is a "port-bundle" in terms
// of the Nodes API. The "Filename" port of a texture node is a port bundle which consists out
// of two sub-ports, the "path" and the "layer" port of the texture. The texture URL must be
// written into the "path" sub-port.
maxon::GraphNode pathPortRustTexNode = rustTexNode.GetInputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0")).FindChild(
maxon::Id("path")) iferr_return;
pathPortRustTexNode.SetPortValue(rustTextureUrl) iferr_return;
maxon::GraphNode pathPortSketchTexNode = sketchTexNode.GetInputs().FindChild(
maxon::Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0")).FindChild(
maxon::Id("path")) iferr_return;
pathPortSketchTexNode.SetPortValue(sketchTextureUrl) iferr_return;
} transaction.Commit() iferr_return;
// Finalize our undo item.
if (!doc->EndUndo())
return maxon::IllegalArgumentError(MAXON_SOURCE_LOCATION, "Failed to finalize undo item."_s);
return maxon::OK;
}
//! [create_redshift_material]
#coding: utf-8
"""Demonstrates setting up a Redshift node material composed out of multiple nodes.
Creates a new node material with a graph in the Redshift material space, containing two texture
nodes and a mix node, in addition to the default RS Standard Material and Output node of the
material.
Topics:
* Creating a node material and adding a graph.
* Adding nodes to a graph.
* Setting the value of ports without wires.
* Connecting ports with a wires.
"""
__author__ = "Ferdinand Hoppe"
__copyright__ = "Copyright (C) 2023 MAXON Computer GmbH"
__date__ = "04/06/2024"
__license__ = "Apache-2.0 License"
__version__ = "2024.0.0"
import c4d
import maxon
doc: c4d.documents.BaseDocument # The active document.
def main() -> None:
"""Runs the example.
"""
# The asset URLs for the "RustPaint0291_M.jpg" and "Sketch (HR basic026).jpg" texture assets in
# "tex/Surfaces/Dirt Scratches & Smudges/". These could also be replaced with local texture URLs,
# e.g., "file:///c:/textures/stone.jpg". These IDs can be discovered with the #-button in the info
# area of the Asset Browser.
urlTexRust: maxon.Url = maxon.Url(r"asset:///file_edb3eb584c0d905c")
urlTexSketch: maxon.Url = maxon.Url(r"asset:///file_3b194acc5a745a2c")
# The node asset IDs for the two node types to be added in the example; the texture node and the
# mix node. These and all other node IDs can be discovered in the node info overlay in the
# bottom left corner of the Node Editor. Open the Cinema 4D preferences by pressing CTRL/CMD + E
# and enable Node Editor -> Ids in order to see node and port IDs in the Node Editor.
idOutputNode: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.node.output")
idStandardMaterial: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.nodes.core.standardmaterial")
idTextureNode: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler")
idMixNode: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.nodes.core.rscolormix")
# Instantiate a material, get its node material, and add a graph for the RS material space.
material: c4d.BaseMaterial = c4d.BaseMaterial(c4d.Mmaterial)
if not material:
raise MemoryError(f"{material = }")
nodeMaterial: c4d.NodeMaterial = material.GetNodeMaterialReference()
redshiftNodeSpaceId: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.class.nodespace")
graph: maxon.GraphModelRef = nodeMaterial.CreateEmptyGraph(redshiftNodeSpaceId)
if graph.IsNullValue():
raise RuntimeError("Could not add Redshift graph to material.")
# Open an undo operation and insert the material into the document. We must do this before we
# modify the graph of the material, as otherwise the viewport material will not correctly display
# the textures of the material until the user manually refreshes the material. It is also
# important to insert the material after we added the default graph to it, as otherwise we will
# end up with two undo steps in the undo stack.
if not doc.StartUndo():
raise RuntimeError("Could not start undo stack.")
doc.InsertMaterial(material)
if not doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, material):
raise RuntimeError("Could not add undo item.")
# Define the user data for the transaction. This is optional, but can be used to tell the Nodes
# API to add the transaction to the current undo stack instead of creating a new one. This will
# then have the result that adding the material, adding the graph, and adding the nodes will be
# one undo step in the undo stack.
userData: maxon.DataDictionary = maxon.DataDictionary()
userData.Set(maxon.nodes.UndoMode, maxon.nodes.UNDO_MODE.ADD)
# Start modifying the graph by opening a transaction. Node graphs follow a database like
# transaction model where all changes are only finally applied once a transaction is committed.
with graph.BeginTransaction(userData) as transaction:
# Add the output, i.e., the terminal end node of the graph, as well as a standard material
# node to the graph.
outNode: maxon.GraphNode = graph.AddChild(maxon.Id(), idOutputNode)
materialNode: maxon.GraphNode = graph.AddChild(maxon.Id(), idStandardMaterial)
# Add two texture nodes and a blend node to the graph.
rustTexNode: maxon.GraphNode = graph.AddChild(maxon.Id(), idTextureNode)
sketchTexNode: maxon.GraphNode = graph.AddChild(maxon.Id(), idTextureNode)
mixNode: maxon.GraphNode = graph.AddChild(maxon.Id(), idMixNode)
# Get the input 'Surface' port of the 'Output' node and the output 'Out Color' port of the
# 'Standard Material' node and connect them.
surfacePortOutNode: maxon.GraphNode = outNode.GetInputs().FindChild(
"com.redshift3d.redshift4c4d.node.output.surface")
outcolorPortMaterialNode: maxon.GraphNode = materialNode.GetOutputs().FindChild(
"com.redshift3d.redshift4c4d.nodes.core.standardmaterial.outcolor")
outcolorPortMaterialNode.Connect(surfacePortOutNode)
# Set the default value of the 'Mix Amount' port, i.e., the value the port has when no
# wire is connected to it. This is equivalent to the user setting the value to "0.5" in
# the Attribute Manager.
mixAmount: maxon.GraphNode = mixNode.GetInputs().FindChild(
"com.redshift3d.redshift4c4d.nodes.core.rscolormix.mixamount")
mixAmount.SetPortValue(0.5)
# Set the path sub ports of the 'File' ports of the two image nodes to the texture URLs
# established above. Other than for the standard node space image node, the texture is
# expressed as a port bundle, i.e., a port which holds other ports. The texture of a texture
# node is expressed as the "File" port, of which "Path", the URL, is only one of the possible
# sub-ports to set.
pathRustPort: maxon.GraphNode = rustTexNode.GetInputs().FindChild(
"com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0").FindChild("path")
pathSketchPort: maxon.GraphNode = sketchTexNode.GetInputs().FindChild(
"com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0").FindChild("path")
pathRustPort.SetPortValue(urlTexRust)
pathSketchPort.SetPortValue(urlTexSketch)
# Get the color output ports of the two texture nodes and the color blend node.
rustTexColorOutPort: maxon.GraphNode = rustTexNode.GetOutputs().FindChild(
"com.redshift3d.redshift4c4d.nodes.core.texturesampler.outcolor")
sketchTexColorOutPort: maxon.GraphNode = sketchTexNode.GetOutputs().FindChild(
"com.redshift3d.redshift4c4d.nodes.core.texturesampler.outcolor")
mixColorOutPort: maxon.GraphNode = mixNode.GetOutputs().FindChild(
"com.redshift3d.redshift4c4d.nodes.core.rscolormix.outcolor")
# Get the fore- and background port of the blend node and the color port of the BSDF node.
mixInput1Port: maxon.GraphNode = mixNode.GetInputs().FindChild(
"com.redshift3d.redshift4c4d.nodes.core.rscolormix.input1")
mixInput2Port: maxon.GraphNode = mixNode.GetInputs().FindChild(
"com.redshift3d.redshift4c4d.nodes.core.rscolormix.input2")
stdBaseColorInPort: maxon.GraphNode = materialNode.GetInputs().FindChild(
"com.redshift3d.redshift4c4d.nodes.core.standardmaterial.base_color")
# Wire up the two texture nodes to the blend node and the blend node to the BSDF node.
rustTexColorOutPort.Connect(mixInput1Port, modes=maxon.WIRE_MODE.NORMAL, reverse=False)
sketchTexColorOutPort.Connect(mixInput2Port, modes=maxon.WIRE_MODE.NORMAL, reverse=False)
mixColorOutPort.Connect(stdBaseColorInPort, modes=maxon.WIRE_MODE.NORMAL, reverse=False)
# Finish the transaction to apply the changes to the graph.
transaction.Commit()
if not doc.EndUndo():
raise RuntimeError("Could not end undo stack.")
c4d.EventAdd()
if __name__ == "__main__":
main()
Hi,
preference data is often even for native c4d features implemented as a PreferenceData
plugin. You have to access that plugin then. To get there you can drag and drop description elements into the python command line, delete the __getitem__()
part (the stuff in brackets), and get the __repr__
of the object. With that you can figure out the plugin ID of the corresponding BasePlugin
and then access your values.
For your case as a script example:
import c4d
from c4d import plugins
# Welcome to the world of Python
def main():
# Search for a plugin ID with a known str __repr__ of a BasePlugin. We got from the console:
# Drag and drop: Plugins[c4d.PREF_PLUGINS_PATHS]
# >>> Plugins
# <c4d.BaseList2D object called 'Plugins/Plugins' with ID 35 at 0x0000028FE8157A50>
pid, op = None, plugins.GetFirstPlugin()
while op:
if 'Plugins/Plugins' in str(op):
pid = op.GetID()
break
op = op.GetNext()
print "pid:", pid
# Use that ID
op = plugins.FindPlugin(pid)
if op:
print op[c4d.PREF_PLUGINS_PATHS] # we know the enum from the console
# Execute main()
if __name__=='__main__':
main()
You can use then that Plugin ID in cpp to do the last part (Use that ID
) there.
Cheers,
zipit