工具概述

面向三维资产生产的可控 AI 磨损纹理生成工具。输入无磨损模型,流程覆盖模型检查、六视图渲染、材质描述、AI 生成、UV 反投影、接缝修复与强度控制,最终输出可调强度的 UV 磨损遮罩和材质分区贴图。支持 FBX、OBJ、GLB、glTF 模型输入,遮罩输出为 8 位灰度 PNG 或 TGA。

管线总览:输入、处理与输出。

模型准备与六视图渲染

工具自动检查 UV:UV 有效的模型进入 uv_model 模式并保留原始结构,缺少 UV 的模型进入 plain_model 模式并自动展开。灯光提供三种模式:Studio 保留材质质感,Flat World 减少明暗变化,Albedo 直接输出基础颜色。渲染前后左右上下六个正交视图,同时生成 Alpha、Depth、Position 和 Camera 数据:RGB 与 Alpha 用于 AI 生成,Depth 用于几何约束,Position 与 Camera 用于多视角反投影。

模型模式、灯光模式与六视图渲染数据。

材质识别与 AI 生成

物品名称、材质类型与表面处理可手动填写,也可由视觉语言模型识别,多材质逐行编辑,识别结果写入同一组输入。提示词拆分为物品、材质、强度、几何保持、Depth、Mask 与 Output 等独立模块,可组合、移除或替换。云端接入 Gemini 与 GPT Image,两者可同时接收六视图 Grid 与 Depth Grid,提示词说明相机与几何关系;本地接入 ComfyUI,可加载 Flux、Qwen、SDXL 及自定义 API JSON 工作流,SDXL 可使用 ControlNet Depth。

材质输入、模块化提示词、模型选择与深度约束。

多视角反投影与接缝修复

AI 生成结果位于二维多视图中,而材质需要 UV 空间的遮罩,各视角之间还存在位置偏差、遮挡差异和边缘接缝。工具先对齐 AI 结果与原始视图,再用 Position Map、Camera Matrix 和 Depth Map 找到每个 UV 纹素对应的可见像素,按视角质量加权融合。内容接缝按世界坐标建立接缝组,统一同一三维表面两侧的取值;采样接缝用距离场向 UV 岛外扩展边界颜色,保证双线性采样连续。

反投影结果与接缝修复前后对比。

强度控制与生产输出

AI 只生成一张重度磨损源遮罩,各档强度由阈值计算,相邻档位之间用二维像素插值生成交互预览。这样减少模型调用和推理开销,并保证各档位的磨损位置与纹理细节一致。载入颜色贴图后,可按 UV 岛手动划分材质区域,每种材质生成独立遮罩,主材质自动接收剩余区域。遮罩以 PNG 或 TGA 输出,可在 Substance、Blender 和 Unreal Engine 中使用。

档位预览、材质分区与输出。

磨损结果对比

三个案例的实际输出:皮革脚凳、红色金属油漆罐和蓝色上漆油桶。拖动图内分隔线比较原始材质与磨损材质,按钮切换 30、60、100 三个档位。

皮革脚凳 · 原始材质
皮革脚凳 · 磨损档位 100

左右拖动图片中央的双向箭头,查看磨损前后的效果。

磨损档位

附:投影与接缝相关源码(Python · 3 个函数)
backproject_level

将各视图采样值按可见性与视图质量融合;处理没有覆盖的 UV 纹素。

projection.py · backproject_levelPython
def backproject_level(diff_maps,visibility,proj_cfg,view_quality=None):
    n=len(visibility[0]["px"]); weights=np.zeros(n,np.float32); count=np.zeros(n,np.int16); per=[]; sampled=[]; view_weights=[]
    quality = list(view_quality or [1.0] * len(visibility))
    for index,(image,vis) in enumerate(zip(diff_maps,visibility)):
        v=sample_image(image,vis["px"],vis["py"],bool(proj_cfg.get("bilinear",True))); w=np.asarray(vis["weight"],np.float32)
        w=w*float(quality[index]); sampled.append(v); view_weights.append(w); weights+=w; count+=(w>0); per.append({"visible_ratio":float((w>0).mean()),"quality":float(quality[index])})
    values=np.stack(sampled,axis=0); confidence=np.stack(view_weights,axis=0); mode=str(proj_cfg.get("fusion","max_confidence"))
    if mode == "weighted_mean":
        out=np.zeros(n,np.float32); np.divide(np.sum(values*confidence,axis=0),weights,out=out,where=weights>0)
    elif mode == "max_confidence":
        relative=np.divide(confidence,np.maximum(np.max(confidence,axis=0,keepdims=True),1e-8),where=np.ones_like(confidence,dtype=bool))
        score=np.where(confidence>0,values*relative,-1.0); index=np.argmax(score,axis=0); out=values[index,np.arange(n)]; out[weights<=0]=0
    elif mode == "top2_consensus":
        order=np.argsort(confidence,axis=0); best=order[-1]; second=order[-2] if confidence.shape[0]>1 else best; columns=np.arange(n)
        high=values[best,columns]; low=values[second,columns]; second_valid=confidence[second,columns]>0
        pair_min=np.minimum(high,low); pair_max=np.maximum(high,low); keep=float(proj_cfg.get("disagreement_keep",.35))
        out=np.where(second_valid,pair_min+keep*(pair_max-pair_min),high); out[weights<=0]=0
    else:
        raise ValueError(f"未知投影融合方式: {mode}")
    return out.astype(np.float32),weights,count,per
