Skip to content

MeshHandler returns raw integers for UNorm and SNorm vertex channels #387

Open

Description

@nichinichisou0609

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions