ModelTranslator: 修复 base 贴图双重 sRGB 编码变亮(well10k 内嵌贴图案例)
实测 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
60c83a3765
commit
c682ab4ebe
@@ -39,11 +39,13 @@ def linear_to_srgb(c):
|
|||||||
1.055 * np.power(c, 1.0 / 2.4) - 0.055)
|
1.055 * np.power(c, 1.0 / 2.4) - 0.055)
|
||||||
|
|
||||||
|
|
||||||
def encode_base_texture_for_png(rgba):
|
def encode_base_texture_for_png(rgba, rgb_is_linear):
|
||||||
"""Base RGB is stored for Unity sRGB import; alpha keeps AO/opacity data."""
|
"""Base RGB is stored for Unity sRGB import; alpha keeps AO/opacity data.
|
||||||
out = np.array(rgba, dtype=np.float32, copy=True)
|
Image.pixels 对 8-bit 图返回原始字节值(已是 sRGB 编码,实测 blender 5.0),
|
||||||
out[..., :3] = linear_to_srgb(out[..., :3])
|
仅线性来源(float 图 / Principled 常量色)需要 linear->sRGB,否则双重 gamma 变亮。"""
|
||||||
out[..., 3] = np.clip(out[..., 3], 0.0, 1.0)
|
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
|
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)
|
base = np.empty((th, tw, 4), dtype=np.float32)
|
||||||
if maps["basecolor"]:
|
if maps["basecolor"]:
|
||||||
base[..., :3] = _scaled_pixels(maps["basecolor"][0], tw, th)[..., :3]
|
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:
|
else:
|
||||||
base[..., :3] = np.array(consts["basecolor"][:3], dtype=np.float32)
|
base[..., :3] = np.array(consts["basecolor"][:3], dtype=np.float32)
|
||||||
|
rgb_is_linear = True # Principled 常量色是线性值
|
||||||
if maps["alpha"]:
|
if maps["alpha"]:
|
||||||
img, ch = maps["alpha"]
|
img, ch = maps["alpha"]
|
||||||
base[..., 3] = _channel(_scaled_pixels(img, tw, th), ch)
|
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)
|
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))
|
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))
|
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)
|
_save_png(mix_png, mix)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -9,15 +9,36 @@ import bl_convert as bc
|
|||||||
|
|
||||||
|
|
||||||
class TestBaseTextureEncoding(unittest.TestCase):
|
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)
|
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)
|
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)
|
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)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user