光照和阴影概念

1.光照模型

        光线由光源发射出来会与其他物体相交,相交结果有两个:散射、吸收。散射只改变光线方向,但不改变光线密度和颜色,散射到物体内部称为折射,到物体外部为反射。吸收只改变光线密度和颜色,但不改变光线方向

        光照模型是指根据材质属性(漫反射属性)、光源信息(光源方向、辐射度等),使用公式去计算沿某个观察方向的着色过程,在标准光照中,把进入摄像头的光线4个部分:

  • 环境光(ambient)

        用于描述其他所有间接光照。环境光非常简单,通常是个全局变量,即场景中所有物体都使用这个环境光:

\large \boldsymbol{c}_{\text {emissive }}=\boldsymbol{m}_{\text {emissive }}

  • 自发光(cmissive)

        用于描述给定方向时,表面本身会向发射发射辐射量,如果没有使用全局光照技术,自发光表面不会真的照亮周围物体,而是本身看起来更亮。它的计算很简单,使用该材质的自发光颜色:

\large \boldsymbol{c}_{\text {emissive }}=\boldsymbol{m}_{\text {emissive }}

  • 漫反射(diffuse)

        用于描述当光线从光源照射到模型表面时,该表面会向每个方向散射多少辐射量,漫反射符合兰伯特定律,反射光线强度与表面法线和光源方向的夹角的余弦值乘正比,因此漫反射公式:

\large \boldsymbol{c}_{\text {diffuse }}=\left(\boldsymbol{c}_{\text {light }} \cdot \boldsymbol{m}_{\text {diffuse }}\right) \max (0, n \cdot I)

Value公司开发《半条命》提出新技术,在Phong光照模型上简单修改,半兰伯特光照模型:

\large \mathbf{c}_{\text {diffuse}}=\left(\boldsymbol{c}_{\text {light }} \cdot \boldsymbol{m}_{\text {diffuse}}\right) \(\alpha \( \hat{\mathbf{n}} \cdot \mathbf{I})+\beta )

m_{\text {fiffuse }}是材质的漫反射颜色。

\boldsymbol{c}_{\text {light}}则是光源颜色和强度。

I是向光源的单位矢量。

n是表面法线。

\large \alpha\large \beta绝大部分情况下的值均为0.5:

fixed halfLambert = dot(worldNormal,worldLightDir) * 0.5 + 0.5;
fixed3 color = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
  • 高光反射(specular)

        用于描述当光线从光源照射到模型时,该表面会在完全反射方向散射多少辐射量,计算高光反射需要知道的信息比较多,如表面法线、视角方向、光源方向、反射方向等,计算反射方向示意图如下:

\large \mathbf{r}=2(\hat{\mathbf{n}} \cdot \mathbf{I}) \hat{\mathbf{n}}-\mathbf{I}

Phong模型计算高光反射:

        \large \mathbf{c}_{\text {spscular }}=\left(\boldsymbol{c}_{\text {light }} \cdot \boldsymbol{m}_{\text {specular }}\right) \max (0, \hat{\mathbf{v}} \cdot \mathbf{r})^{m_{\text {gloss }}}

Blinn提出简单方法得到类似效果,Blinn模型引入了单位矢量h:

 \large \hat{\mathbf{h}}=\frac{\hat{\mathbf{v}}+\mathrm{I}}{|\hat{\mathbf{v}}+\mathrm{I}|}

Blinn模型计算高光反射:

\large \boldsymbol{c}_{\text {specular }}=\left(\mathbf{c}_{\text {light }} \cdot \boldsymbol{m}_{\text {specular }}\right) \max (0, \hat{\mathrm{n}} \cdot \hat{\mathrm{h}})^{m_{\text {glass }}}

m_{\text {gloss }}是材质光泽度(gloss)也被称为反光度。它用于控制高光区域的亮点有多宽。

\boldsymbol{m}_{\text {spscular }}是材质的高光反射颜色,控制高光反射的强度和颜色。

