MacOS Sequoia 15.8.1, C4D 2026.4.0: Bevel Deformer vs. Cartoon Renderer

Moin. Ich habe ein Problem mit dem Cartoon Renderer (nicht Sketch & Toon) und mir einen Workaround gebastelt. Jetzt würde ich gerne wissen, ob es eine elegantere Lösung gibt bzw. ob jemandem von euch dazu was einfällt. Ich schildere euch ein bisschen ausführlich, worum es geht und warum. Ich bin Comiczeichner und nutze Toon Shading sehr oft bei Hintergründen. Im Prinzip wäre Sketch & Toon DIE Lösung, wenn es nur nicht so ewig rechnen würde. Der Cartoon Renderer hat kaum Features, rendert aber gefühlt 100x schneller. Und das Comiczeichnen ist leider an Deadlines gebunden.

Es geht schon teilweise um komplexeren Kram, ich spreche von Szenerien wie diesen – das hier ist mit dem Cartoon Renderer gemacht und diesen Look möchte ich:

Mein “Problem” - der Cartoon-Renderer rendert nur Umrisse, keine Kanten. Soweit, so geläufig. Angenommen, ich habe ein solches Objekt, bei dem ich scharfe Kanten, aber auch Rundungen habe:

Mit dem Cartoon Renderer sieht es dann erstmal so aus:

Ich brauche aber die Details. Ab einem Phong-Winkel von 35° soll gerendert werden, darüber nicht. Mein Trick ist jetzt folgender: Ich mache eine Kopie von dem Objekt und werfe einen Bevel Deformer rein. Dort lege ich eine Grenze von 35° fest und einen Bevel von 0,0001 mm. Ich selektiere alle Polygone, wandle das Ganze in ein Objekt, invertiere die Auswahl und lösche dann diese (haarfeinen) Polygone – also die Bevel:

Dann schmeiße ich das alles in das Bruch-Objekt und stelle es auf “Segmente auflösen”. Das Resultat ist jetzt in seine einzelnen Polygone aufgebrochen, aber erst ab einem Winkel von 35°. Die Rundungen bleiben beieinander, die schärferen Kanten werden getrennt. Und im Cartoon-Renderer sieht das dann so aus:


Genau so möchte ich es haben. Das funktioniert prima auch mit 3D-Pflanzen. Die Punkte in der Szenerie vervielfachen sich dabei natürlich, und irgendwann wird das Navigieren in der Szene etwas behäbig – solange man die Bruch-Objekte aktiv lässt. Wenn man sie abschaltet, geht es. Und damit rendern auch ziemlich komplexe Objekte in wenigen Minuten, wo “Sketch & Toon” noch stundenlang am Rechnen ist, welche Linie wo hingehört…

Das Problem dabei ist – das Original-Modell wird dabei zerstört. Die Punkte sind zwar nur 0,0001 mm voneinander entfernt und man könnte es mit “Optimieren” wieder zusammenkleben, aber da enstehen Unsauberkeiten und Ungenauigkeiten, die bei komplexen Modellen in den Wahnsinn führen. Deswegen muss ich immer eine Originalversion behalten.

Ich tüftle schon eine ganze Weile daran herum. Leider würde ich mich um Nodes gerne herumdrücken, weil ich da kaum durchsteige… Meine Frage ist jetzt – gibt es irgendeine Möglichkeit, dabei prozedural zu bleiben? Einen “Bevel Deformer” auszuführen und die Bevelkanten verschwinden zu lassen? Oder ein Objekt in Einzelteile zu brechen, aber z.b. unter der Bedingung eines Phong-Winkels oder so?

Durchaus möglich, dass ich was übersehe. Aber ich stehe hier wie der Ochse, wenns donnert, und komme nicht weiter.

Randbemerkung – die Toon-Shader von Redshift sind ganz nice, aber mir zu Manga-artig. Und die node-basierten Einstellmöglichkeiten sind ein Krampf und mir viel zu wenig. Da fehlen mir Dinge wie Overshoot, einstellbare Varianzen in der Strichbreite, Deckkraft, Winkelgrenzen etc… ich komme damit nicht klar.

Wie schon gesagt: “Sketch & Toon” wäre die Lösung, es ist genial, aber es zwingt mir den Rechner (Apple M4 Max) in die Knie.

1 „Gefällt mir“

Noch keine Reaktion… zu exotisch, meine Problemstellung? Gibt es Fragen oder hab ich mich irgendwo unklar ausgedrückt?

AI ist keine Option?

