自由学习记录(61)

使用了 #pragma multi_compile_fwdbase

这条编译指令启用了 Unity 内部用于主光源阴影支持的一组关键词变体,如:

  • SHADOWS_SCREEN(屏幕空间阴影贴图)

  • SHADOWS_DEPTH(深度图阴影)

  • SHADOWS_SOFT(软阴影)

使用了 TRANSFER_SHADOW(o)SHADOW_COORDS(n)

这两句宏做了两件事:

  1. TRANSFER_SHADOW(o):在 vert() 阶段把阴影贴图坐标写入插值结构;

  2. SHADOW_COORDS(3):在插值结构中预留了一个 TEXCOORD3 来存储阴影坐标。

使用了 UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)

这是关键:

这个宏会根据场景中的光源和阴影贴图内容,对当前片元应用阴影遮蔽计算,输出值存入 atten

在你的 frag() 函数里这句生效:

(ambient + (diffuse + specular) * atten + i.vertexLight)

说明:

  • 如果当前片元处于阴影中,atten 就会接近 0;

  • 光照(diffuse + specular)会被大幅削弱,从而呈现出“阴影下变暗”的效果;

  • ambient 和 vertexLight 不受阴影影响,保持基本亮度(这也是你看到阴影下仍有些亮度的原因)。

ShadowCaster Pass 是用于“投射阴影”的,不是“接收阴影”的。

