Create Geometry from Python Generator

I want to create a mathematical surface using a Python generator. Essentially, I’m wanting to get a very similar effect to using a formula deformer on a plane, but instead I’m wanting to do it all from within Python. The reason I want to do it from within Python is because I want to model the output of a complex function, in other words, where the input is of the form x + i*z (i is the imaginary unit) and the output y is either the real or imaginary part of some function of that input. This is easy to do with the cmath module and just taking the real and imaginary parts of the output function, but the problem is that I don’t know how to actually generate a surface from it.

In other words, if I am able to specify a set of points (x, y, z), how would I turn this into a surface using a Python generator?

For example, if I have the list of points (x, y, z):
(1, 1, 1), (1, 1, -1)
(0 0, 1), (0, 0, -1)
(-1, 1, 1), (-1, 1, -1)

I would expect something that looks like this:
e729d816-ac48-4852-b660-9092283bee68-image.png
Any ideas how to do this with a Python generator?

Hi @johntravolski,

thank you for reaching out to us. Please remember that we need the user's coding environment, os and Cinema 4D version attached as tags to questions in order to answer them in the best way possible. I have added a Python tag to your posting, please add the missing information. I also moved your posting into the Development forum. You can learn more about the forum's tagging feature and general support procedures in the Forum Guidelines.

About your question:

What you are trying to do can be and should be done with a Python generator object, since such tasks are its main use case. Cinema 4D does however not provide an auto-triangulation routine in the Python generator, like you might be used to from the various plotting environments out there, e.g. matplotlib or Mathematica/Wolfram. The Python generator is not a plotting tool, but a pruned version of an ObjectData plugin, allowing you to build arbitrary shapes (at the cost of not having an automesher like provide by matplotlib.mplot3d for example). Cinema does also not support complex numbers as point coordinates, since its vector space is exclusively - at least for polygon objects. You can however use Python's complex module if you want to deal with complex numbers and then map these into .

Below you will find the code for your example and also a file with a Python generator in it, running that code.

Cheers,
Ferdinand

the file:
wedge_shape_generator.c4d
the code:

import c4d

# Your vertex data expressed as c4d.Vector instances.
VERTEX_DATA = [
    c4d.Vector(1, 1, 1), c4d.Vector(1, 1, -1),
    c4d.Vector(0, 0, 1), c4d.Vector(0, 0, -1),
    c4d.Vector(-1, 1, 1), c4d.Vector(-1, 1, -1),
]

SCALE = 100.

def main():
    """
    """
    # Create a polygon object node setup for 6 vertices and 2 polygons.
    node = c4d.PolygonObject(pcnt=6, vcnt=2)
    # Your data is rather small, so we have to scale it a bit up.
    points = [p * SCALE for p in VERTEX_DATA]
    # Set all points.
    node.SetAllPoints(points)
    # Now we have to create the polygons which are represented by CPolygon in
    # Cinema 4D. Cinema has no rectangular auto-mesher which will
    # automatically triangulate a point cloud provided in a certain way, like
    # for example provided by matplotlib in Python, Mathematica/Wolfram or
    # similar math packages.

    # In this case we could iterate in a convenient fashion over our data
    # due to its regular nature. I will doe it however manually for clarity.

    # The first polygon: CPolygon references point indices in its attributes
    # a, b, c and d. In Cinema all non-gons are represented by CPolygon,
    # including triangles, which just repeat their third index in d.
    # 
    # We index our vertices here. The a bit odd index order is caused by
    # how you did provide your data. Polygons are organized ccw for forward-
    # facing normals in Cinema, read more about it here [1].
    cpoly = c4d.CPolygon(0, 1, 3, 2)
    # Then we add the first polygon to our mesh.
    node.SetPolygon(0, cpoly)

    # Now we do the same with the second polygon. We have to reindex the
    # vertices 2 and 3 here, since they are shared by both polygons.
    cpoly = c4d.CPolygon(2, 3, 5, 4)
    node.SetPolygon(1, cpoly)

    # And we are done and can return our little wedge shape.
    return node

