From c682ab4ebe55c54bf60e2c97480d901f0bd4dbaa Mon Sep 17 00:00:00 2001 From: ud18010 Date: Wed, 15 Jul 2026 20:54:21 +0800 Subject: [PATCH] =?UTF-8?q?ModelTranslator:=20=E4=BF=AE=E5=A4=8D=20base=20?= =?UTF-8?q?=E8=B4=B4=E5=9B=BE=E5=8F=8C=E9=87=8D=20sRGB=20=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E5=8F=98=E4=BA=AE=EF=BC=88well10k=20=E5=86=85?= =?UTF-8?q?=E5=B5=8C=E8=B4=B4=E5=9B=BE=E6=A1=88=E4=BE=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实测 blender 5.0:Image.pixels 对 8-bit 图返回原始字节值(不做色彩变换), sRGB PNG 读出已是编码值,再 linear->sRGB 就双重 gamma(均值 0.33->0.60)。 现仅对线性来源编码:float 图(EXR)与 Principled 常量色。 端到端验证:well10k 重转后 base 与源贴图平均差 0.0013。 Co-Authored-By: Claude Opus 4.8 --- Tools/ModelTranslator/bl_convert.py | 17 ++++++++----- .../ModelTranslator/tests/test_bl_convert.py | 25 +++++++++++++++++-- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/Tools/ModelTranslator/bl_convert.py b/Tools/ModelTranslator/bl_convert.py index 8d0e8097..6dcdf4fe 100644 --- a/Tools/ModelTranslator/bl_convert.py +++ b/Tools/ModelTranslator/bl_convert.py @@ -39,11 +39,13 @@ def linear_to_srgb(c): 1.055 * np.power(c, 1.0 / 2.4) - 0.055) -def encode_base_texture_for_png(rgba): - """Base RGB is stored for Unity sRGB import; alpha keeps AO/opacity data.""" - out = np.array(rgba, dtype=np.float32, copy=True) - out[..., :3] = linear_to_srgb(out[..., :3]) - out[..., 3] = np.clip(out[..., 3], 0.0, 1.0) +def encode_base_texture_for_png(rgba, rgb_is_linear): + """Base RGB is stored for Unity sRGB import; alpha keeps AO/opacity data. + Image.pixels 对 8-bit 图返回原始字节值(已是 sRGB 编码,实测 blender 5.0), + 仅线性来源(float 图 / Principled 常量色)需要 linear->sRGB,否则双重 gamma 变亮。""" + out = np.clip(np.array(rgba, dtype=np.float32, copy=True), 0.0, 1.0) + if rgb_is_linear: + out[..., :3] = linear_to_srgb(out[..., :3]) return out @@ -280,8 +282,11 @@ def _convert_material(mat, model_name, outdir, max_size, used_snames, src_dir): base = np.empty((th, tw, 4), dtype=np.float32) if maps["basecolor"]: base[..., :3] = _scaled_pixels(maps["basecolor"][0], tw, th)[..., :3] + # 8-bit 图 pixels 已是 sRGB 编码值;float 图(EXR 等)才是线性 + rgb_is_linear = bool(maps["basecolor"][0].is_float) else: base[..., :3] = np.array(consts["basecolor"][:3], dtype=np.float32) + rgb_is_linear = True # Principled 常量色是线性值 if maps["alpha"]: img, ch = maps["alpha"] base[..., 3] = _channel(_scaled_pixels(img, tw, th), ch) @@ -330,7 +335,7 @@ def _convert_material(mat, model_name, outdir, max_size, used_snames, src_dir): stem = model_name if sname == model_name else "%s_%s" % (model_name, sname) base_png = os.path.abspath(os.path.join(outdir, "%s_base.png" % stem)) mix_png = os.path.abspath(os.path.join(outdir, "%s_mix.png" % stem)) - _save_png(base_png, encode_base_texture_for_png(base)) + _save_png(base_png, encode_base_texture_for_png(base, rgb_is_linear)) _save_png(mix_png, mix) return { diff --git a/Tools/ModelTranslator/tests/test_bl_convert.py b/Tools/ModelTranslator/tests/test_bl_convert.py index ec737eb3..4fe0934a 100644 --- a/Tools/ModelTranslator/tests/test_bl_convert.py +++ b/Tools/ModelTranslator/tests/test_bl_convert.py @@ -9,15 +9,36 @@ import bl_convert as bc class TestBaseTextureEncoding(unittest.TestCase): - def test_base_texture_rgb_is_encoded_as_srgb_and_alpha_is_preserved(self): + # Blender Image.pixels 对 8-bit 图返回原始字节值(不做色彩变换,实测 blender 5.0: + # 字节 32/128/192 -> 0.1255/0.502/0.7529),即 sRGB PNG 读出来已是 sRGB 编码值。 + # 仅线性来源(float 图 / Principled 常量色)需要 linear->sRGB。 + + def test_linear_source_rgb_is_encoded_as_srgb_and_alpha_is_preserved(self): rgba = np.array([[[0.0, 0.18, 0.5, 0.37]]], dtype=np.float32) - encoded = bc.encode_base_texture_for_png(rgba) + encoded = bc.encode_base_texture_for_png(rgba, rgb_is_linear=True) expected_rgb = np.array([0.0, 0.461356, 0.735357], dtype=np.float32) np.testing.assert_allclose(encoded[0, 0, :3], expected_rgb, rtol=0, atol=1e-5) self.assertAlmostEqual(float(encoded[0, 0, 3]), 0.37, places=6) + def test_byte_source_rgb_passes_through_unchanged(self): + # 8-bit 贴图像素已是 sRGB 编码,再编码一次会双重 gamma(well10k 变亮 bug) + rgba = np.array([[[0.1255, 0.502, 0.7529, 0.37]]], dtype=np.float32) + + encoded = bc.encode_base_texture_for_png(rgba, rgb_is_linear=False) + + np.testing.assert_allclose(encoded[0, 0], rgba[0, 0], rtol=0, atol=1e-6) + + def test_out_of_range_values_are_clipped(self): + rgba = np.array([[[-0.1, 1.2, 0.5, 1.5]]], dtype=np.float32) + + encoded = bc.encode_base_texture_for_png(rgba, rgb_is_linear=False) + + self.assertEqual(float(encoded[0, 0, 0]), 0.0) + self.assertEqual(float(encoded[0, 0, 1]), 1.0) + self.assertEqual(float(encoded[0, 0, 3]), 1.0) + if __name__ == "__main__": unittest.main()