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:
@@ -8,7 +8,10 @@ import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
DEFAULT_RAY_PCT = 0.02 # 自动射线距离 = 低模包围盒对角线 * 2%
|
||||
DEFAULT_RAY_PCT = 0.02 # 测量失败时回退:低模包围盒对角线 * 2%
|
||||
RAY_SAFETY = 1.5 # 自适应射线安全系数(分离 P99 × 此值)
|
||||
RAY_LOWER_PCT = 0.0005 # 射线下限 = 包围盒对角线 * 0.05%
|
||||
RAY_UPPER_PCT = 0.01 # 射线上限 = 包围盒对角线 * 1%
|
||||
BBOX_TOL = 0.10 # 高低模包围盒尺寸相对差警告阈值
|
||||
|
||||
# (输出名, bake type, EMIT 来源的 Principled 输入, 目标图颜色空间)
|
||||
@@ -26,6 +29,33 @@ def estimate_ray_distance(bbox_dims, pct=DEFAULT_RAY_PCT):
|
||||
return math.sqrt(sum(d * d for d in bbox_dims)) * pct
|
||||
|
||||
|
||||
def percentile(values, q):
|
||||
"""插值分位数。空列表或 q∉[0,1] 抛 ValueError。"""
|
||||
if not values:
|
||||
raise ValueError("percentile 需要至少一个值")
|
||||
if not 0.0 <= q <= 1.0:
|
||||
raise ValueError("分位数 q 必须在 [0,1]")
|
||||
ordered = sorted(float(v) for v in values)
|
||||
pos = (len(ordered) - 1) * q
|
||||
lo = int(math.floor(pos))
|
||||
hi = int(math.ceil(pos))
|
||||
if lo == hi:
|
||||
return ordered[lo]
|
||||
return ordered[lo] + (ordered[hi] - ordered[lo]) * (pos - lo)
|
||||
|
||||
|
||||
def adaptive_ray_distance(distances, bbox_dims):
|
||||
"""按低↔高分离距离定射线:P99×RAY_SAFETY,夹在对角线 [0.05%,1%]。"""
|
||||
diagonal = math.sqrt(sum(float(d) * float(d) for d in bbox_dims))
|
||||
if diagonal <= 0.0:
|
||||
raise ValueError("低模包围盒对角线必须为正")
|
||||
p99 = percentile(distances, 0.99)
|
||||
lower = diagonal * RAY_LOWER_PCT
|
||||
upper = diagonal * RAY_UPPER_PCT
|
||||
raw = max(p99 * RAY_SAFETY, lower)
|
||||
return {"distance_p99": p99, "value": min(raw, upper), "capped": raw > upper}
|
||||
|
||||
|
||||
def output_stem(low_name):
|
||||
"""低模名去掉 _low 后缀作为输出前缀(对齐工具3纯网格贴图命名约定)。"""
|
||||
return low_name[:-4] if low_name.endswith("_low") else low_name
|
||||
@@ -65,6 +95,29 @@ def _import_model_joined(path, warnings):
|
||||
return obj
|
||||
|
||||
|
||||
def _measure_separation(low, high, limit=10000):
|
||||
"""低模面心到高模表面最近距离数组(供自适应射线)。"""
|
||||
from mathutils.bvhtree import BVHTree
|
||||
hv = [high.matrix_world @ v.co for v in high.data.vertices]
|
||||
hp = [tuple(p.vertices) for p in high.data.polygons]
|
||||
bvh = BVHTree.FromPolygons(hv, hp, all_triangles=False)
|
||||
polys = low.data.polygons
|
||||
n = len(polys)
|
||||
if n == 0:
|
||||
return []
|
||||
if n <= limit:
|
||||
idxs = range(n)
|
||||
else:
|
||||
idxs = [round(i * (n - 1) / float(limit - 1)) for i in range(limit)]
|
||||
mw = low.matrix_world
|
||||
dists = []
|
||||
for i in idxs:
|
||||
hit = bvh.find_nearest(mw @ polys[i].center)
|
||||
if hit[0] is not None and hit[3] is not None:
|
||||
dists.append(hit[3])
|
||||
return dists
|
||||
|
||||
|
||||
def _setup_bake_target(low, stem):
|
||||
"""低模换成单一烘焙材质,返回接收烘焙结果的 image 节点。"""
|
||||
import bpy
|
||||
@@ -177,7 +230,24 @@ def main():
|
||||
% (BBOX_TOL * 100,
|
||||
[round(d, 3) for d in high_dims],
|
||||
[round(d, 3) for d in low_dims]))
|
||||
ray = estimate_ray_distance(low_dims) if ray_arg == "auto" else float(ray_arg)
|
||||
if ray_arg == "auto":
|
||||
dists = _measure_separation(low, high)
|
||||
if dists:
|
||||
try:
|
||||
adapt = adaptive_ray_distance(dists, low_dims)
|
||||
ray = adapt["value"]
|
||||
warnings.append(
|
||||
"自适应射线距离=%.6f(P99=%.6f%s)"
|
||||
% (ray, adapt["distance_p99"],
|
||||
",已触顶" if adapt["capped"] else ""))
|
||||
except ValueError as exc:
|
||||
ray = estimate_ray_distance(low_dims)
|
||||
warnings.append("自适应射线失败(%s),回退对角线 2%%" % exc)
|
||||
else:
|
||||
ray = estimate_ray_distance(low_dims)
|
||||
warnings.append("分离距离采样为空,回退对角线 2%% 射线")
|
||||
else:
|
||||
ray = float(ray_arg)
|
||||
|
||||
stem = output_stem(os.path.splitext(os.path.basename(low_fbx))[0])
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user