Beispiel #1
0
def unit_normal(a, b, c):
    mat_x = Matrix(((1, a[1], a[2]), (1, b[1], b[2]), (1, c[1], c[2])))
    mat_y = Matrix(((a[0], 1, a[2]), (b[0], 1, b[2]), (c[0], 1, c[2])))
    mat_z = Matrix(((a[0], a[1], 1), (b[0], b[1], 1), (c[0], c[1], 1)))

    x = Matrix.determinant(mat_x)
    y = Matrix.determinant(mat_y)
    z = Matrix.determinant(mat_z)

    magnitude = (x**2 + y**2 + z**2)**.5
    return (x/magnitude, y/magnitude, z/magnitude)
Beispiel #2
0
def unit_normal(a, b, c):
    mat_x = Matrix(((1, a[1], a[2]), (1, b[1], b[2]), (1, c[1], c[2])))
    mat_y = Matrix(((a[0], 1, a[2]), (b[0], 1, b[2]), (c[0], 1, c[2])))
    mat_z = Matrix(((a[0], a[1], 1), (b[0], b[1], 1), (c[0], c[1], 1)))

    x = Matrix.determinant(mat_x)
    y = Matrix.determinant(mat_y)
    z = Matrix.determinant(mat_z)

    magnitude = (x**2 + y**2 + z**2)**.5
    return (x / magnitude, y / magnitude, z / magnitude)
Beispiel #3
0
def pointInTri2D(v, v1, v2, v3):
    global dict_matrix

    key = v1.x, v1.y, v2.x, v2.y, v3.x, v3.y

    # Commented because its slower to do teh bounds check, we should realy cache the bounds info for each face.
    '''
    # BOUNDS CHECK
    xmin= 1000000
    ymin= 1000000

    xmax= -1000000
    ymax= -1000000

    for i in (0,2,4):
        x= key[i]
        y= key[i+1]

        if xmax<x:	xmax= x
        if ymax<y:	ymax= y
        if xmin>x:	xmin= x
        if ymin>y:	ymin= y

    x= v.x
    y= v.y

    if x<xmin or x>xmax or y < ymin or y > ymax:
        return False
    # Done with bounds check
    '''
    try:
        mtx = dict_matrix[key]
        if not mtx:
            return False
    except:
        side1 = v2 - v1
        side2 = v3 - v1

        nor = side1.cross(side2)

        mtx = Matrix((side1, side2, nor))

        # Zero area 2d tri, even tho we throw away zerop area faces
        # the projection UV can result in a zero area UV.
        if not mtx.determinant():
            dict_matrix[key] = None
            return False

        mtx.invert()

        dict_matrix[key] = mtx

    uvw = (v - v1) * mtx
    return 0 <= uvw[0] and 0 <= uvw[1] and uvw[0] + uvw[1] <= 1
def pointInTri2D(v, v1, v2, v3):
    global dict_matrix

    key = v1.x, v1.y, v2.x, v2.y, v3.x, v3.y

    # Commented because its slower to do teh bounds check, we should realy cache the bounds info for each face.
    '''
    # BOUNDS CHECK
    xmin= 1000000
    ymin= 1000000

    xmax= -1000000
    ymax= -1000000

    for i in (0,2,4):
        x= key[i]
        y= key[i+1]

        if xmax<x:	xmax= x
        if ymax<y:	ymax= y
        if xmin>x:	xmin= x
        if ymin>y:	ymin= y

    x= v.x
    y= v.y

    if x<xmin or x>xmax or y < ymin or y > ymax:
        return False
    # Done with bounds check
    '''
    try:
        mtx = dict_matrix[key]
        if not mtx:
            return False
    except:
        side1 = v2 - v1
        side2 = v3 - v1

        nor = side1.cross(side2)

        mtx = Matrix((side1, side2, nor))

        # Zero area 2d tri, even tho we throw away zerop area faces
        # the projection UV can result in a zero area UV.
        if not mtx.determinant():
            dict_matrix[key] = None
            return False

        mtx.invert()

        dict_matrix[key] = mtx

    uvw = (v - v1) * mtx
    return 0 <= uvw[0] and 0 <= uvw[1] and uvw[0] + uvw[1] <= 1