功能所需 Pass是否必须你手动写说明
接收阴影(Receive Shadows)ForwardBase Pass + 阴影宏(如 UNITY_LIGHT_ATTENUATION可自己实现或依赖 fallback表示“自己会变暗”,接受 shadow map 的遮蔽值
投射阴影(Cast Shadows)ShadowCaster Pass必须手动写(或使用 fallback)表示“自己会在别的物体上留下阴影”,写入 shadow map

详细机制:Unity 阴影的运行流程(Forward 渲染)

  1. Unity 在渲染阴影贴图之前,会遍历场景中所有物体

  2. 它寻找所有含有:

    Tags { "LightMode" = "ShadowCaster" }

    的 Shader;

  3. 如果你当前的材质没有 ShadowCaster:

    • Unity 就跳过这个物体;

    • 也可能跳过整个 Shadow Pass 的构建(如果全场都没 ShadowCaster)

    • 最终导致 阴影贴图是空的

  4. ForwardBase Pass 中,虽然你调用了:

    UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

    但这个 atten 的计算要基于阴影贴图。如果贴图是空的(没采到任何阴影源),它就始终为 1(即无阴影遮挡)。

RenderQueue 值确实是:

  • Geometry:2000

  • AlphaTest:2450

  • Transparent:3000

因此,2500 并不是 AlphaTest + 50,而是介于 AlphaTestTransparent 之间的一个值。Unity 将 RenderQueue小于或等于 2500 的物体视为不透明物体,大于 2500 的物体视为透明物体。

根据 Unity 官方文档:

“对于索引小于或等于 2500(即 Geometry + 500)的队列,Unity 使用 OpaqueSortMode.FrontToBack 进行排序。对于索引大于 2500 的队列,Unity 使用 TransparencySortMode.Default 进行排序。”

这意味着:

  • RenderQueue ≤ 2500:物体被视为不透明,参与深度测试和遮挡计算,能够正确接收阴影。

  • RenderQueue > 2500:物体被视为透明,通常不写入深度缓冲区,可能无法正确接收阴影。

  • 阴影接收能力:在 Unity 的内置渲染管线中,只有不透明物体(Render Queue ≤ 2500)才能正确接收主光源的阴影。当物体的渲染队列值大于 2500 时,Unity 会将其视为透明物体,通常不会在其上执行阴影衰减计算,导致这些物体无法正确接收阴影。

  • 深度写入行为:透明物体通常设置为不写入深度缓冲区(ZWrite Off),这会影响后续物体的深度测试,可能导致渲染顺序错误或视觉上的穿透现象。

  • 渲染顺序和排序方式:不透明物体使用前向排序(Front-to-Back),有助于提高渲染效率和正确的遮挡关系;而透明物体使用默认的透明排序方式,主要根据与摄像机的距离进行排序,以实现正确的混合效果。

  • 渲染效果的可控性:如果需要在透明物体上实现正确的阴影接收效果,可能需要采用特殊的渲染技术,如多 Pass 渲染,其中一个 Pass 写入深度信息,另一个 Pass 进行颜色混合。

Queue ≤ 2500Queue > 2500
默认 ZWrite On,写入深度缓冲区默认 ZWrite Off,不写入深度
使用 Front-to-Back 排序(提前遮挡加速)使用 Back-to-Front 排序(为混合效果)
能用于遮挡其他物体并参与深度比较通常无法遮挡别的东西,容易被穿透
  • 渲染从 Queue ≤ 2500 的不透明物体开始;

  • 这些物体默认启用 ZWrite On,所以可以按照前后关系写入 ZBuffer;

  • 不透明物体使用 Front-to-Back 排序,从而尽早遮挡后面的像素,提升效率;

  • 所有不透明物体渲染完后,ZBuffer 已经填好;

  • 接着开始渲染 Queue > 2500 的透明物体;

  • 透明物体一般 ZWrite Off,所以不会覆盖 ZBuffer;

  • 它们使用 Back-to-Front 排序,逐层叠加实现混合,参考当前 ZBuffer 做深度测试(ZTest);

  • 最终渲染出正确的遮挡 +透明混合结果。

🧠 要注意的 几个扩展点或容易误解的点

1. ✅ ZBuffer 在透明阶段不再更新,但仍用于判断遮挡

即:透明物体虽然 ZWrite Off,但 ZTest 是 On,依然会“看到前面不透明物体的遮挡”。

2. ✅ 如果多个透明物体互相重叠,排序失误就会造成视觉错误

这就是 Unity 中的“透明排序问题”本质,需要手动调整 Queue 或使用深度预写法(Dual Pass)。

3. ✅ 有些透明特效(如玻璃)会启用 ZWrite On 强行写深度

→ 这会让它能遮住后面透明物体,但失去正常混合层叠能力。

Surface Shader

Surface Shader 是什么?

它是 Unity 提供的一种 高级语法糖,用一套结构化的语法,自动帮你生成 Vertex/Fragment Shader,并处理光照、阴影、GI、光照贴图、SH、ShadowCaster 等行为。

Surface Shader 不是通往现代图形编程的桥梁,但它是理解 Unity 内置光照管线、与美术系统融合的重要入口。建议作为知识模型建立,不推荐当作主战工具使用。

+---------------------------------------------------+
|                  #pragma surface                  |
|  选择光照模型(Standard等)/是否启用阴影/透明性   |
+---------------------------------------------------+
              ↓                  ↓
     自动生成 Pass           自动编译变体
              ↓
+------------------------+     +------------------------+
|   struct Input         |     |  SurfaceOutputStandard |
| - uv_MainTex           |     | - Albedo               |
| - viewDir              |     | - Normal               |
| - worldPos             |     | - Metallic/Smoothness  |
+------------------------+     +------------------------+
              ↓                              ↓
           surf(IN, out) ←———————你只控制这块逻辑——————
 

Meta Pass 是 Unity 用于光照贴图(Lightmap)和全局光照(GI)烘焙的特殊用途 Pass,
它不是用于实时渲染,而是为光照贴图计算提供材质信息的。

什么是 Meta Pass?

  • Unity 在进行光照贴图、全局光照(GI)烘焙、环境探针、反射探针计算时,不使用 ForwardBase / Deferred。

  • 它会在烘焙阶段,单独执行所有 Shader 中 标记了 "LightMode" = "Meta" 的 Pass。

  • Unity 把每个材质的 Meta Pass 渲染到一张特殊纹理上,用来分析:

    • Albedo

    • Emission

    • Transparency(部分 GI 系统也采样 alpha)

🔧 Meta Pass 渲染什么?

它渲染的是材质的基础反射属性,而不是实时光照。

属性用法
o.Albedo用于光照贴图基础颜色计算
o.Emission用于记录自发光颜色(对光照贴图和环境探针有贡献)
o.Alpha某些混合模式下用于调节影响程度(比如 Cutout)

// 这段宏是 Surface Shader 编译器生成的,用于 Meta Pass:
#include "UnityMetaPass.cginc"
return UnityMetaFragment(metaIN);

实时渲染使用的 Pass:
  • LightMode = "ForwardBase":主光源渲染

  • LightMode = "ForwardAdd":附加光源渲染

  • LightMode = "ShadowCaster":生成阴影贴图

  • LightMode = "Deferred":延迟渲染路径使用的 GBuffer 输出

这些都是 Frame-by-Frame、实时参与屏幕渲染的。

❌ 但 GI / Lightmap 不走这条路线:
  • 不会调用 ForwardBase Pass 中你定义的光照计算逻辑

  • 它不关心实时光照行为,而是关心“材质在静态环境中反射/发光的颜色”

  • 所以会跳过你写在 ForwardBase 中的各种 BlinnPhong、PBR 模拟、透明混合逻辑

光照贴图(Lightmap)和全局光照(GI)是什么?

  • 光照贴图:是一个离线烘焙好的纹理,用来记录光照结果,而非实时计算。贴在静态物体上,减少 GPU 压力。

  • 全局光照(GI):指光之间的间接反射,Unity 会烘焙出多个探针 / 立方体贴图 / 辐射缓存。

这些都不是实时光照,而是预先计算完,用采样实现的。

环境探针 / 反射探针是什么?

  • 环境光探针(Light Probes)

    • 存储的是空间点位处的 Spherical Harmonics 环境光数据(主要用于动态物体受环境光影响)

  • 反射探针(Reflection Probes)

    • 存储的是 cubemap,供标准材质中的反射通道采样用

    • 也来自离线渲染一张“天空图”

这些探针计算的时候,需要知道材质的基础颜色和自发光,这时候 Unity 会专门走你写的 "LightMode" = "Meta" Pass 来采样。

“在图形学上,离线渲染(offline rendering)和实时渲染(real-time rendering)是两种相互对立的渲染范式。为什么 Unity 会把类似离线用途的 Meta Pass 放进和实时渲染同一个 Shader 文件中?这是否违反了两者的原本定义?”

首先我们明确 标准图形学中的“离线渲染” 和“实时渲染” 的定义区别

项目实时渲染(Real-Time)离线渲染(Offline)
应用领域游戏、交互、VR电影、CG动画、渲染农场
性能目标每秒 30~240 帧以上几分钟~几小时生成一帧
精度追求尽量快 + 接近真实光照物理尽可能真实
渲染特性简化光照、近似反射、烘焙静态光Path Tracing、GI、多次反弹光
使用引擎Unity、Unreal、OpenGLArnold、RenderMan、Cycles、Octane
输出类型实时显示到屏幕渲染出视频帧或高质量贴图

Unity 语境中,“Meta Pass 所代表的‘离线渲染’”其实是**“实时渲染流程中的一段非实时准备阶段”**

Unity 不是在用电影工业意义上的“离线渲染”:

❌ “计算 10 分钟渲染一帧的电影级画质”

而是在做:

✅ “为了加快实时运行时效率,提前在编辑器中用一帧一帧的方式,把光照、探针、反射贴图等静态信息先烘焙好。”

这个过程虽然运行在编辑器中、非实时进行,但本质上还是在为 实时渲染服务

它们必须共享以下信息:

  • 材质属性:贴图、颜色、金属度、粗糙度、透明度……

  • 渲染控制权:哪些表面参与烘焙、哪些只参与实时渲染

Meta Pass 是在特定渲染模式下由 Unity Editor 的烘焙流程明确触发的,**和场景中是否“出现在当前视野”无关。

[Lighting 窗口 → 点击 Generate Lighting]
                ↓
[Unity 开始进行 GI / Lightmap 烘焙]
                ↓
[遍历所有 Baked Static / Reflection Probe 包围体内的物体]
                ↓
[使用内置烘焙摄像机,开启 Meta 渲染模式]
                ↓
[调用该物体使用材质的 Shader → 执行 "LightMode"="Meta" 的 Pass]
                ↓
[采集 surf() 函数输出的 o.Albedo, o.Emission, o.Alpha]
                ↓
[输出到临时光照贴图 / 探针采样缓冲区]
                ↓
[GI 系统计算反弹 / 探针插值 / Lightmap UV 映射]
                ↓
[生成最终 Lightmap / Probe 数据,供运行时采样]

触发时机

  • 手动点击 Lighting 窗口的 Generate Lighting

  • 改动了场景中需要烘焙的物体(比如模型或材质)

  • 修改了 Reflection Probe / Light Probe / Lightmap 参数

  • 使用 [Scene View → Baked GI Preview]、[Reflection Probe Preview] 等功能

目标对象筛选机制

Unity 会扫描场景中:

  • 被设置为 Static 且勾选 Lightmap Static 的物体;

  • 包含在 Reflection ProbeLight Probe Group 采样范围内的物体;

  • 使用了任何带有 Meta Pass 的材质。

Pass {
    Tags { "LightMode" = "Meta" }
    ...
}
如果找到,则执行这个 Pass(调用其中的 surf() 函数)

Meta Pass 渲染的输出结构

Meta Pass 不会输出到主帧缓冲,而是:

输出目标内容说明
Meta Color BufferAlbedo表面颜色(不含光照)
Meta Emission BufferEmission自发光,用于 GI 源
Alpha Buffer(可选)Alpha用于 Cutout / 透明遮罩判断(如叶片)

#include "UnityMetaPass.cginc"

void surf(Input IN, inout SurfaceOutputStandard o) {
    o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb;
    o.Emission = _EmissionColor.rgb;
    o.Alpha = 1.0;
}

fixed4 frag(v2f_surf IN) : SV_Target {
    UnityMetaInput meta;
    meta.Albedo = o.Albedo;
    meta.Emission = o.Emission;
    return UnityMetaFragment(meta);
}

#include "UnityMetaPass.cginc"

  • 引入了 Unity 内部提供的封装代码,用来辅助采集 Meta 信息

  • 包含了 UnityMetaFragment() 等函数,它们把你的 AlbedoEmission 输出正确地写入烘焙系统需要的缓冲区

fixed4 frag(v2f_surf IN) : SV_Target {
    UnityMetaInput meta;
    meta.Albedo = o.Albedo;
    meta.Emission = o.Emission;
    return UnityMetaFragment(meta);
}
这部分是 Meta Pass 中实际向 Meta 渲染目标缓冲区 写入的地方。

UnityMetaInput 是 Unity 定义的结构体,通常有这些字段:

.Albedo(基础颜色)

.Emission(自发光)

.Alpha(可选,控制透明剔除时用)

UnityMetaFragment() 是 Unity 封装的函数,它会:

判断当前是正在生成 Lightmap 还是 Probe

把 meta.Albedo 和 meta.Emission 写入烘焙专用的 FrameBuffer

自动处理透明剔除(如 AlphaTest)的剪裁判断

Meta Pass 数据被用来做什么?

目标系统使用方式
☀️ 光照贴图 Lightmap作为每个像素基础反射颜色,参与 GI 反弹计算
🔦 Light Probe(SH 探针)采样物体表面颜色,用于生成球谐系数(SH)
🪞 Reflection Probe(反射贴图)生成近似 cubemap 反射纹理
🎨 Scene View GI 预览允许在编辑器中实时查看 GI 结果

球谐光照是一种用“数学方式描述从四面八方来的光”的压缩表达形式,
它的起源是:我们想在实时渲染中模拟复杂、方向性强的间接光(Global Illumination),但又不能用巨大的存储和计算量,于是采用球谐函数进行近似编码。

我想要让一个点位(或探针)保存“来自四面八方的光照分布”信息,这样动态物体也能感受到场景中的全局光(比如天花板反射、墙面反弹的光)。但如果我真的在每个方向上采样一个颜色,那我就需要存下一个球面纹理,这既慢又占内存。

球谐函数(Spherical Harmonics)

  • 用一组数学函数(球谐基)来近似表达球面上的任意方向分布

  • 类似于在声音中用正弦波做傅里叶变换表达频率 → 它是对方向空间光照的频域压缩

🧪 在图形学中的用法

🎯 用来编码的内容:

  • 一个点上,从各个方向来的环境光照(通常用于 Light Probe)

  • 一个贴图区域上接收到的间接光照(通常用于 Lightmap baking)

  • 甚至可以用来编码 BRDF 的方向性(高级用途)

✨ 用来解码时:

  • 传入一个法线方向(比如动态物体的表面法线)

  • 乘上 SH 系数,得到该方向上应该接受到的环境光颜色

🛠️ 在 Unity 中的实现

Unity 会:

  1. 在烘焙时计算场景中多个 Light Probe 的球谐系数(通常使用 2 阶或 3 阶球谐,共 9 个系数,每个 RGB 通道独立)

  2. 在运行时,对动态物体的每个顶点做 SH 解码,乘以其法线方向,得到环境光颜色

Light Probe 的目标:

预先在空间中的某些点采集全方向环境光照,然后在运行时用球谐函数插值,给动态物体使用。

→ 起源自 Precomputed Radiance Transfer (PRT)Spherical Harmonics Lighting,尤其是 2002 年 Sloan 等人的论文:
“Precomputed Radiance Transfer for Real-Time Rendering in Dynamic, Low-Frequency Lighting Environments”

🎯 Reflection Probe 的目标:

在一些代表性位置预先拍摄 CubeMap,并在运行时使用贴近的环境贴图给材质反射。

→ 起源于 Image-Based Lighting(IBL)Environment Mapping 技术,最早在电影工业中使用 CubeMap 拍摄替代全局光。

“静态物体”和“动态物体”这两个词在 Unity(和一般实时渲染系统)中不仅是分类标签,它们背后代表的是完全不同的渲染路径、烘焙机制、Shader 数据输入源和优化策略。

类型静态物体(Static)动态物体(Dynamic)
意义不会在运行时移动、旋转、缩放会变换位置、动画、生成等
标志设置勾选 Static 标签默认都为动态
烘焙处理可进行 Lightmap 烘焙不可被烘焙,只能实时照明
GI 系统处理可作为光照贡献源(参与反弹)通常只作为光照接收者
数据输入使用 Lightmap UV,访问 _LightMap使用 Light Probe 插值,访问 unity_SH
Shader "Custom/SurfaceShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Ocean fullforwardshadows vertex:vert finalcolor:final

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0
		#include "UnityPBSLighting.cginc"

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

		half4 LightingOcean(SurfaceOutputStandard s, half3 lightDir, half atten)
		{
			fixed4 c;
			fixed diff = max (0, dot (s.Normal, lightDir));
			c.rgb = s.Albedo * _LightColor0.rgb * diff * atten;
			c.a = s.Alpha;
			return c;
		}

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)



		void vert (inout appdata_full v)
		{
			
		}

		void final(Input IN, SurfaceOutputStandard o, inout fixed4 color)
		{
			
		}

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

