乐于分享
好东西不私藏

UE5.8 PoseSearch 源码全拆解:搜索索引、PCA 降维与 Continuing Pose 成本函数

UE5.8 PoseSearch 源码全拆解:搜索索引、PCA 降维与 Continuing Pose 成本函数

一个 PoseSearch Database 里塞了 200 段动画、15 万个采样帧。每帧游戏循环,引擎要从这 15 万个候选中挑出"最像当前运动状态"的那一帧——而且在 PCAKDTree 模式下必须压到 0.3ms 量级。

这不是魔法。这是一套精密的分层搜索管线:Feature Channel 构建特征向量 → PCA 降维 → KDTree/VPTree 空间索引 → Continuing Pose 成本函数保证连续性。Epic 在 PoseSearch 插件里塞了 53,925 行 C++(Runtime 38,955 + Editor 14,970,UE 5.8 实测),我们今天把它一层层拆开。

一、PoseSearch 不是什么

先说清楚 PoseSearch 在整个动画系统里的位置。

它不是 Animation Blueprint 的替代品。它不是状态机。它甚至不直接驱动骨骼——PoseSearch 只做一件事:给定当前角色的运动状态(位置、速度、朝向、骨骼姿态),从动画数据库中找出最匹配的那一帧,返回一个 Pose Index 和 Asset Time

拿到这个结果之后,AnimNode_MotionMatching 负责把它喂给动画蓝图,AnimBlueprint 里的 Blend Stack 负责做混合过渡。PoseSearch 是"搜索引擎",不是"播放器"。

这个分工在源码里非常清晰。UPoseSearchDatabase::Search()PoseSearchDatabase.h:690):

UE_API void Search(UE::PoseSearch::FSearchContext& SearchContext,
                   UE::PoseSearch::FSearchResults& SearchResults)
 const
;
// 紧接着还有一个(标为 Experimental):
UE_API void SearchContinuingPose(UE::PoseSearch::FSearchContext&,
                                 UE::PoseSearch::FSearchResults&)
 const
;

输入是一个 FSearchContext(包含当前查询特征向量、Continuing Pose 信息、事件等),输出是一个 FSearchResults。没有骨骼操作,没有动画播放——纯粹的搜索。注意 SearchContinuingPose 在源码注释里被 Epic 标为 Experimental,与主 Search() 一起构成完整搜索但语义仍在演进。

二、特征向量:把"运动状态"翻译成数字

PoseSearch 的核心问题可以简化成一句话:怎么把"角色正在向前跑、速度 5m/s、右脚即将落地"这个状态,表示成一个可比较的浮点数向量?

答案在 UPoseSearchSchema 和它的 Feature Channels 里。

Schema:特征向量的蓝图

// PoseSearchSchema.h
class UPoseSearchSchema : public UDataAsset
{
    // ...
    int32 SchemaCardinality = 0// 特征向量的总维度
    // ...
};

SchemaCardinality 是特征向量的总维度。一个典型的 locomotion 数据库可能有 30–80 维——每个维度对应一个 Feature Channel 贡献的浮点值。

Schema 通过 IterateChannels 递归遍历所有 Channel(包括嵌套的 Group Channel):

void IterateChannels(IteratePredicateType IteratePredicate) const
{
    IterateChannelsRecursive(GetChannels(), IteratePredicate);
}

Trajectory Channel:运动匹配的灵魂

最重要的 Feature Channel 是 UPoseSearchFeatureChannel_Trajectory——轨迹通道。它定义了"我们要匹配运动的哪些方面"(PoseSearchFeatureChannel_Trajectory.h:28):

USTRUCT()
struct FPoseSearchTrajectorySample
{
    GENERATED_BODY()

    // Offset 秒后的轨迹数据会被用来匹配
    // 例如 Offset = 0.5s, Flags = Position → 匹配未来 0.5 秒的位置
    UPROPERTY(EditAnywhere, Category = Config)
    float Offset = 0.f;

    // 匹配什么:Velocity / Position / VelocityDirection / FacingDirection
    // 还有对应的 XY 变体(去掉 Z 分量)
    UPROPERTY(EditAnywhere, meta = (Bitmask, ...))
    int32 Flags = int32(EPoseSearchTrajectoryFlags::Position);

#if WITH_EDITORONLY_DATA
    UPROPERTY(EditAnywhere, Category = Config)
    float Weight = 1.f;

    // 同 class、同 cardinality、同 NormalizationGroup 的会一起做归一化
    UPROPERTY(EditAnywhere, Category = Config)
    FName NormalizationGroup;

    UPROPERTY(EditAnywhere, Category = Config, meta = (ExcludeFromHash, DisplayPriority = 0))
    FLinearColor DebugColor = FLinearColor::Blue;
#endif // WITH_EDITORONLY_DATA
};

注意 Weight / NormalizationGroup / DebugColor 都在 WITH_EDITORONLY_DATA 里——归一化和权重信息在编辑器里配置,构建阶段"烘"进 SearchIndex 之后,运行时数据结构里不再有这几个字段。

EPoseSearchTrajectoryFlags 的完整定义值得注意:

enum class EPoseSearchTrajectoryFlags : uint32
{
    Velocity            = 1 << 0,
    Position            = 1 << 1,
    VelocityDirection   = 1 << 2,
    FacingDirection     = 1 << 3,
    VelocityXY          = 1 << 4,
    PositionXY          = 1 << 5,
    VelocityDirectionXY = 1 << 6,
    FacingDirectionXY   = 1 << 7,
};

