diff --git a/Tools/ModelTranslator/bl_convert.py b/Tools/ModelTranslator/bl_convert.py new file mode 100644 index 00000000..035f55d3 --- /dev/null +++ b/Tools/ModelTranslator/bl_convert.py @@ -0,0 +1,23 @@ +"""Blender 内运行:FBX -> 纯模型 FBX + XPbr 基础/混合贴图。 +调用:blender -b --factory-startup --python bl_convert.py -- +""" +import numpy as np + + +def encode_normal_rg(n): + """切线法线 (N,3) -> 球面编码 (N,2),范围 [0,1]。 + shader 侧 XPbr_Base.hlsl DecodeSphereMap 先 *2-1 再解码。""" + n = n / np.maximum(np.linalg.norm(n, axis=-1, keepdims=True), 1e-8) + z = np.clip(n[..., 2], -0.99, 1.0) + denom = np.sqrt(2.0 * (1.0 + z)) + enc = n[..., :2] / denom[..., None] + return np.clip(enc * 0.5 + 0.5, 0.0, 1.0) + + +def decode_sphere_map(enc01): + """shader DecodeSphereMap 的 numpy 等价(含 *2-1),仅测试用。""" + e = enc01 * 2.0 - 1.0 + l = 1.0 - e[..., 0] ** 2 - e[..., 1] ** 2 + l = np.clip(l, 0.0, 1.0) + xy = e * np.sqrt(l)[..., None] + return np.concatenate([xy * 2.0, (l * 2.0 - 1.0)[..., None]], axis=-1) diff --git a/Tools/ModelTranslator/tests/bl_test_encode.py b/Tools/ModelTranslator/tests/bl_test_encode.py new file mode 100644 index 00000000..7d689234 --- /dev/null +++ b/Tools/ModelTranslator/tests/bl_test_encode.py @@ -0,0 +1,43 @@ +"""法线球面编码 round-trip 测试。运行: +blender -b --factory-startup --python tests/bl_test_encode.py +成功打印 ENCODE_TEST_OK,失败非零退出。""" +import os +import sys + +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from bl_convert import encode_normal_rg, decode_sphere_map + + +def main(): + rng = np.random.default_rng(7) + # 随机切线空间法线(z>=0 半球),含边缘方向 + n = rng.normal(size=(4096, 3)) + n[:, 2] = np.abs(n[:, 2]) + n /= np.linalg.norm(n, axis=1, keepdims=True) + n = np.vstack([n, [[0, 0, 1], [1, 0, 0], [-1, 0, 0], [0, -1, 0], [0.707, -0.707, 0]]]) + + enc = encode_normal_rg(n) # (N,2) in [0,1] + assert enc.min() >= 0.0 and enc.max() <= 1.0, "encode out of [0,1]" + + enc8 = np.round(enc * 255.0) / 255.0 # 模拟 8bit PNG 存储 + dec = decode_sphere_map(enc8) # shader 同款公式 + dec /= np.linalg.norm(dec, axis=1, keepdims=True) + + cos = np.clip(np.sum(n * dec, axis=1), -1.0, 1.0) + max_err = np.degrees(np.arccos(cos)).max() + assert max_err < 2.0, "max angle error %.3f deg >= 2" % max_err + + # 平面法线编码值应为 (0.5, 0.5) + flat = encode_normal_rg(np.array([[0.0, 0.0, 1.0]])) + assert np.allclose(flat, 0.5, atol=1e-6), "flat normal must encode to 0.5" + + print("ENCODE_TEST_OK max_err=%.4f deg" % max_err) + + +try: + main() +except Exception as e: + print("ENCODE_TEST_FAIL:", e) + sys.exit(1)