UNSOLVED Python uvTag.GetSlow(poly) data not matching what's in Structure Manager

Hi -

I made a script that gets the vertex points of a UV and checks to see if they or within the 0,0 1,1 coordinate system. The script flags any vertex position less than 0 or greater than 1.

I print the output to the script manager identifying which polygon number it is as well as if it's the a, b, c, or d UV vertex. I also print out the full data dictionary for that polygon.

I have one model that is getting flagged by my script for having a vertex less than zero. As I was digging through my script and looking at the model with the Structure Manager pulled up I found an interesting thing. Any vertex that has a V value of 1 in the structure manager is coming through uvTag.GetSlow(poly) as a scientific notation value of -1.1102230246251565e-16. I am not seeing the same issue if the U value is equal to 1.

I took a screen shot of the Structure manager laid next to the python console so you can see what I mean.

Any ideas on how I can program around this?

Thanks,
.del

 ############################
            # check that UV is within bounds
            #
            uvTag = obj.GetTag(c4d.Tuvw)
            polyCount = obj.GetPolygonCount()
            polygons = obj.GetAllPolygons()

            for poly in range(polyCount):
                polygon = polygons[poly]
                data = uvTag.GetSlow(poly)#returns a dictionary {a: Vector( u, v, w), b: Vector( u, v, w), c: Vector( u, v, w), d: Vector( u, v, w)}
                #print("poly" + str(poly) + str(data))
                for vertex in data:
                    if data[vertex][0] < 0 or data[vertex][0] > 1:
                        print(data[vertex][0])

                    if data[vertex][1] < 0 or data[vertex][1] > 1:
                        print(str(poly) +" " +vertex)
                        print(data[vertex][1])
                        print(data)


uvVertex.PNG

@del
The V value of the UVs have switched (since R21 if I am remembering correctly).
Originally, the V value was 0 (top) to 1 (bottom), putting the origin of the UVs at the top left.
Since R21 the origin has been moved to bottom left, and is also visualized as such in the UV editor, and also in the structure manager.
The stored data however, probably for compatibility, has remain untouched and still represents coordinates with origin at top left.

This all means that the stored values and the actual values seen in structure manager are complementary to 1 (the V values are "mirrored")
You can see that with the values of point 8.b:
its V value in structure manager is 0.889, while its stored value actually is 0.111, where 0.889 is the 1 complement of 0.111 (0.889 + 0.111 = 1)
Similarly, the V value of 1 in structure manager is actually stored as 0 ... which you see in the vector representation.

Additionally, the UVs are stored as single precision floats (at least, that was how it was upto R20). I don't know the internals of Python enough, but this might be the reason why you get the strange -1.xxx e-16 value when printed out.
Note that printing out the c4d.Vector does show it as 0 (which is the mirrored value of the value 1 in the structure manager.

Hello @del,

thank you for reaching out to us. As already pointed out by @C4DS (thanks!) and explained in this thread, there is a mismatch between the UVW coordinate system shown in the Structure and UVW Manger in Cinema 4D, and how the data is being stored internally. The internal format is still the old one with the origin being located at the top left, while the new one shown to the user is at the bottom left.

About your core problem of values being not the values you expect them to be:

When I create a simple scene where an object has such uvw vertices where u == 1 or v == 1, I unfortunately cannot reproduce your findings. Since you did not post the code which produced your screenshots - the posted code only prints UVW data when the u or v component of a UVW vector is outside of the interval [0, 1] - I can also not test with your code. What you encounter there seems to be either some kind of value overflow or a malformed scene state. You should provide the actual scene data and your actual code, as it will be otherwise impossible to help you.

Cheers,
Ferdinand

My results - Note that there are multiple u and v components of value 1, shown correctly both in GUI and the script output (when taking into account the coordinate system mismatch between frontend and backend data):
4ba7f453-6914-4401-aa9e-92edd7f24b30-image.png

The scene: plane.c4d

The code:

import c4d

def main():
    """Entry point.
    """
    if op is None:
        raise RuntimeError("Please select an object.")

    tag = op.GetTag(c4d.Tuvw)
    if tag is None:
        raise AttributeError("Selected object has no uvw tag.")

    for i, uvw in ((i, tag.GetSlow(i)) for i in range(tag.GetDataCount())):
        print (f"{i}: {uvw}")

if __name__=='__main__':
    main()

Hi -

Thank you both for responding.

I searched my model group and found other models with vertexes at the bounding box corners and those did not get flagged by my script so your suggestion of a malformed UV or some sort of overflow might be what my issue is. The script also worked as expected on your plane.c4d file as well as a cube.

If the overflow value is consistent perhaps I can test for that and allow it to pass rather than flagging the model?

I'm attaching a scene file with two models made from the same template. Ones fails to pass the test while the other works. They are labeled accordingly.

I've also trimmed the script to focus on the bounding box part. It's not as elegant as yours by any means but I'm a self admitted butcher of code. Just trying to make tools to streamline our workflow. They aren't pretty but they help us get home at the end of the day and I'm appreciative that the sdk is available. Please keep that in mind as you stumble through my vomit pile of a script. My code is not Pythonic.

Thanks for looking at my stuff.

.del



import c4d
import os
import math


from c4d import gui
from c4d import Vector
from c4d import bitmaps
from copy import copy
from c4d.modules import bodypaint



#####





#Start the clean up tasks to prepare for exporting to the library.


doc.SetMode(c4d.Mmodel)

dObjects = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER)
if dObjects == []:
    message = "You must select at least one object"
    c4d.gui.MessageDialog(message, c4d.GEMB_OK)
