Unity URP自定义后处理开发指南:从ScriptableRendererFeature原理到Bloom实战

1. 项目概述:为什么URP的自定义后处理是必学技能?

如果你正在用Unity的URP管线做项目,尤其是对画面表现有要求的项目,比如二次元风格、赛博朋克或者需要特殊滤镜效果,那你大概率已经遇到过内置后处理(Post-Processing Stack)不够用的情况。要么是效果太“标准”,要么是性能开销不符合预期,要么就是想实现一个完全原创的屏幕特效。这时候,深入理解并掌握 Scriptable Renderer Feature (SRF) 来创建自定义后处理,就从“加分项”变成了“必备技能”。

简单来说,URP的Scriptable Renderer Feature是一个强大的扩展点,它允许你将自定义的渲染逻辑(比如一个完整的后处理Pass)注入到URP的渲染管线中,在指定的时机(比如在所有不透明物体渲染之后,但在天空盒之前)执行。这比传统的在 OnRenderImage 里写Blit要强大、可控得多。它让你能像Unity官方一样,以“一等公民”的身份参与渲染流程的编排。

最近在社区里,类似“ug mach3后处理”、“cesium 后处理”这样的热词被频繁讨论,这背后反映了一个共同趋势:无论是工业软件(如UG/NX)的加工模拟,还是地理信息平台(如Cesium)的三维可视化,大家对实时、可定制、高性能的后处理效果需求都在急剧增长。在Unity URP中实现类似的专业级后处理能力,SRF正是那把钥匙。接下来,我将从一个实际开发者的角度,带你从原理到实战,彻底搞懂如何用SRF打造属于你自己的后处理效果。

2. 核心原理:URP渲染管线与Scriptable Renderer Feature的工作机制

要玩转SRF,不能只停留在“抄代码”层面,必须理解它背后的运行机制。这样当效果出问题、顺序错乱或者性能不佳时,你才知道从哪里下手排查。

2.1 URP渲染流程的简化模型

URP的渲染流程可以看作一个由多个“渲染事件”(RenderPassEvent)串联起来的流水线。常见的节点包括:

  • BeforeRendering : 渲染开始前
  • BeforeRenderingShadows : 渲染阴影前
  • AfterRenderingOpaques : 所有不透明物体渲染完成后(这是后处理最常用的插入点之一)
  • BeforeRenderingTransparents : 渲染透明物体前
  • AfterRenderingTransparents : 所有透明物体渲染完成后(另一个常用的后处理插入点)
  • AfterRendering : 所有渲染完成后

你的每一个自定义渲染Pass( ScriptableRenderPass )都需要指定一个 RenderPassEvent ,URP就会在对应的时机执行它。SRF( ScriptableRendererFeature )则是一个容器或管理器,负责创建和管理一个或多个这样的Pass,并将它们注册到渲染器中。

2.2 ScriptableRenderPass的生命周期与核心方法