Beispiel #5
0
def physics_mass_center(mesh):
    '''Calculate (final triangulated) mesh's mass and center of mass based on volume'''
    volume = 0.
    mass = 0.
    com = Vector()
    # Based on Stan Melax's volint
    for face in mesh.polygons:
        a = Matrix((mesh.vertices[face.vertices[0]].co, mesh.vertices[face.vertices[1]].co, mesh.vertices[face.vertices[2]].co))
        vol = a.determinant()
        volume += vol
        com += vol * (a[0] + a[1] + a[2])
    if volume > 0:
        com /= volume * 4.
        mass = volume / 6.
        return mass, com
    else:
        return mass, Vector()
def loadStatueMinusPose(context):
    ob,statue,scn = getMeshes(context)
    ob,rig,posed = applyArmature(context)
    posed.name = "Temporary"

    nVerts = len(ob.data.vertices)

    relMats = {}
    for vg in ob.vertex_groups:
        try:
            pb = rig.pose.bones[vg.name]
        except KeyError:
            pb = None
        if pb:
            relMats[vg.index] = pb.matrix * pb.bone.matrix_local.inverted()
        else:
            print("Skipping vertexgroup %s" % vg.name)
            relMats[vg.index] = Matrix().identity()

    svs = statue.data.vertices
    pvs = posed.data.vertices
    ovs = ob.data.vertices

    skey = createNewMeshShape(ob, statue.name, scn)
    relmat = Matrix()
    y = Vector((0,0,0,1))
    for v in ob.data.vertices:
        vn = v.index
        diff = svs[vn].co - pvs[vn].co
        if diff.length > 1e-4:
            relmat.zero()
            wsum = 0.0
            for g in v.groups:
                w = g.weight
                relmat += w * relMats[g.group]
                wsum += w
            factor = 1.0/wsum
            relmat *= factor

            y[:3] = svs[vn].co
            x = relmat.inverted() * y
            skey.data[vn].co = Vector(x[:3])

            z = relmat * x

            xdiff = skey.data[vn].co - ovs[vn].co

            if False and vn in [8059]:
                print("\nVert", vn, diff.length, xdiff.length)
                print("det", relmat.determinant())
                print("d (%.4f %.4f %.4f)" % tuple(diff))
                print("xd (%.4f %.4f %.4f)" % tuple(xdiff))
                checkRotationMatrix(relmat)
                print("Rel", relmat)
                print("Inv", relmat.inverted())

                s = pvs[vn].co
                print("s ( %.4f  %.4f  %.4f)" % (s[0],s[1],s[2]))
                print("x ( %.4f  %.4f  %.4f)" % (x[0],x[1],x[2]))
                print("y ( %.4f  %.4f  %.4f)" % (y[0],y[1],y[2]))
                print("z ( %.4f  %.4f  %.4f)" % (z[0],z[1],z[2]))
                o = ovs[vn].co
                print("o (%.4f %.4f %.4f)" % (o[0],o[1],o[2]))
                print("r (%.4f %.4f %.4f)" % tuple(skey.data[vn].co))

                for g in v.groups:
                    print("\nGrp %d %f %f" % (g.group, g.weight, relMats[g.group].determinant()))
                    print("Rel", relMats[g.group])

                #halt

    #scn.objects.unlink(statue)
    scn.objects.unlink(posed)