else:
    c4d.StatusSetSpin()
    c4d.StatusSetText("Running Python Script")

    isolate = False
    for obj in dObjects:
        if obj.CheckType(c4d.Opoint):

            ############################
            # check that UV is within bounds
            #
            uvTag = obj.GetTag(c4d.Tuvw)
            polyCount = obj.GetPolygonCount()
            polygons = obj.GetAllPolygons()

            for poly in range(polyCount):
                polygon = polygons[poly]
                data = uvTag.GetSlow(poly)
                print("poly" + str(poly) + str(data))
                if polygon.IsTriangle(): #if the poygon is a triangle instead of a quad we need to delete the 4th key as that position data is skewed and throwing a false positive.
                    del data['d']
                for vertex in data:
                    if data[vertex].x < 0 or data[vertex].x > 1:
                        print(data[vertex].x)

                    if data[vertex].y < 0 or data[vertex].y > 1:
                        print(str(poly) +" " +vertex)
                        print(data[vertex][1])
                        print(data)




c4d.StatusClear()

uv boundary testing.c4d

I've been able to confirm that if I take the suspect vertexes and change them from a value 1 to .99 and then change them back to 1 they no longer cause the problem.

Hello @del,

so, I had a look at your data, and I cannot see any UVW vertex component values there which are 1 in the UI, but when accessed in Python are -1.1102230246251565e-16, i.e., almost 0. There are however a few values which are -1.1102230246251565e-16 which are shown as 0 in the UI and when printing out a c4d.Vector instance. Because vectors round their value when printed. Internally they will always work with the true values and when one wants to print out the true values, one must access the components individually.

# A vector where all three components are very close to zero.
v = c4d.Vector(1E-10)
# Printing out the vector will show the rounded components.
print (f"{v = }")
# Accessing them directly will provide the true values.
print (f"{v.x = }, {v.y = }, {v.z = }")
v = Vector(0, 0, 0)
v.x = 1e-10, v.y = 1e-10, v.z = 1e-10

So, your data is fine, it is just that the UVW data was either created by an artist who rightfully did not care if some values are 0 or 'almost zero', or that the data was created at lower precision and then converted at some point, which lead to these rounding errors.

Below you can find a script which prints out all the UVW data for a selected object and then quantizes the data with a given precision, running QuantizeUvwTag from my script will persistently fix an Uvw tag in the context of very small values and also quantize other values. You would have to implement yourself what you want to be fixed and what not, this is a purely cosmetic issue after all.

Cheers,
Ferdinand

An excerpt from the script output for your file:

Uvw-Data:
uvw polygon 0: {'a': Vector(0.25, 0.152, 0), 'b': Vector(0.433, 0.152, 0), 'c': Vector(0.25, 0.174, 0), 'd': Vector(0, 0, 0)}
    0: a(0)=0.25
    0: a(1)=0.15186521410942078
    0: a(2)=0.0
    0: b(0)=0.4333333373069763
    0: b(1)=0.15186521410942078
    0: b(2)=0.0
    0: c(0)=0.25
    0: c(1)=0.17397046089172363
    0: c(2)=0.0
    0: d(0)=0.0
    0: d(1)=0.0
    0: d(2)=0.0
...
uvw polygon 8: {'a': Vector(0.433, 0, 0), 'b': Vector(0.433, 0.111, 0), 'c': Vector(0.25, 0.111, 0), 'd': Vector(0.25, 0, 0)}
    8: a(0)=0.4333333373069763
    8: a(1)=-1.1102230246251565e-16
    8: a(2)=0.0
    8: b(0)=0.4333333373069763
    8: b(1)=0.11077965795993805
    8: b(2)=0.0
    8: c(0)=0.25
    8: c(1)=0.11081257462501526
    8: c(2)=0.0
    8: d(0)=0.25
    8: d(1)=-1.1102230246251565e-16
    8: d(2)=0.0