一个自定义的 ScriptableRenderPass 就像一个有固定剧本的演员,其生命周期由几个关键方法构成:

  1. Configure 方法 :这是Pass的“准备阶段”。在这里,你通常通过 ConfigureTarget ConfigureClear 来声明这个Pass需要渲染到哪个(些)RenderTexture(RT),以及是否需要清空它。对于后处理,我们经常需要创建一个临时的RT作为“画布”。

    public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    {
        // 根据主相机的纹理描述符创建一个临时RT的描述符
        RenderTextureDescriptor descriptor = cameraTextureDescriptor;
        descriptor.msaaSamples = 1; // 后处理通常不需要MSAA
        descriptor.depthBufferBits = 0; // 后处理通常不需要深度缓冲
    
        // 分配一个临时RT的标识符
        _tempRTId = Shader.PropertyToID(“_TempPostFXRT”);
        cmd.GetTemporaryRT(_tempRTId, descriptor, FilterMode.Bilinear);
        
        // 配置本Pass的颜色渲染目标为这个临时RT
        ConfigureTarget(_tempRTId);
        // 配置在渲染开始前清空颜色和深度
        ConfigureClear(ClearFlag.All, Color.clear);
    }
    
  2. Execute 方法 :这是Pass的“表演阶段”,核心渲染逻辑在这里发生。你会拿到一个 ScriptableRenderContext 和一个 CommandBuffer 。对于后处理,最常见的操作就是“Blit”——将源纹理(通常是相机当前的颜色缓冲 _CameraColorTexture )经过你的材质(Material)处理,渲染到目标纹理。

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        CommandBuffer cmd = CommandBufferPool.Get(“MyCustomPostFX”);
        
        // 核心Blit操作:将源纹理通过材质渲染到目标纹理
        // 这里假设_sourceTextureId是传入的源纹理(如_CameraColorTexture)
        // _targetTextureId是目标纹理(可能是另一个临时RT或直接回屏)
        cmd.Blit(_sourceTextureId, _targetTextureId, _material);
        
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }
    
  3. FrameCleanup 方法 :这是Pass的“收尾阶段”。在 Configure 中申请的临时RT,必须在这里释放,否则会造成内存泄漏。

    public override void FrameCleanup(CommandBuffer cmd)
    {
        if (_tempRTId != -1)
        {
            cmd.ReleaseTemporaryRT(_tempRTId);
        }
    }
    

2.3 数据传递:如何获取相机纹理与传递参数?

这是新手最容易卡住的地方。你的后处理Shader需要输入纹理(如场景原图)和参数(如强度、颜色)。如何传递?

  • 获取源纹理 :在URP中,相机渲染的场景颜色通常存储在 _CameraColorTexture 这个Shader全局属性中。你可以在Pass的 Execute 方法里,通过 renderingData.cameraData.renderer.cameraColorTarget 来获取它的RenderTargetIdentifier,然后传递给Blit命令。
  • 传递自定义参数 :在 Execute 方法里,通过 cmd.SetGlobalVector cmd.SetGlobalFloat cmd.SetGlobalTexture 等命令,将参数设置到Shader的全局属性中。你的Shader直接使用这些同名属性即可。

注意 _CameraColorTexture 是URP内部管理的,你不应该在 FrameCleanup 里释放它。你只需要释放你自己创建的临时RT。

3. 实战:构建一个完整的自定义泛光(Bloom)后处理Feature

理论讲完了,我们动手实现一个相对复杂但经典的效果:自定义泛光(Bloom)。我们将实现一个可调节阈值、强度、迭代次数和散射大小的Bloom,并把它封装成一个干净的SRF。

3.1 第一步:创建Shader与Material

首先,我们需要一个后处理Shader。它至少包含两个核心部分:

  1. 亮度提取(Pre-filter) :根据阈值,提取出场景中高亮的部分。
  2. 模糊/散射(Blur) :对提取出的高亮部分进行多次降采样(DownSample)和升采样(UpSample)的高斯模糊,模拟光晕散射效果。

这里给出亮度提取和一次模糊的核心Shader代码片段:

// Bloom提取Pass (简化版)
half4 FragPrefilter (Varyings input) : SV_Target
{
    half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
    // 计算亮度
    half brightness = max(col.r, max(col.g, col.b));
    // 应用阈值和柔和过渡
    half soft = brightness - _Threshold;
    soft = clamp(soft, 0, _ThresholdSoftness);
    soft = soft * soft / (_ThresholdSoftness + 1e-5f);
    half multiplier = max(soft, brightness - _Threshold) / max(brightness, 1e-5f);
    return col * multiplier;
}

// 高斯模糊Pass (简化版,使用双线性采样优化)
half4 FragBlurHorizontal (Varyings input) : SV_Target
{
    half4 color = 0;
    float2 offset = _MainTex_TexelSize.xy * float2(1.0, 0.0) * _BlurSize;
    color += SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv - offset * 3) * 0.06;
    color += SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv - offset * 2) * 0.24;
    color += SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv - offset * 1) * 0.38;
    color += SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv) * 0.24;
    color += SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv + offset * 1) * 0.06;
    return color;
}
// FragBlurVertical类似,offset方向改为(0.0, 1.0)