\boldsymbol{c}_{\text {light}}则是光源颜色和强度。

h是世界光源方向和视角观察方向相加后归一化的单位向量,具体片段代码如下:

// Get the half direction in world space
fixed3 halfDir = normalize(worldLightDir + viewDir)

// Compute specular term
fiexed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir,halfDir)),_Gloss)

        标准光照模型只是经验模型,它并不完全符合真实世界中的光照现象。但由于它的易用性、计算速度和效果都比较好,因此仍然被广泛使用。但Phong模型有很多局限性。例如菲涅耳反射。其次Blinn-Phong 模型是各项同性的,当固定视角和光源方向旋转这个表面时,例如拉丝金属、毛发等,反射不会发生任何改变。

2.逐顶点或逐像素光照

         给出基本光照模型数学公式后,在哪里计算这些光照模型呢?通常来讲,我们有两种选择:在片元着色器中计算,也被称为逐像素光照(per—pixel lighting);在顶点着色器中计算,也被称为逐顶点光照(per—vertexlighting)。

        在逐像素光照中,以每个像素为基础,得到它的法线(对顶点法线插值得到的,也可以是从法线纹理中采样得到的),然后进行光照模型计算。这种在面片之间对顶点法线进行插值的技术被称为Phong着色(Phong shading),这不同于我们之前讲到的Phong光照模型。

v2f vert(a2v v) {
    v2f o;
    // Transform the vertex from object space to projection space 
    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

    // Transform the normal fram object space to world space 
    o.worldNormal = mul(v.normal,(float3x3)_World20bject); 
    return o;
}

fixed4 frag(v2f i):SV_Target{

    // Get ambient term
    fixed3 ambient =UNITY_LIGHTMODEL_AMBIENT.xyz;

    // Get the normal in world space
    fixed3 worldNormal=normalize(i.worldNormal);

    // Get the light direction in world space
    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

    // Compute diffuse term
    fixed3 diffuse =_LightColor0.rgb *_Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

    // Get the reflect direction in world space
    fixed3 reflectDir = normalize(reflect(-worldLightDir,worldNormal));
    
    // Get the view direction in world space
    fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz)
       
    // Compute specular term
    fiexed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir,viewDir)),_Gloss)

    fixed3 color = ambient + diffuse + specular;

    return fixed4(color, 1.0);
} 

        在逐顶点光照中,在每个顶点计算光照,然后会在光栅化阶段进行线性插值,最后输出成像素颜色。由于顶点数目往往远小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照。但是逐顶点光照依赖于线性插值来得到像素光照,这会导致宣染图元内部的颜色总是暗于顶点处的最高颜色值,这在某些情况下会产生明显的棱角现象。

v2f vert(a2v v){
    v2f o;
    // Transform the vertex from object space to projection space 
    o.pos = mul(UNITY_MATRIX_MVP,v.vertex);

    // Get ambient term
    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

    // Transform the normal fram object space to world space
    fixed3 worldNormal = normalize(mul(v.normal, (float3x3)_World20bject)); 

    // Get the light direction in world space
    fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz); 

    // Compute diffuse term
    fixed3 diffuse =_LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));

    // Get the reflect direction in world space
    fixed3 reflectDir = normalize(reflect(-worldLightDir,worldNormal));
    
    // Get the view direction in world space
    fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(_Object2World, v.vertex).xyz)
       
    // Compute specular term
    fiexed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir,viewDir)),_Gloss)

    o.color= ambient + diffuse + specular; 
    return o;
}

注:在Pass使用的光照模式:Tags {"LightMode" ="ForwardBase" } 

