AirSim 运行时纹理切换(Runtime Texture Swapping)完整实战指南
2026/9/21 18:29:19 网站建设 项目流程

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/AirSim

AirSim 提供了运行时纹理切换能力,让开发者在仿真运行过程中通过 API 动态更换场景中任意 Actor 的贴图,从而支撑领域随机化(Domain Randomization)、训练数据多样性增强等视觉任务。本文以官方文档 docs/retexturing.md 为核心骨架,结合 Unreal 插件源码(TextureShuffleActorWorldSimApi)与 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 中,操作步骤如下:

  1. 选中目标 Actor,打开其蓝图。
  2. 在蓝图的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) TArray<UTexture2D*> 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() TArray<UMaterialInstanceDynamic*> DynamicMaterialInstances; // 动态材质实例缓存 };

也就是说,设置父类后,Actor 会获得两个关键成员:

  • DynamicMaterial(动态材质):换肤时用于实例化的基础材质。官方文档明确要求:场景中的所有 Actor 实例都必须把DynamicMaterial设置为TextureSwappableMaterial(AirSim 提供的可换肤材质资源),推荐在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) { TArray<UStaticMeshComponent*> components; GetComponents<UStaticMeshComponent>(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); TArray<UStaticMeshComponent*> components; GetComponents<UStaticMeshComponent>(components); components[component_id]->SetMaterial(material_id, DynamicMaterialInstances[material_id]); } DynamicMaterialInstances[material_id]->SetTextureParameterValue("TextureParameter", SwappableTextures[tex_id]); }

这里有几个值得注意的实现事实:

  • 越界取模tex_idcomponent_idmaterial_id三个索引在进入实际换肤前都会对各自的合法范围取模,这就是文档中"若tex_id超出某对象纹理集范围,将取模回绕"的源码依据。
  • 惰性初始化与缓存:首次换肤时才创建UMaterialInstanceDynamic并写入网格组件的材质槽,之后复用同一实例,避免反复创建带来的开销。
  • 纹理参数名约定:动态材质实例通过SetTextureParameterValue("TextureParameter", ...)设置纹理,因此TextureSwappableMaterial母材质中必须暴露名为TextureParameter的纹理参数(Texture Parameter),这也是该方案能生效的前提。
  • 若某个 Actor 的SwappableTextures为空(数量小于 1),换肤会被直接忽略。

三、如何定义可选择的纹理集合(Set)

实际场景中,往往是某一批 Actor 子集共享同一组纹理候选(例如同一栋楼的墙壁、同一套家具)。此时可以借助 Unreal Engine 的Group Editing(组编辑)功能来批量配置:

  1. 在场景中选中所有需要共享同一套纹理选择的 Actor 实例。
  2. 在 Details 面板中同时为它们添加候选纹理(即填充SwappableTextures数组)。
  3. 用同样的批量技巧为这组 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.hpp:

std::vector<std::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::vector<std::string>(C++),为成功匹配标签并完成换肤的对象名称列表

官方演示(Python)

import 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']

这个演示揭示了两个关键行为:

  1. 标签寻址粒度"furniture"命中了三件家具;"chair"只命中两把椅子;"table"只命中桌子;而"chair, right"这种复合标签只命中了同时满足两个标签的RetexturableChair2——印证了前面提到的 AND 匹配语义。
  2. 纹理索引是"各自索引":文档特别指出,在这个例子中,同一索引值1在两把椅子上对应的是不同的纹理。也就是说,tex_id是对每个 Actor 各自SwappableTextures数组的索引,而不是全局纹理池的编号。不同 Actor 可以按自己的候选集排列决定"索引 1 显示哪张纹理"。

客户端调用链路

从源码调用链看,一次simSwapTextures的完整路径是:

  1. Python 客户端client.call("simSwapTextures", tags, tex_id, component_id, material_id)(PythonClient/airsim/client.py)经 RPC 发送到仿真端。
  2. RPC 服务端:在 AirLib/src/api/RpcLibServerBase.cpp 中绑定"simSwapTextures"方法并转发给getWorldSimApi()->swapTextures(...)
  3. WorldSimApi 实现:在 Unreal/Plugins/AirSim/Source/WorldSimApi.cpp 的swapTextures中执行标签解析、Actor 查找与换肤,最终把命中的对象名列表返回给客户端。

服务端标签解析与匹配逻辑

WorldSimApi::swapTextures的实现(WorldSimApi.cpp)展示了服务端完整的处理流程:

std::unique_ptr<std::vector<std::string>> WorldSimApi::swapTextures(const std::string& tag, int tex_id, int component_id, int material_id) { auto swappedObjectNames = std::make_unique<std::vector<std::string>>(); UAirBlueprintLib::RunCommandOnGameThread([this, &tag, tex_id, component_id, material_id, &swappedObjectNames]() { // 1. 将标签字符串按逗号拆分为多个独立标签 TArray<FString> 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 TArray<AActor*> shuffleables; UAirBlueprintLib::FindAllActor<ATextureShuffleActor>(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_cast<ATextureShuffleActor*>(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 名称列表,客户端可据此确认自己的标签寻址是否符合预期。

五、其他纹理相关 API:simSetObjectMaterial 与 simSetObjectMaterialFromTexture

除了基于标签批量换肤的simSwapTextures,AirSim 还提供了面向单个具体对象的材质/纹理设置 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.py:

def 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适合"直接喂入外部纹理文件"。实际项目中可根据需求粒度组合使用。

六、常见问题与最佳实践

结合官方文档警告与源码实现,整理以下实践要点:

  1. 父类必须继承TextureShuffleActor:只有继承该父类的 Actor 才会被swapTextures遍历到(服务端用FindAllActor<ATextureShuffleActor>查找),普通 Actor 即使打了标签也不会命中。
  2. DynamicMaterial尽量在 Actor 实例上设置:官方明确提示在蓝图类上静态设置可能引发渲染错误,推荐在场景实例的 Details 面板统一设置TextureSwappableMaterial
  3. 候选纹理务必配齐SwapTextureSwappableTextures为空时会直接返回;若想让换肤生效,每个可换肤实例都要有至少一张候选纹理。
  4. 标签要兼顾分组与细分:利用组编辑从大到小逐层收窄分组,保证"大集合共享、小子集差异"的配置效率;标签匹配是 AND 语义,可通过组合标签实现精确寻址。
  5. 索引越界不会报错tex_idcomponent_idmaterial_id越界都会被取模回绕,这可能让"看似错误的索引"静默产生可用的结果,调试时需留意返回值列表是否符合预期对象集。
  6. 用返回值校验寻址simSwapTextures返回实际执行换肤的对象名列表,是验证标签写法是否正确的最直接手段。
  7. 母材质需暴露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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询