Add AIServer, character art pipeline, and Art package boot integration.

Bring in MaleExplorer assets/shaders, model tooling and Hunyuan updates, plus boot/YooAsset Art package support.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-09-21 17:06:59 +08:00
co-authored by Cursor
parent b7a0bbcb89
commit 6550b8df4b
121 changed files with 313260 additions and 113 deletions
+43
View File
@@ -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)
@@ -0,0 +1,73 @@
import os
import re
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import unity_assets as ua
class TestGuid(unittest.TestCase):
def test_new_guid_is_32_hex(self):
g = ua.new_guid()
self.assertTrue(re.fullmatch(r"[0-9a-f]{32}", g))
def test_guid_for_reuses_existing_meta(self):
with tempfile.TemporaryDirectory() as d:
asset = os.path.join(d, "a.png")
with open(asset + ".meta", "w") as f:
f.write("fileFormatVersion: 2\nguid: 0123456789abcdef0123456789abcdef\n")
self.assertEqual(ua.guid_for(asset), "0123456789abcdef0123456789abcdef")
def test_guid_for_generates_when_no_meta(self):
with tempfile.TemporaryDirectory() as d:
g = ua.guid_for(os.path.join(d, "b.png"))
self.assertTrue(re.fullmatch(r"[0-9a-f]{32}", g))
class TestTemplates(unittest.TestCase):
def test_texture_meta_srgb_on_off(self):
on = ua.texture_meta("a" * 32, srgb=True, max_size=2048)
off = ua.texture_meta("b" * 32, srgb=False, max_size=2048)
self.assertIn("sRGBTexture: 1", on)
self.assertIn("sRGBTexture: 0", off)
self.assertIn("maxTextureSize: 2048", on)
self.assertIn("guid: " + "a" * 32, on)
def test_texture_meta_alpha_is_transparency(self):
default = ua.texture_meta("a" * 32, srgb=True, max_size=2048)
ait = ua.texture_meta("a" * 32, srgb=True, max_size=2048,
alpha_is_transparency=True)
self.assertIn("alphaIsTransparency: 0", default)
self.assertIn("alphaIsTransparency: 1", ait)
def test_material_yaml_refs_and_keyword(self):
y = ua.material_yaml("tong_wood", "s" * 32, "c" * 32, "d" * 32, alpha_test=True)
self.assertIn("m_Name: tong_wood", y)
self.assertIn("guid: " + "s" * 32, y) # shader
self.assertIn("guid: " + "c" * 32, y) # base tex
self.assertIn("guid: " + "d" * 32, y) # mix tex
self.assertIn("_ALPHATEST_ON", y)
self.assertIn("_AlphaTest: 1", y)
def test_material_yaml_no_keyword_when_opaque(self):
y = ua.material_yaml("m", "s" * 32, "c" * 32, "d" * 32, alpha_test=False)
self.assertNotIn("_ALPHATEST_ON", y)
self.assertIn("_AlphaTest: 0", y)
def test_model_meta_external_objects(self):
m = ua.model_meta("e" * 32, [("wood", "f" * 32), ("iron", "1" * 32)])
self.assertIn("name: wood", m)
self.assertIn("guid: " + "f" * 32, m)
self.assertIn("name: iron", m)
self.assertIn("guid: " + "e" * 32, m)
class TestSanitize(unittest.TestCase):
def test_safe_name(self):
self.assertEqual(ua.safe_name("Mat 01.a/b"), "Mat_01_a_b")
if __name__ == "__main__":
unittest.main()