build_content_seam_groups

按三维位置划分体素组,筛选 UV 距离较远而表面位置接近的接缝采样。

seam.py · build_content_seam_groupsPython
def build_content_seam_groups(points,ys,xs,shape,seam_cfg):
    pts=np.asarray(points,np.float32); ys=np.asarray(ys,np.int64); xs=np.asarray(xs,np.int64)
    params=seam_pixel_params(shape,seam_cfg)
    if not len(pts):
        return [],{"voxel_size":0.0,**params}
    lo=pts.min(axis=0); diag=float(np.linalg.norm(np.ptp(pts,axis=0)))
    voxel=max(diag*float(seam_cfg.get("voxel_ratio",.006)),1e-6)
    keys=np.floor((pts-lo[None,:])/voxel).astype(np.int64)
    order=np.lexsort((keys[:,2],keys[:,1],keys[:,0]))
    sorted_keys=keys[order]
    boundaries=np.nonzero(np.any(sorted_keys[1:]!=sorted_keys[:-1],axis=1))[0]+1
    groups=[]
    for idx in np.split(order,boundaries):
        if len(idx)<2:
            continue
        uv=np.column_stack((ys[idx],xs[idx])).astype(np.float32); p3=pts[idx]
        rms_uv=float(np.sqrt(np.mean(np.sum((uv-uv.mean(axis=0))**2,axis=1))))
        rms_3d=float(np.sqrt(np.mean(np.sum((p3-p3.mean(axis=0))**2,axis=1))))
        if rms_uv>float(params["uv_split_px"]) and rms_3d<voxel*1.5:
            groups.append(idx.astype(np.int64,copy=False))
    return groups,{"voxel_size":voxel,**params}
repair_content_seam

按权重统一接缝值,再通过距离变换对接缝带做羽化,并保留有效范围之外的原值。

seam.py · repair_content_seamPython
def repair_content_seam(mask2d,groups,ys,xs,weights,valid,seam_cfg):
    source=np.asarray(mask2d,np.float32); out=source.copy(); valid=np.asarray(valid,bool)
    ys=np.asarray(ys,np.int64); xs=np.asarray(xs,np.int64); weights=np.asarray(weights,np.float32)
    core=np.zeros(valid.shape,bool); target=np.zeros(valid.shape,np.float32)
    before=_group_gap(source,groups,ys,xs)
    for group in groups:
        gy,gx=ys[group],xs[group]; w=np.maximum(weights[group],0); total=float(w.sum())
        value=float(np.sum(source[gy,gx]*w)/total) if total>1e-12 else float(np.mean(source[gy,gx]))
        target[gy,gx]=value; core[gy,gx]=True
    feather=seam_pixel_params(valid.shape,seam_cfg)["feather_px"]
    if np.any(core) and ndimage is not None and feather>0:
        distance,indices=ndimage.distance_transform_edt(~core,return_indices=True)
        band=valid&(distance<=float(feather))
        nearest=target[indices[0],indices[1]]
        alpha=np.clip((float(feather)+1.0-distance)/(float(feather)+1.0),0,1).astype(np.float32)
        out[band]=source[band]*(1-alpha[band])+nearest[band]*alpha[band]
    else:
        band=core&valid; out[band]=target[band]
    out[~valid]=source[~valid]
    after=_group_gap(out,groups,ys,xs); count=int(band.sum()); valid_count=max(int(valid.sum()),1)
    return out,{"band_texels":count,"band_ratio":float(count/valid_count),"content_gap_before":before,"content_gap_after":after,"seam_groups":len(groups)}

展示视频

工具流程完整演示

技术内容整理自作者作品集及项目文件。

下一个项目UE5 二次元角色渲染