每个 "非 XY" 版本是 3D(贡献 3 维 X/Y/Z),每个 XY 变体是 2D(贡献 2 维,源码里通过 EComponentStrippingVector::StripZ 剥掉 Z)。

关键但容易漏的细节:UE 5.8 里 UPoseSearchFeatureChannel_Trajectory 的默认构造函数(PoseSearchFeatureChannel_Trajectory.cpp 里的 AddDefaultChannels)默认加的四组样本,全部使用 XY 变体

Samples.Add({ -0.4f, PositionXY,                        Weight=0.4 });
Samples.Add({  0.0f, VelocityXY | FacingDirectionXY,    Weight=2.0 });
Samples.Add({  0.35f, PositionXY | FacingDirectionXY,   Weight=0.7 });
Samples.Add({  0.7f, VelocityXY | PositionXY | FacingDirectionXY, Weight=0.5 });

也就是说:默认的 Locomotion 匹配是平面 (XY) 的,垂直分量不参与。如果你的角色有跳跃、蹲下、斜坡爬升等需要匹配 Z 的动作,得在 Schema 里手动改用 Position / Velocity 3D flag。

除了 Trajectory,Runtime 里还有独立的 UPoseSearchFeatureChannel_Position / _Velocity / _Heading / _Phase / _Distance / _Curve / _PermutationTime / _TimeToEvent / _Padding / _FilterCrashingLegs / _Pose / _Group 等 Channel(对应文件都在 Source/Runtime/Public/PoseSearch/PoseSearchFeatureChannel_*.h)。所有 Channel 的输出拼接起来,就是完整的特征向量。

三、Search Index:数据库的"编译产物"

特征向量建好后,下一步是把数据库中所有动画的所有采样帧都"编译"成可搜索的索引。这就是 FSearchIndex

基础层:FSearchIndexBase

// PoseSearchIndex.h:504
struct FSearchIndexBase
{
    TAlignedArray<float> Values;                          // 所有帧的特征向量(扁平存储)
    FSparsePoseMultiMap<int32> ValuesVectorToPoseIndexes; // 向量→帧索引映射(去重后一对多)
    TAlignedArray<FPoseMetadata> PoseMetadata;            // 每帧的元数据
    bool bAnyBlockTransition = false;                     // 快速判断:这个索引里有没有过渡帧
    TAlignedArray<FSearchIndexAsset> Assets;              // 动画资产列表
    // Experimental, this feature might be removed without warning
    FEventData EventData;                                 // 事件搜索所需数据
    float MinCostAddend = -MAX_FLT;                       // 最小成本附加值,短路搜索用
#if WITH_EDITORONLY_DATA
    FSearchStats Stats;                                   // 编辑器统计信息
#endif
};

Values 是一个巨大的扁平数组。如果数据库有 15 万个采样帧、每帧 40 维特征向量(一个中等规模的 locomotion database 典型值),那 Values 就是 600 万个 float——约 24MB。扁平数组不是可选项:搜索时要按帧顺序连续访问相邻帧的数据,Eigen 也需要连续内存才能发挥 SIMD。数组用 TAlignedArray 保证 16/32 字节对齐,直接对齐到 SSE/AVX 加载边界。

FPoseMetadataPoseSearchIndex.h:20)记录每帧的关键信息:

struct FPoseMetadata
{
private:
    enum { ValueOffsetNumBits    = 27 };
    enum { AssetIndexNumBits     = 20 };
    enum { BlockTransitionNumBits = 1 };

    uint32 ValueOffset      : ValueOffsetNumBits = 0;  // 在 Values 数组里的偏移
    uint32 AssetIndex       : AssetIndexNumBits  = 0;  // 所属动画资产索引
    bool   bBlockTransition : BlockTransitionNumBits = 0// 是否为段落过渡帧
    FFloat16 CostAddend     = 0.f;                     // 半精度存储的固定成本附加
    // ...
};

三个位宽细节值得注意:ValueOffset 只有 27 bits(1.3 亿上限)、AssetIndex 只有 20 bits(1M asset 上限)、CostAddend 用的是 FFloat16 半精度。三个数字加起来正好 48 bits 装进 uint32 + uint16,Metadata 单条 6 字节起(加上 padding 是 8 字节),15 万帧就是 1.2MB,压得极紧。

bBlockTransition 标记很有意思——动画段落边界的帧不应该被 KDTree 搜到(因为跳到一个段落边界会导致动画衔接不自然),它们只参与 Continuing Pose 搜索。

搜索增强层:FSearchIndex

// PoseSearchIndex.h:575
struct FSearchIndex : public FSearchIndexBase
{
    // 权重平方根(数值稳定性优化,见下面注释)
    TAlignedArray<float> WeightsSqrt;

    // PCA 空间的编码;不是所有帧都有 —— Block Transition 帧被剔除,只参与 Continuing Pose 搜索
    TAlignedArray<float> PCAValues;
    FSparsePoseMultiMap<int32> PCAValuesVectorToPoseIndexes;  // PCA 空间的去重映射
    TAlignedArray<float> PCAProjectionMatrix;
    TAlignedArray<float> Mean;

    FKDTree KDTree;
    FVPTree VPTree;

#if WITH_EDITORONLY_DATA
    TAlignedArray<float> DeviationEditorOnly;
    float PCAExplainedVarianceEditorOnly = 0.f;  // PCA 保留率诊断
#endif
    // ...
};