Kann dir nicht wirklich helfen, wollte nur da lassen, dass das erste Bild richtig geil aufgebaut und gerendert ist. Bin bei HZNKND…sowas wird wohl in AI inzwischen gemacht

1 „Gefällt mir“

Naja, ich bin 1) beruflich Comiczeichner (also de facto Autor) und 2) stehe unter Vertrag. Aus diesen beiden Gegebenheiten entstehen jede Menge Gründe, warum KI keine Option ist.

Ok verstehe, gab es diese Diskusion damals als Computer eingeführt wurden auch? In deinem Beispiel wird ja ne Menge auch nicht mehr gezeichnet. Kann mir vorstellen das die Entwicklung für sowas nicht mehr so interessant ist für Entwickler. Alternativ werfe ich mal die Idee in den Raum mit Claude und Co. ein entsprechendes Plugin zu entwickeln.

Kannst ja mal testen.

"""
Toon-Split – Python-Generator für Cinema 4D
-------------------------------------------
Zerlegt alle Kind-Objekte an Kanten, deren Knickwinkel >= WINKEL ist, in
Einzelobjekte. Rundungen (flache Winkel) bleiben zusammen, harte Kanten
werden getrennt. Ergebnis entspricht: Bevel-Deformer 0,0001 mm -> Bevels
löschen -> Bruch-Objekt "Segmente auflösen" – aber prozedural, das Original
bleibt unangetastet.

Einrichtung:
  1. Python-Generator anlegen, diesen Code komplett einfügen.
  2. Im Python-Generator "Cache optimieren" AUSSCHALTEN
     (der Code prüft selbst, ob sich etwas geändert hat).
  3. Optional: Benutzerdaten hinzufügen -> Typ "Float", Einheit "Grad",
     Name z. B. "Winkel" (muss das erste Benutzerdaten-Feld sein, ID 1).
     Ohne Feld gilt DEFAULT_ANGLE.
  4. Modell(e) als Kind unter den Generator legen.

Übertragen werden: Punkte, Polygone, UVW, Textur-Tags, Phong-Tag,
Polygon-Selektions-Tags (damit auf Selektionen beschränkte Materialien
weiter stimmen). Nicht übertragen: Vertex Maps, Normalen-Tags u. ä.
"""

import c4d
import math
from c4d import utils

DEFAULT_ANGLE = 35.0  # Grad, falls kein Benutzerdaten-Feld existiert

_last_sig = None


# ---------------------------------------------------------------- Helfer ---

def get_angle():
    try:
        v = op[c4d.ID_USERDATA, 1]  # Float mit Einheit Grad kommt als Radiant
        if v is not None:
            return float(v)
    except Exception:
        pass
    return math.radians(DEFAULT_ANGLE)


def walk(o):
    """Objekt, seine Kinder (rekursiv) und alle folgenden Geschwister."""
    while o:
        yield o
        for c in walk(o.GetDown()):
            yield c
        o = o.GetNext()


def children(o):
    out = []
    c = o.GetDown()
    while c:
        out.append(c)
        c = c.GetNext()
    return out


def signature(first):
    flags = c4d.DIRTYFLAGS_DATA | c4d.DIRTYFLAGS_MATRIX | c4d.DIRTYFLAGS_CACHE
    sig = [op.GetDirty(c4d.DIRTYFLAGS_DATA)]
    for o in walk(first):
        sig.append(o.GetDirty(flags))
    return tuple(sig)


def poly_normal(pts, p):
    a, b, c, d = pts[p.a], pts[p.b], pts[p.c], pts[p.d]
    if p.IsTriangle():
        n = (b - a).Cross(c - a)
    else:
        n = (c - a).Cross(d - b)
    return n.GetNormalized()


def poly_edges(p):
    if p.IsTriangle():
        return ((p.a, p.b), (p.b, p.c), (p.c, p.a))
    return ((p.a, p.b), (p.b, p.c), (p.c, p.d), (p.d, p.a))


# ------------------------------------------------------------- Zerlegung ---

def find_groups(obj, cos_limit):
    """Polygone zu Gruppen zusammenfassen; über eine Kante wird nur
    gewandert, wenn der Knickwinkel unter dem Grenzwinkel liegt."""
    pts = obj.GetAllPoints()
    polys = obj.GetAllPolygons()
    normals = [poly_normal(pts, p) for p in polys]

    nb = utils.Neighbor()
    nb.Init(obj)

    group = [-1] * len(polys)
    groups = []
    for start in range(len(polys)):
        if group[start] != -1:
            continue
        gid = len(groups)
        group[start] = gid
        members = [start]
        stack = [start]
        while stack:
            i = stack.pop()
            for a, b in poly_edges(polys[i]):
                j = nb.GetNeighbor(a, b, i)
                if j < 0 or group[j] != -1:
                    continue
                if normals[i].Dot(normals[j]) < cos_limit:
                    continue  # harte Kante -> hier trennen
                group[j] = gid
                members.append(j)
                stack.append(j)
        groups.append(members)
    return groups


