AirSim 运行时纹理切换Runtime Texture Swapping完整实战指南【免费下载链接】AirSimOpen source simulator for autonomous vehicles built on Unreal Engine / Unity, from Microsoft AI Research项目地址: https://gitcode.com/gh_mirrors/ai/AirSimAirSim 提供了运行时纹理切换能力让开发者在仿真运行过程中通过 API 动态更换场景中任意 Actor 的贴图从而支撑领域随机化Domain Randomization、训练数据多样性增强等视觉任务。本文以官方文档 docs/retexturing.md 为核心骨架结合 Unreal 插件源码TextureShuffleActor、WorldSimApi与 Python/C 客户端实现完整讲解如何让 Actor 可换肤、如何为批量 Actor 配置纹理候选集、如何通过simSwapTextures系列 API 在运行时切换纹理并深入解析其底层实现原理。读完本文你将能独立在自定义 Unreal 环境中搭建一套可编程控制的纹理切换系统并理解其与simSetObjectMaterial系列 API 的差异与适用场景。一、什么是运行时纹理切换运行时纹理切换Runtime Texture Swapping是 AirSim 面向视觉类研究如目标检测、领域随机化、自动驾驶感知模型训练提供的一项能力在仿真运行期间不重启场景、不重新烹饪资源即可通过客户端 API 将场景中一批静态网格体 Actor 的贴图替换为另一张纹理。其核心机制是为 Actor 挂载一组候选纹理Texture Set再通过标签Tag批量寻址 Actor最后以纹理索引tex_id触发切换。整个过程基于 Unreal Engine 的动态材质实例UMaterialInstanceDynamic实现纹理切换在游戏线程上执行对客户端透明。从版本演进看simSwapTexturesAPI 是随 AirSim 新增进功能清单的见 docs/CHANGELOG.md 中 AddsimSwapTexturesAPI 条目属于官方持续维护的能力。二、如何让一个 Actor 支持纹理切换要让场景中的某个 Actor 变得可换肤Retexturable必须让其继承自父类TextureShuffleActor。在 Unreal Editor 中操作步骤如下选中目标 Actor打开其蓝图。在蓝图的Class Settings类设置选项卡中将Parent Class父类设置为TextureShuffleActor。从插件源码可以确认该父类的成员结构。在 Unreal/Plugins/AirSim/Source/TextureShuffleActor.h 中ATextureShuffleActor派生自AStaticMeshActor核心成员包括UCLASS() class AIRSIM_API ATextureShuffleActor : public AStaticMeshActor { GENERATED_BODY() protected: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category TextureShuffle) UMaterialInterface* DynamicMaterial nullptr; // 用于生成动态材质实例的母材质 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category TextureShuffle) TArrayUTexture2D* SwappableTextures; // 该 Actor 可切换的候选纹理数组 public: UFUNCTION(BlueprintNativeEvent) void SwapTexture(int tex_id 0, int component_id 0, int material_id 0); private: bool MaterialCacheInitialized false; int NumComponents -1; UPROPERTY() TArrayUMaterialInstanceDynamic* DynamicMaterialInstances; // 动态材质实例缓存 };也就是说设置父类后Actor 会获得两个关键成员DynamicMaterial动态材质换肤时用于实例化的基础材质。官方文档明确要求场景中的所有 Actor 实例都必须把DynamicMaterial设置为TextureSwappableMaterialAirSim 提供的可换肤材质资源推荐在Details细节面板中对每个 Actor 实例逐一设置。SwappableTextures候选纹理数组该 Actor 可被切换到的纹理集合API 调用中的tex_id就是对这个数组的索引。⚠️ 官方文档特别警告在蓝图类上静态设置DynamicMaterial可能导致渲染错误rendering errors。实践经验表明在场景中的 Actor 实例上、通过 Details 面板设置效果更稳定。建议严格遵循此做法避免踩坑。底层换肤原理纹理切换的真正实现在 Unreal/Plugins/AirSim/Source/TextureShuffleActor.cpp 的SwapTexture_Implementation中void ATextureShuffleActor::SwapTexture_Implementation(int tex_id, int component_id, int material_id) { if (SwappableTextures.Num() 1) return; if (!MaterialCacheInitialized) { TArrayUStaticMeshComponent* components; GetComponentsUStaticMeshComponent(components); NumComponents components.Num(); DynamicMaterialInstances.Init(nullptr, components[component_id]-GetNumMaterials()); MaterialCacheInitialized true; } if (NumComponents 0 || DynamicMaterialInstances.Num() 0) return; tex_id % SwappableTextures.Num(); // 纹理索引越界时取模 component_id % NumComponents; // 组件索引越界时取模 material_id % DynamicMaterialInstances.Num(); // 材质槽索引越界时取模 if (DynamicMaterialInstances[material_id] nullptr) { DynamicMaterialInstances[material_id] UMaterialInstanceDynamic::Create(DynamicMaterial, this); TArrayUStaticMeshComponent* components; GetComponentsUStaticMeshComponent(components); components[component_id]-SetMaterial(material_id, DynamicMaterialInstances[material_id]); } DynamicMaterialInstances[material_id]-SetTextureParameterValue(TextureParameter, SwappableTextures[tex_id]); }这里有几个值得注意的实现事实越界取模tex_id、component_id、material_id三个索引在进入实际换肤前都会对各自的合法范围取模这就是文档中若tex_id超出某对象纹理集范围将取模回绕的源码依据。惰性初始化与缓存首次换肤时才创建UMaterialInstanceDynamic并写入网格组件的材质槽之后复用同一实例避免反复创建带来的开销。纹理参数名约定动态材质实例通过SetTextureParameterValue(TextureParameter, ...)设置纹理因此TextureSwappableMaterial母材质中必须暴露名为TextureParameter的纹理参数Texture Parameter这也是该方案能生效的前提。若某个 Actor 的SwappableTextures为空数量小于 1换肤会被直接忽略。三、如何定义可选择的纹理集合Set实际场景中往往是某一批 Actor 子集共享同一组纹理候选例如同一栋楼的墙壁、同一套家具。此时可以借助 Unreal Engine 的Group Editing组编辑功能来批量配置在场景中选中所有需要共享同一套纹理选择的 Actor 实例。在 Details 面板中同时为它们添加候选纹理即填充SwappableTextures数组。用同样的批量技巧为这组 Actor 添加描述性标签Tags——这些标签将用于在 API 中寻址这些 Actor。官方文档给出的最佳操作顺序是从大分组做到小分组——先选中一大组 Actor 批量设置公共属性然后不断**取消选中deselect**来缩小分组范围逐步收窄到更小的子集最后再单独应用个别 Actor 的独有属性。这样既能保证共享属性的一致性又能精确控制每个子集的差异。从底层寻址逻辑看见下文WorldSimApi::swapTextures实现标签匹配遵循的是AND交集语义一个 Actor 必须同时拥有你传入的所有标签才满足匹配条件。因此可以通过组合标签实现非常精确的选择例如标签chair, right只会命中既带chair又带right标签的 Actor。四、通过 API 在运行时切换纹理AirSim 在 C 与 Python 客户端中均暴露了simSwapTexturesAPI。C 声明位于 AirLib/include/api/RpcLibClientBase.hppstd::vectorstd::string simSwapTextures(const std::string tags, int tex_id 0, int component_id 0, int material_id 0);Python 版本位于 PythonClient/airsim/client.py签名完全对应def simSwapTextures(self, tags, tex_id 0, component_id 0, material_id 0): ... return self.client.call(simSwapTextures, tags, tex_id, component_id, material_id)参数说明参数类型默认值含义tagsstring必填以,或,分隔的标签字符串用于确定在哪些 Actor 上执行换肤tex_idint0索引每个参与换肤 Actor 的候选纹理数组若对某对象的纹理集越界将取该对象纹理数量的模component_idint0静态网格组件的索引多组件 Actor 时使用越界时同样取模material_idint0材质槽Material Slot的索引越界时取模返回值list[str]Python/std::vectorstd::stringC为成功匹配标签并完成换肤的对象名称列表。官方演示Pythonimport airsim import time c airsim.client.MultirotorClient() print(c.simSwapTextures(furniture, 0)) time.sleep(2) print(c.simSwapTextures(chair, 1)) time.sleep(2) print(c.simSwapTextures(table, 1)) time.sleep(2) print(c.simSwapTextures(chair, right, 0))运行结果[RetexturableChair, RetexturableChair2, RetexturableTable] [RetexturableChair, RetexturableChair2] [RetexturableTable] [RetexturableChair2]这个演示揭示了两个关键行为标签寻址粒度furniture命中了三件家具chair只命中两把椅子table只命中桌子而chair, right这种复合标签只命中了同时满足两个标签的RetexturableChair2——印证了前面提到的 AND 匹配语义。纹理索引是各自索引文档特别指出在这个例子中同一索引值1在两把椅子上对应的是不同的纹理。也就是说tex_id是对每个 Actor 各自SwappableTextures数组的索引而不是全局纹理池的编号。不同 Actor 可以按自己的候选集排列决定索引 1 显示哪张纹理。客户端调用链路从源码调用链看一次simSwapTextures的完整路径是Python 客户端client.call(simSwapTextures, tags, tex_id, component_id, material_id)PythonClient/airsim/client.py经 RPC 发送到仿真端。RPC 服务端在 AirLib/src/api/RpcLibServerBase.cpp 中绑定simSwapTextures方法并转发给getWorldSimApi()-swapTextures(...)。WorldSimApi 实现在 Unreal/Plugins/AirSim/Source/WorldSimApi.cpp 的swapTextures中执行标签解析、Actor 查找与换肤最终把命中的对象名列表返回给客户端。服务端标签解析与匹配逻辑WorldSimApi::swapTextures的实现WorldSimApi.cpp展示了服务端完整的处理流程std::unique_ptrstd::vectorstd::string WorldSimApi::swapTextures(const std::string tag, int tex_id, int component_id, int material_id) { auto swappedObjectNames std::make_uniquestd::vectorstd::string(); UAirBlueprintLib::RunCommandOnGameThread([this, tag, tex_id, component_id, material_id, swappedObjectNames]() { // 1. 将标签字符串按逗号拆分为多个独立标签 TArrayFString splitTags; FString notSplit FString(tag.c_str()); FString next ; while (notSplit.Split(,, next, notSplit)) { next.TrimStartInline(); // 去除逗号后可能存在的空格兼容 , 分隔 splitTags.Add(next); } notSplit.TrimStartInline(); splitTags.Add(notSplit); // 2. 找出场景中所有 TextureShuffleActor TArrayAActor* shuffleables; UAirBlueprintLib::FindAllActorATextureShuffleActor(simmode_, shuffleables); for (auto* shuffler : shuffleables) { // 3. 校验 Actor 是否拥有全部标签AND 语义 bool invalidChoice false; for (auto required_tag : splitTags) { invalidChoice | !shuffler-ActorHasTag(FName(*required_tag)); if (invalidChoice) break; } if (invalidChoice) continue; // 4. 执行换肤并记录对象名 dynamic_castATextureShuffleActor*(shuffler)-SwapTexture(tex_id, component_id, material_id); swappedObjectNames-push_back(TCHAR_TO_UTF8(*shuffler-GetName())); } }, true); return swappedObjectNames; }值得注意的实现细节标签拆分兼容两种分隔符通过Split(,, ...)拆分会保留逗号后的空格服务端随即用TrimStartInline()去除前导空格因此 API 文档中,与, 两种分隔写法都能正确解析。游戏线程调度整个查找与换肤过程通过RunCommandOnGameThread投递到 Unreal 游戏线程执行保证渲染资源操作线程安全。匹配计数与返回返回的是实际执行了换肤的 Actor 名称列表客户端可据此确认自己的标签寻址是否符合预期。五、其他纹理相关 APIsimSetObjectMaterial 与 simSetObjectMaterialFromTexture除了基于标签批量换肤的simSwapTexturesAirSim 还提供了面向单个具体对象的材质/纹理设置 API官方文档收录于 docs/apis.md 的 Texture APIs 小节simSetObjectMaterial(object_name, material_name, component_id)将指定对象的材质设置为已有的 Unreal 材质资产。material_name传入材质资产名。simSetObjectMaterialFromTexture(object_name, texture_path, component_id)将指定对象的材质设置为纹理文件路径对应的贴图。texture_path指向磁盘上的纹理文件。两者的 Python 接口同样位于 PythonClient/airsim/client.pydef simSetObjectMaterial(self, object_name, material_name, component_id 0): ... return self.client.call(simSetObjectMaterial, object_name, material_name, component_id) def simSetObjectMaterialFromTexture(self, object_name, texture_path, component_id 0): ... return self.client.call(simSetObjectMaterialFromTexture, object_name, texture_path, component_id)从服务端实现WorldSimApi.cpp可以观察到它们的实现特征simSetObjectMaterialFromTexture通过FImageUtils::ImportFileAsTexture2D在运行时从文件导入纹理再基于 AirSim 的领域随机化母材质DomainRandomizationMaterial路径为Material/AirSim/HUDAssets/DomainRandomizationMaterial.DomainRandomizationMaterial在 SimMode/SimModeBase.cpp 中加载创建动态材质实例并同样设置TextureParameter参数最后应用到对象的全部静态网格组件上。simSetObjectMaterial则通过StaticLoadObject加载指定名称的UMaterial资产直接写入组件的材质槽。两个 API 均接受component_id指定材质槽索引且以布尔值返回是否成功失败时会打印 Cannot find material for domain randomization 之类的日志可据此排查资产加载问题。三者分工总结simSwapTextures适合批量、标签寻址、多候选纹理轮换领域随机化/数据增强simSetObjectMaterial适合把某对象整体换成已有材质资产simSetObjectMaterialFromTexture适合直接喂入外部纹理文件。实际项目中可根据需求粒度组合使用。六、常见问题与最佳实践结合官方文档警告与源码实现整理以下实践要点父类必须继承TextureShuffleActor只有继承该父类的 Actor 才会被swapTextures遍历到服务端用FindAllActorATextureShuffleActor查找普通 Actor 即使打了标签也不会命中。DynamicMaterial尽量在 Actor 实例上设置官方明确提示在蓝图类上静态设置可能引发渲染错误推荐在场景实例的 Details 面板统一设置TextureSwappableMaterial。候选纹理务必配齐SwapTexture在SwappableTextures为空时会直接返回若想让换肤生效每个可换肤实例都要有至少一张候选纹理。标签要兼顾分组与细分利用组编辑从大到小逐层收窄分组保证大集合共享、小子集差异的配置效率标签匹配是 AND 语义可通过组合标签实现精确寻址。索引越界不会报错tex_id、component_id、material_id越界都会被取模回绕这可能让看似错误的索引静默产生可用的结果调试时需留意返回值列表是否符合预期对象集。用返回值校验寻址simSwapTextures返回实际执行换肤的对象名列表是验证标签写法是否正确的最直接手段。母材质需暴露TextureParameter参数动态换肤依赖SetTextureParameterValue(TextureParameter, ...)若自定义母材质没有该参数名换肤不会产生可见效果。七、总结AirSim 的运行时纹理切换是一套配置在 Unreal 编辑器、驱动在 API 层的完整链路场景侧通过TextureShuffleActor父类、DynamicMaterial母材质与SwappableTextures候选数组完成换肤能力的装配运行侧通过simSwapTextures(tags, tex_id, ...)以标签批量寻址、按索引轮换纹理底层则依托UMaterialInstanceDynamic的动态材质机制与游戏线程调度保证渲染正确性。配合simSetObjectMaterial/simSetObjectMaterialFromTexture两个单对象 API足以覆盖从单对象替换到大规模批量领域随机化的绝大多数视觉训练与仿真需求。开发者可直接参照 docs/retexturing.md、TextureShuffleActor.cpp 与 WorldSimApi.cpp 在自己的 Unreal 环境中复现整套流程。【免费下载链接】AirSimOpen source simulator for autonomous vehicles built on Unreal Engine / Unity, from Microsoft AI Research项目地址: https://gitcode.com/gh_mirrors/ai/AirSim创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考