# Links
# [1] https://developers.maxon.net/docs/Cinema4DPythonSDK/html/manuals/3d_concept/modeling/polygon_object.html#polygon-object

@ferdinand Thanks, this does help a lot, but I wonder if it would just be easier/faster/more efficient if I could somehow simply displace the points of an existing plane, similar to the way the manual mode of the formula deformer applied to a plane does. I don't imagine it would be easy to manually set all of the faces as you do in your example.

Is it possible to displace vertices of existing geometry with Python? I don't know if a generator would be able to do that.

Thanks for your help. This was the best I could do. Let me know if you have any suggestions or improvements.

import c4d
import math
import cmath

min_x = -3.0*math.pi/2.0
max_x = 1.0*math.pi/2.0
min_y = -2.0*math.pi
max_y = 2.0*math.pi

resolution = 10 # number of points along each axis
point_count = resolution*resolution
poly_count = (resolution - 1)*(resolution - 1)

def maprange(xx, min_in, max_in, min_out, max_out):
    return min_out + (xx - min_in)/float(max_in - min_in)*(max_out - min_out)

def getpoints(time_perc):
    tt = 2*math.pi*time_perc*8
    vecs = []
    for yy_r in range(resolution):
        yp = maprange(yy_r, 0, resolution-1, min_y, max_y)
        for xx_r in range(resolution):
            xp = maprange(xx_r, 0, resolution-1, min_x, max_x)
            com = cmath.exp(xp + (yp + tt)*1j)
            zp = com.real
            
            vecs.append(c4d.Vector(xp, zp, yp))
    return vecs

SCALE = 100

def main():
    time = doc.GetTime().Get()
    maxx = doc.GetMaxTime().Get()
    tt = time/maxx
    node = c4d.PolygonObject(pcnt=point_count, vcnt=poly_count)
    
    points = [p * SCALE for p in getpoints(tt)]
    node.SetAllPoints(points)

    polynum = 0
    for yy in range(resolution - 1):
        for xx in range(resolution - 1):
            
            aa = xx + yy*resolution
            bb = xx + yy*resolution + 1
            cc = xx + (yy + 1)*resolution
            dd = xx + (yy + 1)*resolution + 1
            
            cpoly = c4d.CPolygon(aa, bb, dd, cc)
            node.SetPolygon(polynum, cpoly)
            polynum += 1

    return node

https://www.youtube.com/watch?v=UUC7Cvji_sw

The blue is using zp = com.imag while the red is using zp = com.real. Sorry for switching z and y.

Hi @johntravolski,

looks good to me. Some minor points:

  1. An alternative would be to use a Python generator object and simply copy over an input object. Either by linking it in a BaseLink or by establishing a input object logic and get for example the first child of the generator object as the input. With a full blown plugin solution you could also implement a deformer via ObjectData.ModifyObject, i.e., something like a bend deformer, but for your case.
  2. You can also use a TagData plugin or a Python Programming Tag to deform an object.
  3. You can replace maprange with c4d.utils.RangeMap.
  4. Since your complex expression is just xp + (yp + tt)*1j, i.e., a complex number, you could also use transforms, i.e., c4d.Matrix to carry out that rotation in a plane. When these complex expressions get more complex (pun somewhat intended 😉 ), this won't be very straight forward anymore of course.

But these points are mostly academic, if this works for you, I would say this is absolutely fine. In case you run into performance issues - for larger subdivision counts all that mesh building per frame can get a bit slow, I would recommend moving towards a deformer solution, either in the Python generator input object fashion or in a less hack way via a proper ObjectData deformer or a tag solution.

Cheers,
Ferdinand

@ferdinand Thanks. There is something odd that I've noticed. For some reason, when enough of the Python generator goes offscreen, the whole object seems to vanish. Here is a video demonstrating it. I have plotted the complex part of the gamma function and then cloned the plot on a 3x3 grid so you can see nine instances of the Python generator and how each one disappears when enough of it is offscreen.

https://youtu.be/dYx0xvCWUMg

Do you know why this happens, or what I can do to prevent it? Thanks.

@johntravolski
I figured it out, I needed to add

node.Message(c4d.MSG_UPDATE)

after the for double loop where I set the polygons.