Solved DirectSample doc

Hi guys,

Trying to sample some points against the fields in R21. Referring to doc wrote this

  if fields:
    fieldInput = c4d.modules.mograph.FieldInput(pPos, pCount)
    fieldInfo = c4d.modules.mograph.FieldInfo.Create(c4d.FIELDSAMPLE_FLAG_VALUE, None, doc, 0, 1, fieldInput, op)
    fieldOutput = c4d.modules.mograph.FieldOutput.Create(pCount, c4d.FIELDSAMPLE_FLAG_VALUE)
    fields.DirectInitSampling(fieldInfo)
    fields.DirectSample(fieldInput, fieldOutput, fieldInfo)
    fields.DirectFreeSampling(fieldInfo)

But it fires error:

TypeError: argument 2 must be c4d.modules.mograph.FieldOutputBlock, not c4d.modules.mograph.FieldOutput

Tried to change code to:

fields.DirectSample(fieldInput, fieldOutput, fieldInfo.GetBlock())

Code executes but output values are 0. Any recommendations?

I don't know about your code. But a field is typically sampled using the "Sample" functions. There is an example using SampleListSimple().

hi ,

thanks for the links @PluginStudent

i'm surprised that you code execute.

GetBlock() should be used in your FieldOutput.

        fields.DirectSample(fieldInput, fieldOutput.GetBlock(), fieldInfo)
        fields.DirectFreeSampling(fieldInfo)

It's working here. Are you sure you are sampling value that should be different than 0 ?

Cheers,
Manuel

MAXON SDK Specialist

MAXON Registered Developer

@m_magalhaes Thanks for reply.
I was mistaken with GetBlock() when wrote a post: it should be applied to the FieldOutput object.