PCAValuesVectorToPoseIndexes 是 PCA 空间独立的去重映射——和基类的 ValuesVectorToPoseIndexes两套并存的去重系统。原始空间和 PCA 空间的"哪些帧特征相同"结果不完全一样(PCA 会把细微差异抹掉,导致 PCA 空间的去重集合更大),所以分别存两份。

PCAExplainedVarianceEditorOnly 是编辑器诊断字段,代表当前 NumberOfPrincipalComponents 保留了多少方差。数据库编辑器 preview 会显示这个数——低于 90% 就该增加主成分数或重新审视 Schema。

注意 WeightsSqrt 的设计细节。源码注释里写得很清楚:

"we store weights square roots to reduce numerical errors when CompareFeatureVectors ((VA - VB) * VW).square().sum() instead of ((VA - VB).square() * VW).sum() since (VA - VB).square() could lead to big numbers, and VW being multiplied by the variance of the dataset..."

这是数值稳定性优化。先 (VA - VB) * VW(元素乘)再 square,各项数量级更均匀;反过来先 square 再乘 VW 会放大误差。存 WeightsSqrt 意味着运行时不需要再开根号——这个开根号如果放在热路径里,对搜索延迟是个真实成本。

四、PCA 降维:从高维压缩到低维

15 万个 40 维向量做最近邻搜索,暴力遍历是 15 万 × 40 = 600 万次维度比较,每次比较约 4 FLOP,合计 2400 万 FLOP。桌面 CPU 单核一帧 0.3ms 的预算——BruteForce 打满不够用。

PCA(主成分分析)降维是 PoseSearch 的第一道加速。Epic 把原始特征维度压缩到少数几个主成分,然后用 KDTree 在低维空间做近似搜索。

// PCAValues 存的是 Values 在 PCA 空间的编码
// 不是所有帧都有 PCA 值 —— Block Transition 帧只参与 Continuing Pose 搜索
TAlignedArray<float> PCAValues;
TAlignedArray<float> PCAProjectionMatrix; // 投影矩阵
TAlignedArray<float> Mean;                // 均值向量

FKDTree KDTree;  // 在 PCA 空间构建的 KD 树

PCA 投影的核心 API:

TConstArrayView<floatPCAProject(
    TConstArrayView<float> PoseValues,
    TArrayView<float> BufferUsedForProjection)
 const
;

查询时,当前帧的特征向量先减去 Mean、乘以 PCAProjectionMatrix 投影到低维空间,然后在 KDTree 中找 K 个最近邻,最后对这 K 个候选回到原始高维空间做精确成本计算。

关于默认值(很多资料写错的地方)

  • EPoseSearchMode PoseSearchMode = EPoseSearchMode::PCAKDTree——UE 5.8 默认 PCAKDTree
  • int32 NumberOfPrincipalComponents = 4——默认只有 4 个主成分,范围 [1, 64]
  • int32 KDTreeMaxLeafSize = 16
  • int32 KDTreeQueryNumNeighbors = 200——默认取 200 个近邻,不是网上流传的 20–40

看到 NumberOfPrincipalComponents = 4 很多人第一反应是"太少了吧?",但结合上面提到默认 Schema Cardinality 常在 30–80,4 个主成分保留 90%+ 方差在 locomotion 类数据上完全可能——Locomotion 帧的方差本来就集中在少数几个方向(前后速度、左右速度、朝向)。要用 PCAExplainedVarianceEditorOnly 实测再决定是不是要加。

另外一个隐藏彩蛋:如果 KDTreeQueryNumNeighbors == 1,注释里说 SearchIndex 会把原始 Values 完全 strip 掉只保留 PCAValues——这是极致内存优化模式,代价是只能得到 PCA 空间的近似最优,永远不做原始空间的精确成本回评。

五、搜索模式:四种策略的分工

UPoseSearchDatabase 支持四种搜索模式,EPoseSearchModePoseSearchDatabase.h:30):

enum class EPoseSearchMode : int32
{
    // Database searches will be evaluated extensively.
    BruteForce,

    // Optimized: PCA space kdtree
    PCAKDTree,

    // Experimental: vantage point tree
    VPTree UMETA(DisplayName = "VPTree (Experimental)"),

    // Experimental: search will only be performed on events.
    EventOnly UMETA(DisplayName = "EventOnly (Experimental)")
}
;

注意VPTreeEventOnly 都是 Experimental;EventOnly 意味着"这个 Database 只服务事件驱动搜索,不做特征向量搜索"。

模式 1:BruteForce

遍历所有帧,计算完整的特征向量距离。精度最高但最慢。在编辑器里作为 Ground Truth 用于验证其他模式的结果——源码里有一个 a.MotionMatch.Test.KDTreeConstruct 之类的开关,开启后会跑一遍 BruteForce 做对比验证。

模式 2:PCAKDTree(默认)

先 PCA 降维 → KDTree 找 K 近邻 → 对候选集做精确成本计算。核心逻辑在 SearchPCAKDTree()PoseSearchDatabase.cpp):

  1. 用 PCA 投影查询向量
  2. KDTree.FindNeighbors 找 K 个最近邻(默认 K=200)
  3. 通过 PCAValuesVectorToPoseIndexes 把去重后的 PCA 向量展开到原始帧
  4. 对每个候选,用 CompareFeatureVectors 在原始空间计算精确成本

