Hello @d_schmidt,
@d_schmidt said in Field Refusing Object:
This does seem out of scope of the initial question, but I'm not sure if I should make a different thread or not.
Technically true, but we are fine here.
With your cube example: This works because the Bulge deformer is looking at Cache of the Cube and then returning the Deform Cache of it, right? So there is no infinite loop because it isn't modify the Deform Cache.
Yes, that is mostly what is happening. The problem your plugin has, is that the generation of its cache can depend on its own cache. Deformers are a common pattern to avoid such cyclic definitions. There can be cases where they do not fully solve this, i.e., where you then have a ping-pong case of when the deformer is constantly reevaluated, i.e., can bring Cinema 4D to a crawl, but you will not produce stack overflows, i.e., crashes. My bulge deformer case is such an example of constant reevaluation. When you set the sampling mode of the point layer of the cube to volume in the layer tab and then set cube subdivisions to something like 100³, it will bring Cinema to a crawl, because it will constantly chew on reevaluating the cube. This happens of course constantly, but for lightweight to medium sized deformations it will not be noticeable. And just to be clear: This only happens when you have these cyclic definitions.
Is a Deformer capable of doing what I need it to in this case? My understanding is that a Deformer generally moves points around and the like. What I want is to get a float value from the Field to then generator a different cache with that value in GetContour. This wouldn't be just moving points around but creating completely new ones or utterly changes existing ones.
Yes, deformers are mostly meant for moving stuff around, but you can also modify the point or polygon count of things. The bevel deformer does that for example. At the end of this posting, you will find an extension of the previously posted Python example which doubles the vertex count of an input spline and adds a bit of jitter to the added intermediate points. The plugin is now a deformer ObjectData
which is close to what you are trying to do.
This seems like it would have the same problem, since my main plugin wouldn't be modified, but recalculating based on changes from the Deformer. So, the Cache would rebuild which would trigger infinite rebuilds as a result.
As stated above, yes, the problem remains, although you avoid causing stack overflows and therefor crashes. In the end this is an unsolvable riddle. A cyclic definition is a cyclic definition. When your cache depends on building your cache, you have a problem. One approach is to sanity check everything that moves for not being a self-reference. This is of course labor intensive to do. Splitting things up shifts the burden to Cinema at the cost of having a sluggish scene when the user overdoes it.
Cheers,
Ferdinand
The effect of the deformer, in red the deformed spline adding points in between, and in white a copy of the input spline:

