Forum: Poser Python Scripting


Subject: Exporting posed and premorphed figure to modeller

adp001 opened this issue on Nov 28, 2020 ยท 40 posts


HartyBart posted Fri, 18 July 2025 at 6:15 AM

Here is the August 2021 version of im_export.py, which I have in my P12 folder and so presumably works with P12 and P13...


from __future__ import print_function, division

import os
import sys
import time
from shutil import copyfile

import numpy as np
import wx

if sys.version_info.major > 2:
    basestring = str
    map = lambda a, b: [a(_b) for _b in b]

try:
    import poser
except ImportError:
    import POSER_FAKE as poser

IMEX_CONFIG = globals().setdefault("IMEX_CONFIG", dict())
NP_PRECISION = np.float32
PRECISION = 6
vertex_frmt = " ".join(["{:.%df}" % PRECISION] * 3)
tex_vertex_frmt = " ".join(["{:.%df}" % PRECISION] * 2)


def set_precision(decimal_places):
    global vertex_frmt, tex_vertex_frmt
    vertex_frmt = " ".join(["{:.%df}" % decimal_places] * 3)
    tex_vertex_frmt = " ".join(["{:.%df}" % decimal_places] * 2)


def ErrDialog(err, msg=None):
    dlg = wx.MessageDialog(None, caption=err, message=msg, style=wx.ICON_ERROR)
    dlg.ShowModal()
    dlg.Close()
    return False


def get_or_create_morphdial(actor, parametername):
    assert isinstance(actor, poser.ActorType)
    p = actor.Parameter(parametername)
    if not p:
        actor.SpawnTarget(parametername)
        p = actor.Parameter(parametername)
    return p


def collect_geometry(figure, use_world=True):
    if figure is None:
        return None

    def np_vertex(v):
        return np.array((v.X(), v.Y(), v.Z()), NP_PRECISION)

    def used_list(_geom):
        """
        Return a list with 1 at positions of used vertices
        and 0 at unused vertices.
        """
        sets = _geom.Sets()
        ar = np.zeros(_geom.NumVertices(), np.int8)
        for poly in _geom.Polygons():
            start = poly.Start()
            numv = poly.NumVertices()
            for _idx in sets[start:start + numv]:
                ar[_idx] = 1
        return ar

    def crosslist(_used):
        length = len(_used)
        cl = np.zeros(length, np.int32)
        not_empty_idx = 0
        for _idx in range(length):
            if _used[_idx] != 0:
                cl[_idx] = not_empty_idx
                not_empty_idx += 1
        return cl

    geom, actorlist, actor_indices = figure.UnimeshInfo()
    used = used_list(geom)

    if use_world:
        newverts = np.zeros((geom.NumVertices(), 3), NP_PRECISION)
        for ac_idx, ac in enumerate(actorlist):
            ac_worldverts = ac.Geometry().WorldVertices()
            for i, vertex_idx in enumerate(actor_indices[ac_idx]):
                newverts[vertex_idx] = np_vertex(ac_worldverts[i])
    else:
        newverts = np.array([[_v.X(), _v.Y(), _v.Z()]
                               for _v in geom.Vertices()], NP_PRECISION)

    return dict(vertices=newverts,
                geom=geom,
                actorlist=actorlist,
                actor_indices=actor_indices,
                crosslist=crosslist(used),
                used=used
                )


def write_matfile(filename, materials):
    """
    Write out a simple material-file.
    """

    def move_image(map2move, imgpath="./"):
        fname = os.path.join(imgpath, os.path.basename(map2move))

        if os.path.exists(fname):
            if os.path.getmtime(map2move) == os.path.getmtime(fname):
                copyfile(map2move, fname)
        else:
            copyfile(map2move, fname)
        return fname

    try:
        tmp = open(filename, "w")
        tmp.close()
    except IOError:
        return ErrDialog("File Error.", "Can't create or write to file '{}'.\n"
                                        "Make sure directory '{}' exist and is writable.".
                         format(filename, os.path.dirname(filename)))

    with open(filename, "w") as mfh:
        move_tex = IMEX_CONFIG.get("CTRL_MoveTextures", False)
        imgpath = os.path.dirname(filename)

        for mat in materials:  # type: poser.MaterialType
            print("newmtl", mat.Name(), file=mfh)
            print("Ns", mat.Ns(), file=mfh)
            print("Ka", "0 0 0", file=mfh)
            print("Kd", " ".join(map(str, mat.DiffuseColor())), file=mfh)
            print("Ks", "0 0 0", file=mfh)
            if mat.TransparencyMax():
                print("D", mat.TransparencyMax(), file=mfh)

            if mat.TransparencyMapFileName():
                texmap = mat.TextureMapFileName()
                if move_tex:
                    texmap = os.path.basename(move_image(texmap, imgpath))
                print("map_D", texmap, file=mfh)

            if mat.TextureMapFileName():
                texmap = mat.TextureMapFileName()
                if move_tex:
                    texmap = os.path.basename(move_image(texmap, imgpath))
                print("map_Kd", texmap, file=mfh)

            if mat.BumpMapFileName():
                texmap = mat.BumpMapFileName()
                if move_tex:
                    texmap = os.path.basename(move_image(texmap, imgpath))
                print("map_Bump -bm %s" % (mat.BumpStrength() / 10.0), texmap, file=mfh)