模式 3:VPTree(实验性)

Vantage-Point Tree 在高维空间的性能理论上优于 KDTree(KDTree 维度超过 20 后剪枝效率崩溃)。Epic 在注释里明确写 "not for production use",说明还在调优。

VPTree 的实现在 VPTree.h/cpp 中,基于经典 VP Tree 算法。

模式 4:EventOnly(实验性)

当 Database 只服务事件命中的动画(比如"开门"、"起跳"这种特化 Database),可以设为 EventOnly——彻底跳过 KDTree/BruteForce 分支,只走 EventData 命中。

六、成本函数:怎么定义"最像"

所有搜索模式最终都调用同一个成本计算核心——CompareFeatureVectorsPoseSearchIndex.cpp:8):

void CompareFeatureVectors(TConstArrayView<float> A,           // 查询向量
                           TConstArrayView<float> B,           // 候选向量
                           TConstArrayView<float> WeightsSqrt, // 权重平方根
                           TArrayView<float> Result)
           // 逐维度的成本
{
    check(A.Num() == B.Num() && A.Num() == WeightsSqrt.Num() && A.Num() == Result.Num());

    Eigen::Map<const Eigen::ArrayXf> VA(A.GetData(), A.Num());
    Eigen::Map<const Eigen::ArrayXf> VB(B.GetData(), B.Num());
    Eigen::Map<const Eigen::ArrayXf> VW(WeightsSqrt.GetData(), WeightsSqrt.Num());
    Eigen::Map<Eigen::ArrayXf>       VR(Result.GetData(), Result.Num());

    VR = ((VA - VB) * VW).square();
}

成本 = Σ( (特征差 × sqrt权重)² )。Epic 用 Eigen 而没有手写 SIMD——Eigen 根据编译目标自动选 SSE2 / SSE4 / AVX / AVX2 / NEON,比手写 intrinsics 更好维护。这也解释了为什么 ValuesTAlignedArray:SIMD 加载需要对齐。

PoseSearchCost.h:20FPoseSearchCost 的完整构造函数是:

FPoseSearchCost(float DissimilarityCost,
                float InNotifyCostAddend = 0.f,
                float InContinuingPoseCostAddend = 0.f,
                float InContinuingInteractionCostAddend = 0.f,
                float InContinuingContextInteractionCostAddend = 0.f)
    : TotalCost(DissimilarityCost + InNotifyCostAddend
              + InContinuingPoseCostAddend
              + InContinuingInteractionCostAddend
              + InContinuingContextInteractionCostAddend)
#if WITH_EDITORONLY_DATA
    , NotifyCostAddend(InNotifyCostAddend)
    , ContinuingPoseCostAddend(InContinuingPoseCostAddend)
    , ContinuingInteractionCostAddend(InContinuingInteractionCostAddend)
    , ContinuingContextInteractionCostAddend(InContinuingContextInteractionCostAddend)
#endif
{ }

关键实现细节:四个 CostAddend 分解字段只有编辑器构建里存,runtime 打包只保留 TotalCost 一个 float。所以 shipping 版本没法在游戏内读到"这个匹配是被哪个 CostAddend 加分了"——想调试要走 PIE / Editor / Rewind Debugger。

DynamicWeightsSqrt 是运行时可变的——UPoseSearchDatabase::CalculateDynamicWeightsSqrt()SearchIndex.WeightsSqrt 初始化,但 Schema 的 Channel 可以通过 IPoseSearchFilter 接口动态修改权重。同一个数据库可以在不同 Gameplay 上下文下产生不同的搜索结果——战斗时更看重脚步,潜行时更看重身体朝向。

七、Continuing Pose:动画连续性的秘密

Motion Matching 最容易被忽视但最关键的设计是 Continuing Pose——当前正在播放的动画帧。

为什么需要它?假设你搜到了"跑动第 37 帧"作为最佳匹配,下一帧你又搜到了"跑动第 82 帧"。如果直接跳过去,动画会"瞬移"。Continuing Pose 的作用是:给当前正在播放的帧一个独立参赛资格,让它以"完整成本 + 一个额外的鼓励项"参与最终排序——鼓励继续沿当前动画播下去,除非有更好的候选把它比下去。

// PoseSearchDatabase.h:691 — 注意方法本身被标为 Experimental
UE_API void SearchContinuingPose(FSearchContext&, FSearchResults&) const;

大致逻辑(PoseSearchDatabase.cpp 中的实现):

  1. FSearchContext::GetContinuingPoseSearchResult() 拿到"上一次搜索选中的 PoseIdx"
  2. SearchIndex.GetPoseValues(ContinuingPoseIdx) 取该帧特征向量
  3. 与当前查询向量做 CompareFeatureVectors
  4. 加上 ContinuingPoseCostAddend(通常是负值,等于鼓励)
  5. 结果参与 SearchResults 排序,标 bIsContinuingPoseSearch = true

Block Transition 帧被排除在 KDTree 之外正是配合这个机制——它们只能被 Continuing Pose 搜索选中,避免搜到段落边界导致衔接断裂。

Continuing Pose 管"选谁的连续性"。搜索频率本身有一个独立参数控制:FAnimNode_MotionMatching::SearchThrottleTimeAnimNode_MotionMatching.h:135):