I clear my issue now - I have custom point positions list (based on object's polygon center points), and I want to sample them.
But code below:

    pPos = [c4d.Vector(-100, 0, -100), c4d.Vector(100, 0, -100), c4d.Vector(-100, 0, 100), c4d.Vector(100, 0, 100)]
    fieldInput = c4d.modules.mograph.FieldInput(pPos, len(pPos))
    fieldInfo = c4d.modules.mograph.FieldInfo.Create(c4d.FIELDSAMPLE_FLAG_VALUE, None, doc, 0, 1, fieldInput)
    fieldOutput = c4d.modules.mograph.FieldOutput.Create(pCount, c4d.FIELDSAMPLE_FLAG_VALUE)
    fields.DirectInitSampling(fieldInfo)
    samplingSuccess = fields.DirectSample(fieldInput, fieldOutput, fieldInfo)
    fields.DirectFreeSampling(fieldInfo)
    print samplingSuccess, fieldOutput._value

prints False [0.0, 0.0, 0.0, 0.0]

I see that documentation FieldInfo says callers parameter is optional, but is it true?
Any recommendations?

I made a hack with temporary object

    pPos = [c4d.Vector(-100, 0, -100), c4d.Vector(100, 0, -100), c4d.Vector(-100, 0, 100), c4d.Vector(100, 0, 100)]
    pCount = len(pPos)
    tmpPoly = c4d.BaseObject(c4d.Opolygon)
    tmpPoly.ResizeObject(pCount , 0)
    tmpPoly.SetAllPoints(pPos)
    fieldInput = c4d.modules.mograph.FieldInput(pPos, pCount)
    fieldInfo = c4d.modules.mograph.FieldInfo.Create(c4d.FIELDSAMPLE_FLAG_VALUE, None, doc, 0, 1, fieldInput, tmpPoly)
    fieldOutput = c4d.modules.mograph.FieldOutput.Create(pCount, c4d.FIELDSAMPLE_FLAG_VALUE)
    fields.DirectInitSampling(fieldInfo)
    samplingSuccess = fields.DirectSample(fieldInput, fieldOutput, fieldInfo)
    fields.DirectFreeSampling(fieldInfo)
    print samplingSuccess, fieldOutput._value

Seems it works fine - prints True and list of values

Now I'm facing issue with sampling uvw as well:

    pPos = [c4d.Vector(-100, 0, -100), c4d.Vector(100, 0, -100), c4d.Vector(-100, 0, 100), c4d.Vector(100, 0, 100)]
    pCount = len(pPos)
    uvw = [c4d.Vector(0, 0, 0), c4d.Vector(1, 0, 0), c4d.Vector(0, 1, 0), c4d.Vector(1, 1, 0)]
    tmpPoly = c4d.BaseObject(c4d.Opolygon)
    tmpPoly.ResizeObject(pCount , 0)
    tmpPoly.SetAllPoints(pPos)
    fieldInput = c4d.modules.mograph.FieldInput(position=pPos, allocatedCount=pCount, transform=op.GetMg(), uvw=uvw)
    fieldInfo = c4d.modules.mograph.FieldInfo.Create(c4d.FIELDSAMPLE_FLAG_VALUE, None, doc, 0, 1, fieldInput, tmpPoly)
    fieldOutput = c4d.modules.mograph.FieldOutput.Create(pCount, c4d.FIELDSAMPLE_FLAG_VALUE)
    fields.DirectInitSampling(fieldInfo)
    samplingSuccess = fields.DirectSample(fieldInput, fieldOutput, fieldInfo)
    fields.DirectFreeSampling(fieldInfo)
    print samplingSuccess, fieldOutput._value

prints False [0.0, 0.0, 0.0, 0.0]

Alternative code:

    fieldInput = c4d.modules.mograph.FieldInput(pPos, pCount, op.GetMg(), pCount, None, uvw)

returns error:

StandardError: The arguments don't match any supplied constructors

Would appreciate your help

Hi @baca sorry for the long delay we forget you, but here you are.

The culprit is in the FieldInput initialization, that fail (you can check it by calling fieldInput.IsPopulated() will return False in your case).

First, there is a little bug, if no arguments are passed then an empty FieldInfo is returned, but we don't check for keywords only for argument, so be sure to pass at least one parameter, not as a keyword argument.

Then because in C++ you have multiple overrides of a function (aka multiple signatures for one function) but in python, this is not possible and we have to handle it internally by emulating it and by marking argument optimal while they are not really.

So only few constructors are possibles which are:

  • no parameters passed = an empty FieldInput is returned.
  • position, allocatedCount.
  • position, allocatedCount, transform.
  • position, allocatedCount, transform, fullCount.
  • position, direction, allocatedCount, transform, fullCount.
  • position, direction, uvw, allocatedCount, transform, fullCount.

So that means if you want to define the uvw you also need to define the position, direction, uvw(of course) the allocatedCount, transform, and the fullCount.

So this gives us

import c4d

fields = op[c4d.FIELDS]

pPos = [c4d.Vector(-100, 0, -100), c4d.Vector(100, 0, -100), c4d.Vector(-100, 0, 100), c4d.Vector(100, 0, 100)]
pCount = len(pPos)
uvw = [c4d.Vector(0, 0, 0), c4d.Vector(1, 0, 0), c4d.Vector(0, 1, 0), c4d.Vector(1, 1, 0)]

tmpPoly = c4d.BaseObject(c4d.Opolygon)
tmpPoly.ResizeObject(pCount , 0)
tmpPoly.SetAllPoints(pPos)

# This is important to pass pPos without a keyword due to a bug see my post https://plugincafe.maxon.net/topic/12723/directsample-doc/6
fieldInput = c4d.modules.mograph.FieldInput(pPos, direction=uvw,  uvw=uvw, allocatedCount=pCount, transform=op.GetMg(), fullCount=pCount)
fieldInfo = c4d.modules.mograph.FieldInfo.Create(c4d.FIELDSAMPLE_FLAG_VALUE, None, doc, 0, 1, fieldInput, tmpPoly)
fieldOutput = c4d.modules.mograph.FieldOutput.Create(pCount, c4d.FIELDSAMPLE_FLAG_VALUE)

fields.DirectInitSampling(fieldInfo)
samplingSuccess = fields.DirectSample(fieldInput, fieldOutput.GetBlock(), fieldInfo)
fields.DirectFreeSampling(fieldInfo)
print(samplingSuccess, fieldOutput._value)

Hope this helps. In any case, I will try to improve the documentation on this topic and I will create a bug report about the fact that if you pass only keyword argument then the default empty FieldList is returned.

Cheers,
Maxime.

@m_adam said in DirectSample doc:

position, direction, uvw, allocatedCount, transform, fullCount

Thanks Maxime, this is works now. Much appreciated your details and solution.

PS: I noticed it's still necessary to create temporary Polygon Object, otherwise it wouldn't sample

tmpPoly = c4d.BaseObject(c4d.Opolygon)
tmpPoly.ResizeObject(pCount , 0)
tmpPoly.SetAllPoints(pPos)

Sorry to bump this thread.
But I'm also wondering, why the temporary poly object is needed.
I'm currently in R21 (as my customer) and there I also get sample values only if such poly object is passed. The pure list of positions is not enough in results in sampled values always being zero.

Hello @a_block,

thank you for reaching out to us. @m_adam is on vacation, so I will have to pick up here for now. And I unfortunately struggle with understanding your question.

But I'm also wondering, why the temporary poly object is needed.

I assume it is because the author of the Python function FieldInfo.Create has moved the callers argument and the indicated in the documentation that it is option with typing.Optional?

When you look at the C++ documentation, you will see that caller is mandatory for all overloads of FieldInfo::Create(). Nothing indicates that you could pass a null pointer for caller, quite the opposite, in the cases where caller is a BaseList2D, there are instructions that it must be attached to a document. So, the Python implementation is a bit counter intuitive with placing caller at the end of the argument list and also technically incorrect with the optional indicator.

If your question is meant more philosophical, as in the pure list of points should be enough to sample that point cloud, I must unfortunately point out that we cannot discuss our APIs unless there is bug or a clear contradiction of functionality vs purpose.

PS: I still have the feeling that I misunderstood something here.

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

Not philosophical at all.

a) Why would we need to pass a list of point positions (or would have an option for it), if the information is taken from an object only. Is it only to restrict the sampled points? Why wouldn't we pass point indices instead of vectors, then?
b) FieldInfo.Create() docs clearly state the callers to be optional.