3.Unity的渲染路径

    在Unity里,渲染路径决定了光照是如何应用到UnityShader中的。因此要和光源打交道,需要为每个Pass指定它的渲染路径,Unity会通过渲染路径把光源和处理后的光照信息都放在这些数据里,正确选择和设置了渲染路径,Shader光照计算才能被正确执行。

  • 前向渲染

        对于前向渲染来说,Unity Shader通常会定义Base Pass(Base Pass 可以定义多次,例如双面渲染等情况)以及Additional Pass。Base Pass 仅会执行一次(定义多个Base Pass情况外),而Additional Pass会根据影响该物体的其他逐像素光源的数目被多次调用,即每个逐像素光源会执行Additional Pass

        每进行完整的前向渲染,需要计算两个缓冲区的的信息(颜色缓冲区、深度缓冲区)。利用深度缓冲来决定片元是否可见,然后决定更新颜色缓冲区中的颜色值。前向渲染路径大致过程的伪代码如下:

Pass{
    for(each primitive in this model){
        for(each fragment covered by this primitive){
            if(failedin depthtest){ //深度测试函数可自定义
                //如果没有通过深度测试,说明该片元是不可见的
                discard;
            }else{
                //准备光照相关参数
                workLightParams;
                //自定义的顶点着色器函数
                struct v2f = VertexShading(materialInfo, pos, normalightDir, viewDir, lightParams);
                //自定义的片元着色器函数
                float4 color= FragmentShading(v2f, materialInfo, pos, normalightDir, viewDir, lightParams);
                //更新帧缓冲
                writeFrameBuffer(fragment,Color);
            } 
        }
    }
}
  • 顶点照明渲染

        对硬件配置要求最少、运算性能最高,但得到的效果也是最差的,不支持那些逐像素才能得到的效果,例如阴影、法线映射、高精度的高光反射等。实际上,它仅仅是前向渲染路径子集,也就是说使用顶点照明渲染路径,那么Unity会只填充那些逐顶点相关的光源变量,意味着不可以用一些逐像素光照变量

  • 延迟渲染

        当场景中包含大量实时光源时,前向渲染的性能会急速下降。例如在场景的某块区域放置多个光源,这些光源影响的区域互相重叠,那么为了得到最终的光照效果,为该区域内的每个物体执行多个Pass来计算不同光源对该物体的光照结果,然后在颜色缓存中把这些结果混合起来得最终光照。然而每执行Pass都需要重新渲染一遍,但很多计算实际上是重复的。
        延迟渲染是古老的渲染方案,但由于上述前向染可能造成的瓶颈问题,近几年又流行起来。延迟渲染还会利用额外的缓冲区,被统称为G缓冲(G-buffer),G缓冲区存储了渲染关心的表面信息(通常指离摄像机最近的表面),例如该表面法线、位置、用于光照计算的材质属性等
        延迟渲染原理主要包含了两个Pass。在第一个Pass中,不进行任何光照计算,仅仅计算哪些片元是可见的,通过深度缓冲技术来实现,当发现片元是可见的,把相关信息存储到G缓冲区中。第二个Pass中,利用G缓冲区各个片元信息,例如表面法线、视角方向、漫反射系数等,进行真正的光照计算,延迟渲染路径大致过程的伪代码如下:

Pass 1{
    //
    //第一个Pass不进行真正的光照计算,仅仅把光照计算需要的信息存储到G缓冲中
    fo(each primitive in this model){
        for(each fragment covered by this primitive){
            if(failedin depth test){
                //如果没有通过深度测试,说明该片元是不可见的
                discard;
            }else{
                //如果该片元可见就把需要的信息存储到G缓冲中
                writeGBuffer(materialInfo,posnormal,lightDir,viewDir);
            }
        }
    }
}

Pass 2{
    //利用G缓冲中的信息进行真正的光照计算 
    for(each pixel in thescreen){
        if(the pixelisvalid){
             //如果该像素是有效的,读取它对应的G缓冲中的信息
             readGBuffer(pixel,materialInfor posnormal lightDirviewDir);
             //自定义的顶点着色器函数
             struct v2f = VertexShading(materialInfo, pos, normalightDir, viewDir, lightParams);
             //自定义的片元着色器函数
             float4 color= FragmentShading(v2f, materialInfo, pos, normalightDir, viewDir, lightParams);
             //更新缓冲
             writeFrameBuffer(pixelcolor);
        }
    }
}