// Minimum amount of time to wait between searching for a new pose segment.
// default for locomotion is searching every update, but you may only want
// to search once for other situations, like jump.
UPROPERTY(EditAnywhere, Category = Settings, meta = (PinHiddenByDefault, ClampMin="0"))
float SearchThrottleTime = 0.f;

默认是 0——每帧都搜。这和"Motion Matching 靠节流控制性能"的普遍印象不符:Epic 的默认策略是每帧都搜,靠 Continuing Pose 的成本鼓励保证不乱跳;只有 jump 之类特殊 case 才把 Throttle 调大到 0.2s 左右锁定一段动画不允许中途换。

八、性能管线全景

把上面的所有组件串起来,一次完整的 PoseSearch 查询流程是:

  1. 构建查询向量FSearchContext::GetOrBuildQuery(Schema) 从当前角色的运动状态构建特征向量
  2. 事件搜索前置:如果 EventData 有效,先尝试事件命中
  3. Continuing Pose 评估:单独计算当前播放帧的成本,加入结果集
  4. PCA 投影(查询向量 - Mean) * PCAProjectionMatrix → 低维向量
  5. KDTree 搜索:在 PCA 空间找 KDTreeQueryNumNeighbors(默认 200)个最近邻
  6. 去重展开:通过 PCAValuesVectorToPoseIndexes 把去重后的向量映射回原始帧索引
  7. 精确成本计算:对每个候选帧,在原始空间跑 CompareFeatureVectors
  8. 结果排序:按 TotalCost 排序,选出最佳匹配

性能参考(默认参数、PCAKDTree 模式、Lyra 数据规模):

  • 数据库规模:100–300 段动画,5–20 万帧
  • Schema Cardinality:30–80 维
  • PCA 主成分数(NumberOfPrincipalComponents):默认 4,可调
  • KNN 数(KDTreeQueryNumNeighbors):默认 200
  • 单次搜索耗时(桌面 i7/Ryzen):0.1–0.5ms

九、三个你可能没注意到的设计细节

1. 去重是两层的

PoseSearch 有两套独立的去重机制:

  • PruneDuplicateValues → 在原始特征空间去重,映射存 FSearchIndexBase::ValuesVectorToPoseIndexes
  • PruneDuplicatePCAValues → 在 PCA 空间独立去重,映射存 FSearchIndex::PCAValuesVectorToPoseIndexes

同一段"跑步循环"里第 12 帧和第 13 帧在原始空间不同(骨骼相位差一点),但 PCA 投影到 4 维后可能被压成同一个点。所以 PCA 空间的去重集合往往比原始空间更大,两套映射不能合并共用。

2. Block Transition 帧不参与空间搜索

动画段落边界的帧被标记为 bBlockTransition = true。这些帧的 PCA 值会被 PrunePCAValuesFromBlockTransitionPoses 移除——它们只参与 Continuing Pose 搜索。这避免了搜索跳到段落边界导致动画衔接断裂。FSearchIndexBase::bAnyBlockTransition 是快速判断标志。

3. 权重是动态的

DynamicWeightsSqrt 不是编译时固定的。UPoseSearchDatabase::CalculateDynamicWeightsSqrt()SearchIndex.WeightsSqrt 初始化,但 Schema 的 Channel 可以在运行时通过 IPoseSearchFilter 接口动态修改权重。这意味着同一个数据库可以在不同 Gameplay 上下文中产生不同的搜索结果——战斗时更看重脚步,潜行时更看重身体朝向。

十、怎么在自己的项目里调优 PoseSearch

1. 认清默认值

新建 Database 的默认参数在 UE 5.8 是:

PoseSearchMode              = PCAKDTree
NumberOfPrincipalComponents = 4           // 别下意识调到 20+
KDTreeMaxLeafSize           = 16
KDTreeQueryNumNeighbors     = 200         // 默认已经很大

先跑默认,看 Cost Breakdown 有没有问题,再调。

2. 调 Trajectory Sample 的 Offset 和 Weight

默认配置全是 XY 变体。如果你的角色需要匹配跳跃、下蹲、Z 轴速度,必须手动把 XY flags 改成 3D 版本(去掉 XY 后缀)。这是从默认 Locomotion 走向"完整 3D matching"的最重要一步。

根据角色移动特性调 Weight:

  • 快速角色(赛车/飞行):增大未来 Offset(0.5–1.0s),减小过去 Offset
  • 慢速角色(潜行):减小所有 Offset,增大当前位置 Weight
  • 频繁转向:增大 FacingDirection 通道的 Weight

3. 用 Console Variable 调试

在 UE 5.8 里实测存在的 CVar(可以在 PoseSearchLibrary.cpp:37AnimNode_MotionMatching.cpp:40 附近搜到 FAutoConsoleVariableRef 声明):

a.MotionMatch.DrawQuery.Enable 1              # 可视化查询向量
a.MotionMatch.DrawMatch.Enable 1              # 可视化匹配结果
a.AnimNode.MotionMatching.DebugDrawQuery 1    # AnimNode 层的查询绘制
a.AnimNode.MotionMatching.DebugDrawCurResult 1
a.MotionMatch.Test.ForceIndexing 1            # 强制重建 SearchIndex,验证 DDC
a.MotionMatch.Test.KDTreeConstruct 1          # 验证 KDTree 构建

4. 控制数据库规模

