TiXL 实时图形与 Spout:SpoutOutput 算子跨进程视频发送实战指南
【免费下载链接】t3TiXL is an open source software to create realtime motion graphics.项目地址: https://gitcode.com/GitHub_Trending/t3/t3
本指南以 TiXL 算子库
Lib.io.video命名空间下的SpoutOutput算子为核心,讲解如何把 TiXL 渲染的任意纹理(Texture)以 Spout 协议实时发送给 Resolume、TouchDesigner、OBS 等其他支持 Spout 的应用程序,实现多软件间零拷贝、低延迟的实时视频共享。读完本文你将掌握 SpoutOutput 的输入输出参数、推荐纹理格式(R8G8B8A8_UNorm)、RenderTarget 链路搭建方法,以及底层基于 SpoutDX 的发送实现与常见故障排查要点。
1. SpoutOutput 是什么
SpoutOutput是 TiXL 在Lib.io.video命名空间下提供的实时视频输出算子,位于 Operators/Spout/Symbols/lib/io/video/SpoutOutput.cs。它把 TiXL 内部渲染出的纹理,通过Spout协议以“发送方(Sender)”身份广播给同一台机器上的其他应用程序——例如 Resolume、MadMapper、TouchDesigner、OBS 等——从而让 TiXL 生成的实时动态图形(realtime motion graphics)可以无缝进入对方的合成流程。
Spout 是一种基于 GPU 共享纹理(shared texture)的实时视频共享协议:发送方与接收方在同一张显卡上共享纹理内存,不需要经过 CPU 回读或编码传输,因此延迟极低,非常适合现场演出、VJ(Visual Jockey)场景。与同命名空间下的 NdiOutput(走网络编码传输)不同,Spout 是本机进程间传输方案,通常延迟更小、带宽消耗为零。
与之配套的是SpoutInput算子(SpoutInput.cs),用于在本机接收其他应用通过 Spout 发送的纹理,二者共同构成 TiXL 与外部软件的实时视频互通桥梁。
2. 输入输出参数速览
SpoutOutput 算子对外暴露两个输入参数和一个输出,官方文档原文如下:
Input Parameters
| Name (Relevancy & Type) | Description |
|---|---|
| Texture(Texture2D Required) | 要发送的纹理,必填 |
| SenderName(String) | Spout 发送方名称,供接收端识别 |
Outputs
| Name | Type |
|---|---|
| TextureOutput | T3.Core.DataTypes.Texture2D |
从源码可以确认这两个输入的定义(SpoutOutput.cs):
[Input(Guid = "d4b5c642-9cb9-4f41-8739-edbb9c6c4857")] public readonly InputSlot<Texture2D> Texture = new(); [Input(Guid = "7C27EBD7-3746-4B70-A252-DD0AC0445B74")] public readonly InputSlot<string> SenderName = new();对应的算子模板文件 SpoutOutput.t3 中保存了这两个输入的默认值,其中SenderName的默认值是字符串"tixl":
{ "FormatVersion": 3, "Id": "13be1e3f-861d-4350-a94e-e083637b3e55"/*SpoutOutput*/, "Inputs": [ { "Id": "7c27ebd7-3746-4b70-a252-dd0ac0445b74"/*SenderName*/, "DefaultValue": "tixl" }, { "Id": "d4b5c642-9cb9-4f41-8739-edbb9c6c4857"/*Texture*/, "DefaultValue": null } ], "Children": [], "Connections": [] }要点说明:
- Texture(必填):这是真正被发送的内容。如果输入纹理为空(
null),发送逻辑会直接返回,不会产生任何输出(源码SendTexture方法第一行即检查frame == null)。 - SenderName:发送方的名字,接收端应用正是通过这个名字来枚举并选择发送源的。默认值为
tixl;如果本机存在多个同名发送方,Spout 会为实际创建的 sender 追加后缀区分,源码在InitializeSpout中会把 Spout 返回的实际名字回写到输入框(SenderName.SetTypedInputValue(_senderName)),保证界面上显示的名字与真实 sender 名一致。 - TextureOutput:一个直通输出(pass-through),把输入的纹理原样输出。它被标记为
DirtyFlagTrigger.Animated(SpoutOutput.cs),意味着每帧都会重新求值发送,保证实时性。
3. 推荐的纹理格式:R8G8B8A8_UNorm
官方文档明确给出建议:
We recommend using the R8G8B8A8_UNorm texture format for output. You can adjust the render format by using a [RenderTarget] like in this example: [TorusMesh]->[DrawMesh]->[RenderTarget]->[SpoutOutput].
即推荐使用R8G8B8A8_UNorm(8 位 RGBA,非归一化)格式作为输出纹理格式。之所以推荐,是因为它是 Spout 生态中最通用、兼容性最好的格式——绝大多数接收端(Resolume、TouchDesigner 等)对 R8G8B8A8 的共享纹理支持最完善。
从源码可以进一步看到格式的实际影响。SendTexture中维护了一个支持格式白名单(SpoutOutput.cs):
if (readableImage.Description.Format != Format.B8G8R8A8_UNorm && readableImage.Description.Format != Format.B8G8R8A8_Typeless && readableImage.Description.Format != Format.R8G8B8A8_UNorm && readableImage.Description.Format != Format.R16G16B16A16_UNorm && readableImage.Description.Format != Format.R16G16B16A16_Typeless && readableImage.Description.Format != Format.R16G16B16A16_Float) { if (!_conversionWarning) { Log.Debug($"Spout doesn't support {readableImage.Description.Format}, trying to fallback to R16G16B16A16_Float"); _conversionWarning = true; } dxTex = (DXTexture2D)_textureConverter.ConvertToCpuReadableBgra(readableImage); }这意味着:
- 如果输入纹理正好是
R8G8B8A8_UNorm(或白名单中的其他格式),纹理会被直接发送,不经过任何转换,性能最好; - 如果输入是白名单以外的格式(例如浮点 HDR 格式 R32G32B32A32_Float 等),算子会通过
TextureBgraReadAccess转换器(实例化为targetFormat: R16G16B16A16_Float)做一次格式回退转换,并仅在第一次转换时打印一条日志(_conversionWarning保证只警告一次)。转换会带来额外的 GPU 开销,因此提前在渲染管线里把格式设成 R8G8B8A8_UNorm 是避免性能损耗的最佳做法。
3.1 如何得到 R8G8B8A8_UNorm 纹理
文档给出的推荐链路是:
[TorusMesh] -> [DrawMesh] -> [RenderTarget] -> [SpoutOutput]即用一个RenderTarget算子接管绘制,让 RenderTarget 的输出纹理直接以可控格式交给 SpoutOutput。RenderTarget 算子在仓库中的实现位于 Operators/Lib/Symbols/image/generate/basic/RenderTarget.cs,它提供ColorBuffer、DepthBuffer、NormalBuffer三个输出槽,并且实现了IRenderStatsProvider,可以在渲染统计面板中查看其开销。在实际工程中,将 RenderTarget 的ColorBuffer连到 SpoutOutput 的Texture输入即可。
如果你的纹理来自其他算子且格式不受控,也可以先在中间插入格式转换算子(如ConvertFormat)把格式归一为 R8G8B8A8_UNorm 再送入 SpoutOutput。
4. 发送链路的核心实现:从纹理到 Spout
SpoutOutput 的发送逻辑集中在两个方法:Update(每帧入口)与SendTexture(实际发送)。整体数据流如下:
Texture 输入 │ ▼ Update() ──► 取 Texture 与 SenderName 的值 │ TextureOutput.Value = texture (直通输出) ▼ SendTexture(senderName, ref frame) ├─► 读取纹理宽高与格式 ├─► InitializeSpout():惰性创建 OpenGL 上下文 + DirectX11 设备 + SpoutDX sender ├─► 按需创建共享纹理(含 2 个环形缓冲纹理) ├─► CopyResource 把帧拷贝到可读纹理 ├─► 格式检查(白名单 / 回退转换) └─► _spoutDX.SendTexture(_texture) —— 真正发送到 Spout 共享纹理几个值得展开的实现细节:
4.1 惰性初始化与 OpenGL 上下文
InitializeSpout(SpoutOutput.cs)在第一次发送时做一次性初始化:
- 通过
DeviceContext.Create()创建 OpenGL 设备上下文,并把当前线程的 OpenGL 上下文切换为“主上下文”——这是 SpoutDX 底层要求的前置条件; - 通过
ID3D11Device.__CreateInstance(((IntPtr)ResourceManager.Device))拿到 TiXL 正在使用的 Direct3D 11 设备(TiXL 的渲染后端是 D3D11); - 创建
SpoutDX.SpoutDX实例并调用OpenDirectX11(_device),随后设置SenderName。
初始化失败时会打印错误日志:"Initialization of Spout failed. Are Spout.dll and SpoutDX.dll present in the executable folder?",并清理 sender 对象。这条日志提示了关键前提——Spout 的两个原生 DLL 必须存在(见下一节)。
4.2 共享纹理与环形缓冲
发送的核心是把 D3D11 纹理标记为Shared资源(ResourceOptionFlags.Shared),这样其他进程才能通过共享句柄访问。源码创建纹理的描述如下(SpoutOutput.cs):
var imageDesc = new Texture2DDescription { BindFlags = BindFlags.ShaderResource, Format = currentDesc.Format, Width = currentDesc.Width, Height = currentDesc.Height, MipLevels = 1, SampleDescription = new SampleDescription(1, 0), Usage = ResourceUsage.Default, OptionFlags = ResourceOptionFlags.Shared, // 关键:跨进程共享 CpuAccessFlags = CpuAccessFlags.None, ArraySize = 1 };算子内部维护了2 个(NumTextureEntries = 2)可共享纹理,采用环形缓冲(_currentIndex = (_currentIndex + 1) % NumTextureEntries)交替使用,用空间换时间,避免 CPU 等待 GPU 完成回读,从而降低阻塞、提高吞吐。
4.3 发送失败与资源回收
SendTexture对初始化失败和发送异常都做了 try/catch 处理:失败时释放 sender(ReleaseSender、CloseDirectX11、Dispose),下一次求值会重新初始化,具备一定自愈能力。Dispose方法中也会完整清理纹理、sender 与 OpenGL/D3D 设备,并且用静态引用计数_instance保证多个 SpoutOutput 实例并存时设备只在最后一个实例销毁时才真正释放。
5. 原生依赖:Spout.dll 与 SpoutDX.dll
SpoutOutput 依赖两个原生库,它们随算子包分发,位于 Operators/Spout/dependencies/:
Spout.dll:Spout 核心运行时;SpoutDX.dll:Spout 的 DirectX 11 接口封装。
在 spout.csproj 中,这两个 DLL 通过Content Include="./dependencies/**/*"被复制到输出目录(CopyToOutputDirectory="PreserveNewest")。如果运行时在可执行文件目录下找不到这两个 DLL,SpoutOutput 初始化会直接失败(对应 4.1 节那条错误日志)。因此在使用 SpoutOutput 前,请确认 TiXL 可执行文件所在目录下存在Spout.dll与SpoutDX.dll。
与原生库的互操作通过 CppSharp 自动生成的绑定完成:Operators/Spout/lib/interop/SpoutDX.cs(约 8.5 万行的自动生成代码),其中可以看到 spoutDX 类的完整原生接口,包括OpenDirectX11、SetSenderName、SendTexture(ID3D11Texture2D*)、ReleaseSender、CloseDirectX11等。SpoutOutput 使用的正是这套接口。
6. 快速上手指南
6.1 最小发送链路
在 TiXL 的图形编辑器(Graph Editor)中创建如下链路:
[TorusMesh] -> [DrawMesh] -> [RenderTarget] -> [SpoutOutput] (Texture <- ColorBuffer)- 从算子库
Lib.io.video分类中找到SpoutOutput并放入画布; - 把 RenderTarget 的ColorBuffer输出连接到 SpoutOutput 的Texture输入(RenderTarget 的格式建议设为
R8G8B8A8_UNorm); - SenderName保留默认值
tixl,或改成你自己的标识(例如MyTiXLOutput),以便接收端辨认; - 在另一台支持 Spout 的应用(或本机 Spout 查看器)中选择名为
tixl的 sender 即可看到实时画面。
6.2 在接收端验证
- 在 Resolume / TouchDesigner / OBS(带 Spout2 插件)中,新建一个 Spout 接收源;
- 从发送方列表中选择你在 SenderName 中填写的名字(默认
tixl); - 若列表为空或黑屏,按第 7 节排查。
6.3 配合 SpoutInput 回环
在同一台机器上,你也可以用SpoutInput把外部应用(或另一个 TiXL 工程)发送的纹理接收回来继续处理。SpoutInput 的参数为Command与ReceiverName,输出为Texture与UpdateCount(SpoutInput.md),UpdateCount每收到一帧新画面自增 1,可用于驱动节拍或事件逻辑。
7. 常见问题与排查
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 日志报 "Initialization of Spout failed" | Spout.dll/SpoutDX.dll不在可执行文件目录 | 确认两个 DLL 随程序部署(见第 5 节),重启 TiXL |
| 接收端找不到 sender | SenderName 与接收端所选名字不一致 | 检查接收端枚举的 sender 名;多实例同名时 Spout 会自动改名,注意回写后的实际名字 |
| 画面格式异常或性能下降 | 输入纹理不是推荐格式,触发了回退转换 | 在 RenderTarget / ConvertFormat 中把格式设为R8G8B8A8_UNorm |
| 多显卡机器上发送失败 | Spout 依赖当前 OpenGL/D3D 上下文对应的适配器 | 确保 TiXL 运行在与接收端相同的 GPU 上(Spout 共享纹理跨 GPU 不可用) |
| 输出黑屏但无报错 | Texture 输入为空 | 检查上游 DrawMesh/RenderTarget 是否成功求值并产生纹理 |
8. 延伸阅读
- SpoutInput:本机 Spout 实时视频输入算子
- NdiOutput:基于网络的视频输出方案(注意其同样要求 R8G8B8A8_UNorm 或 R8G8B8A8_Typeless 格式)
- Lib.io.video 算子目录:视频输入/输出算子全览
- SpoutOutput 源码:发送实现细节
- RenderTarget 源码:推荐链路的渲染目标算子
- spout.csproj:算子包的构建、依赖分发与打包配置
【免费下载链接】t3TiXL is an open source software to create realtime motion graphics.项目地址: https://gitcode.com/GitHub_Trending/t3/t3
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考