引言
什么是蒸汽波?
蒸汽波(Vaporwave)是一种20世纪10年代初兴起的音乐和视觉文化风格,以复古、梦幻和怀旧的元素为特点。它常常利用80年代和90年代的流行文化素材,比如霓虹色调、CRT电视的视觉效果,以及经典计算机用户界面的美学,来创造一种既熟悉又超现实的感官体验。
蒸汽波风格的视觉效果通常具有以下特点:
-
颜色与光效:紫色、粉色和蓝绿色的色调主导,常伴有强烈的霓虹效果。
-
图像噪声:加入模拟老旧电视机的雪花点和信号干扰效果。
-
几何与失真:通过几何扭曲和鱼眼效果创造一种未来感和不稳定感。
-
低分辨率或马赛克:模仿早期数字技术的限制。
这种风格因其怀旧的特质和独特的美学价值被广泛应用于艺术设计、游戏开发和视频制作中。
步骤 1:创建 RendererFeature 文件
-
创建脚本文件:
- 在 Unity 的
Assets
文件夹下,右键选择 Create > C# Script。 - 命名脚本,例如:
VaporwaveFeature.cs
。
- 在 Unity 的
在实现蒸汽波滤镜时,为了提高代码的可读性和可维护性,我们可以将需要的 Shader 和实时可调节的参数分别封装到两个独立的类中。首先,ShaderSetting
类用于集中存储所有滤镜效果所需的 Shader 引用,通过统一管理可以清晰地扩展和维护。其次,Setting
类用于存储滤镜的动态配置参数,例如颜色偏移、对比度、鱼眼强度等,这些参数可以通过 Inspector 面板实时调节,从而快速测试和调整效果。这种分离式设计不仅使代码更模块化,还显著提升了架构的扩展性和灵活性,非常适合需要多特效集成的复杂滤镜开发。
[Serializable]
public class ShaderSetting
{
public Shader ConvoluteShader = Shader.Find("Vaporwave/Convolute");
public Shader BlitGraphShader = Shader.Find("Vaporwave/BlitGraph");
public Shader BlitGraphInvertShader = Shader.Find("Vaporwave/BlitGraphInvert");
public Shader SnowShader = Shader.Find("Vaporwave/Snow");
public Shader InvertLightShader = Shader.Find("Vaporwave/InvertLight");
public Shader InterlacedShader = Shader.Find("Vaporwave/Interlaced");
public Shader TransposeXShader = Shader.Find("Vaporwave/TransposeX");
public Shader YUVHandleShader = Shader.Find("Vaporwave/YUVHandle");
public Shader RGB2YUVShader = Shader.Find("Vaporwave/RGB2YUV");
public Shader YUV2RGBShader = Shader.Find("Vaporwave/YUV2RGB");
public Shader GraphNoiseShader = Shader.Find("Vaporwave/GraphNoise");
public Shader EvLEDShader = Shader.Find("Vaporwave/EvLED");
public Shader FishEyeShader = Shader.Find("Vaporwave/FishEye");
public Shader WaterMarkShader = Shader.Find("Vaporwave/WaterMark");
}
[Serializable]
public class Setting
{
public bool yuvHandleXEnable = true;
public float shiftX = 6f;
public float shiftY = 4f;
public float shiftU = 0f;
public float shiftV = 0f;
public float level = 4f;
public float contrast = 1f;
public float light = 1f;
public float darkFade = 0f;
public float brightFade = 0f;
public float vividU = 1.18f;
public float vividV = 0.93f;
public bool interlacedEnable = true;
public int interlaced = 1;
public int interlacedLine = 4;
public float interlacedLight = 0.2f;
public bool transposeXEnable = false;
public float transposeX = 0.04f;
public float transposePow = 6.7f;
public float transposeNoise = 0.616f;
public bool qualityEnable = true;
public float darkNoise = 5;
public float lightNoise = 0;
public bool LEDResolutionEnable = false;
public int LEDResolutionLevel = 1;
public bool FishEyeEnable = true;
public float FishEyeIntensity_X = 0.1f;
public float FishEyeIntensity_Y = 0.1f;
public float FishEyePow = 0.5f;
public bool WaterMarkEnable = false;
public Vector4 MarkTextureRect = new Vector4(0.5f, 0.21f, 1f, 1f);
public float MarkTextureAlpha = 1;
public Texture2D MarkTexture;
public GameObject MarkTextPrefab;
public string MarkTextFiled;
public bool emphasizeLines = true;
public ConvolutionKernels.KernelType convoluteType = ConvolutionKernels.KernelType.RightTilt;
public bool snowEffect = false;
public bool invertLight = false;
}
1. 定义关键变量
我们需要在 ScriptableRendererFeature
中定义以下几个核心变量:
ShaderSetting
和Setting
实例:用于管理滤镜所需的 Shader 和可调节参数。- 自定义渲染 Pass (
VaporwaverPass
):负责执行具体的滤镜逻辑。 RenderPassEvent
:决定 Pass 在渲染管线中的执行时机。
public ShaderSetting shaderSetting; // 存储 Shader 引用
public Setting m_Setting; // 存储动态配置参数
private VaporwaverPass m_ScriptablePass; // 自定义 Pass
public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing; // Pass 执行时机
2. Create
方法
Create
方法在渲染器初始化时被调用,用于创建并配置 Pass 的实例。
- 初始化 Pass:通过
m_Setting
和shaderSetting
实例化VaporwaverPass
。 - 设置 Pass 属性:根据
renderPassEvent
调整 Pass 的行为(如是否启用反转渲染等)。
public override void Create()
{
m_ScriptablePass = new VaporwaverPass(m_Setting, shaderSetting); // 初始化 Pass
// 设置 Pass 的执行属性
m_ScriptablePass.invert = renderPassEvent == RenderPassEvent.AfterRendering;
m_ScriptablePass.renderPassEvent = renderPassEvent;
}
3. AddRenderPasses
方法
AddRenderPasses
方法在每帧渲染时被调用,用于将自定义 Pass 添加到渲染流水线中。
- 检查 Pass 是否有效:避免因未初始化或设置错误导致运行时异常。
- 添加到渲染管线:调用
renderer.EnqueuePass
方法将 Pass 注册到当前帧的渲染任务中。
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (m_ScriptablePass != null)
{
renderer.EnqueuePass(m_ScriptablePass); // 将 Pass 添加到渲染队列
}
}
在蒸汽波滤镜的实现中,为了动态计算滤镜效果,我们需要生成临时材质和 RenderTextures,并在渲染过程中通过 Shader 实现效果处理。以下是核心步骤和逻辑的详细说明:
1. 生成临时材质
在 VaporwaverPass
构造函数中,我们使用 ShaderSetting
中定义的 Shader 引用,通过 CoreUtils.CreateEngineMaterial
动态生成所需的临时材质。
目的:
- 管理材质资源: 为每种滤镜效果生成独立的材质。
- 与 Shader 绑定: 每个材质对应一个特定的 Shader,实现具体的滤镜逻辑。
public Material convoluteMat;
public Material blitMat;
public Material blitMatInvert;
// ...其他材质定义
public VaporwaverPass(Setting setting, ShaderSetting shaderSetting)
{
this.setting = setting;
// 动态生成材质
convoluteMat = CoreUtils.CreateEngineMaterial(shaderSetting.ConvoluteShader);
blitMat = CoreUtils.CreateEngineMaterial(shaderSetting.BlitGraphShader);
blitMatInvert = CoreUtils.CreateEngineMaterial(shaderSetting.BlitGraphInvertShader);
snowMat = CoreUtils.CreateEngineMaterial(shaderSetting.SnowShader);
invertLight = CoreUtils.CreateEngineMaterial(shaderSetting.InvertLightShader);
interlaced = CoreUtils.CreateEngineMaterial(shaderSetting.InterlacedShader);
transposeX = CoreUtils.CreateEngineMaterial(shaderSetting.TransposeXShader);
yuvHandle = CoreUtils.CreateEngineMaterial(shaderSetting.YUVHandleShader);
rgb2yuv = CoreUtils.CreateEngineMaterial(shaderSetting.RGB2YUVShader);
yuv2rgb = CoreUtils.CreateEngineMaterial(shaderSetting.YUV2RGBShader);
graphNoise = CoreUtils.CreateEngineMaterial(shaderSetting.GraphNoiseShader);
EvLED = CoreUtils.CreateEngineMaterial(shaderSetting.EvLEDShader);
FishEye = CoreUtils.CreateEngineMaterial(shaderSetting.FishEyeShader);
WaterMark = CoreUtils.CreateEngineMaterial(shaderSetting.WaterMarkShader);
}
2. 定义临时 RTHandle
在滤镜计算过程中,我们需要两个临时的 RenderTextures (temRT0
和 temRT1
),用于在效果处理时进行 Ping-Pong 传递。
目的:
- 数据传递: 通过 Ping-Pong 机制在两个 RTHandle 之间交替处理数据。
- 资源优化: 动态创建和复用 RT,避免重复分配和释放内存。
public RTHandle temRT0;
public RTHandle temRT1;
public float2 texSize;
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
var desc = renderingData.cameraData.cameraTargetDescriptor;
RenderTextureDescriptor temRTDescriptor;
// 设置 RT 的分辨率
texSize = new float2(desc.width, desc.height);
temRTDescriptor = new RenderTextureDescriptor(Mathf.CeilToInt(desc.width), Mathf.CeilToInt(desc.height), RenderTextureFormat.ARGBFloat, 0, 0);
temRTDescriptor.msaaSamples = 1;
temRTDescriptor.useMipMap = false;
temRTDescriptor.colorFormat = renderingData.cameraData.cameraTargetDescriptor.colorFormat;
temRTDescriptor.sRGB = false;
// 动态分配临时 RT
RenderingUtils.ReAllocateIfNeeded(ref temRT0, temRTDescriptor);
RenderingUtils.ReAllocateIfNeeded(ref temRT1, temRTDescriptor);
// 配置 RT 的目标和清除方式
ConfigureTarget(temRT0);
temRT0.rt.wrapMode = TextureWrapMode.Repeat;
ConfigureClear(ClearFlag.None, Color.black);
ConfigureTarget(temRT1);
temRT1.rt.wrapMode = TextureWrapMode.Repeat;
ConfigureClear(ClearFlag.None, Color.black);
}
3. 使用 Ping-Pong 机制传递数据
通过在 temRT0
和 temRT1
之间交替传递数据,实现滤镜效果的分步处理。
关键逻辑:
GetSourceRT
和GetTargetRT
方法: 动态确定当前的输入和输出 RT。SwapRT
方法: 每次处理后交换 RT,确保下一个步骤使用正确的输入。
public RTHandle GetSourceRT()
{
return pingpong ? temRT1 : temRT0;
}
public RTHandle GetTargetRT()
{
return pingpong ? temRT0 : temRT1;
}
public void SwapRT()
{
pingpong = !pingpong;
}
4. 滤镜效果的执行
最终在滤镜计算完成后,将处理结果从临时 RT 写回到渲染通道。
逻辑步骤:
- 初始化临时 RT。
- 使用材质和 Shader 处理数据。
- 将结果从目标 RT 写回到渲染管线。
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
RTHandle cameraTarget = renderingData.cameraData.renderer.cameraColorTargetHandle;
if (cameraTarget.rt != null)
{
CommandBuffer cmd = CommandBufferPool.Get("Varporwave Pass");
pingpong = false;
cmd.Blit(cameraTarget, temRT0, blitMat);
//---------------------------------------
if (setting.emphasizeLines)
{
Convolute(cmd);
}
if (setting.WaterMarkEnable && setting.MarkTexture != null)
{
WaterMarkEffect(cmd, cameraTarget);
}
if (setting.snowEffect)
{
SnowEffect(cmd);
}
else
{
if (setting.yuvHandleXEnable)
{
YUVHandleEffect(cmd);
}
if (setting.invertLight)
{
InvertLightEffect(cmd);
}
if (setting.FishEyeEnable)
{
FishEyeEffect(cmd);
}
if (setting.interlacedEnable)
{
InterlacedEffect(cmd);
}
if (setting.transposeXEnable)
{
TransposeXEffect(cmd);
}
}
if (setting.qualityEnable)
{
QualitySetting(cmd);
}
if (setting.LEDResolutionEnable)
{
LEDEffect(cmd);
}
//---------------------------------------
if (pingpong)
{
cmd.Blit(temRT1, cameraTarget, blitMat);
}
else
{
cmd.Blit(temRT0, cameraTarget, blitMat);
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
}
通过这种设计,我们构建了一个灵活且模块化的多滤镜架构。利用这种方式,我们不仅可以轻松地在代码中添加或移除滤镜,还可以通过清晰的结构直观地管理滤镜的执行顺序和启用状态。
结语与参考资源
通过本篇文章,我们构建了一个灵活且模块化的多滤镜架构,为实现炫酷的蒸汽波风格效果奠定了坚实的基础。在后续文章中,我们将进一步探索如何实现具体的滤镜效果,如颜色偏移、鱼眼失真和图像噪声等。
如果你对本文提到的内容感兴趣,欢迎参考以下资源:
- 本教程相关代码和项目托管在 GitHub:Higgins-PT/Vaporwave_Unity。这是对蒸汽波滤镜的优化与适配,专为 Unity 环境设计。
- 本项目的灵感来源于 itorr 的开源项目 Vaporwave。这是一个独立的蒸汽波特效项目,为滤镜设计提供了重要的技术参考。
后记
本项目的部分代码由一位正在学习的初级 TA 编写(是为了让他练习,才不是因为作者太懒了不想亲自写),因此可能存在某些不够规范或需要进一步优化的地方,还请大家多多包涵。如果你在使用过程中发现问题或有任何改进建议,非常欢迎通过 GitHub 提交 Issue 或 Pull Request,让我们一同完善这个项目。