Beispiel #7
0
def generate_bezier(
    points: List[Vector],  # puntos
    first: int,  # idx primer punto
    last: int,  # idx último punto
    parametro: List[float],  # parámetro
    that_1: Vector,  # tangente al primer punto
    that_2: Vector
) -> List[Vector]:  # tangente al último punto

    n_pts = last - first + 1

    a = np.ndarray([n_pts, 2], dtype=Vector)

    C = Matrix([[0, 0], [0, 0]])

    X = Vector((0, 0))
    tmp: Vector
    bez_curve = [Vector((0, 0, 0)) for _ in range(4)]

    # Compute the A's
    for i in range(n_pts):
        a[i][0] = that_1 * b_1(parametro[i])
        a[i][1] = that_2 * b_2(parametro[i])

    P_0 = points[first]
    P_3 = points[last]
    # Create the C and X matrices
    for i in range(n_pts):
        C.row[0][0] += a[i][0].length_squared
        C.row[0][1] += a[i][0].dot(a[i][1])
        C.row[1][0] = C.row[0][1]
        C.row[1][1] = a[i][1].length_squared

        tmp = (points[first + i] - ((P_0 * b_0(parametro[i])) +
                                    ((P_0 * b_1(parametro[i])) +
                                     ((P_3 * b_2(parametro[i])) +
                                      (P_3 * b_3(parametro[i]))))))

        X[0] += a[i, 0].dot(tmp)
        X[1] += a[i, 1].dot(tmp)

        # Compute the determinants of C and X
        # C.row[0][0] * C.row[1][1] - C.row[1][0]*C.row[0][1]
        det_C0_C1 = C.determinant()
        det_C0_X = C.row[0][0] * X[1] - C.row[1][0] * X[0]
        det_X_C1 = X[0] * C.row[1][1] - X[1] * C.row[0][1]

        # Finally, derive alpha values
        alpha_l = 0.0 if det_C0_C1 == 0 else det_X_C1 / det_C0_C1
        alpha_r = 0.0 if det_C0_C1 == 0 else det_C0_X / det_C0_C1

        # If alpha negative, use the Wu/Barsky heuristic (see text)
        # (if alpha is 0, you get coincident control points that lead to
        # divide by zero in any subsequent NewtonRaphsonRootFind() call.

        segLength = (points[first] - points[last]).length
        epsilon = 1.0e-6 * segLength

        if alpha_l < epsilon or alpha_r < epsilon:
            # fall back on standard(probably inaccurate) formula,
            # and subdivide further if needed.
            dist = segLength / 3.0
            bez_curve[0] = points[first]
            bez_curve[3] = points[last]
            bez_curve[1] = (that_1 * dist) + bez_curve[0]
            bez_curve[2] = (that_2 * dist) + bez_curve[3]
            return bez_curve

        # First and last control points of the Bezier curve are
        # positioned at the first and last data points
        # Control points 1 and 2 are positioned an alpha distance out
        # on the tangent vectors, left and right, respectively
        bez_curve[0] = points[first]
        bez_curve[3] = points[last]
        bez_curve[1] = (that_1 * alpha_l) + bez_curve[0]
        bez_curve[2] = (that_2 * alpha_r) + bez_curve[3]
        return bez_curve