不要无脑把所有动画都扔进一个 Database。按运动类型分库(Locomotion / Combat / Interaction),每个库 50–200 段动画。多库搜索时,PoseSearch 会对每个库独立搜索然后合并结果。

十一、PoseSearch 的边界

PoseSearch 不是银弹。它有几个明确的局限:

  1. 不处理动画混合——那是 AnimBlueprint 和 Blend Stack 的事
  2. 不生成新动画——它只从已有数据库中选,不会像 Motion Matching 的学术论文那样做 Motion Synthesis
  3. 数据库质量决定一切——垃圾进垃圾出。动画采样密度不够、Trajectory 配置不合理,搜出来的结果就是"抽搐"
  4. 对动画数据格式敏感——镜像动画(Mirrored)、Blend Space 采样、多资产数据库的配置复杂度随规模指数增长

理解这些边界,比理解它能做什么更重要。


以上是原本的核心内容。接下来这几节把水面下的东西挖出来——PCA 到底怎么训练的、Cost 分解怎么读、Interaction Asset 是什么、决策边界在哪。

十二、PCA 是怎么训练出来的:SearchIndex 的"编译期"

前面章节把 PCAProjectionMatrixMean 当成"给定的数据"用了。但这两个东西不是天上掉下来的——它们是在数据库 Build 阶段计算出来并序列化到 uasset / DDC 里的。

数学骨架

PCA 的标准流程一句话:给一个 N×D 的样本矩阵 X(N 个样本、D 个特征),求协方差矩阵 C = (X-μ)ᵀ(X-μ) / N 的前 k 大特征值对应的特征向量,把它们按列拼起来,就是 D×k 的投影矩阵 P。任意样本 x 的低维编码就是 Pᵀ(x - μ)。

放到 PoseSearch 里,N 是有效帧数(去掉 Block Transition),D 是 SchemaCardinality(例如 40),k 是 NumberOfPrincipalComponents(默认 4)。

源码怎么做

Epic 用 Eigen 的 SelfAdjointEigenSolver 对协方差矩阵做特征分解(PoseSearchDerivedData.cpp:781):

const Eigen::SelfAdjointEigenSolver<ColMajorMatrix> EigenSolver(CovariantMatrix);
ComputationInfo = EigenSolver.info();

if (ComputationInfo == Eigen::Success)
{
    // 特征值升序返回,特征向量按列
    const ColMajorMatrix EigenVectors = EigenSolver.eigenvectors().real();
    // ... 取最大的 k 个
    const RowMajorVector EigenValues = EigenSolver.eigenvalues().real();
    // ...
    const float TotalVariance = EigenValues.sum();
    // 保留率 = 累计方差 / 总方差
    SearchIndex.PCAExplainedVarianceEditorOnly =
        TotalVariance > UE_KINDA_SMALL_NUMBER
            ? AccumulatedVariance / TotalVariance
            : 0.f;
}

三个实现细节:

  1. 对称矩阵专用求解器SelfAdjointEigenSolver 而不是通用 EigenSolver。协方差矩阵天然对称正半定,对称专用求解器数值更稳、更快。
  2. 保留率是可诊断的PCAExplainedVarianceEditorOnly 字段直接告诉你"当前 k 保留了多少方差"。Editor 里的 Database Preview 会显示这个数。默认 k=4 在很多 Locomotion 数据上能达到 90%+,这就是 4 这个看似不合理的默认值敢用的底气。
  3. 信息不够就得加 k:如果保留率 < 90%,要么增加 NumberOfPrincipalComponents,要么重新审视 Schema——是不是有大量高方差 channel 在贡献噪声而不是信号。

什么时候会失效

PCA 假设"重要的方向是方差大的方向"。当你的数据库里 95% 的帧都是"匀速直线跑",而你真正想区分的"急停"、"急转"只占 5% 时,PCA 会把主成分对齐到直线跑的方向,急停和急转就会被压缩到低方差主成分里丢失。解决方案要么给少数派样本手动加权,要么按运动类型分库让每个库的方差分布更均匀。

十三、Cost 分解:Editor Only 的调试通道

PoseSearchCost 的四个 addend 分解字段(NotifyCostAddend / ContinuingPoseCostAddend / ContinuingInteractionCostAddend / ContinuingContextInteractionCostAddend)都在 #if WITH_EDITORONLY_DATA 保护内。这有一个直接后果:Shipping 打包里没有 Cost 分解信息,你在 game 端读 FPoseSearchCost 只有 TotalCost 一个 float。

要读 Cost 分解只有两条路:

  1. PIE / Editor:Database Editor 有 Preview + Cost Breakdown 面板,实时显示每个 channel 的贡献
  2. Rewind Debugger:PoseSearch 在 PoseSearchCost.h:45 定义了 operator<< 序列化,用于 Rewind Debugger 抓取回放数据。回放里能看到分解——但源码注释里 Epic 自己也承认这块设计有点尴尬("when captures come from the game the cost addend breakdowns will be invalid")。

这也说明一个问题:如果你做的是 online 项目、需要跨机器复盘 Motion Matching 决策,PoseSearch 自身不提供这个能力,你得自己在 game 端记录额外元数据。别在生产代码里假设能读到 CostAddend 分解。

读 Cost 分解怎么调 Schema

  • 如果 Trajectory Position 占总成本 60%+:位置权重太高,会追未来预测过头,脚步匹配质量降。降 Position 采样的 Weight,或者拆一些采样出来
  • 如果所有 Bone Position 加起来占 50%+:脚步/骨骼权重过高,角色会"拧着走"(脚步好但轨迹跟不上)
  • 如果 CostAddend 占大头:说明帧固定成本设得太狠了,等于人为屏蔽了某些帧