创建好Shader后,在Unity中生成一个Material。

3.2 第二步:实现ScriptableRenderPass

我们将创建一个 BloomRenderPass 类。它的核心职责是管理Bloom效果所需的多个渲染步骤。

public class BloomRenderPass : ScriptableRenderPass
{
    private Material _bloomMaterial;
    private BloomSettings _settings; // 一个存储阈值、强度等参数的数据结构
    
    private int[] _bloomPyramidIds; // 用于存储降采样和升采样过程中各层级RT的ID
    private int _pyramidSize; // 金字塔的层级数
    
    private RenderTargetIdentifier _source;
    private RenderTargetIdentifier _destination;
    
    public void Setup(Material material, BloomSettings settings, RenderTargetIdentifier source, RenderTargetIdentifier dest)
    {
        _bloomMaterial = material;
        _settings = settings;
        _source = source;
        _destination = dest;
        
        // 根据迭代次数计算金字塔大小,并预分配RT ID数组
        _pyramidSize = settings.iterations;
        _bloomPyramidIds = new int[_pyramidSize * 2];
        for (int i = 0; i < _pyramidSize * 2; i++)
        {
            _bloomPyramidIds[i] = Shader.PropertyToID(“_BloomPyramid” + i);
        }
    }
    
    public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    {
        // 为金字塔的每一层配置临时RT描述符,尺寸逐级减半
        RenderTextureDescriptor desc = cameraTextureDescriptor;
        desc.width = cameraTextureDescriptor.width;
        desc.height = cameraTextureDescriptor.height;
        desc.depthBufferBits = 0;
        
        for (int i = 0; i < _pyramidSize; i++)
        {
            desc.width /= 2;
            desc.height /= 2;
            desc.width = Mathf.Max(desc.width, 1);
            desc.height = Mathf.Max(desc.height, 1);
            
            // 分配降采样和升采样用的RT
            cmd.GetTemporaryRT(_bloomPyramidIds[i], desc, FilterMode.Bilinear); // 降采样
            cmd.GetTemporaryRT(_bloomPyramidIds[i + _pyramidSize], desc, FilterMode.Bilinear); // 升采样
        }
    }
    
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        CommandBuffer cmd = CommandBufferPool.Get(“Bloom”);
        
        // 1. 亮度提取与第一次降采样 (从_source到金字塔第一层)
        cmd.Blit(_source, _bloomPyramidIds[0], _bloomMaterial, 0); // Pass 0是提取Pass
        
        // 2. 构建降采样金字塔 (多次模糊并降采样)
        for (int i = 1; i < _pyramidSize; i++)
        {
            cmd.Blit(_bloomPyramidIds[i-1], _bloomPyramidIds[i], _bloomMaterial, 1); // Pass 1是模糊Pass
        }
        
        // 3. 构建升采样金字塔并混合 (模糊并升采样)
        for (int i = _pyramidSize - 2; i >= 0; i--)
        {
            cmd.Blit(_bloomPyramidIds[i + _pyramidSize + 1], _bloomPyramidIds[i + _pyramidSize], _bloomMaterial, 1);
            // 这里可以进行叠加混合
        }
        
        // 4. 最终混合:将升采样后的最高层Bloom结果与原始图像混合,输出到目标
        cmd.SetGlobalTexture(“_BloomTex”, _bloomPyramidIds[_pyramidSize]);
        cmd.Blit(_source, _destination, _bloomMaterial, 2); // Pass 2是最终混合Pass
        
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }
    
    public override void FrameCleanup(CommandBuffer cmd)
    {
        // 释放所有临时RT
        for (int i = 0; i < _bloomPyramidIds.Length; i++)
        {
            cmd.ReleaseTemporaryRT(_bloomPyramidIds[i]);
        }
    }
}

3.3 第三步:实现ScriptableRendererFeature

BloomRendererFeature 负责创建和配置 BloomRenderPass ,并将其插入到URP渲染器中。