uvw polygon 9: {'a': Vector(0.433, 0.174, 0), 'b': Vector(0.433, 0.152, 0), 'c': Vector(0.477, 0.152, 0), 'd': Vector(0.25, 0, 0)}
    9: a(0)=0.4333333373069763
    9: a(1)=0.17397046089172363
    9: a(2)=0.0
    9: b(0)=0.4333333373069763
    9: b(1)=0.15186521410942078
    9: b(2)=0.0
    9: c(0)=0.476666659116745
    9: c(1)=0.15186521410942078
    9: c(2)=0.0
    9: d(0)=0.25
    9: d(1)=-1.1102230246251565e-16
    9: d(2)=0.0
...


Quantized Uvw-Data:
uvw polygon 0: {'a': Vector(0.25, 0.152, 0), 'b': Vector(0.433, 0.152, 0), 'c': Vector(0.25, 0.174, 0), 'd': Vector(0, 0, 0)}
    0: a(0)=0.25
    0: a(1)=0.15186521410942078
    0: a(2)=0.0
    0: b(0)=0.4333333373069763
    0: b(1)=0.15186521410942078
    0: b(2)=0.0
    0: c(0)=0.25
    0: c(1)=0.17397046089172363
    0: c(2)=0.0
    0: d(0)=0.0
    0: d(1)=0.0
    0: d(2)=0.0
...
uvw polygon 8: {'a': Vector(0.433, 0, 0), 'b': Vector(0.433, 0.111, 0), 'c': Vector(0.25, 0.111, 0), 'd': Vector(0.25, 0, 0)}
    8: a(0)=0.4333333373069763
    8: a(1)=-0.0
    8: a(2)=0.0
    8: b(0)=0.4333333373069763
    8: b(1)=0.11077965795993805
    8: b(2)=0.0
    8: c(0)=0.25
    8: c(1)=0.11081257462501526
    8: c(2)=0.0
    8: d(0)=0.25
    8: d(1)=-0.0
    8: d(2)=0.0
uvw polygon 9: {'a': Vector(0.433, 0.174, 0), 'b': Vector(0.433, 0.152, 0), 'c': Vector(0.477, 0.152, 0), 'd': Vector(0.25, 0, 0)}
    9: a(0)=0.4333333373069763
    9: a(1)=0.17397046089172363
    9: a(2)=0.0
    9: b(0)=0.4333333373069763
    9: b(1)=0.15186521410942078
    9: b(2)=0.0
    9: c(0)=0.476666659116745
    9: c(1)=0.15186521410942078
    9: c(2)=0.0
    9: d(0)=0.25
    9: d(1)=-0.0
    9: d(2)=0.0
...

The script:

"""Showcases how to quantize data in a UvwTag.
"""

import c4d

def InspectUvwTag(tag: c4d.UVWTag):
    """Prints out the uvw data for the passed UVWTag.
    
    Args:
        tag: The tag to inspect.
    """
    if not isinstance(tag, c4d.UVWTag):
        raise TypeError(f"Unexpected argument type: {tag = }")

    countGenerator = range(tag.GetDataCount())
    for i, uvwPolygon in ((i, tag.GetSlow(i)) for i in countGenerator):
        print (f"uvw polygon {i}: {uvwPolygon}")
        for vertexName, vertex in uvwPolygon.items():
            for componentName, component in enumerate((vertex.x, 
                                                       vertex.y, 
                                                       vertex.z)):
                print (f"\t{i}: {vertexName}({componentName})={component}")

def QuantizeUvwTag(tag: c4d.UVWTag, digits: int = 12):
    """Quantizes the uvw data in #tag with a stride of #digits.
    
    Args:
        tag: The tag to modify.
        digits (optional): The number of digits of precision, defaults 
        to 12.
    """
    if not isinstance(tag, c4d.UVWTag):
        raise TypeError(f"Unexpected argument type: {tag = }")

    countGenerator = range(tag.GetDataCount())
    for i, uvwPolygon in ((i, tag.GetSlow(i)) for i in countGenerator):
        quantizedUvwPolygon = {}
        for key, vertex in uvwPolygon.items():
            # I am only modifying the precision of the values here, I am
            # not clamping them. Which will lead to values of -0.0 to be
            # written. Modify this to your requirements, this is a very
            # cosmetic task after all. -1.1e-16 is when push comes to shove
            # the same as 0.0 in the context of UVW data. So this quantization
            #  is not really required, unless you want 'nice' data.
            quantizedUvwPolygon[key] = c4d.Vector(round(vertex.x, digits), 
                                                  round(vertex.y, digits),
                                                  round(vertex.z, digits))
        tag.SetSlow(i, 
            quantizedUvwPolygon["a"], 
            quantizedUvwPolygon["b"], 
            quantizedUvwPolygon["c"],
            quantizedUvwPolygon["d"])