The code (mostly the same as above, I only added ModifyObjects
, the two hash functions and changed the registration call, so that the plugin is now a deformer):
"""Example for emulating refusing a field layer in field list with
NodeData::SetParameter.
As discussed in:
https://plugincafe.maxon.net/topic/13387/
"""
import c4d
import math
import struct
# In the C++ you do not have to do these hashing gymnastics I do here,
# because Cinema does provide the Random class there. Python's random is
# however ill suited for this task, which is why I am providing my own
# hash functions here.
def Hash11(x, seed=1234., magic=(1234.5678, 98765.4321)):
"""Returns a pseudo random floating-point hash in the interval [-1, 1].
Args:
x (float): The value to get the pseudo random hash for.
seed (float, optional): The seed offset for the hash.
magic (tuple[float, float], optional): The magic numbers.
Defaults to (1234.5678, 98765.4321).
Returns:
float: The random hash for x in the interval [-1, 1].
"""
return math.modf(math.sin((x * seed) * magic[0]) * magic[1])[0]
def Hash13(x, seed=1234., magic=(1234.5678, 98765.4321)):
"""Returns a pseudo vector floating-point hash in the interval [-1, 1].
Wraps around Hash11.
Args:
as Hash11.
Returns:
c4d.Vector: The random hash for x in the interval [-1, 1].
"""
xc = Hash11(x, seed, magic)
yc = Hash11(x * xc, seed, magic)
zc = Hash11(x * yc, seed, magic)
return c4d.Vector(xc, yc, zc)
class PC13387(c4d.plugins.ObjectData):
"""Example ObjectData plugin that has two parameters.
* ID_REJECTION - A base-link for an object to reject.
* ID_FIELDLIST - A field list which could contain a a field layer for
that object to reject and which we want to prevent.
"""
ID_PLUGIN = 1057375
@staticmethod
def FieldListContains(doc, testNode, fieldList):
"""Determines if a node is contained in a field list or not.
Iterating over field lists is not trivial, and I do not have the time
to cover here any corner case, feel free to ask questions once you
get stuck. Important to understand is also that a field list does
contain field layers which are not equal to their object manager
representation. The object manager repr. of a field layer has to be
retrieved with GetLinkedObject() if there is one (which is not a
given).
Args:
doc (c4d.documents.BaseDocument): The document the field list is
contained in.
testNode (c4d.GeListNode): The node to test for.
fieldList (c4d.FieldList): The field list to test.
Returns:
bool: If testNode is contained or not.
"""
def getFieldLayers(node):
"""
"""
visited = []
end = node.GetUp() if isinstance(node, c4d.GeListNode) else None
while node:
# If the current node is a newly encountered one.
if node not in visited:
visited.append(node)
# When it is a field layer, yield it and deal with group
# fields ...
if isinstance(node, c4d.modules.mograph.FieldLayer):
yield node
# For the special case of a group field, we have to unpack
# the data of the field by accessing its content via its
# object manager representation. Since what one does see
# in the field list, i.e., fields below an group field, is
# actually not there, but only a view. The data is stored
# with the field list of the group field. So we cannot
# access this data by iterating over the field tree we
# are currently iterating over, i.e.,
# myGroupFieldLayer.GetDown() will yield None.
# Get the object manger representation of the current
# field layer. If there is one and its type is that of
# a field group:
linkedNode = node.GetLinkedObject(doc)
if linkedNode and linkedNode.CheckType(c4d.Fgroup):
# Get the field list and iterate over it.
fieldList = linkedNode[c4d.FIELDGROUP_FIELDS]
root = fieldList.GetLayersRoot()
for nestedNode in getFieldLayers(root):
yield nestedNode
# There are more special cases in the field list hierarchy,
# e.g., folders, which also have to be treated specifically.
# I did not do that here.
# Normal depth-first traversal of a node tree
if node.GetDown() and node.GetDown() not in visited:
node = node.GetDown()
elif node.GetNext():
node = node.GetNext()
else:
node = node.GetUp()
if node == end:
return
# Get the root layer of the field list and traverse it.
root = fieldList.GetLayersRoot()
for fieldLayer in getFieldLayers(root):
# When the object linked to the field layer is equal to the one
# we want to reject, then we return True, as we found it.
linkedNode = fieldLayer.GetLinkedObject(doc)
if linkedNode == testNode:
return True
return False
def Init(self, node):
"""Not documented.
"""
self.InitAttr(node, c4d.BaseList2D, c4d.ID_REJECTION)
self.InitAttr(node, c4d.FieldList, c4d.ID_FIELDLIST)
return True
def SetDParameter(self, node, id, t_data, flags):
"""Overwrite the process of writing a parameter.
When a field list is being modified, e.g., by a drag and drop event,
we validate that to be written field list by iterating over it. When
we encounter the object we want to reject, we signal that we did carry
out the writing of the parameter without doing it, causing the old
state of the field list to remain, i.e., the drag and dropped object
has been rejected.
Args:
node (c4d.BaseList2D): The node containing the parameter.
id (c4d.DescID): The DescID of the parameter to write.
t_data (any): The parameter data.
flags (int): The write flags.
Returns:
(bool, int): The outcome and the write flags.
"""
# If the parameter to write is our field list ...
if id[0].id == c4d.ID_FIELDLIST:
# Get the node's document and the node to reject.
doc = node.GetDocument()
nodeToReject = node[c4d.ID_REJECTION] or node
# And ask FieldListContains() if nodeToReject is contained in
# t_data.
if PC13387.FieldListContains(doc, nodeToReject, t_data):
# If so, signal that we did carry out the operation without
# doing it, causing the old state to remain.
return True, flags | c4d.DESCFLAGS_SET_PARAM_SET
return True
def ModifyObject(self, mod, doc, op, op_mg, mod_mg, lod, flags, thread):
"""
"""
# Get out when the input object is not a line object.
if not isinstance(op, c4d.LineObject):
print (f"Illegal modifier input: {op}")
return False
# And get out when the input object has less than 2 points.
pcnt = op.GetPointCount()
if pcnt <= 1:
print (f"Invalid spline input: {op}")
return False
# Now we go over all points and just subdivide the line segments by
# calculating an intermediate point.
points = op.GetAllPoints()
newPoints = []
for i in range(1, pcnt - 1):
# A line segment.
o, q = points[i - 1], points[i]
# The interpolated point in between with a little bit of jitter
# to make it a bit more exciting to look at.
p = (o + q) * .5 + Hash13(float(i)) * 25
newPoints += [o, p]
# Resize the line object.
newCnt = len(newPoints)
print ("newCnt:", newCnt, op.GetSegmentCount())
op.ResizeObject(newCnt)
# Write the new point information.
op.SetAllPoints(newPoints)
# One problem with this is that the underlying segment data is not
# being updated. I am not sure if this is just a Python thing, or if
# you will encounter this also in C++. The segment count of the tag
# is still the old value, which will cause the returned line object
# to have the correct point count, but rendered will only be the old
# number of segments. So, we are going to fix this. In C++ there are
# easier ways to do this, in Python we have to use raw memory access.
# But you can also use this approach in C++ if you want to.
segmentTag = op.GetTag(c4d.Tsegment)
if segmentTag is None:
return False
data = segmentTag.GetLowlevelDataAddressW()
# Unpack the first 4 bytes, the segment count, a segment is just this:
#
# struct Segment
# {
# Int32 cnt;
# Bool closed;
# };
cnt = struct.unpack("i", data[:4])
print ("Segment count:", cnt)
# Write the new segment count.
data[:4] = struct.pack("i", newCnt)
# This will get a bit more complicated when there are multiple Segment
# instances, I kept things simple here for a single segment case.
# Now we are good to go.
return True
if __name__ == '__main__':
c4d.plugins.RegisterObjectPlugin(
id=PC13387.ID_PLUGIN,
str="Spline Deformer",
g=PC13387,
description="Opc13387",
info=c4d.OBJECT_MODIFIER,
icon=None)