[CreateAssetMenu(menuName = “Rendering/URP/Bloom Feature”)]
public class BloomRendererFeature : ScriptableRendererFeature
{
    [System.Serializable]
    public class BloomSettings
    {
        public Material material;
        public float threshold = 0.8f;
        public float intensity = 1.0f;
        public float scatter = 0.7f;
        public int iterations = 4;
        public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
    }
    
    public BloomSettings settings = new BloomSettings();
    
    private BloomRenderPass _bloomPass;
    
    public override void Create()
    {
        if (settings.material == null)
        {
            Debug.LogWarning(“Bloom feature需要一个材质!”);
            return;
        }
        _bloomPass = new BloomRenderPass();
        // 配置Pass的执行时机
        _bloomPass.renderPassEvent = settings.renderPassEvent;
    }
    
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (settings.material == null || !renderingData.cameraData.postProcessEnabled)
        {
            return;
        }
        
        // 设置Pass所需的参数
        _bloomPass.Setup(settings.material, settings, renderer.cameraColorTarget, renderer.cameraColorTarget);
        // 将Pass添加到渲染器中
        renderer.EnqueuePass(_bloomPass);
    }
}

3.4 第四步:在URP Renderer Asset中启用

  1. 在Project窗口右键,通过 Create -> Rendering -> URP -> Bloom Feature 创建你的Feature资产。
  2. 打开你的URP Renderer Asset(通常名为 UniversalRenderer_xxx )。
  3. Renderer Features 列表下方,点击 Add Renderer Feature ,选择 Bloom Renderer Feature ,并添加你刚创建的资产。
  4. 在Inspector中配置Bloom的参数(阈值、强度等)。

至此,一个功能完整的自定义Bloom后处理就集成到你的URP项目中了。运行游戏,你应该能看到高光区域产生了光晕效果。

4. 性能优化与调试技巧

自定义后处理功能强大,但性能陷阱也不少。以下是一些关键的优化和调试经验。

4.1 性能优化核心策略

  1. 降低分辨率渲染 :对于全屏后处理,性能开销与像素数直接相关。像Bloom、景深这类效果,完全可以在半分辨率(甚至四分之一分辨率)下进行模糊等耗时计算,最后再上采样混合。这在我们的Bloom实现中已经体现(构建金字塔的过程就是逐级降采样)。
  2. 精确控制执行条件 :不是所有相机都需要后处理。在 AddRenderPasses 方法中,利用 renderingData.cameraData 进行判断。
    // 例如:只对主相机生效
    if (!renderingData.cameraData.isSceneViewCamera && !renderingData.cameraData.isPreviewCamera)
    {
        // 添加Pass
    }
    // 或者根据相机类型判断
    if (renderingData.cameraData.cameraType == CameraType.Game)
    {
        // 添加Pass
    }
    
  3. 善用临时RT池与重用 :频繁创建和销毁RT开销很大。URP内部有 RTHandle 系统来管理RT的生命周期和缩放。对于进阶使用,建议学习并使用 RTHandle 来代替直接使用 CommandBuffer.GetTemporaryRT ,它能更好地适配动态分辨率等需求。
  4. Shader优化
    • 减少纹理采样次数 :使用双线性采样配合巧妙的UV偏移,可以用5次采样模拟高质量高斯模糊(如我们示例所示),而不是标准的17次。
    • 使用分支要谨慎 :在Shader中,特别是片段着色器里,避免动态分支( if 语句基于变量判断)。尽量用 step lerp 等数学函数替代。
    • 精度选择 :对于颜色计算, half 精度通常足够,比 float 更快。

4.2 调试与问题排查