什么是 Shader Model?

  • Shader Model 是由 GPU 驱动程序定义的一套功能等级规范,决定了:

    • 可使用的纹理数量

    • 指令数量上限

    • 是否支持动态分支、纹理 LOD 等功能

Shader Model代表意义Unity 常用用途
2.0最基础,最多 8 个纹理、无动态分支非常过时,仅老硬件兼容
3.0支持动态分支、32 个寄存器等现代特性Surface Shader 最小推荐
4.0/4.5支持结构体 buffer,compute shaderHDRP / DX11+ 平台
5.0完全现代 GPU 语言高端特效/Compute Shader

#include "UnityPBSLighting.cginc"

PBS 是什么?

PBS = Physically Based Shading(物理基础光照)

这个文件中包含:

  • Unity 的内建 BRDF 函数(Cook-Torrance 模型)

  • 标准 Metallic Workflow 的参数设置

  • 支持 Unity 标准光照路径的函数(如 LightingStandard, LightingStandardSpecular

不过你这段代码虽然引入了 PBS,但实际上没有使用 PBR 模型,而是定义了自己的简化光照函数 LightingOcean。因此这个 include 虽然保留着,是冗余的,可以移除(除非你从里面引用函数或结构)。

#pragma surface surf Ocean fullforwardshadows vertex:vert finalcolor:final

这行是 Surface Shader 的核心入口定义。下面来 分解每一部分含义

#pragma surface surf Ocean ...

部分作用
surfSurface 函数名:你的 void surf(Input IN, inout SurfaceOutputStandard o)
Ocean自定义光照函数名:你定义的 half4 LightingOcean(...) 会替代默认光照计算方式

fullforwardshadows

启用所有类型光源的阴影支持(包括 Spot、Point、Directional 光),否则默认只开启主方向光阴影。

✅ 它会自动插入 ShadowCaster pass,不需要你手动写投影逻辑

vertex:vert

指定一个 自定义顶点函数 void vert(inout appdata_full v),允许你在顶点阶段执行变形、UV 操作等。

✅ 自动为你生成:

  • ShadowCaster Pass(支持投射阴影)

  • ForwardBase、ForwardAdd、Meta 等完整 Pass

  • GI(光照贴图、Light Probe 支持)

  • 反射探针、动态光照、逐像素/逐顶点混合

  • Lightmap、SH9 相关数据插入

  • 多光源处理

  • 内置变量(如 o.Albedo, o.Emission)绑定到渲染流程

❗你需要放弃的“控制”:

  • 不能精细控制每个 Pass 的具体结构(除非你自己写 Custom Shader)

  • 必须按 Unity 规定的结构写 surf() 函数,不能随意输出颜色

  • 如果想做多 RenderTarget(MRT)、延迟渲染 GBuffer 输出 → Surface Shader 不适合,得改用 ShaderLab 手写多个 Pass

  • 如果使用 Unity 内建模型(如 Standard):

    • 会自动支持:

      • Reflection Probe(支持这个是会让unity做些什么)

      • 环境光贴图(这个贴图是怎么产生的,传给这个处理需要哪些数据)

      • BRDF Lookup(这是干什么的)

      • GI 烘焙联动(为什么叫这个)

  • 如果你用 自定义光照函数名

    • Unity 不会插入其默认 PBR 结构

    • 你必须在 LightingXxx() 中手动处理光照、阴影、Attenuation 等

    • 少了如 SpecularColor, FresnelTerm 等自动变量绑定(自动绑定的变量是怎么来的,为什么可以自动绑定,是unity做的吗)

可选参数(surface 指令中的附加关键词)

这些是在 #pragma surface 后面你可以加的关键词,用于控制 Shader 自动生成时的附加行为。

关键词作用举例限制或功能
alpha启用透明(需要输出 o.Alpha#pragma surface surf Standard alpha会自动生成 Blend、AlphaTest 支持
alphatest:_Cutoff启用透明裁剪,使用某个属性控制alphatest:_Cutoffclip() 配合使用(这个启动之后是干什么的,透明裁剪是一个功能吗,还是变量)
addshadow即使是 Unlit 也能生成 ShadowCaster pass用于自定义 Shader 也能投影阴影
fullforwardshadows支持所有类型光源的阴影(包括点光、聚光)默认只支持主光源方向光阴影
vertex:xxx使用自定义顶点函数vertex:vert必须提供 void vert(...)
finalcolor:xxx使用自定义最终颜色函数finalcolor:final会在输出颜色前调用
keepalpha保留 Alpha,不自动写入 1常用于透明混合
nofog禁用内置雾效
noambient不使用全局环境光(Unity 的 Ambient Light)用于 Stylized Shader
noshadow禁止生成阴影 Pass轻量场景可用
exclude_path:deferred只在 Forward 渲染中启用避免延迟路径下编译

Shader 中启用 Reflection Probe 时,Unity 插入了哪些变量与贴图?如何控制使用哪一个 Reflection Probe?

Unity 会在编译时自动为支持反射的 Shader 插入 Reflection Probe 相关的 Cubemap 采样变量、采样函数,以及物体 → 环境贴图 的映射逻辑。**你不需要手动采样贴图,但你必须使用 Unity 支持的光照模型(如 Standard)或遵循它的约定。

插入以下 采样函数:

half3 Unity_GlossyEnvironment (UNITY_GLOSSY_ENV_DATA data, half3 reflUVW, half perceptualRoughness);

  • reflUVW:你计算出来的反射方向(world space)

  • perceptualRoughness:由 _Glossiness 推导而来

  • UNITY_GLOSSY_ENV_DATA:包含 unity_SpecCube0_HDR 的结构体包装

Unity 如何决定使用哪张 Reflection Probe?

这是由 Unity 的 C++ 渲染系统在运行时做的(不是你 Shader 自己判断):

步骤:

  1. 每个物体在场景中,Unity 会计算它当前所处的位置

  2. 查找最近的 Reflection Probe(包围盒内的 Probe)

  3. 进行 Cubemap 的混合/插值(最多两个 Probe)##(怎么插值的,probe是什么,还有具体的数量?这种图cubemap是skybox的相关产物吗,需要再静态物体上才能生成吗,这中sampletexture的形成方式和一般的vf shader下写像素到缓冲区有什么区别)

  4. 把这个结果 绑定到材质的 unity_SpecCube0

  5. 如果没有 Reflection Probe,默认使用 Skybox 的环境贴图(probe是有很多种?各种之间的共性是什么,都能做到什么,)

When you add a new scene, Unity automatically creates a hidden default Reflection Probe that stores Unity’s built-in Default Skybox cubemap.

天空盒影响光照颜色

Unity 是什么时候绑定这张 Cubemap 的?

你在 Shader 中只是用到了 samplerCUBE unity_SpecCube0;,它的贴图绑定早在 DrawCall 提交前由 Unity 内部完成。

Built-in 渲染管线 + 使用 Surface Shader 或标准光照模型 时,Unity 自动插入以下变量:

🌐 环境反射(Reflection Probe)

插入的变量类型说明
samplerCUBE unity_SpecCube0cubemap 采样器当前物体使用的反射探针贴图(Cubemap)
float4 unity_SpecCube0_HDRHDR 解码参数用于解码反射贴图中的 RGBM 编码数据
float3 unity_SpecCube0_BoxMinBox 投影区域最小点用于 Box Projection 模式的插值
float3 unity_SpecCube0_BoxMaxBox 投影区域最大点同上
float4 unity_SpecCube0_ProbePosition反射探针的世界坐标Box Projection 使用中心点

这些变量可由 Unity_GlossyEnvironment() 函数在 Unity 标准光照模型中调用来使用。

这些变量 不会出现在你写的 Shader 源码中,而是:

  • 通过 #pragma surface surf Standard 等指令

  • 被 Unity 的 Surface Shader 编译器插入(包括引用了 UnityPBSLighting.cgincUnityGlobalIllumination.cginc 等)

  • 仅在 Built-in RP 中有效。URP 和 HDRP 使用不同的绑定机制(CBuffer)

无法在 Shader 中主动控制使用哪个 Probe,它是由 Unity 的 C++ 渲染系统运行时决定的:

运行时流程:

  1. 查找最近的 Reflection Probe

    • 如果物体在某个 Probe 的包围盒(box projection)中 → 使用它。

    • 如果有多个合适的 Probe → 最多混合两个(加权插值)。

  2. 插值并绑定 Cubemap

    • Unity 在 CPU 端计算混合结果 → 绑定成纹理传入 unity_SpecCube0

  3. Shader 中使用 unity_SpecCube0 即可采样最终贴图

Unity 默认会添加一个 隐藏的反射探针,它使用场景的 Skybox 来生成 Cubemap:

  • 这个隐藏 Probe 会作为 fallback 使用。

  • 如果 Lighting 设置中选择了 “Generate Lighting”,会生成一个新的 default Reflection Probe。

  • 该贴图仍然会传给 unity_SpecCube0

Reflection Probe 是 Unity 提供的一个场景组件,其作用是:

  • 捕捉周围环境并生成一个 Cubemap

  • 用于在物体表面模拟真实反射

✔️ Probe 类型:
  • Baked:在编辑器里通过 Lighting 面板烘焙一次,生成贴图并保存为资源。

  • Realtime:运行时根据摄像机或自身位置动态渲染六个方向生成 Cubemap(性能开销大)。

  • Custom:手动设置 Cubemap 纹理。

这些探针会将捕获到的图像 编码成 HDR 格式(RGBM、LogLuv),并存储为 Cubemap。

如果你没有添加任何 Reflection Probe:

  • Unity 会使用场景中的 Skybox 来自动生成一张全局环境 Cubemap。

  • 这个默认 Cubemap 会被绑定到内置变量 unity_SpecCube0

  • 通常发生在你使用 Standard Shader / Surface Shader 并启用反射时

内置变量名含义
samplerCUBE unity_SpecCube0当前可用的环境反射贴图(Cubemap)
float4 unity_SpecCube0_HDR用于 HDR 反射贴图解码(RGBM 编码)
float3 unity_SpecCube0_BoxMin/MaxBox Projection 使用的投影边界
float4 unity_SpecCube0_ProbePositionBox Projection 使用的探针中心位置

这些变量并非你手动写入,而是:

  • 在使用 Surface Shader(如 #pragma surface surf Standard)时

  • Unity 编译器会自动插入

你可以使用 Unity 提供的函数(标准 PBR Shader 已内置):

// 计算反射向量
float3 worldRefl = reflect(-viewDir, normal);
// 采样环境贴图
half4 env = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, worldRefl, mipLevel);
// 解码 HDR
half3 reflColor = DecodeHDR(env, unity_SpecCube0_HDR);

这就是标准 BRDF 光照模型中实现高光反射的重要一环。

如果有说贴图错误可以检查模型是否generate shadow uv

  • ☠️ 一旦你在运行时改变 Baked 或 Mixed 光源的方向,贴图就不再有效或不更新,因为烘焙贴图是基于烘焙时的光源方向预计算生成的。

  • Unity 会发出警告(或直接无效),因为静态光照贴图只支持“不可变”光源。

✅ Realtime(完全动态):

  • 每一帧都根据光源位置/颜色/强度来计算光照。

  • 显著消耗性能,但适合可变场景(如手电筒、日夜变化)。

🟡 Mixed(混合光照):

  • 静态物体 → 光照信息预烘焙(Lightmap or Light Probe)。

  • 动态物体 → 使用实时光照(通常只计算主光源 + 近似阴影)。

  • “Shadowmask” 或 “Subtractive” 模式控制阴影来源。

🔒 Baked(完全静态):

  • 灯光在烘焙时作用于场景,之后不再实时影响任何物体。

  • 非常适合场景建筑、山体等完全不动的静态元素。

  • 实时性能开销接近 0。

当灯光设置为 Baked 时
  • Unity 只在 烘焙阶段 让这盏灯影响 静态物体

  • 在运行时,这盏灯 完全不会参与实时渲染

  • 所以:

    • ✅ 静态物体 → 光照来自 Lightmap 或 Light Probe(提前烘焙好了)

    • ❌ 动态物体 → 不受这盏灯影响

    • ❌ 不产生实时阴影

当灯光设置为 Realtime 时
  • Unity 每一帧实时计算光照和阴影,这盏灯:

    • ✅ 会影响静态物体(通过实时计算)

    • ✅ 会影响动态物体

    • ✅ 可以投射阴影

    • 不参与光照贴图的生成(不会被烘焙)

当灯光设置为 Mixed 时
  • 静态物体:受这盏灯影响的光照和阴影 可以被烘焙进贴图中

  • 动态物体:仍然可以接受这盏灯的实时光照,并且可以产生实时阴影(配置 Shadowmask / Subtractive 时可能略有不同)。

  • 一句话:静态预烘焙,动态实时计算。

gi,将光设置成baked,将物体设置为light static,static也行,然后关注阴影区域 

模式静态物体:光照静态物体:阴影动态物体:光照动态物体:阴影特点总结
Baked Indirect💡 实时直接光🟡 烘焙间接光(lightmap / probe)不生成 shadowmask,阴影全实时💡 实时直接光🟡 无间接反射☑️ 实时阴影(全动态)性能较差,与distance的关系是,超出distance之后里面的直接不渲染了,连baked都不留
Shadowmask🟢 直接光烘焙 / 实时(视实际设置)🟡 间接光烘焙shadowmask 贴图中保存静态物体投射的阴影💡 实时直接光🟡 probe 提供间接光估算☑️ 实时阴影 + shadowmask 混合📏(近距离实时,远距离烘焙)推荐默认选项,质量与性能平衡
Subtractive🟢 直接光烘焙(仅主光)🟡 间接光烘焙静态阴影完全 baked⚠️ 只支持一盏主方向光💡 只有主光源能照亮🟡 无间接光☑️ 仅主光源有实时阴影极限优化,只建议用于低端硬件或卡通场景
  • 最终的 shadowmask 使用方式还取决于质量设置中的 Shadowmask Mode(Distance 或 Shadowmask)

设置名描述
Shadowmask所有阴影混合使用 shadowmask 和实时阴影
Distance Shadowmask靠近摄像机的使用实时阴影,远处使用 shadowmask 降低负担

开了gi的效果

关掉之后

移动之后问题就可以看出了

一个物体可以产生多个 ShadowCaster。
Stats 窗口中的 Shadow Casters 数量不严格等于游戏物体数,因为它统计的是参与阴影渲染的渲染元素(Draw Calls 级别的 Shadow Pass 实例),而不是单一的物体个数。

mixed lighting设置为subtractive

场景只有这一个动态物体

此时light设置的是Runtime

改为 mixed--subtractive 模式,之后右边的cube 实时光照的点光源不再产生阴影

左边的因为是烘焙出来的,所以缓存依然存在

shadowmask----

当没有设置成distance时,相比于subtractive,变化的是点光源这些非主方向光的光源,也可以照射物体并且投射出阴影

但此时仍然不是实时的,也就是如果物体移动到别的物体的遮蔽下,是不会更新别的物体照射到自己身上的阴影的

 镜头靠近到1m之内,才更新实时阴影

如果你用 Unlit Shader 或者自写了一个没有 Lightmap 支持的 Surface Shader,即使场景设置了 Baked Lighting、Mixed Lighting,物体也完全不会受到影响。

Unity 的 Lighting Settings 和 Shader 不是互相覆盖的,而是“Shader 决定了 Lighting 是否生效”。Lighting 设置只是“提供可能”,Shader 才是“是否用上”的决定者。

写这种及时切换的函数降低性能的消耗

如果在物体遮蔽下立刻换成distance shadowmask

(反正可以实时用unity脚本切换)

shadowmask在这种情况下会相对真实的处理,而不是直接的阴影叠加

subtractive纯纯的叠加,没有其他的合理性调整 

  • Unity 不会自动用 Shadowmap 给静态物体投影,这通常通过 Shadowmask 技术实现(混合光照时)。

  • Lightmap 中可包含 Shadowmask 信息(由混合灯光预烘焙而来),但它只是“静态阴影权重”,不是实时判断的遮挡。

  • Fresnel Term(菲涅尔反射系数)

  • Geometry Term(遮蔽和遮挡)

  • NDF(Normal Distribution Function)(微表面分布)

为了加速这些计算,Unity 使用了一张预生成的 2D 查找表纹理 —— unity_BRDFLut(又名 UnityStandardBRDF)。

inout 的含义:

在 HLSL/Cg 中,inout 是一种参数修饰符,表示这个参数是“输入 + 输出”:

  • 输入(in):调用函数时传入的值可以被读取;

  • 输出(out):函数内部可以修改它的值,修改后的结果在函数外也能访问;

  • inout 综合两者,即:传进去,改完还能带回来。

在 Surface Shader 中,这用于对 SurfaceOutputStandard o 进行材质计算,让你在 surf() 函数里修改 AlbedoSmoothnessAlpha 等字段。

Input 是什么、它来自哪里?

它并不是像 appdata 一样来自 Unity 的 cginc 文件,而是 Unity 在编译 Surface Shader 时 动态“合成”的一个中间结构,用于将 Unity 自动插入的顶点插值变量传递给你的 surf() 函数

📦 它的行为可以总结为:

  • 你在 Input 结构体中声明什么字段;

  • Unity 就会根据你写的字段,自动在 vertex → fragment 插值中生成对应代码

  • 不声明就不会有,声明了就会自动插值并传入 surf() 函数。

struct Input {
    float2 uv_MainTex;  // 会插入 TEXCOORD0
    float3 worldPos;    // 会插入 worldPos 并计算好
    float3 viewDir;     // 会插入 view direction
};

这个 Input 能不能自己声明并扩展?

可以修改、重命名、扩展字段,但:

  • 名字必须叫 Input(除非你通过 #pragma surface surf Standard input:MyInputStruct 修改绑定);

  • 字段名称最好用 Unity 支持的特定字段,如 uv_MainTex, worldPos,否则不会自动插值。

它和 appdata 有什么区别?

项目appdataInput
阶段顶点着色器输入片元阶段 surf() 输入
位置须使用 Unity 定义的 appdata_full 等结构可自己定义成员(受限)
内容顶点属性,如 POSITION, NORMAL, TEXCOORD0插值后的结果,如 worldPos, uv_MainTex

HLSL 支持的参数修饰符有: 

修饰符含义默认常用场合
in只读传入默认值常用于输入数据,如 position
out写出传值多输出目标
inout引用读写surf() 中的 o 参数

为什么另外的surface shader部分和outline pass都没有执行出来,这不是同一个shader里多个pass的执行先后顺序的问题,是renderqueue里,opaque 下前的物体shader在前

v2f vert(appdata_base v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex); // <- 这是拷贝值
    ...
    return o;
}
v 是结构体 appdata_base 的一个拷贝副本

即便你在函数中改变了 v.vertex、v.normal,也不会影响模型顶点本身

GPU 在每个顶点执行一次这个 vert 函数时,都会分发副本数据给它处理

  • in:只读引用(常规情况)

  • out:只写引用(函数返回该值)

  • inout:可读可写引用(传引用并修改)

surf()这种函数相当于自己的二次定义,所以实际上如果去掉前面的inout,就和appdata一样是一份值拷贝了

void surf(Input IN, SurfaceOutputStandard o)
那这个 o 变量就不再是引用,而是一份拷贝。结果是:

你对 o 所做的任何赋值和修改,都不会返回给 Unity 内部光照流程使用

不会影响最终的表面渲染结果,等于“白写”

这就和你在 vert(appdata v) 中拿到的是输入数据副本是一个道理。

float toon = floor(difLight * _Steps) / _Steps;
difLight = lerp(difLight, toon, _ToonEffect);
floor(difLight * _Steps) / _Steps 就是把连续的光照值 离散化(比如只分 3 阶灰度),这就是 toon 的精髓。

_ToonEffect 控制了“真实光照”和“toon 化光照”的权重插值,越接近 1 就越卡通。

Standard

使用 Unity Standard PBR 光照模型(Physically Based Rendering)。它和 Lambert, BlinnPhong 等传统模型不同,支持:

  • Metallic / Smoothness 工作流

  • Unity 内置的环境光照、反射探针、BRDF Lookup

  • 更真实的光照表现

noshadow

阻止 Unity 生成 ShadowCaster Pass。也就是说:

  • 该物体 不会投射阴影

  • 适用于一些特殊材质(如透明、效果 Shader)

addshadow

启用阴影接收支持,即使光照模型不是 Unity 默认支持的(如 Unlit 自定义光照函数):

  • 即使是 CustomLighting 模式也能接收阴影

  • 自动引入 UNITY_LIGHT_ATTENUATION 宏等逻辑

这在你使用 LightingXxx 自定义函数时尤为重要。

Unity 的 Surface Shader 会“自动生成”一组 Pass,包括:

  1. ForwardBase Pass

  2. ForwardAdd Pass(用于额外光源)

  3. ShadowCaster Pass(用于投射阴影) ← 默认自动生成

  4. Meta Pass(用于烘焙 GI / Lightmap)

  5. Deferred Pass(如果支持)

  6. Shadow Collector / DepthOnly(如果需要)

这全部是由 Surface Shader 的编译器生成的,你只需要写 #pragma surface ...,Unity 会自动编译出这些 pass

Unity 会根据 Pass 上的 Tags { "LightMode" = "Xxx" } 来决定每个用途该调用哪一个 Pass。

比如:

LightMode用途
ShadowCaster投射阴影时调用
ForwardBase渲染主光源 + 环境光
ForwardAdd渲染附加光源(加亮 Blend)
Meta用于 Lightmap 烘焙

只要你写的 Pass 有正确的 LightMode,Unity 就会使用它,而不使用自动生成的(或跳过生成)。

为什么花草 Shader 常常用 noshadow

1. 避免产生“不自然的投影块”

花草材质大多是用透明裁剪(AlphaTest)+ 单张贴图来模拟许多叶片或花瓣,但它们本身是一个扁平的面

如果它投射了阴影(默认会生成 ShadowCaster Pass):

  • 投射的阴影就是 这个面的一整块阴影

  • 即使你已经做了 clip()alphatest:_Cutoff,阴影通常仍然是硬边矩形块,而不是贴图上花瓣的形状。

  • 整个草坪会变得“不透气”、阴影非常“实”。

➡️ 所以很多项目会禁用花草投射阴影,让它们更通透自然

如果用了 alpha:blend强烈建议也写 RenderType="Transparent",否则 Unity 可能会在错误的渲染队列中处理该对象,导致 ZTest 错误或后处理丢失。

❌ 问题本质:

alpha:blend 自动设置了 ZWrite Off(关闭写入深度),这意味着:

虽然你加了 addshadowUnity 编译时仍不会生成 ShadowCaster Pass,或者生成了也不会工作,因为透明物体默认是不会参与阴影投射的。

clip() 本身不依赖 Tags

clip 本身不是赋值给谁,而是直接控制当前像素是否被丢弃,进而影响 Unity 是否生成阴影。

Tags 会影响 Unity 怎么处理这个 Shader 的不同 Pass,间接影响阴影是否生效。

Tags 作用是否影响阴影
"RenderType"="Transparent"✅ 可能让 Unity 忽略生成 ShadowCaster
"RenderType"="TransparentCutout"✅ Unity 认为这个 Shader 支持剪切透明,因此会自动生成 ShadowCaster

FallBack "Diffuse" 的作用:补充了一个默认不透明的 ShadowCaster

  • Diffuse 是 Unity 内置的一个旧式不透明 Shader

  • 默认携带了一个 ShadowCaster Pass,它用当前物体的网格来生成一个简单投影(通常是一个完整的几何阴影方块)

  • 所以你看到的是 fallback 提供的阴影,而不是你的主 shader 生成的阴影

手动添加 ShadowCaster Pass(适合你坚持用 alpha:blend

你可以在 SubShader 外手动添加:

Pass
{
    Name "ShadowCaster"
    Tags { "LightMode" = "ShadowCaster" }
    ZWrite On
    ZTest LEqual
    Cull Off

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #include "UnityCG.cginc"

    struct v2f {
        float4 pos : SV_POSITION;
    };

    v2f vert(appdata_base v)
    {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        return o;
    }

    float frag(v2f i) : SV_Target
    {
        return 0;
    }
    ENDCG
}

塞一个“实体轮廓”的阴影投射。

不过要注意,这会让透明部分也投影(不真实)

ShadowCaster Pass 的 fragment 函数根本不管你返回什么

在 Unity 的 ShadowCaster Pass 里:

最终决定是否投射阴影的,是你有没有写入深度值,而不是你 return 什么。

  • ShadowCaster Pass 是带有 LightMode = "ShadowCaster" 的 Pass。

  • 它的 fragment shader 通常返回的是 float 类型(不是颜色)。

  • 但这个返回值并不会写入屏幕颜色缓冲(RenderTarget),而是被忽略掉。

  • 真正关键的是该像素是否被 discard(或者 clip 掉),以及是否写入深度。

  • clip(...) 决定了某个 fragment 是否写入 ShadowMap

  • 被 clip 掉的 fragment 就像没存在一样 → 不参与遮挡光线

  • 没被 clip 掉的 → 写入 depth → 会在阴影中投射

为什么必须写INTERNAL_DATA?

因为 Unity 的 Surface Shader 编译器(不是我们写的 fragment shader)会依赖于 Input 结构中是否包含 INTERNAL_DATA 来:

  1. 决定是否生成对阴影、环境光、反射探针等的处理支持

  2. 能否正常使用 TRANSFER_SHADOWSHADOW_COORDS 等宏

如果你用了光照模型(如 StandardLambert),并想使用 Unity 的完整光照(阴影、实时光照、环境球),你必须声明 INTERNAL_DATA,否则 Unity 编译器就无法给你注入正确的数据流。

struct Input
{
    float2 uv_MainTex;
    float3 worldNormal;
    INTERNAL_DATA
};

void surf(Input IN, inout SurfaceOutputStandard o)
{
    fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
    o.Albedo = c.rgb;
    o.Alpha = c.a;
}
这段代码在 Unity 编译 Surface Shader 时,INTERNAL_DATA 会让 Input 带上 _ShadowCoord、worldPos 等字段(可能是 TEXCOORD2~TEXCOORD6),从而在后续光照阶段启用阴影和 GI。

这个宏做的事是:

传输从当前顶点到光源的 shadow map 坐标

在你的 Input 结构体中插入一个 float4 _ShadowCoord,用 TEXCOORD1 通道传输。

fixed3 wNormal = WorldNormalVector(IN, normalTex);

切线空间中的法线(normal map 提供的)转换成世界空间法线(world space normal)

IN

  • 是你的 Input 结构体的实例(传进来的插值数据)。

  • 它必须包含以下字段,供 WorldNormalVector 使用:

    • worldNormal:表面原始法线(Surface Shader 自动插入)

    • tangent(可选):如果你用切线空间 normal map

float3 worldN;
worldN.x = dot(_unity_tbn_0, o.Normal);
worldN.y = dot(_unity_tbn_1, o.Normal);
worldN.z = dot(_unity_tbn_2, o.Normal);
worldN = normalize(worldN);
o.Normal = worldN;

它们是 Unity 自动传给你的 TBN 矩阵行向量(通常是 float3):

名字含义
_unity_tbn_0Tangent 方向(T)
_unity_tbn_1Bitangent(B)
_unity_tbn_2Normal(N)

TBN 构成了从切线空间到世界空间的变换矩阵

你传入的 o.Normal 是在切线空间中采样出的法线(例如从 normal map 得到的 UnpackNormal() 值),例如 (0, 0, 1) 为“垂直贴图面”的默认方向。

Baked Indirect

  • 直射光: 实时计算(只针对动态物体)

  • 间接光: 来自 Lightmap

  • 阴影: 只有静态物体间的烘焙阴影,动态物体无法投影阴影也不会接收 Shadowmask 中的阴影

  • ✅ 用于性能优先、对阴影不敏感的场景

折射

				v2f vert(appdata v)
				{
					v2f o;
					o.vertex = UnityObjectToClipPos(v.vertex);
					o.uv = TRANSFORM_TEX(v.uv, _MainTex);
					o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
					o.worldNormal = UnityObjectToWorldNormal(v.normal);
					o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);

					o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefracRotio);

					UNITY_TRANSFER_FOG(o,o.vertex);
					return o;
				}

菲涅尔反射描述的是:光在穿过两种介质交界面时,反射与折射的能量比例会根据视角(入射角)变化。
它既不是单独的“反射”,也不是“折射”,而是决定“反射多少、折射多少”的规则

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值