十四、Interaction Asset:多角色动作的独立通路

UE 5.4/5.5 引入、5.8 完善的 Interaction 系统——很多资料错误地说它是"新增的 Feature Channel",实际结构不是这样。

组织形式

PoseSearchInteractionAsset.h:49

class UPoseSearchInteractionAsset : public UMultiAnimAsset

它是一个 UMultiAnimAsset(多动画资产),不是 UPoseSearchFeatureChannel_*。这个区别很重要:Interaction 不是塞进单角色特征向量的一个 channel,而是一个可以同时驱动多个 Actor 的复合动画资产——两个人抱在一起打招呼,一个 asset 里就有 Actor A 和 Actor B 的两条动画数据。

配套的 Runtime 支撑:

  • PoseSearchInteractionSubsystem.h — WorldSubsystem,全局跟踪当前场景里哪些角色正在参与 interaction
  • PoseSearchInteractionIsland.h — 一组正在协作的角色构成 Island
  • PoseSearchInteractionAvailability.h — 描述"这个角色现在能参加哪些 interaction"
  • PoseSearchInteractionValidator.h — 验证 interaction 组合是否合法

为什么这么设计

传统 Motion Matching 只匹配角色自身状态。但两人交互(拥抱、格挡、抓取)的关键不是"两个角色分别的姿态",而是"两个角色相对位姿是否匹配"。放到独立的 Multi-Anim Asset 里,PoseSearch 可以对整个组合做匹配——搜索出来的结果是"Actor A 该播哪个帧、Actor B 同时该播哪个帧",两条动画在时空上被强绑定。

与 FPoseSearchCost 的关联

第六节 FPoseSearchCost 里那两个"看起来重复"的字段:

ContinuingInteractionCostAddend
ContinuingContextInteractionCostAddend

现在能看懂了:一旦角色进入某个 interaction,就应该倾向于继续与同一个目标交互(ContinuingInteractionCostAddend),也应该倾向于保持当前上下文ContinuingContextInteractionCostAddend)——比如两个人抱在一起,不能因为 KDTree 找到了一个"更像单人跑步的"帧就把互动打断。

SearchContinuingPose 处理单角色帧连续性,Continuing Interaction 处理多角色 Island 的连续性。两层机制,一个针对帧、一个针对组合。

十五、Chooser 集成与 DDC 缓存

到这里为止都假设"有一个数据库"。实际项目里往往有几十个数据库:徒手 locomotion、持刀 locomotion、受伤 locomotion、隐身 locomotion……

Chooser Table

PoseSearch Runtime 里有独立的 Source/Runtime/Private/Chooser/ 子目录,专门做数据库选择的 Chooser 集成。运行时给一组输入(GameplayTag、bool、数值),Chooser 从上到下匹配第一条命中的行,返回 UPoseSearchDatabase 引用给 AnimNode。

不写 Chooser、直接在 AnimBlueprint 里手动 switch database,是很多团队第一次上 Motion Matching 后放弃的常见原因——上下文条件排列组合很快就会爆。

DDC 与增量构建

PoseSearchDerivedData.cpp 里 PoseSearch 的 SearchIndex 构建走的是标准 UE DDC(Derived Data Cache):

  • 数据库配置发生变化 → 计算新的 DDC Key
  • DDC 命中 → 从缓存拉回序列化的 SearchIndex(PCA 矩阵、KDTree、Values 全在里面)
  • 未命中 → 走 FPoseSearchDatabaseAsyncBuildTask,包括采样动画、特征提取、PCA 训练、KDTree/VPTree 构建

Editor 里保存/编辑 Database 时的漫长等待就是在跑这套。源码里的 a.MotionMatch.Test.Determinism.* 一大堆 CVar 都是保证构建流程跨机器、跨执行确定性的测试开关——你在编辑器里改了权重、alt-tab 一下再切回来,Cook 出来的 SearchIndex 必须字节一致,否则会污染团队共享 DDC。

十六、什么时候不该用 Motion Matching

Motion Matching 是好东西,但不适合所有项目。写在最后作为决策清单。

该用

  • 3A 品质角色(主角、精英 NPC),需要平滑的方向切换、精确的脚步匹配、可对接 Interaction 系统
  • 有专门动画师能产出高密度、覆盖各种运动状态的素材(100+ 段起步)
  • CPU 有预算容纳 0.1–0.5ms/帧的搜索开销
  • 交付平台是 PC / 主机(PS5、Xbox Series)

不该用(或该谨慎用)

  • 移动端项目——即使 PCA + KDTree 加速,PoseSearch 的内存开销(几十 MB SearchIndex + PCA Values)在中低端安卓上是硬伤
  • 数据库规模上不去(< 50 段动画)——PCA 起不到降维意义,KDTree 优势有限,不如 Blend Space + State Machine
  • 大量小怪 / 群体角色 —— 每个 pawn 都在搜索,成本相加会爆
  • 项目美术资源以少量高质量循环动画为主 —— 传统方案更简单
  • 需要严格的确定性(回放系统、rollback 网络同步)—— PoseSearch 结果依赖浮点计算和多个 CVar,跨机器一致性需要额外工程;何况 CostAddend 分解只在 Editor 里能读