As a Python developer I do not tend to look into the C++ docs, if Python docs are existing.

And Maxime's example above does not insert the temporary poly object, does it?

Hello @a_block,

Not philosophical at all [...] Why would we need to pass a list of point positions (or would have an option for it), if the information is taken from an object only. Is it only to restrict the sampled points? Why wouldn't we pass point indices instead of vectors, then? [...] FieldInfo.Create() docs clearly state the callers to be optional.

Well, that is exactly the philosophical question I mentioned :) Philosophical was probably a poor term to describe what I meant, but there is no good answer for "why do I have to do A when I do B which requires me to do A?". As explained by Maxime in his posting, a caller is simply effectively required. As also indicated by me, the typing.Optional is quite questionable although technically true. I could unpack the technical details here as far as I understand them, but that does not help much IMHO (in short: FieldInfo.Create() in Python effectively wraps FieldInfo::Create(const FieldCallerStack &caller, ...) in C++, and while an empty stack will result in a FieldInfo which is valid, it is practically not very useful, resulting in erroneous sampling attempts).

As a Python developer I do not tend to look into the C++ docs, if Python docs are existing.

Yes, I understand that. I often felt the same when I was a user. But it is still advisable to look at the C++ documentation, especially when the Python documentation raises questions or seems ambiguous.

And Maxime's example above does not insert the temporary poly object, does it?

Yes, he does not do this, which is a flaw of his example. It will cause the underlying field sampling to assume the active document to be the relevant document for the sampling. Which pans here out due to the setup, but it is better to pass a caller which is part of the relevant document, as this will also make the code work in scenarios where the sampling does not happen in the active document. Now that I read Maxime's code more closely, I also spotted that he was passing tmpPoly as the caller. Which is not specifically wrong, but a bit nonsensical and might have caused the 'philosophical' conundrum of yours. Usually, you pass as the caller the entity (BaseObject etc.) which owns the c4d.FieldList which is being sampled.

And for clarity: There is also no need to use the more complex sampling with DirectSample() if you just want to sample a list of points which are not part of a PointObject. Find a simple example at the end of my post.

I hope this helps and cheers,
Ferdinand

The result:
fieldblah.gif

The code:

"""Simple example for sampling a set of points.
"""

import c4d
import typing


doc: c4d.documents.BaseDocument  # The active document
op: typing.Optional[c4d.BaseObject]  # The active object, can be None.

# The points to sample in the field list, ten points placed with a stride of 25 units on the y-axis.
SAMPLE_POINTS: list[c4d.Vector] = [c4d.Vector(0, i * 25, 0) for i in range(10)]

def main() -> None:
    """
    """
    if not isinstance(op, c4d.BaseObject):
        raise RuntimeError("Please select an object.")

    fieldList: c4d.FieldList = op[c4d.FIELDS]
    if not isinstance(fieldList, c4d.FieldList):
        raise TypeError(f"The object {op} has no fields parameter.")

    print (f"The object containing the field list: {op}")

    # Prepare a field input with the points to sample.
    inputField = c4d.modules.mograph.FieldInput(SAMPLE_POINTS, len(SAMPLE_POINTS))

    # Sample all the points in SAMPLE_POINTS with #op as the caller, i.e., the entity for which is
    # being sampled. When you use the method DirectSample() instead, which requires specific
    # initialization and a FieldInfo instance, op would have be passed there in FieldInfo::Create
    # as the caller. The example by Maxime was there at least a bit odd as it passed the polygon
    # object instead, which is not specifically wrong, but results in the 'philosophical' conundrum
    # of 'why am I passing the points when I am passing the point object?'.
    output = fieldList.SampleListSimple(op, inputField, c4d.FIELDSAMPLE_FLAG_VALUE)

    for point, weight in zip(SAMPLE_POINTS, output._value):
        print (f"{point = }, {weight = }")


if __name__ == '__main__':
    main()

MAXON SDK Specialist
developers.maxon.net

Thanks, Ferdinand