4.Unity的阴影设置

Unity阴影实现原理:(1条消息) 纹理原理和应用_臣定保幼主周全ぃ的博客-CSDN博客

此章节主要介绍阴影设置和内置宏计算光源阴影的作用,阴影设置(MeshRenderer)窗口如下:

其中难理解的是Two Sided选型,可以创建两个Plane相互垂直,然后平行光背面斜照向垂直Plane,观察地面Plane是否存在阴影?经过测试发现On选型是没有阴影生成的。

主要原因是背面剔除后,不存在任何顶点信息,因此不会写入ShadowMap。这种情况有两种解决方案:去掉剔除(Cull off)、使用Two Sided选型

        Cast Shadow开启时,此Mesh就会参与ShadowMap深度写入,另外Shader要添加一个LightMode为ShadowCaster的Pass,需要考虑透明混合和透明测试情况,由于透明度混合关闭深度写入,所以无法产生阴影,需要在每个光源空间下严格按照从后往前顺序进行渲染,这让阴影处理变得非常复杂,而且也会影响性能,虽然使用 dirty trick 可以强制为半透明物体生成阴影。透明测试相对简单,Fallback为Transparent Cutout Vertex-Lit,由于内部使用_Cutoff变量进行透明测试判断,所以创建的Shader需要定义同名变量。透明镂空顶点光照的如下:

Shader "Custom/Transparent/Cutout/VertexLit" {
	Properties{
		_Color("Main Color", Color) = (1,1,1,1)
		_SpecColor("Spec Color", Color) = (1,1,1,0)
		_Emission("Emissive Color", Color) = (0,0,0,0)
		_Shininess("Shininess", Range(0.1, 1)) = 0.7
		_MainTex("Base (RGB) Trans (A)", 2D) = "white" {}
		_Cutoff("Alpha cutoff", Range(0,1)) = 0.5
	}

	SubShader{
		Tags {"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
		LOD 100

		// Non-lightmapped
		Pass {
			Tags { "LightMode" = "Vertex" }
			Alphatest Greater[_Cutoff]
			AlphaToMask True
			ColorMask RGB
			Material {
				Diffuse[_Color]
				Ambient[_Color]
				Shininess[_Shininess]
				Specular[_SpecColor]
				Emission[_Emission]
			}
			Lighting On
			SeparateSpecular On
			SetTexture[_MainTex] {
				Combine texture * primary DOUBLE, texture * primary
			}
		}

		// Lightmapped, encoded as dLDR
		Pass {
			Tags { "LightMode" = "VertexLM" }
			Alphatest Greater[_Cutoff]
			AlphaToMask True
			ColorMask RGB

			BindChannels {
				Bind "Vertex", vertex
				Bind "normal", normal
				Bind "texcoord1", texcoord0 // lightmap uses 2nd uv
				Bind "texcoord", texcoord1 // main uses 1st uv
			}
			SetTexture[unity_Lightmap] {
				matrix[unity_LightmapMatrix]
				constantColor[_Color]
				combine texture * constant
			}
			SetTexture[_MainTex] {
				combine texture * previous DOUBLE, texture * primary
			}
		}

		// Lightmapped, encoded as RGBM
		Pass {
			Tags { "LightMode" = "VertexLMRGBM" }
			Alphatest Greater[_Cutoff]
			AlphaToMask True
			ColorMask RGB

			BindChannels {
				Bind "Vertex", vertex
				Bind "normal", normal
				Bind "texcoord1", texcoord0 // lightmap uses 2nd uv
				Bind "texcoord1", texcoord1 // unused
				Bind "texcoord", texcoord2 // main uses 1st uv
			}

			SetTexture[unity_Lightmap] {
				matrix[unity_LightmapMatrix]
				combine texture * texture alpha DOUBLE
			}
			SetTexture[unity_Lightmap] {
				constantColor[_Color]
				combine previous * constant
			}
			SetTexture[_MainTex] {
				combine texture * previous QUAD, texture * primary
			}
		}

		// Pass to render object as a shadow caster
		Pass {
			Name "Caster"
			Tags { "LightMode" = "ShadowCaster" }

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

			struct v2f {
				V2F_SHADOW_CASTER;
				float2  uv : TEXCOORD1;
			};

			uniform float4 _MainTex_ST;

			v2f vert(appdata_base v)
			{
				v2f o;
				TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				return o;
			}

			uniform sampler2D _MainTex;
			uniform fixed _Cutoff;
			uniform fixed4 _Color;

			float4 frag(v2f i) : SV_Target
			{
				fixed4 texcol = tex2D(_MainTex, i.uv);
				clip(texcol.a* _Color.a - _Cutoff);
				SHADOW_CASTER_FRAGMENT(i)
			}
			ENDCG
		}
	}
}

而Receive Shadow决定是否获取阴影系数(阴影纹理进行深度测试,返回0说明处于阴影),最后在漫反射、高光反射乘上阴影系数。Shader代码段如下:

fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);

        SHADOW_COORDSTRANSFER_SHADOWSHADOW_ATTENUATION是计算阴影时的“三剑客”。这些内置宏用于计算光源的阴影,在AutoLight.cginc声明。SHADOW_COORDS在顶点着色器的输出结构体中添加,用于阴影纹理的保存。