Motion Matching 是把复杂度从"动画蓝图状态机"迁到"数据库设计 + Schema 调参 + Chooser 组织"。这不是"变简单了",是"复杂度换了个地方"。你的团队真正擅长哪一头,决定你该选哪一头。


PoseSearch 是 UE5 动画系统里最"计算机科学"的模块——特征工程(Schema + Channel)、降维(PCA)、空间索引(KDTree/VPTree)、成本函数(CompareFeatureVectors + FPoseSearchCost)、连续性约束(Continuing Pose + Continuing Interaction)、上下文过滤(Chooser + IPoseSearchFilter)、DDC 缓存。每一步都是经典的搜索与推荐系统问题,只是应用场景变成了游戏角色的每一帧运动。

理解它的关键不是记住 API,而是把它放回搜索问题的语境:查询、索引、加速结构、连续性、上下文、缓存。去掉任何一层系统立刻退化——BruteForce 慢、无 PCA 内存爆、无 Continuing Pose 抽搐、无 Chooser 状态机爆炸、无 Interaction 多角色对不齐、无 DDC 编辑器卡死。

我每周拆一个 UE5 底层系统,从源码级讲到可运行的配置。下一篇我们来拆 PoseSearch 的 Interaction Subsystem 内部——UPoseSearchInteractionSubsystem 是怎么把多个角色组织成 Island 并驱动一次匹配的,为什么 UE 5.4 之后把这块提出来做成一等公民。

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-07-15 16:16:01 HTTP/1.1 GET : https://www.yeyulingfeng.com/a/861211.html
  2. 运行时间 : 0.309312s [ 吞吐率:3.23req/s ] 内存消耗:4,824.33kb 文件加载:145
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=5cd528d701704dadfbf579d2904b0fe3
  1. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_static.php ( 6.05 KB )
  7. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/ralouphie/getallheaders/src/getallheaders.php ( 1.60 KB )
  10. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  11. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  12. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  13. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  14. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  15. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  16. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  17. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  18. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  19. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions_include.php ( 0.16 KB )
  21. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions.php ( 5.54 KB )
  22. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  23. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  24. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  25. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/provider.php ( 0.19 KB )
  26. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  27. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  28. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  29. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/common.php ( 0.03 KB )
  30. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  32. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/alipay.php ( 3.59 KB )
  33. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  34. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/app.php ( 0.95 KB )
  35. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cache.php ( 0.78 KB )
  36. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/console.php ( 0.23 KB )
  37. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cookie.php ( 0.56 KB )
  38. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/database.php ( 2.48 KB )
  39. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/filesystem.php ( 0.61 KB )
  40. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/lang.php ( 0.91 KB )
  41. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/log.php ( 1.35 KB )
  42. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/middleware.php ( 0.19 KB )
  43. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/route.php ( 1.89 KB )
  44. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/session.php ( 0.57 KB )
  45. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/trace.php ( 0.34 KB )
  46. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/view.php ( 0.82 KB )
  47. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/event.php ( 0.25 KB )
  48. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  49. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/service.php ( 0.13 KB )
  50. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/AppService.php ( 0.26 KB )
  51. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  52. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  53. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  54. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  55. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  56. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/services.php ( 0.14 KB )
  57. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  58. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  59. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  60. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  61. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  62. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  63. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  64. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  65. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  66. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  67. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  68. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  69. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  70. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  71. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  72. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  73. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  74. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  75. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  76. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  77. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  78. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  79. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  80. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  81. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  82. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  83. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  84. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  85. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  86. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  87. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/Request.php ( 0.09 KB )
  88. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  89. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/middleware.php ( 0.25 KB )
  90. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  91. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  92. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  93. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  94. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  95. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  96. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  97. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  98. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  99. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  100. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  101. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  102. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  103. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/route/app.php ( 3.94 KB )
  104. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  105. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  106. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Index.php ( 9.87 KB )
  108. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/BaseController.php ( 2.05 KB )
  109. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  110. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  111. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  112. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  113. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  114. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  115. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  116. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  117. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  118. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  119. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  120. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  121. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  122. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  123. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  124. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  125. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  126. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  127. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  128. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  129. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  130. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  131. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  132. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  133. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  134. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  135. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Es.php ( 3.30 KB )
  136. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  137. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  138. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  139. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  140. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  141. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  142. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  143. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  144. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/runtime/temp/c935550e3e8a3a4c27dd94e439343fdf.php ( 31.50 KB )
  145. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.001225s ] mysql:host=127.0.0.1;port=3306;dbname=wenku;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001850s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000824s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000707s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001343s ]
  6. SELECT * FROM `set` [ RunTime:0.000607s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001448s ]
  8. SELECT * FROM `article` WHERE `id` = 861211 LIMIT 1 [ RunTime:0.001441s ]
  9. UPDATE `article` SET `lasttime` = 1784103361 WHERE `id` = 861211 [ RunTime:0.001500s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.010658s ]
  11. SELECT * FROM `article` WHERE `id` < 861211 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.006124s ]
  12. SELECT * FROM `article` WHERE `id` > 861211 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.001249s ]
  13. SELECT * FROM `article` WHERE `id` < 861211 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.022347s ]
  14. SELECT * FROM `article` WHERE `id` < 861211 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.013275s ]
  15. SELECT * FROM `article` WHERE `id` < 861211 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.059362s ]
0.310994s