class RipConversion(object):
    def __init__(self):
        self.matrix = Matrix().to_3x3()
        self.flip_winding = False
        self.use_normals = True
        self.use_weights = True
        self.filter_unused_attrs = True
        self.filter_unused_textures = True
        self.normal_max_int = 255
        self.normal_scale = [(1.0, 0.0)] * 3
        self.uv_max_int = 255
        self.uv_scale = [(1.0, 0.0)] * 2
        self.filter_duplicates = False
        self.attr_override_table = None
        self.dedup = BaseDuplicateTracker()

    def find_attrs(self, rip, semantic):
        if self.attr_override_table:
            if semantic in self.attr_override_table:
                return [rip.attributes[i] for i in self.attr_override_table[semantic] if i < len(rip.attributes)]
            else:
                return []
        else:
            return rip.find_attrs(semantic)

    def find_attrs_used(self, rip, semantic, filter=True):
        attrs = self.find_attrs(rip, semantic)

        if rip.shader_vert and filter:
            return [attr for attr in attrs if rip.is_used_attr(attr)]

        return attrs

    def scale_normal(self, comp, val):
        return val * self.normal_scale[comp][0] + self.normal_scale[comp][1]

    def convert_normal(self, rip, vec_id, norm):
        return (self.scale_normal(0,norm[0]), self.scale_normal(1,norm[1]), self.scale_normal(2,norm[2]))

    def find_normals(self, rip):
        return self.find_attrs_used(rip, 'NORMAL', self.filter_unused_attrs)

    def get_normals(self, rip):
        normals = self.find_normals(rip)
        if len(normals) == 0:
            return None

        normdata = normals[0].as_floats(3, self.normal_max_int)
        for i in range(len(normdata)):
            normdata[i] = self.convert_normal(rip, i, normdata[i])
        return normdata

    def scale_uv(self, comp, val):
        return val * self.uv_scale[comp][0] + self.uv_scale[comp][1]

    def convert_uv(self, rip, vec_id, uv):
        return (self.scale_uv(0,uv[0]), self.scale_uv(1,uv[1]))

    def find_uv_maps(self, rip):
        return self.find_attrs_used(rip, 'TEXCOORD', self.filter_unused_attrs)

    def get_uv_maps(self, rip):
        maps = self.find_uv_maps(rip)
        if len(maps) == 0:
            return []

        # Output each pair of UV values as a map
        all_uvs = concat_attrs(list(map(lambda attr: attr.as_floats(4, self.uv_max_int), maps)))

        count = int((len(all_uvs[0])+1)/2)
        result_maps = []
        for i in range(count):
            result_maps.append([])

        for i in range(rip.num_verts):
            data = all_uvs[i]
            for j in range(count):
                pair = data[2*j:2*j+2]
                if len(pair) == 1:
                    pair = (pair[0], 0.0)
                result_maps[j].append(self.convert_uv(rip, i, pair))

        return result_maps

    def find_colors(self, rip):
        return self.find_attrs_used(rip, 'COLOR', self.filter_unused_attrs)

    def get_weight_groups(self, rip):
        indices = self.find_attrs_used(rip, 'BLENDINDICES', self.filter_unused_attrs)
        weights = self.find_attrs_used(rip, 'BLENDWEIGHT', self.filter_unused_attrs)
        if len(indices) == 0 or len(weights) == 0:
            return {}

        all_indices = concat_attrs(list(map(lambda attr: attr.data, indices)))
        all_weights = concat_attrs(list(map(lambda attr: attr.as_floats(), weights)))
        count = min(len(all_indices[0]), len(all_weights[0]))
        groups = {}

        for i in range(rip.num_verts):
            for j in range(count):
                idx = all_indices[i][j]
                weight = all_weights[i][j]
                if weight != 0:
                    if idx not in groups:
                        groups[idx] = {}
                    groups[idx][i] = weight

        return groups

    def apply_matrix(self, vec):
        return self.matrix * Vector(vec).to_3d()

    def apply_matrix_list(self, lst):
        return list(map(self.apply_matrix, lst))

    def convert_mesh(self, rip):
        pos_attrs = self.find_attrs(rip, 'POSITION')
        if len(pos_attrs) == 0:
            pos_attrs = rip.attributes[0:1]

        vert_pos = self.apply_matrix_list(pos_attrs[0].as_floats(3))

        # Rewind triangles when necessary
        faces = rip.faces
        if (self.matrix.determinant() < 0) != self.flip_winding:
            faces = list(map(lambda f: (f[1],f[0],f[2]), faces))

        # Create mesh
        mesh = bpy.data.meshes.new(rip.basename)
        mesh.from_pydata(vert_pos, [], faces)

        # Assign normals
        mesh.polygons.foreach_set("use_smooth", [True] * len(faces))

        if self.use_normals:
            normals = self.get_normals(rip)
            if normals is not None:
                mesh.use_auto_smooth = True
                mesh.show_normal_vertex = True
                mesh.show_normal_loop = True
                mesh.normals_split_custom_set_from_vertices(self.apply_matrix_list(normals))

        mesh.update()

        # Switch to bmesh
        bm = bmesh.new()
        vgroup_names = []
        try:
            bm.from_mesh(mesh)
            bm.verts.ensure_lookup_table()

            # Create UV maps
            uv_maps = self.get_uv_maps(rip)

            for idx,uvdata in enumerate(uv_maps):
                layer = bm.loops.layers.uv.new('uv'+str(idx))

                for i,vert in enumerate(bm.verts):
                    uv = mathutils.Vector(uvdata[i])
                    for loop in vert.link_loops:
                        loop[layer].uv = uv

            # Create color maps
            colors = self.find_colors(rip)

            def add_color_layer(name,cdata):
                layer = bm.loops.layers.color.new(name)
                for i,vert in enumerate(bm.verts):
                    color = mathutils.Vector(cdata[i])
                    for loop in vert.link_loops:
                        loop[layer] = color

            for idx,cattr in enumerate(colors):
                if cattr.items < 3:
                    continue

                cdata = cattr.as_floats(4, 255)
                add_color_layer('color'+str(idx), list(map(lambda v: v[0:3], cdata)))

                if cattr.items == 4:
                    add_color_layer('alpha'+str(idx), list(map(lambda v: (v[3],v[3],v[3]), cdata)))

            # Create weight groups
            if self.use_weights:
                groups = self.get_weight_groups(rip)

                for group in sorted(groups.keys()):
                    id = len(vgroup_names)
                    vgroup_names.append(str(group))
                    layer = bm.verts.layers.deform.verify()
                    weights = groups[group]

                    for vid in weights.keys():
                        bm.verts[vid][layer][id] = weights[vid]

            bm.to_mesh(mesh)
        finally:
            bm.free()

        # Finalize
        mesh.update()

        if self.dedup.is_sharing_mesh():
            mesh.materials.append(None)

        if len(vgroup_names) > 0:
            mesh["ninjarip_vgroups"] = ','.join(vgroup_names);

        return mesh

    def mesh_datakey(self, rip):
        key = rip.data_hash
        if self.filter_unused_attrs and rip.shader_vert:
            indices = [str(i) for i in range(len(rip.attributes)) if rip.is_used_attr(rip.attributes[i])]
            key = key + ':' + ','.join(indices)
        return key

    def create_object(self, rip, obj_name, mesh, mat):
        nobj = bpy.data.objects.new(obj_name, mesh)

        if mat:
            if self.dedup.is_sharing_mesh():
                nobj.material_slots[0].link = 'OBJECT'
                nobj.material_slots[0].material = mat
            else:
                mesh.materials.append(mat)

        if 'ninjarip_vgroups' in mesh:
            for vname in mesh["ninjarip_vgroups"].split(','):
                nobj.vertex_groups.new('blendweight'+vname)

        for i in range(len(rip.shaders)):
            nobj["shader_"+str(i)] = rip.shaders[i]

        return nobj

    def convert_object(self, rip, scene, obj_name):
        mesh_key = self.mesh_datakey(rip)
        mesh = self.dedup.get_mesh(mesh_key, lambda: self.convert_mesh(rip))

        # Textures
        texset = rip.get_textures(self.filter_unused_textures)

        mat = None
        matkey = '*'
        if len(texset) > 0:
            mat = self.dedup.get_material(rip, texset)
            matkey = mat['ninjarip_datakey']

        # Create or find object
        objkey = '|'.join([mesh_key,matkey])

        if self.filter_duplicates:
            nobj = self.dedup.get_object(objkey, lambda: self.create_object(rip, obj_name, mesh, mat))
        else:
            nobj = self.create_object(rip, obj_name, mesh, mat)
            nobj['ninjarip_datakey'] = objkey

        # Select object
        found = False

        for o in scene.objects:
            o.select = False
            if o == nobj:
                found = True

        if not found:
            scene.objects.link(nobj)
            scene.update()

        nobj.select = True
        scene.objects.active = nobj

        return nobj
