SplineMesh:控制点必须先转换到组件局部空间

自动关联目录:SplineMesh:控制点必须先转换到组件局部空间

核心判断:USplineMeshComponent::SetStartAndEnd 接收的是组件局部空间的位置与切线。把 World 坐标直接写进去,Actor 位于原点时可能看似正确,一旦宿主平移、旋转、缩放或重新挂接,就会重复或漏掉 Transform,碰撞也会跟着错。

一、业务冲突:轨迹和空气墙都由 Spline 生成,空间归属却不同

抛射轨迹需要按曲线生成可见段,动态边界需要沿控制点生成既有网格又有碰撞的墙体。两者都适合使用 SplineMesh,但源 Spline、目标组件和宿主 Actor 可能拥有不同 Transform;编辑器 Construction、运行时 Spawn 和网络复制又会在不同阶段重建组件。

二、实际实现:两条路径形成了直接对照

一条轨迹路径从 Spline 获取 Local 位置和 Local 切线,把动态 SplineMesh 挂到同一根组件,再注册并提交起终点。只要两者共享局部基准,这条链路是闭合的。

另一条边界路径在重建前先销毁旧 SplineMesh,这一点正确;随后却从 Spline 查询 World 位置和 World 切线,直接传给新组件的 SetStartAndEnd,组件也没有先建立与源 Spline 一致的 Attachment。它实际上依赖“新组件 Transform 恰好是单位矩阵”。

这种问题最隐蔽的地方是原点测试会通过:World 与 Local 数值相同。地图把 Actor 移走、父节点旋转或美术调整缩放后,错误才显现;如果 SplineMesh 开启碰撞,错误几何还会同时进入查询和物理接触,不只是画面偏移。

三、目标方案:先选目标空间,再只转换一次

最简单的规则是:若 Spline 与所有 SplineMesh 共用同一父 Transform,就统一查询 ESplineCoordinateSpace::Local。若两者不共父级,则通过 World 中转,明确转换到目标组件局部空间:

FSegmentGeometry ConvertSegment(
    const USplineComponent& Source,
    const FTransform& TargetWorld,
    int32 StartIndex,
    int32 EndIndex)
{
    const FTransform SourceWorld = Source.GetComponentTransform();
    const FVector P0s = Source.GetLocationAtSplinePoint(StartIndex, Local);
    const FVector T0s = Source.GetTangentAtSplinePoint(StartIndex, Local);
    const FVector P1s = Source.GetLocationAtSplinePoint(EndIndex, Local);
    const FVector T1s = Source.GetTangentAtSplinePoint(EndIndex, Local);

    return {
        TargetWorld.InverseTransformPosition(SourceWorld.TransformPosition(P0s)),
        TargetWorld.InverseTransformVector(SourceWorld.TransformVector(T0s)),
        TargetWorld.InverseTransformPosition(SourceWorld.TransformPosition(P1s)),
        TargetWorld.InverseTransformVector(SourceWorld.TransformVector(T1s))
    };
}

位置使用 Position 变换,切线使用 Vector 变换,不能让平移污染切线。非单位与负缩放会改变切线长度和朝向,必须在同一变换链中处理,不能只旋转方向再沿用原长度。

动态组件的推荐顺序是:创建组件、设置稳定 Attachment 与相对 Transform、批量写入网格/材质/碰撞和所有段参数,最后只注册一次;若注册后的运行时更新必须分步完成,再用延迟更新参数并统一 UpdateMesh。视觉和碰撞必须消费同一份 FSegmentGeometry,禁止各自重新查询不同空间的 Spline。

四、重建性能:正确销毁不等于适合高频修改

重建前 DestroyComponent 能避免旧段残留,是值得保留的做法。但若控制点在运行时频繁变化,每次销毁并 NewObject 会重复支付 UObject 分配、组件注册、SceneProxy 和 BodySetup/Physics State 更新。

更稳妥的是按段数维护组件池:复用已有段,只为新增段创建组件,多余段禁用或销毁;记录脏控制点区间,只更新受相邻点影响的段。碰撞更新可降频或在编辑完成时提交,避免拖动控制点时每个采样都重建物理状态。

五、验证与指标

测试不能只把 Actor 放在原点。至少覆盖平移、任意旋转、父级旋转、均匀/非均匀缩放、负缩放、运行时重新挂接和网络端重建。每个控制点都应验证:渲染段端点、碰撞命中点和 Spline World 位置在容差内一致。

性能侧记录 SegmentsCreated、SegmentsReused、DirtySegments、SceneProxyRebuildTime、PhysicsRebuildTime。如果只看 Draw Call,很容易忽略动态组件和碰撞重建已经把成本移回 Game Thread 与 Physics Thread。

六、反思

两条实际实现的差异说明,这不是 Spline API 难用,而是没有把目标空间写成接口契约。Transform 问题最怕“数值暂时相等”:一旦输入没有携带空间语义,原点上的成功只是把错误推迟到内容变化以后。