Forum: Poser Python Scripting


Subject: Move morph as a whole in any direction

adp001 opened this issue on Aug 14, 2020 ยท 7 posts


adp001 posted Sun, 12 September 2021 at 7:53 AM

And a new one. This time able to translate and rotate morphs!

Bildschirmfoto vom 2021-09-12 14-18-15.png

The "Mover" is a cube generated on the fly. (green if inactive, red if active). Grab that cube and move it around (after you selected an actor and a morph and clicked "Start"). Or use the dials to position more exactly. The morph will follow the movement.

This is not new. But now you can rotate the "mover" and your morph will rotate! Rotation center is the position of the mover. This is why "No Translation" exist: If you check it, you can move the mover to a new position to set a rotation center.

Note: It may last a moment after pressing "Start" until the "mover" appears. Poser has to create that new grouping object first.

I'll post the source here, but to be sure better download it from here, because the editor here may destray parts of the script. And the source code is more readable on my website.

Download from here.

from __future__ import print_function

import math
import sys

import numpy as NP
import wx

try:
    import poser
except ImportError:
    # Not required while inside Poser Python, but very helpfull for external editors.
    # See https://adp.spdns.org
    from PoserLibs import POSER_FAKE as poser

if sys.version_info.major > 2:
    # Python 3 (Poser 12 and above)
    map = lambda a, b: [a(_b) for _b in b]
    basestring = str
else:
    range = xrange

SCENE = poser.Scene()
X, Y, Z = range(3)


def actor_has_geom(ac):
    return isinstance(ac, poser.ActorType) 
           and hasattr(ac, "Geometry") 
           and ac.Geometry() is not None 
           and ac.Geometry().NumVertices() > 0


def is_morphtarget(parm):
    return isinstance(parm, poser.ParmType) 
           and parm.IsMorphTarget() 
           and parm.NumMorphTargetDeltas() != 0


def _rotate(ar, theta, axis=(0, 0, 0), order="XYZ"):
    AX, AY, AZ = axis

    def _X(c, s, ar=ar):
        return NP.dot(ar, NP.array([
            [1.0, AY, AZ],
            [AX, c, -s],
            [AX, s, c]
        ]))

    def _Y(c, s, ar=ar):
        return NP.dot(ar, NP.array([
            [c, AY, -s],
            [AX, 1.0, AZ],
            [s, AY, c]
        ]))

    def _Z(c, s, ar=ar):
        return NP.dot(ar, NP.array([
            [c, -s, AZ],
            [s, c, AZ],
            [AX, AY, 1.0],
        ]))

    for entry in order.upper():
        idx = "XYZ".index(entry)
        ar = locals().get("_" + entry)(math.cos(theta[idx]), math.sin(theta[idx]), ar)

    return ar


def rotated_poserVerts(verts, theta, order):
    return _rotate(NP.array([[v.X(), v.Y(), v.Z()] for v in verts]), theta, order)


def rotate_actor(actor, theta, order):
    if isinstance(actor, basestring):
        try:
            actor = SCENE.Actor(actor)
        except poser.error:
            actor = SCENE.ActorByInternalName(actor)
    geom = actor.Geometry()
    for verts, vv in zip(rotated_poserVerts(geom.Vertices(), theta, order), geom.Vertices()):
        vv.SetX(verts[0])
        vv.SetY(verts[1])
        vv.SetZ(verts[2])