Beispiel #9
0
    def texdata(self, face, mesh, obj):
        mat = None
        width = height = 64
        if obj.material_slots:
            mat = obj.material_slots[face.material_index].material
        if mat:
            if mat.node_tree:
                for node in mat.node_tree.nodes:
                    if node.type == 'TEX_IMAGE':
                        if node.image.has_data:
                            width, height = node.image.size
                            break
            texstring = mat.name.replace(" ", "_")
        else:
            texstring = self.option_skip

        V = [loop.vert.co for loop in face.loops]
        uv_layer = mesh.loops.layers.uv.active
        if uv_layer is None:
            uv_layer = mesh.loops.layers.uv.new("dummy")
        T = [loop[uv_layer].uv for loop in face.loops]

        # UV handling ported from: https://bitbucket.org/khreathor/obj-2-map

        if self.option_format == 'Valve':
            # [ Ux Uy Uz Uoffs ] [ Vx Vy Vz Voffs ] rotation scaleU scaleV
            dummy = ' [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1\n'

            height = -height  # workaround for flipped v

            # Set up "2d world" coordinate system with the 01 edge along X
            world01 = V[1] - V[0]
            world02 = V[2] - V[0]
            world01_02Angle = world01.angle(world02)
            if face.normal.dot(world01.cross(world02)) < 0:
                world01_02Angle = -world01_02Angle
            world01_2d = Vector((world01.length, 0.0))
            world02_2d = Vector((math.cos(world01_02Angle),
                                 math.sin(world01_02Angle))) * world02.length

            # Get 01 and 02 vectors in UV space and scale them
            tex01 = T[1] - T[0]
            tex02 = T[2] - T[0]
            tex01.x *= width
            tex02.x *= width
            tex01.y *= height
            tex02.y *= height
            '''
            a = world01_2d
            b = world02_2d
            p = tex01
            q = tex02

            [ px ]   [ m11 m12 0 ] [ ax ]
            [ py ] = [ m21 m22 0 ] [ ay ]
            [ 1  ]   [ 0   0   1 ] [ 1  ]

            [ qx ]   [ m11 m12 0 ] [ bx ]
            [ qy ] = [ m21 m22 0 ] [ by ]
            [ 1  ]   [ 0   0   1 ] [ 1  ]

            px = ax * m11 + ay * m12
            py = ax * m21 + ay * m22
            qx = bx * m11 + by * m12
            qy = bx * m21 + by * m22

            [ px ]   [ ax ay 0  0  ] [ m11 ]
            [ py ] = [ 0  0  ax ay ] [ m12 ]
            [ qx ]   [ bx by 0  0  ] [ m21 ]
            [ qy ]   [ 0  0  bx by ] [ m22 ]
            '''

            # Find an affine transformation to convert
            # world01_2d and world02_2d to their respective UV coords
            texCoordsVec = Vector((tex01.x, tex01.y, tex02.x, tex02.y))
            world2DMatrix = Matrix(((world01_2d.x, world01_2d.y, 0,
                                     0), (0, 0, world01_2d.x, world01_2d.y),
                                    (world02_2d.x, world02_2d.y, 0,
                                     0), (0, 0, world02_2d.x, world02_2d.y)))
            try:
                mCoeffs = solve(world2DMatrix, texCoordsVec)
            except:
                return texstring + dummy
            right_2dworld = Vector(mCoeffs[0:2])
            up_2dworld = Vector(mCoeffs[2:4])

            # These are the final scale values
            # (avoid division by 0 for degenerate or missing UVs)
            scalex = 1 / max(0.00001, right_2dworld.length)
            scaley = 1 / max(0.00001, up_2dworld.length)
            scale = Vector((scalex, scaley))

            # Get the angles of the texture axes. These are in the 2d world
            # coordinate system, so they're relative to the 01 vector
            right_2dworld_angle = math.atan2(right_2dworld.y, right_2dworld.x)
            up_2dworld_angle = math.atan2(up_2dworld.y, up_2dworld.x)

            # Recreate the texture axes in 3d world coordinates,
            # using the angles from the 01 edge
            rt = world01.normalized()
            up = rt.copy()
            rt.rotate(Matrix.Rotation(right_2dworld_angle, 3, face.normal))
            up.rotate(Matrix.Rotation(up_2dworld_angle, 3, face.normal))

            # Now we just need the offsets
            rt_full = rt.to_4d()
            up_full = up.to_4d()
            test_s = V[0].dot(rt) / (width * scale.x)
            test_t = V[0].dot(up) / (height * scale.y)
            rt_full[3] = (T[0].x - test_s) * width
            up_full[3] = (T[0].y - test_t) * height

            texstring += f" [ {self.printvec(rt_full)} ]"\
                        f" [ {self.printvec(up_full)} ]"\
                        f" 0 {self.printvec(scale)}\n"

        elif self.option_format == 'Quake':
            # offsetU offsetV rotation scaleU scaleV
            dummy = ' 0 0 0 1 1\n'

            # 01 and 02 in 3D space
            world01 = V[1] - V[0]
            world02 = V[2] - V[0]

            # 01 and 02 projected along the closest axis
            maxn = max(abs(round(crd, self.option_fp)) for crd in face.normal)
            for i in [2, 0, 1]:  # axis priority for 45 degree angles
                if round(abs(face.normal[i]), self.option_fp) == maxn:
                    axis = i
                    break
            world01_2d = Vector((world01[:axis] + world01[(axis + 1):]))
            world02_2d = Vector((world02[:axis] + world02[(axis + 1):]))

            # 01 and 02 in UV space (scaled to texture size)
            tex01 = T[1] - T[0]
            tex02 = T[2] - T[0]
            tex01.x *= width
            tex02.x *= width
            tex01.y *= height
            tex02.y *= height

            # Find affine transformation between 2D and UV
            texCoordsVec = Vector((tex01.x, tex01.y, tex02.x, tex02.y))
            world2DMatrix = Matrix(((world01_2d.x, world01_2d.y, 0,
                                     0), (0, 0, world01_2d.x, world01_2d.y),
                                    (world02_2d.x, world02_2d.y, 0,
                                     0), (0, 0, world02_2d.x, world02_2d.y)))
            try:
                mCoeffs = solve(world2DMatrix, texCoordsVec)
            except:
                return texstring + dummy

            # Build the transformation matrix and decompose it
            tformMtx = Matrix(((mCoeffs[0], mCoeffs[1], 0),
                               (mCoeffs[2], mCoeffs[3], 0), (0, 0, 1)))
            rotation = math.degrees(tformMtx.inverted_safe().to_euler().z)
            scale = tformMtx.inverted_safe().to_scale()  # never zero
            scale.x *= math.copysign(1, tformMtx.determinant())

            # Calculate offsets
            t0 = Vector((T[0].x * width, T[0].y * height))
            v0 = Vector((V[0][:axis] + V[0][(axis + 1):]))
            v0.rotate(Matrix.Rotation(math.radians(-rotation), 2))
            v0 = Vector((v0.x / scale.x, v0.y / scale.y))
            offset = t0 - v0
            offset.y *= -1  # V is flipped

            finvals = [offset.x, offset.y, rotation, scale.x, scale.y]
            texstring += f" {self.printvec(finvals)}\n"

        return texstring