struct v2f{
    float4 pos:SV_POSITION;
    float3 worldNormal :TEXCOORDO; 
    float3 worldPos :TEXCOORD1;
    SHADOW_COORDS(2)//纹理序列0、1已被占用,阴影纹理使用TEXCOORD2
}; 

TRANSFER_SHADOW执行ShadowMap深度保存,SHADOW_ATTENUATION执行深度测试获取阴影系数,在Shader中调用代码如下:

v2f vert(a2v v) 
{
    v2f o;
    // Pass shadow coordinates to pixel shader 
    TRANSFER_SHADOW(o);
    return o;
}

fixed4 frag(v2f i) : SV_Target {
    // Use shadow coordinates to sample shadow map 
    fixed shadow = SHADOW_ATTENUATION(i); 
}

由于上述宏会使用上下文变量进行相关计算,必须保证a2f结构体中的顶点坐标变量名为vertex,顶点着色器的输出结构体v2f命名为v,且v2f中顶点位置为pos

UNITY_LIGHT_ATTENUATION是Unity内置的用于计算光照衰减和阴影的宏,用于代替SHADOW_ATTENUATION,具体Shader代码如下:

fixed4 frag (v2f i) : SV_Target {
    // UNITY LIGHT ATTENUATION not only compute attenuation, but also shadow infos 
    UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
    return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}

5.UnityShader标签、变量、函数

LightMode标签支持的渲染路径
标签名描述
Always不管使用哪种渲染路径,该Pass总是会被渲染,但不会计算任何光照
ForwardBase用于前向渲染。该Pass会计算环境光、最重要的平行光、逐顶点/SH光源和 Lightmaps
ForwardAdd用于前向渲染。该Pass会计算额外的逐像素光源,每个Pass对应一个光源
Deferred用于延迟渲染。该Pass会渲染G缓冲(G-buffer)
ShadowCaster把物体的深度信息渲染到阴影映射纹理(shadowmap)或一张深度纹理中
PrepassBase用于遗留的延迟渲染。该Pass会渲染法线和高光反射的指数部分
PrepassFinal用于遗留的延迟渲染。该 Pass通过合并纹理、光照和自发光来渲染得到最后的颜色

Vertex