class Mover(object):
    """
    This class uses Posers callbacks. Because of this no Poser object
    can be directly hold in a variable (actor, morph, etc.).
    Internal names are used instead to avoid crashes.
    """

    __slots__ = "_saved_active", 
                "_figure_iname", "_moveactor_iname", "_morphactor_iname", "_morphname", 
                "morphindices", "morphdata", "morph_origdata", "morphcenter", 
                "_morph_min", "_morph_max", 
                "mover_startpos", "last_coords", "use_trans", "use_rot", 
                "current_rotation"

    def __init__(self, morphparameter):
        assert is_morphtarget(morphparameter), "Parameter has no Morphdata."
        assert actor_has_geom(morphparameter.Actor()), "Actor to morph must have Geometry."

        self.last_coords = NP.array([0, 0, 0], NP.float)
        self.current_rotation = NP.array([0, 0, 0], NP.float)

        self._saved_active = [False, ]
        self._figure_iname = None
        self._morphname = self._morphactor_iname = None
        self._moveactor_iname = None
        self.morphindices = self.morphdata = self.morph_origdata = None
        self.use_trans = self.use_rot = False

        self.createMover()
        self.setmorph(morphparameter)
        self.morphcenter = self.get_morph_center(False)

        self._morph_min, self._morph_max = self.get_morph_minmax(True)
        self.mover_startpos = self.mover_defaultpos()
        self.move_mover(self.mover_startpos)
        self.set_movercolor(0, 255, 0)

        SCENE.DrawAll()

    @property
    def active(self):
        return self._saved_active[-1]

    @property
    def morphactor(self):
        try:
            return SCENE.ActorByInternalName(self._morphactor_iname)
        except poser.error:
            print("Actor '%s' does not exist anymore." % self._morphactor_iname)
            self.stop()

    @morphactor.setter
    def morphactor(self, actor):
        if actor is None:
            self._morphactor_iname = None
        else:
            assert isinstance(actor, poser.ActorType)
            self._morphactor_iname = actor.InternalName()

    @property
    def moveactor(self):
        try:
            return SCENE.ActorByInternalName(self._moveactor_iname)
        except poser.error:
            print("Move actor does not exist anymore.")
            self.stop()


    @moveactor.setter
    def moveactor(self, actor):
        if actor is None:
            self._moveactor_iname = None
        else:
            assert isinstance(actor, poser.ActorType)
            self._moveactor_iname = actor.InternalName()

    @property
    def figure(self):
        try:
            return SCENE.FigureByInternalName(self._figure_iname)
        except poser.error:
            return None

    @figure.setter
    def figure(self, figure):
        if figure is None:
            self._figure_iname = None
        else:
            assert isinstance(figure, poser.FigureType)
            self._figure_iname = figure.InternalName()

    @property
    def morph(self):
        try:
            return self.morphactor.Parameter(self._morphname)
        except Exception:
            print("Morph '%s' does not exist anymore." % self._morphname)

    @morph.setter
    def morph(self, parameter):
        assert isinstance(parameter, poser.ParmType)
        self._morphname = parameter.InternalName()

    def mover_defaultpos(self):
        c = NP.array(self.get_morph_center(True))
        c[2] = self._morph_max[2] + 0.02
        return c

    def push_active(self, v):
        self._saved_active.append(bool(v))
        return v

    def pop_active(self):
        return None if not self._saved_active 
            else self._saved_active.pop()

    def start(self):
        SCENE.SelectFigure(self.figure)
        self._saved_active = [False, ]
        self.reset_mover()
        self._start_mover()
        self.set_movercolor(255, 0, 0)
        self._saved_active = [True, ]
        SCENE.SelectActor(self.moveactor)

    def stop(self):
        SCENE.SelectFigure(self.figure)
        self._saved_active = [False, ]
        self._stop_mover()
        self.set_movercolor(0, 255, 0)

    def set_movercolor(self, r, g, b):
        s = self.moveactor.Material("Preview").ShaderTree()
        s.Node(0).Input(0).SetColor(r, g, b)
        s.UpdatePreview()

    def setmorph(self, morphparm):
        self.push_active(False)
        if isinstance(morphparm, basestring):
            morphparm = SCENE.CurrentActor().Parameter(morphparm)

        if is_morphtarget(morphparm):
            self.morph = morphparm
            self.morphactor = ac = morphparm.Actor()

            fig = ac.ItsFigure() if ac.IsBodyPart() else SCENE.Figures()[0]
            self.figure = fig if fig else None

            numv = ac.Geometry().NumVertices()
            self.morph_origdata = NP.array([morphparm.MorphTargetDelta(idx)
                                            for idx in range(numv)])
            self.morphindices = NP.array([idx for idx, v in enumerate(self.morph_origdata)
                                          if not all(v == (0, 0, 0))], NP.int32)
            self.morphdata = self.morph_origdata[self.morphindices]

        else:
            print("Choosen Parameter is not a morphtarget with geometry.")
            print("Actor:", self._morphactor_iname, "- Parameter:", self._morphname)

        self.pop_active()

    def rotate_morphdata(self, theta, order):
        if not self.use_rot:
            self.morphdata = _rotate(self.morphdata, theta, self.morphcenter, order=order)

    def update_morph(self):
        """
        Called from callbacks if mover was moved or
        rotated and morph needs to be updated.
        """
        self.last_coords = NP.array(self.moveactor.WorldDisplacement())
        diff = [0, 0, 0] if self.use_trans 
            else (self.last_coords - self.mover_startpos) * .25
        ar = self.morphdata + diff
        m = self.morph
        for idx, (x, y, z) in zip(self.morphindices, ar):
            m.SetMorphTargetDelta(idx, x, y, z)
        SCENE.DrawAll()

    def undo_morph(self):
        m = self.morph
        if m:
            for idx in self.morphindices:
                x, y, z = self.morph_origdata[idx]
                m.SetMorphTargetDelta(idx, x, y, z)
            self.morphdata = self.morph_origdata.copy()
            self.mover_startpos = self.moveactor.WorldDisplacement()
            SCENE.DrawAll()

    def current_morph_permanent(self):
        self.push_active(False)
        self.update_morph()  # make sure last changes are done.
        self.setmorph(self.morph)  # setting it will take the data currently in morph
        center = self.get_morph_center(True)
        self.morphcenter = center
        self.mover_startpos = self.mover_defaultpos()
        self.move_mover(self.mover_startpos)
        SCENE.DrawAll()
        self.pop_active()

    def get_morph_minmax(self, world=False):
        geom = self.morph.Actor().Geometry()
        verts = NP.array([[v.X(), v.Y(), v.Z()]
                          for v in (geom.WorldVertices() if world else geom.Vertices())])
        verts = verts[self.morphindices] + NP.array([self.morph.MorphTargetDelta(i)
                                                     for i in self.morphindices])
        return NP.min(verts, axis=0), NP.max(verts, axis=0)

    def get_morph_center(self, world=False):
        """:rtype: NP.ndarray"""
        _min, _max = self.get_morph_minmax(world)
        return _min + (_max - _min) / 2.0

    def __trans__(self, parm, value):
        """Callback on translation changes."""
        if self.active:
            idx = [poser.kParmCodeXTRAN, poser.kParmCodeYTRAN, poser.kParmCodeZTRAN].index(parm.TypeCode())
            if self.last_coords[idx] != parm.Actor().WorldDisplacement()[idx]:
                self.update_morph()
        return value

    def __rot__(self, parm, value):
        """Callback on rotation changes."""
        if self.active:
            idx = [poser.kParmCodeXROT, poser.kParmCodeYROT, poser.kParmCodeZROT].index(parm.TypeCode())
            r = math.radians(value)
            if self.current_rotation[idx] != r:
                self.current_rotation[idx] = r
                self.rotate_morphdata(self.current_rotation, "XYZ"[idx])
                self.update_morph()
        return value

    def createMover(self):
        try:
            ac = SCENE.Actor("MorphMover")
        except poser.error:
            ac = SCENE.CreateGrouping()
            ac.SetName("MorphMover")
            ac.ParameterByCode(poser.kParmCodeASCALE).SetValue(.15)
            ac.SetDisplayStyle(poser.kDisplayCodeFLATLINED)
        ac.SetVisible(True)
        self._moveactor_iname = ac.InternalName()
        return ac

    def move_mover(self, coords):
        ma = self.moveactor
        if ma:
            self.push_active(False)
            ma.ParameterByCode(poser.kParmCodeXTRAN).SetValue(coords[X])
            ma.ParameterByCode(poser.kParmCodeYTRAN).SetValue(coords[Y])
            ma.ParameterByCode(poser.kParmCodeZTRAN).SetValue(coords[Z])
            self.last_coords = NP.array(coords, NP.float)
            SCENE.DrawAll()
            self.pop_active()

    def reset_mover(self):
        ma = self.moveactor
        if ma:
            self.mover_startpos = NP.array(self.moveactor.WorldDisplacement())
            self.move_mover(self.mover_startpos)

    def _start_mover(self):
        ma = self.moveactor
        if ma:
            for code in (poser.kParmCodeXROT, poser.kParmCodeYROT, poser.kParmCodeZROT):
                p = ma.ParameterByCode(code)
                p.SetValue(0)
                p.SetUpdateCallback(self.__rot__)

            for code in (poser.kParmCodeXSCALE, poser.kParmCodeYSCALE, poser.kParmCodeZSCALE):
                v = ma.ParameterByCode(code).Value()
                ma.ParameterByCode(code).SetUpdateCallback(lambda a, b: v)

            for code in (poser.kParmCodeXTRAN, poser.kParmCodeYTRAN, poser.kParmCodeZTRAN):
                ma.ParameterByCode(code).SetUpdateCallback(self.__trans__)

    def _stop_mover(self):
        ma = self.moveactor
        if ma:
            for code in (poser.kParmCodeXTRAN, poser.kParmCodeYTRAN, poser.kParmCodeZTRAN,
                         poser.kParmCodeXROT, poser.kParmCodeYROT, poser.kParmCodeZROT,
                         poser.kParmCodeASCALE,
                         poser.kParmCodeXSCALE, poser.kParmCodeYSCALE, poser.kParmCodeZSCALE):
                ma.ParameterByCode(code).ClearUpdateCallback()
            for code in (poser.kParmCodeXROT, poser.kParmCodeYROT, poser.kParmCodeZROT):
                ma.ParameterByCode(code).SetValue(0)
            for code in (poser.kParmCodeXSCALE, poser.kParmCodeYSCALE, poser.kParmCodeZSCALE):
                ma.ParameterByCode(code).SetValue(1)