当你发现效果不对、没有生效或者有奇怪的视觉错误时,可以按以下步骤排查:

  1. 检查RenderPassEvent :这是最常见的问题。你的Pass可能在不透明物体渲染前就执行了,当然看不到任何物体。确保它被设置在 AfterRenderingOpaques AfterRenderingTransparents 之后。
  2. 使用Frame Debugger :Unity的Frame Debugger是神器。打开它(Window -> Analysis -> Frame Debugger),播放游戏,然后一帧一帧地查看渲染事件列表。找到你的RenderPass,点击查看它的详细绘制调用(Draw Calls)和渲染目标(Render Target)。你可以清晰地看到每一步的输入输出纹理是否正确。
  3. 检查Shader编译错误 :在Console窗口中,确保没有“Shader error”或“Material doesn‘t have a property”之类的错误。一个编译失败的Shader会导致整个Material失效,Pass也就不会执行。
  4. 验证参数传递 :在 Execute 方法中,使用 cmd.SetGlobalVector(“_MyParam”, new Vector4(1,0,0,1)) 设置一个醒目的测试值,在Shader中输出这个值看看是否收到。也可以临时在Shader里直接返回一个纯色,看看这个Pass是否真的有输出。
  5. 注意RT的清理与保留 :确保在 FrameCleanup 中释放了所有自己申请的临时RT。同时,要清楚哪些RT是URP管理的(如 _CameraColorTexture ),不要误释放。

实操心得 :调试自定义渲染功能时,养成“分步验证”的习惯。先让Pass做一个最简单的操作(比如把屏幕变红),确保它能被正确调用。然后逐步添加复杂逻辑(亮度提取、一次模糊),每步都确认结果符合预期。这样比一次性写完所有代码再调试效率高得多。

5. 进阶应用:组合多个效果与全屏Shader技巧

掌握了单个效果后,你可以像搭积木一样组合多个SRF,实现复杂的画面风格。

5.1 多效果串联与执行顺序

你可以在同一个Renderer Asset中添加多个Renderer Feature。它们的执行顺序由两个因素决定:

  1. 在列表中的顺序 :从上到下执行。
  2. 各自的 RenderPassEvent :每个Feature内的Pass可以设置不同的时机。

例如,你可以创建三个Feature:

  1. BloomFeature (Event: AfterRenderingTransparents)
  2. ColorGradingFeature (Event: AfterRenderingTransparents)
  3. VignetteFeature (Event: AfterRenderingTransparents)

它们会按照1->2->3的顺序,在透明物体渲染后依次执行。后一个效果的输入是前一个效果的输出。 这里有一个关键点 :如果效果B需要基于效果A处理前的原始图像(而不是A处理后的图像),你就需要更精细地控制RT的流转。通常的做法是,在第一个Pass开始时,将原始的 _CameraColorTexture 拷贝到一个自定义的RT中保存起来,后续的Pass可以读取这个备份。

5.2 常用全屏后处理Shader技巧

除了模糊和颜色调整,这里再分享几个实用的全屏Shader技巧,你可以将它们封装成独立的SRF:

  • 屏幕扭曲(Screen Distortion) :基于一张噪声图或渲染纹理(如水流图)对采样UV进行偏移。
    float2 distortUV = input.uv + _DistortStrength * (SAMPLE_TEXTURE2D(_DistortTex, sampler_DistortTex, input.uv * _Tiling + _Time.x * _Speed).rg * 2 - 1);
    half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, distortUV);
    
  • 像素化(Pixelation) :将UV坐标量化,实现复古游戏风格。
    float2 pixelatedUV = floor(input.uv * _Resolution) / _Resolution;
    half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, pixelatedUV);
    
  • 边缘检测(Edge Detection) :使用Sobel或Roberts算子对比周围像素的深度或颜色差异,常用于卡通描边或特殊提示效果。
  • 自定义LUT(颜色查找表) :将屏幕颜色作为索引,去查询一张预设的渐变纹理(LUT图),实现高度风格化的色彩变换,这是实现电影感调色的核心技术之一。

5.3 与URP Volume系统集成

为了让美术或策划能在运行时动态调整后处理参数(如调节Bloom强度),最好的方式是将你的SRF与URP的Volume系统集成。你需要创建一个继承自 VolumeComponent 的类,并在你的 ScriptableRendererFeature 中读取这些参数。