def build_piece(src, members):
    pts = src.GetAllPoints()
    polys = src.GetAllPolygons()

    remap = {}
    new_pts = []
    new_polys = []
    for idx in members:
        p = polys[idx]
        ids = []
        for v in (p.a, p.b, p.c, p.d):
            if v not in remap:
                remap[v] = len(new_pts)
                new_pts.append(pts[v])
            ids.append(remap[v])
        new_polys.append(c4d.CPolygon(ids[0], ids[1], ids[2], ids[3]))

    piece = c4d.PolygonObject(len(new_pts), len(new_polys))
    piece.SetAllPoints(new_pts)
    for i, cp in enumerate(new_polys):
        piece.SetPolygon(i, cp)

    # Tags in Originalreihenfolge übertragen
    pred = None
    for t in src.GetTags():
        ttype = t.GetType()
        nt = None
        if ttype == c4d.Tuvw:
            nt = c4d.UVWTag(len(members))
            for i, idx in enumerate(members):
                d = t.GetSlow(idx)
                nt.SetSlow(i, d["a"], d["b"], d["c"], d["d"])
        elif ttype == c4d.Tpolygonselection:
            nt = c4d.SelectionTag(c4d.Tpolygonselection)
            nt.SetName(t.GetName())
            src_bs = t.GetBaseSelect()
            bs = nt.GetBaseSelect()
            for i, idx in enumerate(members):
                if src_bs.IsSelected(idx):
                    bs.Select(i)
        elif ttype in (c4d.Ttexture, c4d.Tphong):
            nt = t.GetClone()
        if nt is not None:
            piece.InsertTag(nt, pred)
            pred = nt

    piece.Message(c4d.MSG_UPDATE)
    return piece


def replace_with_pieces(obj, cos_limit):
    holder = c4d.BaseObject(c4d.Onull)
    holder.SetName(obj.GetName())
    holder.SetMl(obj.GetMl())

    for c in children(obj):
        c.Remove()
        c.InsertUnderLast(holder)

    for k, members in enumerate(find_groups(obj, cos_limit)):
        piece = build_piece(obj, members)
        piece.SetName("%s_%d" % (obj.GetName(), k))
        piece.InsertUnderLast(holder)

    holder.InsertAfter(obj)
    obj.Remove()


# ------------------------------------------------------------------ main ---

def main():
    global _last_sig

    first = op.GetDown()
    if first is None:
        return None

    sig = signature(first)
    cache = op.GetCache()
    if sig == _last_sig and cache is not None:
        for o in walk(first):
            o.Touch()
        return cache.GetClone(c4d.COPYFLAGS_NONE)

    cos_limit = math.cos(get_angle())
    out = c4d.BaseObject(c4d.Onull)
    out.SetName("Toon-Split")

    for child in children(op):
        res = utils.SendModelingCommand(
            c4d.MCOMMAND_CURRENTSTATETOOBJECT, [child],
            c4d.MODELINGCOMMANDMODE_ALL, c4d.BaseContainer(), doc)
        if not res:
            continue
        root = res[0]
        root.InsertUnderLast(out)
        for obj in list(walk(root)):
            if obj.CheckType(c4d.Opolygon) and obj.GetPolygonCount() > 0:
                replace_with_pieces(obj, cos_limit)

    # Originale ausblenden (werden vom Generator "verbraucht")
    for o in walk(first):
        o.Touch()

    _last_sig = signature(first)
    return out

1 „Gefällt mir“

Ich geh am Stock. Hab mich noch nie mit Python befasst, aber wie geil ist das denn bitte. Exakt das ist es, was ich gesucht habe. Vielen Dank! :grinning_face: Ist gleich in die Presets gewandert.

Bist du mit nem kleinen Cameo einverstanden? “HZNKND” irgendwo als Schriftzug, als Schild oder Leuchtreklame in einer Szenerie? Würde reinpassen… der Comic spielt in Berlin, im Jahr 2099…

Hehe, ja gerne. Schick mir dann bitte nen Screenshot oder so.