def main():
    """Entry point.
    """
    if op is None:
        raise RuntimeError("Please select an object.")

    tag = op.GetTag(c4d.Tuvw)
    if tag is None:
        raise AttributeError(f"Selected object {op} has no uvw tag.")

    # Inspect the tag. This will only print out different results than the
    # second call of InspectUvwTag() the first time the script is being run,
    # because after that the tag will already have been quantized.
    print ("Uvw-Data:")
    InspectUvwTag(tag)
    # Quantize its data.
    QuantizeUvwTag(tag)
    # And inspect it again.
    print ("\n\nQuantized Uvw-Data:")
    InspectUvwTag(tag)


if __name__=='__main__':
    main()

@ferdinand Thank you so much for digging into this. I'll look at your example more and try quantitizing the numbers. In the meantime I had written a catch for the -1.1102230246251565e-16 to treat it as zero but it's probably not a very robust solution. I like your idea of rounding them off instead. I didn't fully understand what that number represented. I tried searching it but didn't get any useful results. I tried again today and now I see what it is. I must have had something else in my search field when I originally tried.

We have a script for creating atlases of textures and if the UV is outside of the bounds it gets placed incorrectly on the atlased texture. This new script is intended to catch those so they can be corrected before hand. I think rounding anything within two decimal places of zero will work for our needs.

Is there a way to change the tags for this this thread as it's not a bug report. I thought it was but it turns out it's not.

Thanks you for the help. It's greatly appreciated.

@del said in Python uvTag.GetSlow(poly) data not matching what's in Structure Manager:

Is there a way to change the tags for this this thread as it's not a bug report. I thought it was but it turns out it's not.

You would have to edit your own first posting in this thread and then remove the tag. I took the liberty of doing it for you here and thanks for bringing it up!

Cheers,
Ferdiand

Hi @del,

did not really have the time yesterday, and this is slightly out of scope of support, but below you can find a bit more graceful approach to quantizing the values in case you are interested.

import math

def quantize(v: float, binSize: float = .1, tolerance: float = 1E-8) -> float:
    """Quantizes #v to a multiple of #binSize with a distance of #tolerance.
    """
    vq = float(v) / float(binSize)
    vqmin, vqmax = math.floor(vq) * binSize, math.ceil(vq) * binSize
    if (abs(vqmin - v) < tolerance):
        return vqmin
    elif (abs(vqmax - v) < tolerance):
        return vqmax
    return v


def main():
    """
    """
    # Quantize some values.

    # Will quantize to 0 because the next multiple of .1 to v is 0.0 and
    # the distance from v to that multiple is less than the tolerance.
    print (f"{quantize(0.000001, binSize = .1, tolerance=1E-5) = }")
    # Same call as the prior, just a different v, will snap to .2
    print (f"{quantize(0.200001, binSize = .1, tolerance=1E-5) = }")
    # A value almost touching .75, this will quantize to 0, .25, .5, .75 and
    # 1.0 with a tolerance of 1E-5.
    print (f"{quantize(0.749999999, binSize = .25, tolerance=1E-5) = }")

    # Quantize to either 0 or 1. I picked here a low tolerance so that that 
    # things are easier to read.

    # Wont' t be touched because (1 - .51234) and abs(0 - .51234) are much 
    # larger than the tolerance. Other than with simple rounding we can
    # preserve precision with the values which are not quantized.
    print (f"{quantize(0.51234, binSize = 1., tolerance=1E-2) = }")

    # Will snap to 0.
    print (f"{quantize(0.00002, binSize = 1., tolerance=1E-2) = }")
    # Will snap to 1.
    print (f"{quantize(0.99999, binSize = 1., tolerance=1E-2) = }")

if __name__ == "__main__":
    main()
quantize(0.000001, binSize = .1, tolerance=1E-5) = 0.0
quantize(0.200001, binSize = .1, tolerance=1E-5) = 0.2
quantize(0.749999999, binSize = .25, tolerance=1E-5) = 0.75
quantize(0.51234, binSize = 1., tolerance=1E-2) = 0.51234
quantize(0.00002, binSize = 1., tolerance=1E-2) = 0.0
quantize(0.99999, binSize = 1., tolerance=1E-2) = 1.0