用于遗留的顶点照明渲染
渲染路径内置的光照计算相关变量
名 称类 型描 述
前向渲染路径的内置变量
LightColor0float4该Pass处理的逐像素光源的颜色
LightColor0float4该Pass处理的逐像素光源的颜色
_WorldSpaceLightPos0float4_WorldSpaceLightPos0.xyz 是该Pass处理的逐像素光源的位 置。如果该光源是平行光,那么_ _WorldSpaceLightPos0.w是0, 其他光源类型w值为1
_LightMatrix0float4X4从世界空间到光源空间的变换矩阵。可以用于采样cookie和光 强衰减(attenuation) 纹理

unity_4LightPosX0, unity_4LightPosY0,

unity_4LightPosZ0

float4仅用于Base Pass。前4个非重要的点光源在世界空间中的位置
unity_4LightAtten0float4仅用于Base Pass。 存储了前4个非重要的点光源的衰减因子
unity_LightColorhalf4[4]仅用于Base Pass。 存储了前4个非重要的点光源的颜色
顶点照明渲染路径的内置变量
unity_LightColorhalf4[8]光源颜色
unity_LightPositionfloat4[8]xyz分量是视角空间中的光源位置。如果光源是平行光,那么分量值为0,其他光源类型z分量值为1
unity_LightAttenhalf4[8]光源衰减因子。如果光源是聚光灯,分量是cos(spotAngle/2),y分量是1/cos(spotAngle/4);如果是其他类型的光源,分量是-l,y分量是1。z分量是衰减的平方,W分量是光源范围开根号的结果
unity_SpotDirectionfloat4[8]如果光源是聚光灯的话,值为视角空间的聚光灯的位置;如果是其他类型的光源,值为(0,0,1,0)
延迟渲染路径的内置变量
_LightColorfloat4光源颜色
_LightMatrix0float4X4从世界空间到光源空间的变换矩阵。可以用于采样cookie和光强衰减纹理
内置帮助函数
函数名功能
实用内置函数
float saturate(float)使值范围取到[0,1]
float3 WorldSpaceViewDir(float4 v)输入模型空间顶点位置,返回世界空间从摄像机到该点观察方向,内部使用了UnityWorldSpaceViewDir
float3 UnityWorldSpaceViewDir(float4 v)输入世界空间顶点位置,返回世界空间从摄像机到该点观察方向
float3 ObjectSpaceViewDir(float4 v)输入模型空间顶点位置,返回模型空间从摄像机到该点观察方向
float3 UnityObjectToWorldNormal(float3 norm)把法线方向从模型空间转换到世界空间中
float3 UnityObjectToWorldDir(in float3 dir)把方向矢量从模型空间变换到世界空间中
float3 UnityWorldToObjectDir

把方向矢量从世界空间变换到模型空间中

前向渲染内置光照函数
float3 WorldSpaceLightDir (float4 v)仅可用于前向渲染中。输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向。内部使用UnityWordSpaceLightDir函数视线。没有被归一化
float3 UnityWorldSpaceLightDir (float4 v)仅可用于前向渲染中。输入一个世界空间中的顶点位置,返回世界空间中从该点到光源的光照方向。没有被归一化
float3 ObjSpaceLightDir(float4 v)仅可用于前向渲染中。输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向。没有被归一化
float3 Shade4PointLight (...)仅可用于前向渲染中。计算四个点光源的光照,它的参数是已经打包进矢量的光照数据,通常就是表9.2中的内置变量,如unity_4LightPosX0, unity_4LightPosY0, uity_4Lightposz0. uniy_ighgcColor和uity4Lighttn0等。前向渲染通常会使用这 个函数来计算逐顶点光照
顶点渲染内置光照函数
float3 ShadeVertexLights (float4 vertex,float3 normal)输入模型空间中的顶点位置和法线,计算四个逐顶点光源的光照以及环境 光。内部实现实际上调用了ShadeVertexLightsFull函数
float3 ShadeVertexLightsFull(float4 vertex, float3 normal, int lightCount, bool spotLight)输入模型空间中的顶点位置和法线,计算lightCount个光源的光照以及环境光。如果spotLight值为 true,那么这些光源会被当成聚光灯来处理,虽然结果更精确,但计算更加耗时;否则,按点光源处理

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值