From a19e342bf4e6f33b04fc3e99ccb4db0b59e453b4 Mon Sep 17 00:00:00 2001 From: ud18010 Date: Tue, 21 Jul 2026 11:37:19 +0800 Subject: [PATCH] =?UTF-8?q?ModelTranslator:=20=E6=96=B0=E5=A2=9E=20uv=5Fpr?= =?UTF-8?q?eview.render=5Fuv=5Fpng=E2=80=94=E2=80=94PIL=20=E7=BB=98=20UV?= =?UTF-8?q?=20=E7=BA=BF=E6=A1=86=20PNG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ModelTranslator/tests/test_uv_preview.py | 48 +++++++++++++++++++ Tools/ModelTranslator/uv_preview.py | 31 ++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 Tools/ModelTranslator/tests/test_uv_preview.py create mode 100644 Tools/ModelTranslator/uv_preview.py diff --git a/Tools/ModelTranslator/tests/test_uv_preview.py b/Tools/ModelTranslator/tests/test_uv_preview.py new file mode 100644 index 00000000..44abdef0 --- /dev/null +++ b/Tools/ModelTranslator/tests/test_uv_preview.py @@ -0,0 +1,48 @@ +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import uv_preview as up +from PIL import Image + + +class TestToPx(unittest.TestCase): + def test_origin_bottom_left_maps_to_bottom_left_pixel(self): + # UV(0,0) 原点在左下 -> 图像左下角(x=pad, y=size-pad) + x, y = up._to_px(0.0, 0.0, size=100, pad=10) + self.assertAlmostEqual(x, 10.0) + self.assertAlmostEqual(y, 90.0) + + def test_top_right(self): + x, y = up._to_px(1.0, 1.0, size=100, pad=10) + self.assertAlmostEqual(x, 90.0) + self.assertAlmostEqual(y, 10.0) + + +class TestRenderUvPng(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.mkdtemp() + + def _out(self, name="uv.png"): + return os.path.join(self.tmp, name) + + def test_writes_png_of_requested_size(self): + polys = [[[0.1, 0.1], [0.4, 0.1], [0.25, 0.4]], + [[0.6, 0.6], [0.9, 0.6], [0.9, 0.9], [0.6, 0.9]]] + out = up.render_uv_png(polys, self._out(), size=128) + self.assertTrue(os.path.isfile(out)) + with Image.open(out) as im: + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "PNG") + + def test_empty_polygons_still_writes_blank(self): + out = up.render_uv_png([], self._out("blank.png"), size=64) + self.assertTrue(os.path.isfile(out)) + with Image.open(out) as im: + self.assertEqual(im.size, (64, 64)) + + def test_degenerate_polygon_skipped_no_crash(self): + out = up.render_uv_png([[[0.5, 0.5]]], self._out("deg.png"), size=64) + self.assertTrue(os.path.isfile(out)) diff --git a/Tools/ModelTranslator/uv_preview.py b/Tools/ModelTranslator/uv_preview.py new file mode 100644 index 00000000..fdb44df0 --- /dev/null +++ b/Tools/ModelTranslator/uv_preview.py @@ -0,0 +1,31 @@ +"""UV 线框观察图渲染:把 UV 多边形(归一化 0-1 坐标)画成 PNG 供人工查阅。 +在系统 Python(有 Pillow)中运行——Blender 自带 Python 无 PIL,故 UV 几何由 +bl_decimate 导出为 JSON,本模块负责绘制。""" +from PIL import Image, ImageDraw + + +def _to_px(u, v, size, pad): + """UV(0-1, 原点左下) -> 像素(原点左上),含边距。""" + span = size - 2 * pad + x = pad + u * span + y = pad + (1.0 - v) * span # V 轴翻转:图像 y 向下 + return x, y + + +def render_uv_png(polygons, out_png, size=1024, pad=8, + line=(30, 30, 30, 255), fill=(80, 140, 220, 64)): + """polygons: [[[u,v], ...], ...] 每个多边形一组 UV 顶点。 + 白底 + 半透明填充 + 深色线框;写 PNG 到 out_png,返回 out_png。""" + base = Image.new("RGBA", (size, size), (255, 255, 255, 255)) + overlay = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(overlay, "RGBA") + for poly in polygons: + pts = [_to_px(u, v, size, pad) for (u, v) in poly] + if len(pts) >= 3: + draw.polygon(pts, fill=fill) + for poly in polygons: + pts = [_to_px(u, v, size, pad) for (u, v) in poly] + if len(pts) >= 2: + draw.line(pts + pts[:1], fill=line, width=1) + Image.alpha_composite(base, overlay).convert("RGB").save(out_png, "PNG") + return out_png