def read_vertices(filename):
    """
    Read Wavefront obj-file saved to file. Typically a figure exported
    from Poser and modified with an external modeller (Blender etc).
    """

    vertices = list()
    try:
        with open(filename, "r") as fh:
            for line in fh:
                if not line:
                    break
                c, _, v = line.strip().partition(" ")
                if c == "v":
                    vertices.append(map(float, v.split()))

                # Remove following line if vertices are not in one block,
                # so the whole file is processed.
                elif c in ("vt", "vn", "f"):
                    break
    except IndexError:
        return ErrDialog("Vertex Error.",
                         "Vertices in file '%filename' corrupted.")
    except IOError:
        return ErrDialog("File Error.",
                         "File '{}' does not exist or is not accessible.".
                         format(filename))

    return np.array(vertices, NP_PRECISION)


def do_export(figure, filename=None, scale=1, use_groups=False, use_mat=True,
              resetmorph_first=None):
    assert isinstance(figure, poser.FigureType)
    if filename is None:
        filename = figure.Name() + ".obj"

    if not os.path.exists(os.path.dirname(filename)):
        return ErrDialog("Export Error", "Given path\n%s\ndoes not exist." % filename)

    oldvalues = list()
    if resetmorph_first:
        # Use this if you want to save the model without
        # the actual morph (morphname must be given in resetmorph_first).
        for ac in figure.Actors():
            for parm in ac.Parameters():
                if parm.Name() == resetmorph_first:
                    oldvalues.append((ac.Name(), parm.Value()))
                    parm.SetValue(0)

    unigeom = collect_geometry(figure=figure, use_world=True)
    vertices = unigeom.get("vertices") * scale
    geom = unigeom.get("geom")  # type: poser.GeomType
    vertex_crosslist = unigeom.get("crosslist")
    used = unigeom.get("used")

    with open(filename, "w") as fh:
        print("### Date    : %s" % time.asctime(), file=fh)
        print("### Figure  : %s" % figure.Name(), file=fh)
        print("### Vertices: %s" % len(vertices), file=fh)

        if use_mat and geom.Materials():
            matfile = filename.rsplit(".", 1)[0] + ".mtl"
            write_matfile(matfile, geom.Materials())
            print("mtllib ./" + os.path.basename(matfile), file=fh)

        print("o %s" % figure.Name(), file=fh)
        for idx, vertex in enumerate(vertices):
            if used[idx] != 0:
                print("v", vertex_frmt.format(*vertex), file=fh)

        if use_mat:
            for idx, tvert in enumerate(geom.TexVertices()):
                print("vt {} {}".format(tvert.U(), tvert.V()), file=fh)

        current_groups = list()
        current_mat = list()

        s_done = False
        polys = geom.Polygons()
        tpolys = geom.TexPolygons()
        sets = geom.Sets()
        for idx in range(len(sets)):
            sets[idx] = vertex_crosslist[sets[idx]]
        tsets = geom.TexSets()

        for index, poly in enumerate(polys):
            if use_groups:
                if poly.Groups() != current_groups:
                    current_groups = poly.Groups()
                    print("g", ", ".join(current_groups), file=fh)

            if use_mat:
                if poly.MaterialName() != current_mat:
                    current_mat = poly.MaterialName()
                    print("usemtl", current_mat, file=fh)

            if not s_done:
                print("s 1", file=fh)
                s_done = True

            line = [str(sets[idx + poly.Start()] + 1) for idx in range(poly.NumVertices())]
            if use_mat:
                tpoly = tpolys[index]
                for tidx, v in enumerate((tsets[idx + tpoly.Start()] + 1) for idx in range(tpoly.NumTexVertices())):
                    line[tidx] += "/%d" % v

            print("f", " ".join(map(str, line)), file=fh)

    if resetmorph_first:
        for ac_name, value in oldvalues:
            figure.Actor(ac_name).Parameter(resetmorph_first).SetValue(value)

    return filename


