diff --git a/UnityPy/helpers/MeshHelper.py b/UnityPy/helpers/MeshHelper.py index edbd6bee..9a4556e6 100644 --- a/UnityPy/helpers/MeshHelper.py +++ b/UnityPy/helpers/MeshHelper.py @@ -508,7 +508,7 @@ def decompress_compressed_mesh(self): shape=(uvDim,), ) setattr(self, f"m_UV{uv_channel}", m_UV) - uvSrcOffset = uvDim * m_VertexCount + uvSrcOffset += uvDim * m_VertexCount else: self.m_UV0 = unpack_floats(m_CompressedMesh.m_UV, 0, m_VertexCount * 2, shape=(2,)) if m_CompressedMesh.m_UV.m_NumItems >= m_VertexCount * 4: @@ -608,7 +608,7 @@ def decompress_compressed_mesh(self): # we read three weights, but they don't add up to one. calculate the fourth one, and read # missing bone index. continue with next vertex. elif j == 3: # - boneWeights[vertexIndex][j] = 1 - sum + boneWeights[vertexIndex][j] = (31 - sum) / 31 boneIndices[vertexIndex][j] = next(boneIndicesIterator) vertexIndex += 1 diff --git a/tests/test_compressed_mesh.py b/tests/test_compressed_mesh.py new file mode 100644 index 00000000..252df7bf --- /dev/null +++ b/tests/test_compressed_mesh.py @@ -0,0 +1,110 @@ +from types import SimpleNamespace + +import pytest + +from UnityPy.classes.generated import ( + AABB, + ChannelInfo, + CompressedMesh, + Mesh, + PackedBitVector, + Vector3f, + VertexData, +) +from UnityPy.helpers.MeshHelper import MeshHandler + +VERSION = (2021, 3, 0, 0) + + +def pack_ints(values, bit_size): + data = bytearray((len(values) * bit_size + 7) // 8) + for i, value in enumerate(values): + for bit in range(bit_size): + if value >> bit & 1: + pos = i * bit_size + bit + data[pos // 8] |= 1 << (pos % 8) + return PackedBitVector(m_Data=list(data), m_NumItems=len(values), m_BitSize=bit_size) + + +def pack_floats(values, bit_size, start=0.0, range_=1.0): + top = (1 << bit_size) - 1 + packed = pack_ints([round((value - start) / range_ * top) for value in values], bit_size) + packed.m_Start = start + packed.m_Range = range_ + return packed + + +def compressed_mesh(vertex_count, m_UVInfo=0, **vectors): + names = ( + "m_BoneIndices", + "m_NormalSigns", + "m_Normals", + "m_TangentSigns", + "m_Tangents", + "m_Triangles", + "m_UV", + "m_Weights", + ) + compressed = CompressedMesh( + m_Vertices=pack_floats([0.0] * vertex_count * 3, 8), + m_UVInfo=m_UVInfo, + **{name: vectors.get(name, PackedBitVector(m_Data=[], m_NumItems=0)) for name in names}, + ) + mesh = Mesh( + m_BindPose=[], + m_CompressedMesh=compressed, + m_IndexBuffer=[], + m_IndexFormat=0, + m_LocalAABB=AABB(m_Center=Vector3f(0, 0, 0), m_Extent=Vector3f(1, 1, 1)), + m_MeshCompression=1, + m_Name="mesh", + m_SubMeshes=[], + m_VertexData=VertexData( + m_DataSize=b"", + m_VertexCount=0, + m_Channels=[ChannelInfo(dimension=0, format=0, offset=0, stream=0)], + ), + ) + mesh.object_reader = SimpleNamespace(version=VERSION) + handler = MeshHandler(mesh) + handler.process() + return handler + + +def test_compressed_bone_weights(): + # vertex 0: three weights that do not reach 31/31, so the fourth weight is implied + # vertex 1: one weight of 31/31, the remaining influences are empty + handler = compressed_mesh( + 2, + m_Weights=pack_ints([10, 10, 5, 31], 5), + m_BoneIndices=pack_ints([0, 1, 2, 3, 7], 4), + ) + expected = [(10 / 31, 10 / 31, 5 / 31, 6 / 31), (1.0, 0, 0, 0)] + assert len(handler.m_BoneWeights) == len(expected) + for weights, expected_weights in zip(handler.m_BoneWeights, expected): + assert weights == pytest.approx(expected_weights) + assert sum(weights) == pytest.approx(1.0) + assert handler.m_BoneIndices == [(0, 1, 2, 3), (7, 0, 0, 0)] + + +def test_compressed_uv_sets(): + # sets 0, 1 and 3 with 2, 3 and 2 components, stored back to back in m_UV + uv_sets = { + 0: [(0.125, 0.25), (0.375, 0.5)], + 1: [(0.5, 0.625, 0.75), (0.875, 1.0, 0.0)], + 3: [(0.0625, 0.1875), (0.3125, 0.4375)], + } + uv_info = 0 + for channel, values in uv_sets.items(): + uv_info |= (4 | (len(values[0]) - 1)) << (channel * 4) + handler = compressed_mesh( + 2, + m_UVInfo=uv_info, + m_UV=pack_floats([v for values in uv_sets.values() for uv in values for v in uv], 16), + ) + for channel, values in uv_sets.items(): + decoded = getattr(handler, f"m_UV{channel}") + assert len(decoded) == len(values) + for uv, expected in zip(decoded, values): + assert uv == pytest.approx(expected, abs=1e-4) + assert handler.m_UV2 is None