Repository navigation
MeshHandler returns raw integers for UNorm and SNorm vertex channels #387
Description
MeshHandler.read_vertex_data unpacks every vertex channel with the struct code of its VertexFormat and stores the values as they are. For the normalized formats (UNorm8, SNorm8, UNorm16, SNorm16) that leaves the integer representation: a color stored as UNorm8 comes back as (255, 128, 0, 255) instead of (1.0, 0.502, 0.0, 1.0), and UVs stored as UNorm16 come back as 65535 and 32768, which the OBJ exporter writes as vt 65535 32768. The Python path and UnityPyBoost return the same values.
The other sources of the same attributes are normalized: compressed meshes divide packed colors by 255, and so does copy_from_mesh for m_Colors of older meshes. So m_Colors holds floats in [0, 1] or integers in [0, 255] depending on how the mesh was stored. Unity's VertexAttributeFormat describes these formats as normalized values in [0, 1] and [-1, 1], and AssetStudio's BytesToFloatArray converts them with / 255, / 65535, max(v / 127, -1) and max(v / 32767, -1).
Reproduction
import struct
from types import SimpleNamespace
import UnityPy
from UnityPy.classes.generated import AABB, ChannelInfo, CompressedMesh, Mesh, PackedBitVector, SubMesh, Vector3f, VertexData
from UnityPy.export.MeshExporter import export_mesh_obj
from UnityPy.helpers.MeshHelper import MeshHandler
print("UnityPy", UnityPy.__version__)
# one vertex: Float x3 position, UNorm8 x4 color, UNorm16 x2 uv0, SNorm16 x2 uv1
vertex = struct.pack("<3f4B2H2h", 0, 0, 0, 255, 128, 0, 255, 65535, 32768, -32767, 16384)
# VertexFormat since 2019: 0 Float, 2 UNorm8, 4 UNorm16, 5 SNorm16
channels = [
ChannelInfo(dimension=3, format=0, offset=0, stream=0), # position
ChannelInfo(dimension=0, format=0, offset=0, stream=0), # normal
ChannelInfo(dimension=0, format=0, offset=0, stream=0), # tangent
ChannelInfo(dimension=4, format=2, offset=12, stream=0), # color
ChannelInfo(dimension=2, format=4, offset=16, stream=0), # uv0
ChannelInfo(dimension=2, format=5, offset=20, stream=0), # uv1
]
empty = PackedBitVector(m_Data=[], m_NumItems=0)
names = ("m_BoneIndices", "m_NormalSigns", "m_Normals", "m_TangentSigns", "m_Tangents", "m_Triangles", "m_UV", "m_Vertices", "m_Weights")
aabb = AABB(m_Center=Vector3f(0, 0, 0), m_Extent=Vector3f(1, 1, 1))
mesh = Mesh(
m_BindPose=[],
m_CompressedMesh=CompressedMesh(**{name: empty for name in names}),
m_IndexBuffer=[],
m_IndexFormat=0,
m_LocalAABB=aabb,
m_MeshCompression=0,
m_Name="mesh",
m_SubMeshes=[SubMesh(firstByte=0, firstVertex=0, indexCount=0, localAABB=aabb, vertexCount=1, baseVertex=0, topology=0)],
m_VertexData=VertexData(m_DataSize=vertex, m_VertexCount=1, m_Channels=channels),
)
mesh.object_reader = SimpleNamespace(version=(2021, 3, 0, 0))
handler = MeshHandler(mesh)
handler.process()
print("m_Colors (UNorm8)", handler.m_Colors, "expected", [(1.0, 128 / 255, 0.0, 1.0)])
print("m_UV0 (UNorm16) ", handler.m_UV0, "expected", [(1.0, 32768 / 65535)])
print("m_UV1 (SNorm16) ", handler.m_UV1, "expected", [(-1.0, 16384 / 32767)])
print("OBJ:", [line for line in export_mesh_obj(mesh).splitlines() if line.startswith("vt ")])Output on 417d595 (1.25.4), with and without UnityPyBoost:
UnityPy 1.25.4
m_Colors (UNorm8) [(255, 128, 0, 255)] expected [(1.0, 0.5019607843137255, 0.0, 1.0)]
m_UV0 (UNorm16) [(65535, 32768)] expected [(1.0, 0.5000076295109483)]
m_UV1 (SNorm16) [(-32767, 16384)] expected [(-1.0, 0.500015259254738)]
OBJ: ['vt 65535 32768']
Converting these channels to floats in read_vertex_data (or in assign_channel_vertex_data) would make all paths agree. It changes the values callers currently get for vertex colors stored as UNorm8, which is the common color format, so I am asking first: should normalized channels be returned as floats? If so, I can send a PR covering the four formats on both the Python and the UnityPyBoost path.
Tested with UnityPy 417d595 (1.25.4) and CPython 3.11.13 on Windows x86_64.
Activity