[Serializable, VolumeComponentMenu(“CustomPostProcessing/Bloom”)]
public class BloomVolume : VolumeComponent
{
    public FloatParameter threshold = new FloatParameter(0.8f);
    public FloatParameter intensity = new FloatParameter(1.0f);
    public ClampedFloatParameter scatter = new ClampedFloatParameter(0.7f, 0.0f, 1.0f);
    public ClampedIntParameter iterations = new ClampedIntParameter(4, 1, 8);
    
    public bool IsActive() => intensity.value > 0;
}

然后在你的 BloomRenderPass.Execute 中,通过 VolumeManager.instance.stack.GetComponent<BloomVolume>() 获取当前生效的Volume参数,并覆盖Feature自身的默认设置。这样,在场景中放置一个 Global Volume 并添加 Bloom 覆盖,就能实时控制效果了。

6. 常见问题与解决方案速查表

在实际开发中,你肯定会遇到各种奇怪的问题。下表整理了一些典型问题及其排查思路:

问题现象 可能原因 排查与解决方案
效果完全没有出现 1. RenderPassEvent设置过早(如BeforeRendering)。
2. Material为空或Shader编译错误。
3. Feature未添加到Renderer Asset中。
4. 相机未使用该Renderer Asset。
1. 检查Pass的 renderPassEvent ,改为 AfterRenderingOpaques 等靠后的事件。
2. 检查Console错误,确保Material已赋值且Shader无误。
3. 在URP Renderer Asset的Inspector中确认Feature已添加并启用。
4. 检查相机 Renderer 列表是否选择了正确的Renderer Asset。
效果只出现一部分,或屏幕部分区域异常 1. 视口(Viewport)或裁剪(Scissor)设置错误。
2. 临时RT的尺寸或格式与源纹理不匹配。
3. Blit的源/目标纹理ID弄反。
1. 在Frame Debugger中查看该Pass的绘制命令,检查渲染目标视图。
2. 确认 Configure 中创建的RT描述符(如宽度、高度、格式)是否正确。
3. 仔细核对 cmd.Blit(source, dest, material) 中的参数。
画面闪烁或出现上一帧残留 1. 临时RT未在 FrameCleanup 中释放,导致累积。
2. RT被意外清理或覆盖。
1. 确保所有 GetTemporaryRT 都有对应的 ReleaseTemporaryRT ,且在 FrameCleanup 中调用。
2. 检查是否有其他Pass在错误的时间点清空或写入了你正在使用的RT。
性能开销巨大 1. 在全分辨率下进行多次复杂模糊。
2. Shader中存在高开销操作(如循环、大量分支、高精度计算)。
3. 每帧都创建/销毁大量RT。
1. 对模糊、散射类效果实施降采样策略(构建金字塔)。
2. 使用Frame Debugger和GPU Profiler定位瓶颈,优化Shader。
3. 考虑使用 RTHandle 系统复用RT。
在编辑器Scene视图生效,Game视图不生效 默认情况下,Scene视图相机和Game视图相机可能使用不同的渲染路径或设置。 AddRenderPasses 中,通过 renderingData.cameraData.cameraType 判断相机类型,决定是否添加该Pass。或者确保Game视图相机的渲染设置正确。
与URP自带后处理冲突 URP内置的后处理(通过Volume添加)也是一个Renderer Feature。多个全屏效果顺序叠加可能导致意外结果。 理清你需要的效果顺序。必要时,可以尝试禁用URP内置的某个效果(如Bloom),完全用自己的版本替代。在Frame Debugger中观察所有后处理Pass的执行顺序。

掌握这些排查方法,能让你在开发自定义后处理时更加从容。最后,再分享一个我个人的小习惯:为每一个自定义的SRF都创建一个简单的调试用材质,这个材质的Shader只做一件事——输出一个纯色或非常简单的图案。在开发初期,先用这个调试材质替换掉复杂的效果材质,能快速验证你的RenderPass管线是否畅通无阻,排除掉Shader本身复杂逻辑带来的干扰。当确认管线没问题后,再换上真正的效果Shader进行细节调试,这样可以极大提升开发效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值