# ----------------------------------------------------------------
# wxPython UI related
# ----------------------------------------------------------------

def collectActornames(figure=None):
    if figure is None:
        figure = SCENE.CurrentFigure()
    return [ac.Name() for ac in (SCENE.Actors() if not figure else figure.Actors())]


def collectMorphnames(actor=None, filter=None):
    if filter is None:
        filter = lambda n: True
    if actor is None:
        actor = SCENE.CurrentActor()
    return [p.Name() for p in actor.Parameters() if filter(p.Name()) and is_morphtarget(p)]


class MyFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        super(MyFrame, self).__init__(parent, wx.ID_ANY, *args, **kwargs)

        self.morph_mover = None  # type: None or Mover
        self._selected_actor = None
        self._selected_parameter = dict()
        self.init_ui()

        self.Bind(wx.EVT_CLOSE, self.ev_close)
        self.Bind(wx.EVT_SIZE, self.ev_size)
        self.Bind(wx.EVT_CHECKBOX, self.on_check)
        self.Bind(wx.EVT_BUTTON, self.on_button)
        self.Bind(wx.EVT_CHOICE, self.on_choice)

    @property
    def selected_actor(self):
        if self._selected_actor:
            try:
                return SCENE.ActorByInternalName(self._selected_actor)
            except poser.error:
                pass
        return None

    @property
    def selected_parameter(self):
        if self.selected_actor:
            return self._selected_parameter.get(self._selected_actor, None)
        return None

    def startMover(self):
        if self.morph_mover is not None:
            self.morph_mover.start()
        elif self.selected_parameter is not None:
            ac = self.selected_actor
            if ac:
                try:
                    parm = ac.Parameter(self.selected_parameter)
                    self.morph_mover = Mover(parm)
                    self.morph_mover.start()
                except poser.error:
                    print("Can't use parameter '%s' in actor '%s'." % (
                    self.selected_parameter, self.selected_actor.Name()))
        if self.morph_mover:
            self.morph_mover.use_trans = self.FindWindowByName("cb_notranslation").IsChecked()
            self.morph_mover.use_rot = self.FindWindowByName("cb_norotation").IsChecked()

    def ev_close(self, event):
        if self.morph_mover is not None:
            self.morph_mover.stop()
            self.morph_mover.moveactor.Delete()
        event.Skip()

    def ev_size(self, event):
        self.SetSize(event.Size)
        self.Refresh()
        event.Skip()

    def init_ui(self):
        panel = wx.Panel(self)
        main_sz = wx.BoxSizer(wx.VERTICAL)
        sz_standard = (0, wx.EXPAND | wx.LEFT | wx.RIGHT, 8)
        sz_choice = (0, wx.EXPAND | wx.LEFT | wx.RIGHT, 8)
        sz_check = (1, wx.ALIGN_CENTER | wx.ALL, 8)
        btn_style = wx.NO_BORDER
        choice_style = wx.CB_SORT | wx.NO_BORDER
        spacersize = 12

        main_sz.Add(wx.StaticText(panel, wx.ID_ANY, "Select Actor"), *sz_choice)
        main_sz.Add(wx.Choice(panel, wx.ID_ANY, style=choice_style, name="choice_actors",
                              choices=collectActornames()), *sz_choice)
        main_sz.AddSpacer(spacersize)
        main_sz.Add(wx.StaticText(panel, wx.ID_ANY, "Select Morph"), *sz_choice)
        main_sz.Add(
            wx.Choice(panel, wx.ID_ANY, style=choice_style, name="choice_parameters",
                      choices=collectMorphnames()), *sz_choice)
        main_sz.AddSpacer(spacersize)
        main_sz.AddStretchSpacer(1)

        btn_sz = wx.GridSizer(4, 2, vgap=2, hgap=2)
        btn_sz.Add(wx.CheckBox(panel, wx.ID_ANY, label="No Translation", name="cb_notranslation"), *sz_check)
        btn_sz.Add(wx.CheckBox(panel, wx.ID_ANY, label="No Rotation", name="cb_norotation"), *sz_check)
        btn_sz.Add(wx.Button(panel, wx.ID_ANY, label="Start", style=btn_style, name="btn_start"), *sz_standard)
        btn_sz.Add(wx.Button(panel, wx.ID_ANY, label="Stop", style=btn_style, name="btn_stop"), *sz_standard)
        btn_sz.Add(wx.Button(panel, wx.ID_ANY, label="Reload", style=btn_style, name="btn_reload"), *sz_standard)
        btn_sz.Add(wx.Button(panel, wx.ID_ANY, label="Reset Mover", style=btn_style, name="btn_reset"), *sz_standard)
        btn_sz.Add(wx.Button(panel, wx.ID_ANY, label="Reset Morph", style=btn_style, name="btn_undo"), *sz_standard)
        btn_sz.Add(wx.Button(panel, wx.ID_ANY, label="Morph Permanent", style=btn_style, name="btn_permanent"),
                   *sz_standard)
        main_sz.Add(btn_sz, 0, wx.ALIGN_CENTER)
        main_sz.AddSpacer(spacersize)

        main_sz.AddStretchSpacer(1)
        main_sz.Layout()
        panel.SetSizer(main_sz)

        self.reload_actors()

    def reload_actors(self):
        ac = SCENE.CurrentActor()
        self._selected_actor = ac.InternalName()
        ac_choice = self.FindWindowByName("choice_actors")
        ac_choice.Clear()
        map(ac_choice.Append, collectActornames(None if not hasattr(ac, "ItsFigure")
                                                else ac.ItsFigure()))
        ac_choice.SetStringSelection(ac.Name())
        self.reload_parms()

    def reload_parms(self):
        ac = self.selected_actor
        if ac:
            choice = self.FindWindowByName("choice_parameters")  # type: wx.Choice
            choice.Clear()
            for name in collectMorphnames(ac, filter=lambda n: n[0] not in ".-"):
                choice.Append(name)
            sel = self.selected_parameter
            if not sel:
                sel = choice.GetString(0)
                self._selected_parameter[self._selected_actor] = sel
            choice.SetStringSelection(sel)

    def on_check(self, event):
        obj = event.GetEventObject()  # type: wx.CheckBox
        name = obj.Name.lower()
        if self.morph_mover:
            if "notrans" in name:
                self.morph_mover.use_trans = obj.IsChecked()
            if "norot" in name:
                self.morph_mover.use_rot = obj.IsChecked()
            self.morph_mover.update_morph()

    def on_button(self, event):
        obj = event.GetEventObject()  # type: wx.Button
        name = obj.Name.lower()
        if "start" in name:
            self.startMover()
        elif "stop" in name:
            self.morph_mover.stop()
        elif "reload" in name:
            self.reload_actors()
        elif "reset" in name:
            self.morph_mover.reset_mover()
        elif "undo" in name:
            self.morph_mover.undo_morph()
        elif "permanent" in name:
            self.morph_mover.current_morph_permanent()

    def on_choice(self, event):
        obj = event.GetEventObject()  # type: wx.Choice
        name = obj.Name.lower()
        sel_str = obj.GetStringSelection()
        if "actor" in name:
            self._selected_actor = SCENE.Actor(sel_str).InternalName()
            self.reload_parms()
        elif "parameter" in name:
            self._selected_parameter[self._selected_actor] = sel_str
            if self.morph_mover is not None:
                self.morph_mover.stop()
                self.morph_mover = None
                self.startMover()


if __name__ == "__main__":
    app = poser.WxApp()
    aui = poser.WxAuiManager()  # type: wx.aui.AuiManager
    root = aui.GetManagedWindow()  # type: wx.Window
    main = MyFrame(root)
    main.Show()

By the way: This is not meant for production but to show and test what is possible with Poser Python.