def do_import(figure, new_filename, old_filename, morphname,
              add_morphdata=False, force_center=False, scale=1, PRECISION=6):
    assert isinstance(figure, poser.FigureType)

    verts_new = read_vertices(new_filename)
    verts_old = read_vertices(old_filename)
    if len(verts_old) != len(verts_new):
        ErrDialog("Vertices mismatch.", "Old number of vertices: {}."
                                        "New number of vertices: {}.".
                  format(len(verts_old), len(verts_new)))
        return

    vertices, geom, actorlist, actor_indices, \
    verts_crosslist, used = collect_geometry(figure, use_world=False).values()

    # Overwrite vertices with the diff of loaded files.
    # The original vertices are of no use here.
    vertices = (verts_new - verts_old) / int(scale)
    del verts_new
    del verts_old

    body = figure.RootActor()
    masterdial = body.Parameter(morphname)
    if masterdial is None:
        body.CreateValueParameter(morphname)
    masterdial = body.Parameter(morphname)
    if masterdial is None:
        return ErrDialog("Morph Error.", "Can't find or create morph in body actor.")

    for actor_idx, actor in enumerate(actorlist):
        morph = list()
        for i, v_idx in enumerate(actor_indices[actor_idx]):
            x, y, z = map(lambda a: round(a, PRECISION),
                          vertices[verts_crosslist[v_idx]])
            if x != 0 or y != 0 or z != 0:
                morph.append((i, x, y, z))

        if len(morph) == 0:
            continue

        morphparm = get_or_create_morphdial(actor, morphname) # type: poser.ParmType
        zero_idx = set()
        if force_center:
            for idx, v in enumerate(actor.Geometry().Vertices()):
                if v.X() == 0:
                    zero_idx.add(idx)

        if morphparm is None:
            return ErrDialog("Morph Error", "Can't create Morphtarget.")
        if not morphparm.IsMorphTarget():
            return ErrDialog("Morph error.", "Morph Parametername ('%s')\n"
                                             "already exist but is not a morph."
                             % morphname)

        if add_morphdata:
            # Leave the old morph as it is and add the new data to it.
            # Use this if the figure is again exported (and reloaded in
            # the modeller) so that the diff between the old and new morph
            # is just your last change made in the modeller; the morph
            # in the figure is not just overwritten.
            for i, x, y, z in morph:
                xx, yy, zz = morphparm.MorphTargetDelta(i)
                if force_center and i in zero_idx:
                    xx = 0
                morphparm.SetMorphTargetDelta(i, x + xx, y + yy, z + zz)
        else:
            for i in range(actor.Geometry().NumVertices()):
                morphparm.SetMorphTargetDelta(i, 0, 0, 0)
            for i, x, y, z in morph:
                if force_center and i in zero_idx:
                    x = 0
                morphparm.SetMorphTargetDelta(i, x, y, z)

        if force_center:
            fix_morph_at_zero_x(actor, morphparm)

        while morphparm.NumValueOperations():
            morphparm.DeleteValueOperation(0)

        morphparm.AddValueOperation(poser.kValueOpTypeCodeKEY, masterdial)
        vop = morphparm.ValueOperations()[0]
        vop.InsertKey(0, 0)
        vop.InsertKey(1, 1)

    masterdial.SetMinValue(0)
    masterdial.SetMaxValue(1)
    masterdial.SetForceLimits(1)
    masterdial.SetValue(1)
    return morphname


def fix_morph_at_zero_x(poser_obj, morph2fix=None):
    """
    Replace morph X movement where original vertices are at zero position.

    """
    def fix_actor(ac, morphname=morph2fix):
        def fix_morph(morph, indices):
            for vert_idx in indices:
                x, y, z = morph.MorphTargetDelta(vert_idx)
                if x != 0:
                    morph.SetMorphTargetDelta(vert_idx, 0, y, z)

        if ac.IsBodyPart() and hasattr(ac, "Geometry") and ac.Geometry() is not None:
            vert_indices = []
            for idx, v in enumerate(ac.Geometry().Vertices()):
                if v.X() == 0:
                    vert_indices.append(idx)

            if isinstance(morphname, basestring):
                fix_morph(ac.Parameter(morphname), vert_indices)
            elif isinstance(morphname, poser.ParmType):
                fix_morph(morphname, vert_indices)
            elif morphname is None:
                for morph in (p for p in ac.Parameters() if p.IsMorphTarget()):  # type: poser.ParmType
                    fix_morph(morph, vert_indices)

    if isinstance(poser_obj, poser.FigureType):
        for ac in poser_obj.Actors(): # type: poser.ActorType
            fix_actor(ac)
    elif isinstance(poser_obj, poser.ActorType):
        fix_actor(poser_obj)



Learn the Secrets of Poser 11 and Line-art Filters.