Useful Python Script

  • @adp being self-taught (well, not if I include all the book and helpful web reference material I've avidly inhaled on the subject over the years) in Python, and only for hobby, not professional use, but having come from a software engineering background, I'm always delighted to find useful instances of techniques I've yet to try, such as accessing globals(). It's also instructive to pick apart others' variable nomenclature quirks and speculate where they've come from :-)

    I was looking for something similar recently, when trying to use Poser Pro 11's measurement tools, only to be desperately frustrated by their inability to be applied to arbitrary locations (not identified by a vertex), and propensity for locking onto vertices on the environment sphere at virtual infinity, rather than the object I wanted to select (a camera, whose vertices can't be selected at all!). Perhaps your scripts will be a useful starting point for that kind of scenario. I ended up having to just hide every single actor in the line of sight which wasn't what I wanted to measure, and parent a prop to the camera to get the result I was after.

  • Poser Ambassadors

    @adp said in Useful Python Script:

    Thanks, looks very useful.

  • I made an extension to hide/unhide anything but the selected actor or figure.
    Hidden status of actors are preserved, so that after 'Unhide' all actors are visible or not as they where before.

    Here is the new

    import os
    import sys
    import re
    from types import NoneType
        import poser
    except ImportError:
        raise RuntimeError("Must be run in Poser")
    DEBUG = False
    SCENE = poser.Scene()
    def global_const(const_str, start_idx=-1):
        start_idx = int(start_idx)
        assert isinstance(const_str, (unicode, str))
        flag = start_idx < 0
        if flag:
            start_idx = globals().setdefault("__const_idx__", 0)
        for i, s in enumerate(re.split("[\s,]+", const_str)):
            assert re.match("[a-z]", s[0], re.IGNORECASE), "Constants must start with a letter: %s" % s
            globals()[s] = i + start_idx
            if flag:
                globals()["__const_idx__"] = i + start_idx
    def resolve_str(ss):
        if isinstance(ss, (list, tuple)):
            return [resolve_str(p) for p in ss]
        if isinstance(ss, dict):
            d = dict()
            for k, v in ss.items():
                d[k] = resolve_str(v)
            return str(d)
            return str(ss.Name())
        except Exception as err:
            return str(ss)
    def debug(*debug_strings):
        if not DEBUG:
        for s in debug_strings:
            print resolve_str(s),
    global_const("NO_MODE, FIGURE_MODE, ACTOR_MODE", 0)
    def _cb_(iScene, iEventType):
        if (iEventType & poser.kEventCodeACTORSELECTIONCHANGED) != 0:
            cb_data = globals().get("__cb_data__")
            if not cb_data:
            mode = int(cb_data.get("mode", NO_MODE))
            if mode == ACTOR_MODE:
                cb_actor, active = cb_data.setdefault("cb_actor", (None, False))
                if cb_actor and active:
            elif mode == FIGURE_MODE:
                cb_figure, active = cb_data.setdefault("cb_figure", (None, False))
                if cb_figure and active:
                    ac = SCENE.CurrentActor()
                    if ac.ItsFigure() != cb_figure:
                        cb_actor, active = cb_data.setdefault("cb_actor", (None, False))
                        if cb_actor and active:
                        cb_data["cb_actor"] = (ac, True)
            elif mode != NO_MODE:
                debug("Mode not in range:", mode)
    def set_figureMode(figure=None, actor=None):
        cb_data = globals().setdefault("__cb_data__", dict())
        if figure is None:
            figure = SCENE.CurrentFigure()
            if figure is None:
                cb_data["mode"] = NO_MODE
                cb_data["cb_figure"] = (None, False)
        if isinstance(actor, (unicode, str)):
            actor = SCENE.ActorByInternalName(actor)
        if actor is None:
            actor = SCENE.CurrentActor()
        if actor.ItsFigure() != figure:
            actor = SCENE.ActorByInternalName("BODY")
        if not isinstance(actor, poser.ActorType):
            globals()["__cb_data__"] = dict()
            raise RuntimeError("Actor unknown: %s" % str(actor))
        assert isinstance(figure, poser.FigureType)
        assert isinstance(actor, poser.ActorType)
        cb_data["mode"] = FIGURE_MODE
        cb_data["cb_figure"] = (figure, True)
        cb_data["cb_actor"] = (actor, True)
        debug("Figuremode set:\n", cb_data)
    def set_actorMode(actor=None):
        cb_data = globals().setdefault("__cb_data__", dict())
        if actor is None:
            actor = SCENE.CurrentActor()
            if actor is None:
                cb_data["mode"] = NO_MODE
                debug("No actor to set.")
        cb_data["mode"] = ACTOR_MODE
        cb_data["cb_actor"] = (actor, True)
        cb_data["cb_figure"] = (actor.ItsFigure(), True)
        debug("Actormode set:\n", cb_data)
    def clearmode():
        globals()["__cb_data__"] = dict(mode=NO_MODE,
                                        cb_figure=(None, False),
                                        cb_actor=(None, False))
        debug("Mode cleared.")
    def setmode(mode):
        if mode not in range(3):
            raise SyntaxError("Mode must be a value: 0, 1 or 2.\n" +
                              "Try global names NO_MODE (0), FIGURE_MODE (1), ACTOR_MODE (2)")
        (clearmode, set_figureMode, set_actorMode)[mode]()
    def hideUnhide(ac_or_fig_tohide=None):
        assert isinstance(ac_or_fig_tohide, (poser.ActorType, poser.FigureType, NoneType))
        cb_data = globals().setdefault("__cb_data__", dict())
        hidden = cb_data.setdefault("hidden", list())
        if ac_or_fig_tohide is None:
            idx = 0
            while hidden:
                idx += 1
                ac, vis = hidden.pop()
            debug(idx, "Actors set to visible.")
            while hidden:
                debug("Restoring", len(hidden), "actors.")
                ac, vis = hidden.pop()
            if isinstance(ac_or_fig_tohide, poser.ActorType):
                debug("Hiding actors")
                for ac in SCENE.Actors():
                    if ac == ac_or_fig_tohide:
                    geom = ac.Geometry() # we assume an actor to hide/unhide has a geometry
                    if hasattr(geom, "NumVertices") and geom.NumVertices() > 0:
                        hidden.append((ac, ac.Visible()))
            elif isinstance(ac_or_fig_tohide, poser.FigureType):
                debug("Hiding figures")
                for ac in ac_or_fig_tohide.Actors():
                    if not ac.IsBodyPart():
                    hidden.append((ac, ac.Visible()))
            debug("Actors hidden:", len(hidden))
            cb_data["hidden"] = hidden
    path = os.path.abspath(os.path.dirname(sys.argv[0]))
    poser.DefineScriptButton(1, os.path.join(path, ""), "Fixed Selection: Figure")
    poser.DefineScriptButton(2, os.path.join(path, ""), "Fixed Selection: Actor")
    poser.DefineScriptButton(3, os.path.join(path, ""), "Hide unselected Figures")
    poser.DefineScriptButton(4, os.path.join(path, ""), "Hide unselected Actors")
    poser.DefineScriptButton(5, os.path.join(path, ""), "Unhide")
    poser.DefineScriptButton(6, os.path.join(path, ""), "Debug on/off")
    poser.DefineScriptButton(7, "", "")
    poser.DefineScriptButton(8, "", "")
    poser.DefineScriptButton(9, "", "")
        poser.DefineScriptButton(10, mainButtonsPath, "Back")
    except NameError:
        poser.DefineScriptButton(10, "", "")

    Additionally 3 miniscripts to activate the buttons are required:
    Save as

        import sys, os
        execfile(os.path.join(os.path.dirname(sys.argv[0]), ""))

    Save as

        import sys, os
        execfile(os.path.join(os.path.dirname(sys.argv[0]), ""))

    Save as

        import sys, os
        execfile(os.path.join(os.path.dirname(sys.argv[0]), ""))

  • @adp Looks very useful. But when I click on the script's name, I get "’s server IP address could not be found."

    Oh, wait! I'm supposed to copy and save as? I need more coffee.

  • @willshetterly I uploaded a fixed and modified version to ShareCG for download:

  • @adp thank you for this :-)

  • @anomalaus What about fixing move directions? I mean: prevent an actor or prop from moving in X and/or Y and/or Z?
    I have to delay my Importer/Exporter to think about the fastest method to find duplicate vertices in Python, output corrected polygons and read them back correctly. So I may have some time to write this.

  • @adp I've just been setting limits and forceLimits to do that kind of thing for rotations. I guess it would work as well for translations, provided Poser will apply the forced limits (I seem to remember there being some things it didn't enforce).

    On your Im/Exporter, I was just looking at your post in the other thread and it reminded me I'd recently done something relevant with creating a 2-manifold version of a hair prop from the original transmapped, non-manifold version, because I wanted to be able to use SuperFly Volumetrics for wet hair effects without getting transmap artifacts (due to transparency only applying to UV mapped surface facets, not the volume enclosed by the model). Anyway, TL;DR, the code I have specifically detects facet seams with duplicated vertices in the geometry, and has code for direct import and export of wavefront obj geometry, if you're interested, I will post the file.

  • @adp

    from the comment header:

    # Convert the currently selected prop from an open (non-manifold) surface to a 2-manifold enclosed volume.
    # Non-2-manifolds are detected by a non-zero count of edges which border only one facet. The vertices on such edges
    # remain unchanged, while all other vertices are duplicated and offset by a user-specified distance along the inverse
    # of the original vertex's normal. All facets will be duplicated, incorporating either duplicate vertices or original
    # edge vertices. All morphs will have the additional vertices appended, with delta values identical to the original
    # deltas of the vertex from which they were derived. 

    The script contains an object class definition which collects lots of additional analysis information of the mesh, such as seams and borders, and methods to load from in-scene geometry or direct from .obj file. Within the CreateManifold() method, there's a pure .obj file creation section which bypasses Poser's obj export routines and writes the object class instance geometry straight to a .obj file, so it can be subsequently loaded by Poser's import routine, which is the only way to create new facet groups within the current Python API (without resorting to manually using the Grouping Tool).

    The script can also preserve existing morphs on the newly cloned object.

  • @adp I should add that Make2Manifold is not particularly optimised for speed (though it does make use of frozensets), so please don't hesitate to suggest ways to improve performance. :-) Please feel free to use anything you find beneficial.

  • @anomalaus Sounds useful. Where is the code? :)

  • @anomalaus Oh, I'm blind... Overlooked the link.

  • @anomalaus Sorry, can't load anything from with any of my browsers on no machine I have. This is what I get:
    0_1531062175656_Bildschirmfoto vom 2018-07-08 17-01-04.png

  • @adp Oh, that's frustrating. I've not seen that before. Do you have an ad blocker or something that's preventing javascript?

    I see this when I click the link (though I have a mega account so that may be relevant):
    0_1531063007359_Screen Shot 2018-07-09 at 1.15.04 am.png

    OK, try this first time I've uploaded anything to ShareCG!

  • @anomalaus I disabled adblockers in firefox and chromium, tried with a very simple webbrowser (without any extensions), and tried with tablets/smartphones.
    But: I'm behind a firewall that blocks .ru and .cn IPs and rejects calls to/from IPs listed as malicious.
    I can ping and get the first part from their index-page. But it seems they do load something from somewhere my firewall calls "malicious".

    Anyway, thanks for uploading to ShareCG. Now I'm off for a moment or two, looking at your script...

  • I think I'll go with a binary searched list to get a pseudo-sorted vertexlist.
    In a test I was able to compute and write an OBJ file with a vertexlist (no tex-vertices jet) and new constructed polygon-indices from Roxie (~30000 vertices) within rund about 2 seconds (but the polygons are not correctly constructed yet :)).

    Tested with an older laptop (SSD, 8 gig Memory, I5 processor), Poser Python, empty scene, just plain Roxie loaded.
    With a "busy" Poser scene and more vertices it may last a bit longer because of memory swapping. I'll test that later. First I have to look why I get a polygonset that looks like a pot full of spaghetti. :)

  • @adp assuming you don't have an out-by-one error in your vertex indexing (that happened to me all the time, in development, because the memory array indices are zero based, but the vertex references in the obj file are one based), make sure you haven't re-ordered any of the vertex references in the facets, because that could randomise the normals (reverse vertex order flips the normal, determined by curled Right Hand fingers match the vertex winding and RH thumb gives the normal direction).

  • @anomalaus No, it wasn't this well know "out-by-one" thing :)
    I simply didn't change the polygon-sets right. Now, if I export Roxie, I get:

    Export to: F:\Python\Morphs+Mags\OBJ-TRANSFER\Roxie.obj
    Time used to export: 1.927 seconds.

    Aaaaand: I can load it perfectly well into my modeler. Without groups, without duplicate vertices!
    :) :) :) :) :) :) :) :) (and a lot more of this...)

    Ok, still no tex-vertices written, but for morphs this is not required.

    Next step is to import the changed model and create a FBM. But only for the actors with moved vertices.

  • @anomalaus said in Useful Python Script:

    @adp make sure you haven't re-ordered any of the vertex references in the facets, because that could randomise the normals (reverse vertex order flips the normal, determined by curled Right Hand fingers match the vertex winding and RH thumb gives the normal direction).

    I construct and write a complete different mesh. I have to, because I have to remove the doubled vertices on groups-boundaries. And I have to create a new set of polygons.

    I do not write normals, because my modeller constructs normals automatically. As far as I know, Poser ignores normales in an OBJ file.

  • @adp it is correct that Poser ignores the OBJ file, vn x y z normal lines, as it creates the normals based on the implicit winding of the facets (which could, theoretically contradict vertex normals), so in effect, poser uses facet normals, rather than vertex normals.

    The other thing to note is that poser IS perfectly capable of creating (and subsequently exporting without corruption) an internal geometry mesh from python scripts (without recourse to writing and reading obj files manually in python), as long as you don't use groups at all (the Python API does not yet support mesh group creation).

    Have you compared the vertex order you generate with that of the original obj file for the figure? Are you able to reconstruct that order so that external morphs you create can load on the original obj version of the figure?