Morphed mesh is different than Daz Studio

Issue #749 resolved
Suttisak Denduangchai created an issue

First, salute to you Thomas and the team for the fantastic software. Your new “Morphing armature” feature help me move my workflow from Daz to Blender a lot easier.

Yet, I have some problem. I try to morph many charactors (Victoria 8.1, Brooke 8.1, Beverly who is based on Brook8.1, August 8.1). The result meshed where the geografts (Futalicious, Breatsacular, Full Monty BBQ) attaching to are distorted and different from Daz Studio.

I currently use import-daz 1.61.0654 commit 4ae4b59, Blender 3.0 Alpha (20 October 2021 build), Daz 4.15.1.72 build

M workflow are

  1. Export save the duf file at Base resolution and export dbz file.
  2. Import the duf file using Easy Import
  3. Import morph and then transfer the shape keys.

Comments (21)

  1. Thomas Larsson repo owner

    I think you merged geografts before loading the custom morphs, perhaps by ticking that checkbox in easy import. If you do that you can still load morphs afterwards, but they will only affect the original body verts, not the part of the body that originally belonged to the grafts.

    So you need to use a slightly more cumbersome workflow:

    1. Save the duf file at Base resolution and export dbz file.
    2. Import the duf file using Easy Import with Merge Geografts and Merge Lashes unchecked.
    3. Import all morphs.
    4. Transfer shapekeys to geografts, lashes, and clothes (body hair in your case)
    5. Merge grafts and lashes to the body.

    Alternatively, you could create a file with favorite morphs and load that during easy import, cf. https://diffeomorphic.blogspot.com/2021/05/morph-presets-and-easy-import.html. Then the morphs will be loaded and transferred before the geografts are merged. Make sure that the body type is set to All so the shapekeys are transferred both to geografts and lashes.

  2. Suttisak Denduangchai reporter

    Thanks, @Thomas. But, I’ve already done step 1-5 but the problem is still the same. I start to suspect is it somehow related to rigidity group when I tick ignore rigidity group the area where geograft is attaching to look better but the geograft mesh is then distorted.

  3. Alessandro Padovani

    You say you use blender 3.0 that’s not tested yet so you have to expect bugs there. Does it work with blender 2.93 ?

  4. Suttisak Denduangchai reporter

    I also test in blender 2.93. It’s also not working. I suspect the problem maybe in def correctForRigidity(self, ob, skey): in transfer.py

  5. Suttisak Denduangchai reporter

    I’m trying to fix it. I notice a mistake in correctForRigidity method. I’ve check in Daz Studio, if the geograft has rigidity map (such as Breastacular) but not rigidity group. The rigidity effect will not apply in Daz studio. So I change correctForRigidity to checking the DazRigidityGroups first.

    def correctForRigidity(self, ob, skey):
            from mathutils import Matrix
    
            for rgroup in ob.data.DazRigidityGroups:
                rotmode = rgroup.rotation_mode
                scalemodes = rgroup.scale_modes.split(" ")
                maskverts = [elt.a for elt in rgroup.mask_vertices]
                refverts = [elt.a for elt in rgroup.reference_vertices]
                nrefverts = len(refverts)
                
                if nrefverts == 0:
                    continue
    
                if rotmode != "none":
                    raise RuntimeError("Not yet implemented: Rigidity rotmode = %s" % rotmode)
    
                xcoords = [ob.data.vertices[vn].co for vn in refverts]
                ycoords = [skey.data[vn].co for vn in refverts]
    
                if "Rigidity" in ob.vertex_groups.keys():
                    idx = ob.vertex_groups["Rigidity"].index
                    for v in ob.data.vertices:
                        if(v.index in refverts and not(v.index in maskverts)):
                            continue
                        for g in v.groups:
                            if g.group == idx:
                                x = skey.data[v.index] 
                                x.co = v.co + (1-g.weight)*(x.co - v.co)
    
                xsum = Vector((0,0,0))
                ysum = Vector((0,0,0))
                for co in xcoords:
                    xsum += co
                for co in ycoords:
                    ysum += co
                xcenter = xsum/nrefverts
                ycenter = ysum/nrefverts
    
                xdim = ydim = 0
                for n in range(3):
                    xs = [abs(co[n]-xcenter[n]) for co in xcoords]
                    ys = [abs(co[n]-ycenter[n]) for co in ycoords]
                    xdim += sum(xs)
                    ydim += sum(ys)
                if xdim == 0 or ydim == 0:
                    print("Rigidity division by zero")
                    continue
                scale = ydim/xdim
                smat = Matrix.Identity(3)
                for n,smode in enumerate(scalemodes):
                    #if smode == "primary":
                        smat[n][n] = scale
    
                for n,vn in enumerate(maskverts):#[maskvert for maskvert in maskverts if maskvert not in refverts]):
                    skey.data[vn].co = smat @ (ob.data.vertices[vn].co - xcenter) + ycenter
    

    The Breastacular shape after morphed with Beverly is now comparable to Daz. But there is still a problem with Futalicous. I’m still fixing it.

  6. Suttisak Denduangchai reporter

    I have change the correctForRigidity method to be like this. The result is much better now. I’ve separate the scaling factor to 3-dimension. I still try to figure the Axis scaling (in Rigidity Group Editor) in Daz3d. Right now, I just scaling x,y,z axis respectively but I think to get corrected morped mesh I need to understand what are “Primary” “Secondary” “Tertiary” scaling mode.

     def correctForRigidity(self, ob, skey):
            from mathutils import Matrix
            for rgroup in ob.data.DazRigidityGroups:
                rotmode = rgroup.rotation_mode
                maskverts = [elt.a for elt in rgroup.mask_vertices]
                refverts = [elt.a for elt in rgroup.reference_vertices]
                nrefverts = len(refverts)
                
                if nrefverts == 0:
                    continue
    
                if rotmode != "none":
                    raise RuntimeError("Not yet implemented: Rigidity rotmode = %s" % rotmode)
    
                scalemodes = rgroup.scale_modes.split(" ")
    
                base_coords = [ob.data.vertices[vn].co for vn in refverts]
                shapekey_coords = [skey.data[vn].co for vn in refverts]
    
                xsum = Vector((0,0,0))
                ysum = Vector((0,0,0))
                for co in base_coords:
                    xsum += co
                for co in shapekey_coords:
                    ysum += co
                xcenter = xsum/nrefverts
                ycenter = ysum/nrefverts
    
                scale = Vector((1,1,1))
                for n in range(3):
                    xdim = ydim = 0
                    xs = [abs(co[n]-xcenter[n]) for co in base_coords]
                    ys = [abs(co[n]-ycenter[n]) for co in shapekey_coords]
                    xdim += sum(xs)
                    ydim += sum(ys)
                    if xdim == 0 or ydim == 0:
                        print("Rigidity division by zero")
                        continue
                    scale[n] = ydim/xdim
    
                smat = Matrix.Identity(3)
                for n,smode in enumerate(scalemodes):
                    #if smode == "primary":
                        smat[n][n] = scale[n]-1
    
                if "Rigidity" in ob.vertex_groups.keys():
                    idx = ob.vertex_groups["Rigidity"].index
                    for n,vn in enumerate(maskverts):
                        for v in ob.data.vertices:
                            if(v.index == vn):
                                for g in v.groups:
                                    if g.group == idx:
                                        skey.data[vn].co = (smat*(1-g.weight) @ (ob.data.vertices[vn].co - xcenter)) + (ob.data.vertices[vn].co - xcenter) + ycenter
    

  7. Suttisak Denduangchai reporter

    I’ve update the code to (maybe) handle the primary-tertiary axis scaling

        def correctForRigidity(self, ob, skey):
            from mathutils import Matrix
            for rgroup in ob.data.DazRigidityGroups:
                rotmode = rgroup.rotation_mode
                maskverts = [elt.a for elt in rgroup.mask_vertices]
                refverts = [elt.a for elt in rgroup.reference_vertices]
                nrefverts = len(refverts)
                
                if nrefverts == 0:
                    continue
    
                if rotmode != "none":
                    raise RuntimeError("Not yet implemented: Rigidity rotmode = %s" % rotmode)
    
                scalemodes = rgroup.scale_modes.split(" ")
    
                base_coords = [ob.data.vertices[vn].co for vn in refverts]
                shapekey_coords = [skey.data[vn].co for vn in refverts]
    
                xsum = Vector((0,0,0))
                ysum = Vector((0,0,0))
                for co in base_coords:
                    xsum += co
                for co in shapekey_coords:
                    ysum += co
                xcenter = xsum/nrefverts
                ycenter = ysum/nrefverts
    
                scale = Vector((1,1,1)) #(x,y,z)
                for n in range(3):
                    xdim = ydim = 0
                    xs = [abs(co[n]-xcenter[n]) for co in base_coords]
                    ys = [abs(co[n]-ycenter[n]) for co in shapekey_coords]
                    xdim += sum(xs)
                    ydim += sum(ys)
                    if xdim == 0 or ydim == 0:
                        print("Rigidity division by zero")
                        continue
                    scale[n] = ydim/xdim
    
                # Calculate dimension of reference group for determining primary secondary and tertiary axes
                transpose = np.array(base_coords).T
                xdimension = abs(max(transpose[0])-min(transpose[0]))
                ydimension = abs(max(transpose[1])-min(transpose[1]))
                zdimension = abs(max(transpose[2])-min(transpose[2]))
                refverts_base_dimension = [[xdimension,scale[0]],[ydimension,scale[1]],[zdimension,scale[2]]]
                refverts_base_dimension.sort(key=lambda x: -x[0])
    
                smat = Matrix.Identity(3)
                for n,smode in enumerate(scalemodes):
                    if smode == "primary":
                        smat[n][n] = refverts_base_dimension[0][1]-1
                    elif smode == "secondary":
                        smat[n][n] = refverts_base_dimension[1][1]-1
                    elif smode == "tertiary":
                        smat[n][n] = refverts_base_dimension[2][1]-1
                    else: # No-scale
                        smat[n][n] = 1-1
    
                if "Rigidity" in ob.vertex_groups.keys():
                    idx = ob.vertex_groups["Rigidity"].index
                    for n,vn in enumerate(maskverts):
                        for v in ob.data.vertices:
                            if(v.index == vn):
                                for g in v.groups:
                                    if g.group == idx:
                                        skey.data[vn].co = (smat*(1-g.weight) @ (ob.data.vertices[vn].co - xcenter)) + (ob.data.vertices[vn].co - xcenter) + ycenter
    

  8. Alessandro Padovani

    Exactly which morphs are you trying to load to futalicious ? Does it work fine without morphs ? Please note that you don’t have to transfer Beverly to futalicious since it’s already baked in the dbz.

    For us to look at the issue please upload a duf file for testing, possibly using free assets when possible. That is, G8F with the pear FBM instead of Beverly for example. Then the detailed steps you’re following, I mean including all the details, specifically in this case what futalicious morphs you’re trying to load. If they’re actor morphs you’ll have to use the “morphing armature“ tool.

  9. Suttisak Denduangchai reporter

    I try to load Victoria 8.1 FBM, Brooke 8.1 FBM and Beverly MCM (It will apply after Brooke 8.1) on Genesis 8.1 mesh. The morph is load successfully and correctly morph the Genesis 8.1 mesh. The problem start when I transfer the morph to Breastacular and Futalicous. When I dial Victoria 8.1 FBM to 100% I notice that Futalicous and Breastacular geocraft borders which should be correctly align with genesis 8 main mesh are slightly off. When I merge the geograft, The final mesh when dialing Victoria 8.1 is mostly fine except at the seam between main mesh and merged geoshell. The problem is extremely bad (My top post) when I use Brooke 8.1 FBM or Beverly MCM which extremely distort the main mesh. Then I notice that if I tick “Ignore rigidity group” the shape of Breastacular is better and comparable to Daz3d shape. The Futalicious, however, has a good seam (like Daz3d) but incorrect shape.

    So, I decide to look at the source code and try to fix the problem. The result is almost the same as the final meshed in Daz3D.

  10. Alessandro Padovani

    As I understand it that is not the intended workflow and I don’t know if you can work that way. I mean, usually you export the baked dbz with the shape you want then import custom morphs for posing. That is also what Thomas was referring to in his answer I guess.

  11. Suttisak Denduangchai reporter

    @Alessandro I need to transfer the morph in the first place because I want to move a lot of my workflow out of Daz3D. My workflow is

    1. My duf file is just Genesis 8.1 attaching Breastacular, Fender Bender, Futalicious, Full Monty BBQ with no single morph applied.
    2. I import the file (with dbz) to Blender with Easy Import (I check all default morph such as FACs. JCM … but I uncheck merge toe, merge geograft, merge lashed)
    3. I load custom body morph to main mesh
    4. I transfer Genesis 8.1 shapekeys to 1st layer geograft (with Nearest Face method) (everything except Full Monty BBQ)
    5. I transfer all shapekeys in Futalicious to Full Monty BBQ (This help fixing the problem with some FBM incorrectly distort Full Monty BBQ if I transfer from Genesis 8.1 directly
    6. I merge all geografts
    7. The problem I describe in the 1st post appear.

    I then fix it and redo step 1-6.

    So, Now I can use a lot of FBM (such as morph 50% Victoria 8.1 50% August8.1 with working and correctly shape Geograft) without need to reexport from Daz3d again.

  12. Alessandro Padovani

    Again in my opinion that is an extremely cumbersome workflow. For example consider that most figures get their own custom jcms as well. The purpose of diffeomorphic is not to turn blender into daz studio. You create your custom figure in daz studio then export to blender.

    We better wait the answer by Thomas if it is supposed that you can use diffeomorphic that way.

  13. Suttisak Denduangchai reporter

    For anyone who may be interest, My workflow is now create a master.blend file contain all morphs for geograft and base genesis 8.1 body. I then save this file “master.blend” then I create a copy and import character-specific morphs (such as JCM) like Brooke 8.1 and Beverly like the image. The fix I post earlier helps me fix the geograft mesh problem when merge with main mesh. Now, If I want to create a new character such as Victoria 8.1, I just copy master.blend then load morphs to it.

    I also tweak morph_armature.py to support futalicious and full monty bbq armature like this. (My python skill is newbie; the code add may not be the best practice)

    def getEditBones(rig):
        scale = rig.DazScale
        def d2b90(v):
            return scale*Vector((v[0], -v[2], v[1]))
    
        def isOutlier(vec):
            return (vec[0] == -1 and vec[1] == -1 and vec[2] == -1)
    
        heads = {}
        tails = {}
        offsets = {}
        for pb in rig.pose.bones:
            if isOutlier(pb.DazHeadLocal):
                pb.DazHeadLocal = pb.bone.head_local
            if isOutlier(pb.DazTailLocal):
                pb.DazTailLocal = pb.bone.tail_local
            heads[pb.name] = Vector(pb.DazHeadLocal)
            tails[pb.name] = Vector(pb.DazTailLocal)
            offsets[pb.name] = d2b90(pb.HdOffset)
        for pb in rig.pose.bones:
            if pb.name[-5:] == "(drv)":
                bname = pb.name[:-5]
                fbname = "%s(fin)" % bname
                heads[bname] = heads[fbname] = heads[pb.name]
                tails[bname] = tails[fbname] = tails[pb.name]
                offsets[bname] = offsets[fbname] = offsets[pb.name]
        for pb in rig.pose.bones:
            if "shaft" in pb.name or "Testicle" in pb.name or "scrotum" in pb.name or "Labium" in pb.name or "clitoris" in pb.name or "vagina" in pb.name:
                offsets[pb.name] = offsets[pb.name] + offsets["pelvis"]
                if pb.name[-5:] == "(drv)":
                    bname = pb.name[:-5]
                    fbname = "%s(fin)" % bname
                    offsets[bname] = offsets[fbname] = offsets[pb.name]
        return heads, tails, offsets
    

  14. Alessandro Padovani

    Thank you Suttisak for your work. Though personally I don’t think I’ll go for that workflow, it sounds extremely interesting. It’s kinda having daz studio into blender for figures generation. Your “master.blend” file could be called “genesis_81.blend“ as well since that’s what it is intended for.

    I can’t help with rigs so I’m curious myself to hear from Thomas what he thinks.

  15. Suttisak Denduangchai reporter

    I think this code produce good enough result for me

    transfer.py

        def correctForRigidity(self, ob, skey):
            from mathutils import Matrix
            for rgroup in ob.data.DazRigidityGroups:
                rotmode = rgroup.rotation_mode
                maskverts = [elt.a for elt in rgroup.mask_vertices]
                refverts = [elt.a for elt in rgroup.reference_vertices]
                nrefverts = len(refverts)
                
                if nrefverts == 0:
                    continue
    
                if rotmode != "none":
                    raise RuntimeError("Not yet implemented: Rigidity rotmode = %s" % rotmode)
    
                scalemodes = rgroup.scale_modes.split(" ")
    
                base_coords = [ob.data.vertices[vn].co for vn in refverts]
                shapekey_coords = [skey.data[vn].co for vn in refverts]
    
                # I think Daz3d use Singular Value Decomposition to determine which X,Y,Z scaling between shapekey_coords and base_coords
                # https://www.daz3d.com/forums/discussion/comment/636426/
                # https://gregorygundersen.com/blog/2018/12/10/svd/
                
                base_center_coords = np.average(base_coords, axis=0)
                shapekey_center_coords = np.average(shapekey_coords, axis=0)
    
                # Transfrom Base Coordinate to be relative to its center
                base_coords_relative_to_base_center_coords = base_coords - base_center_coords
                # Singular value decomposition
                U, S1, V = np.linalg.svd(base_coords_relative_to_base_center_coords)
                # Transfrom Shapekey Coordinate to be relative to its center
                shapekey_coords_relative_to_shapekey_center_coords = shapekey_coords - shapekey_center_coords
                # Singular value decomposition
                U, S2, V = np.linalg.svd(shapekey_coords_relative_to_shapekey_center_coords)
                # U matrix is average coordinates of polygon, S is matrix is how coordinates dilate and reflex. The dilated and reflexed shape (without rotation) is U cross S
                scale_between_shapekey_and_base_averagecoords = S2/S1
    
                refverts_base_dimension = [["X",S1[0],scale_between_shapekey_and_base_averagecoords[0]],["Y",S1[1],scale_between_shapekey_and_base_averagecoords[1]],["Z",S1[2],scale_between_shapekey_and_base_averagecoords[2]]]
                # Sort from max dimension to min dimension to determine Primary (scaling of max dimension) to Tertiary (scaling of min dimension) scale mode
                # ex. [["Y",10,1.1],["X",5,1.2],["Z",2,1.05]]
                refverts_base_dimension.sort(key=lambda x: -x[1]) 
                                                                  
                # Determine First - Thrid axis by target object (eg. Geograft) dimensions
                target_dimension= [["X",ob.dimensions.x,1],["Y",ob.dimensions.y,1],["Z",ob.dimensions.z,1]]
                target_dimension.sort(key=lambda x: -x[1])
                
                for n,smode in enumerate(scalemodes): # Scale mode of First to Third axis which is defined in Rigidity group editor in Daz3d
                    if smode == "primary":
                        target_dimension[n][2] = refverts_base_dimension[0][2]
                    elif smode == "secondary":
                        target_dimension[n][2] = refverts_base_dimension[1][2]
                    elif smode == "tertiary":
                        target_dimension[n][2] = refverts_base_dimension[2][2]
                    # No-scale No need to reassign 1 again
                target_dimension.sort(key=lambda x: x[0])
                smat = Matrix.Identity(3)
                base_center_vector = Vector((0,0,0))
                shapekey_center_vector = Vector((0,0,0))
                for n in range(3):
                    base_center_vector[n] = base_center_coords[n]
                    shapekey_center_vector[n] = shapekey_center_coords[n]
                for n in range(3):
                    smat[n][n]= target_dimension[n][2]
                print(smat)
                if "Rigidity" in ob.vertex_groups.keys():
                    idx = ob.vertex_groups["Rigidity"].index
                    for n,vn in enumerate(maskverts): # Called Rigidity Participant Vertex in Daz3D
                        for v in ob.data.vertices:
                            if(v.index == vn):
                                for g in v.groups:
                                    if g.group == idx:
                                        # Max Rigidity (Rigidity=1) coordinate
                                        max_rigidity_coordinate = (smat @ (ob.data.vertices[vn].co - base_center_vector)) + shapekey_center_vector
                                        # Min Rigidity (Rigidity=0) coordinate
                                        min_rididity_coordinate = skey.data[vn].co
                                        # Mix both coordinate using Rigidity Weight Map
                                        skey.data[vn].co = (max_rigidity_coordinate * g.weight) + ((1-g.weight)*min_rididity_coordinate)
    

  16. Thomas Larsson repo owner

    It was a long time since I looked at the rigidity code, and I don’t quite remember what it does, so I copied you code in the last commits. Didn’t see your last suggestion though, will look at that later.

  17. Suttisak Denduangchai reporter

    Thanks, Thomas. if you have a time, I recommend you merge the code, which contains “Singular Value Decomposition”in my last comment. It produces the shape almost same as Daz3d. It's still had a slight difference especially at the junction between main mesh and geograft through.

  18. Thomas Larsson repo owner

    Your new code is incorporated in latest commit. I don’t quite understand what it does, but it seems to work well.

  19. Alessandro Padovani

    @Thomas Rigidity maps are for excluding some parts of a mesh from being deformed by morphs. As for geografts this can be used to exclude the geograft itself from being affected from the deformations of the graft area. Below an example with futalicious setting a rigidity map and groups.

    http://docs.daz3d.com/doku.php/public/software/dazstudio/4/userguide/creating_content/rigging/tutorials/rigidity/start

    Then I can’t understand the code by Suttisak so I can’t tell how it’s good. I’d like to hear from @engetudouiti or @xin what they think. Also, in #223 we introduced a smooth group on the graft area that I guess may interfere with the code by Suttisak, if it’s good I mean.

  20. Thomas Larsson repo owner

    Well, Suttisak’s code is definitely better than mine in some respects, e.g. he covers secondary and tertiary scale axes and not only primary axes. Since it solves his problem, and the cases that I tested seem to work well, I will settle for Suttisak’s code unless new problems appear.

  21. Log in to comment