Unity之表皮着色器 官方示例详解

下面是一些 表面着色器(Surface Shaders) 的示例。下面的示例都是使用的内置光照模式(lighting models),关于如何实现自定义光照模式可以参考 表面着色器光照范例(Surface Shader Lighting Examples)。

Simple 简单的示例

我们从分析和建立一个简单的着色器开始。下面是这个着色器仅仅设置了表面颜色( surface color)为"白色"。它使用了内置的 Lambert (diffuse)光照模式(lighting model)。
    
    
Shader "Example/Diffuse Simple" { SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM # pragma surface surf Lambert struct Input { float4 color : COLOR ; } ; void surf ( Input IN , inout SurfaceOutput o ) { o . Albedo = 1 ; } ENDCG } Fallback "Diffuse" }
注解:
    
    
struct SurfaceOutput { half3 Albedo ; //漫反射的颜色值 half3 Normal ; //法线坐标 half3 Emission ; //自发光颜色 half Specular ; //镜面反射系数 half Gloss ; //光泽系数 half Alpha ; //透明度系数 } ;
pragma surface surfaceFunction lightModel [optionalparams] surfaceFunction,没什么好说,肯定是函数名了。 lightModel是所采用的光照模型。可以自己写也可使用内置如Lambert和BlinnPhong. optionalparams:可选参数,一堆可选包括透明度,顶点与颜色函数,投射贴花shader等等。具体用到可以细选。
o.Albedo=1;漫反射的颜色是一个rgb值,如果给一个1,其实就是 float3(1,1,1),就是反射出来的颜色为白色, 如果为100,则是加强反射强度,并不会改变其颜色。为0或为负数时道理类似。 Fallback "Diffuse",Diffuse是自带的shader,可以用自己自定义好的。这里这句的意思是, 如果所有subshader在当前显卡都不支持,则默认返回自带的Diffuse。
下面是效果。它看起来像是在模型上设置了两个 光照(lights )。

Texture 纹理

看一个白模是相当枯燥的。所以我们来给它添加纹理(texture)。我们将在着色器的 属性(Properties )块中添加。在材质球(Material)中我们选择一个纹理(texture)。有变化的地方在下面用粗体字表示出来了。
    
    
Shader "Example/Diffuse Texture" { Properties { _MainTex ( "Texture" , 2D ) = "white" { } } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM # pragma surface surf Lambert struct Input { float2 uv_MainTex ; } ; sampler2D _MainTex ; void surf ( Input IN , inout SurfaceOutput o ) { o . Albedo = tex2D ( _MainTex , IN . uv_MainTex ) . rgb ; } ENDCG } Fallback "Diffuse" }
注解: Tex2D,这个方法是根据UV上的点找指定 2DSample上的Texture信息,此处需要其RGB信息, 就打出来赋给了漫反射值。所以对有材质图的情况下,要显示出图,还是要相应的反射其原图的rgb值。

Normal mapping 法线贴图

我们来添加一些法线贴图(normal map)。
    
    
Shader "Example/Diffuse Bump" { Properties { _MainTex ( "Texture" , 2D ) = "white" { } _BumpMap ( "Bumpmap" , 2D ) = "bump" { } } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM # pragma surface surf Lambert struct Input { float2 uv_MainTex ; float2 uv_BumpMap ; } ; sampler2D _MainTex ; sampler2D _BumpMap ; void surf ( Input IN , inout SurfaceOutput o ) { o . Albedo = tex2D ( _MainTex , IN . uv_MainTex ) . rgb ; o . Normal = UnpackNormal ( tex2D ( _BumpMap , IN . uv_BumpMap ) ) ; } ENDCG } Fallback "Diffuse" }
注解: 这个UnpackNormal是unity自带的标准解压法线用的,所谓解压,就是将法线的区间进行变换。由于 tex2D(_BumpMap, IN.uv_BumpMap)取出的是带压缩的[0,1]之间,需要转成[-1,1]。这个函数会针对移动平台或OPENGL ES平台采用 RGB法线贴图,其他采用DXT5nm贴图。因此也可自己写。

Rim Lighting 边缘光照

现在我们试着添加边缘光照(Rim Lighting),在对象的边缘部分增加亮度。我们要在表面法线(surface normal)和视图方向(view direction)的基础上添加散射光照(emissive light)。为了实现它,我们要使用 viewDir,这是表面着色器(surface shader)内置的一个变量。
    
    
Shader "Example/Rim" { Properties { _MainTex ( "Texture" , 2D ) = "white" { } _BumpMap ( "Bumpmap" , 2D ) = "bump" { } _RimColor ( "Rim Color" , Color ) = ( 0 . 26 , 0 . 19 , 0 . 16 , 0 . 0 ) _RimPower ( "Rim Power" , Range ( 0.5 , 8.0 ) ) = 3.0 } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM # pragma surface surf Lambert struct Input { float2 uv_MainTex ; float2 uv_BumpMap ; float3 viewDir ; } ; sampler2D _MainTex ; sampler2D _BumpMap ; float4 _RimColor ; float _RimPower ; void surf ( Input IN , inout SurfaceOutput o ) { o . Albedo = tex2D ( _MainTex , IN . uv_MainTex ) . rgb ; o . Normal = UnpackNormal ( tex2D ( _BumpMap , IN . uv_BumpMap ) ) ; half rim = 1.0 - saturate ( dot ( normalize ( IN . viewDir ) , o . Normal ) ) ; o . Emission = _RimColor . rgb * pow ( rim , _RimPower ) ; } ENDCG } Fallback "Diffuse" }
注解: viewDir 意为World Space View Direction。就是当前坐标的视角方向。
最里层是Normalize函数,用于获取到的viewDir坐标转成一个单位向量且方向不变,外面再与点的法线做点积。最外层再用 saturate算出一[0,1]之间的最靠近(最小值但大于所指的值)的值。这样算出一个rim边界。为什么这么做。原理以下解释: 1.这里o.Normal就是单位向量。外加Normalize了viewDir。因此求得的点积就是夹角的cos值。 2.因为cos值越大,夹角越小,所以,这时取反来。这样,夹角越大,所反射上的颜色就越多。于是就得到的两边发光的效果。哈哈这样明了吧。 half:CG里还有类似的float和fixed。half是一种低精度的float,但有时也会被选择成与float一样的精度。 fragment是一定会支持fixed类型,同时也会有可能将其精度设成与float一样,这个比较复杂,后面篇章学到fragment时再深入探讨。

Detail Texture 细节纹理

为了实现不同的效果。让我们来添加细节纹理(detail)。它是与基础纹理(base texture)的结合。细节纹理(detail texture)与基础纹理(base texture)使用相同的UV。但是在材质球(Material)中平铺(Tiling)值通常是不同的。所以我们必须输入结构(input structure)中使用不同的UV坐标。
    
    
Shader "Example/Detail" { Properties { _MainTex ( "Texture" , 2D ) = "white" { } _BumpMap ( "Bumpmap" , 2D ) = "bump" { } _Detail ( "Detail" , 2D ) = "gray" { } } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM # pragma surface surf Lambert struct Input { float2 uv_MainTex ; float2 uv_BumpMap ; float2 uv_Detail ; } ; sampler2D _MainTex ; sampler2D _BumpMap ; sampler2D _Detail ; void surf ( Input IN , inout SurfaceOutput o ) { o . Albedo = tex2D ( _MainTex , IN . uv_MainTex ) . rgb ; o . Albedo *= tex2D ( _Detail , IN . uv_Detail ) . rgb * 2 ; o . Normal = UnpackNormal ( tex2D ( _BumpMap , IN . uv_BumpMap ) ) ; } ENDCG } Fallback "Diffuse" }
注解: 在原先的反射基础上,在加一层,Texture的反射。
使用一个方格纹理(chercker texture)并没有实际的意义。但至少让我明白了会产生什么效果:

Detail Texture in Screen Space 在屏幕空间使用细节纹理

怎样在屏幕空间(screen space)使用细节纹理(detail texture)?它对一个士兵的头部模型没有多大意义。但它说明了在输入结构(input structure)中如何使用内置的screenPos。
    
    
Shader "Example/ScreenPos" { Properties { _MainTex ( "Texture" , 2D ) = "white" { } _Detail ( "Detail" , 2D ) = "gray" { } } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM # pragma surface surf Lambert struct Input { float2 uv_MainTex ; float4 screenPos ; } ; sampler2D _MainTex ; sampler2D _Detail ; void surf ( Input IN , inout SurfaceOutput o ) { o . Albedo = tex2D ( _MainTex , IN . uv_MainTex ) . rgb ; float2 screenUV = IN . screenPos . xy / IN . screenPos . w ; screenUV *= float2 ( 8 , 6 ) ; o . Albedo *= tex2D ( _Detail , screenUV ) . rgb * 2 ; } ENDCG } Fallback "Diffuse" }
注解: 是从上个例子的基础上将第二层叠加上的2D Texture根据当前屏幕的UV进行叠加,而不是根据自身的UV。这样带有含此shader材质的物体的贴图就会跟着移动到的位置而变换图片。 这里只需要说三点: 1.关于screenPos:screenPos是一个三维点,但是用齐次坐标的形式表示出来就是(x,y,z,w),根据齐次坐标的性质。 (x,y,z,w)的齐次坐标对应三维点(x/w,y/w,z/w)。因此把w值除掉可以看来是一种Normalize的作法,这样就取出了实际的屏幕 xy的UV值。 2.对screenUV进行倍剩:此处剩float2(8,6)意为将原获取到屏幕尺寸进行拉大的倍数。即x轴拉大8倍,y轴拉大6倍。 3.如何就平铺了刚好一行8个,一列6个了呢? 原因我觉得是在于2d Texture自己是按Normalize后进行铺的,因此在//2(刚转完标准的)screenPos后,将其剩多少即便将原图铺多少张。 这个东西可以拿来做放大镜的应用。
从着色器中删除法线贴图(normal mapping)只是为了试着色器代码简短一点。

Cubemap Reflection 立方图反射

在这里要在输入结构(input structure)中使用内置的worldRefl 来做立方图反射(cubemap reflection)。它实际是与内置的 Reflective/Diffuse着色器非常类似。
    
    
Shader "Example/WorldRefl" { Properties { _MainTex ( "Texture" , 2D ) = "white" { } _Cube ( "Cubemap" , CUBE ) = "" { } } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM # pragma surface surf Lambert struct Input { float2 uv_MainTex ; float3 worldRefl ; } ; sampler2D _MainTex ; samplerCUBE _Cube ; void surf ( Input IN , inout SurfaceOutput o ) { o . Albedo = tex2D ( _MainTex , IN . uv_MainTex ) . rgb * 0.5 ; o . Emission = texCUBE ( _Cube , IN . worldRefl ) . rgb ; } ENDCG } Fallback "Diffuse" }
因为它指定了和自发光(Emission)一样的反射颜色(reflection color),所以我们得到了一个非常有光泽的士兵。
如果你想在这基础上做反射效果,它是要受到法线贴图(normal map)的影响的。这需要在输入结构(input structure)中加入一个稍微复杂的 INTERNAL_DATA。在世界反射向量(WorldReflectionVector)函数中计算每个像素(per-pixel)反射向量(reflection vector),然后你要在输出结构(output structure)中写入法线(normal)。
    
    
Shader "Example/WorldRefl Normalmap" { Properties { _MainTex ( "Texture" , 2D ) = "white" { } _BumpMap ( "Bumpmap" , 2D ) = "bump" { } _Cube ( "Cubemap" , CUBE ) = "" { } } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM # pragma surface surf Lambert struct Input { float2 uv_MainTex ; float2 uv_BumpMap ; float3 worldRefl ; INTERNAL_DATA } ; sampler2D _MainTex ; sampler2D _BumpMap ; samplerCUBE _Cube ; void surf ( Input IN , inout SurfaceOutput o ) { o . Albedo = tex2D ( _MainTex , IN . uv_MainTex ) . rgb * 0.5 ; o . Normal = UnpackNormal ( tex2D ( _BumpMap , IN . uv_BumpMap ) ) ; o . Emission = texCUBE ( _Cube , WorldReflectionVector ( IN , o . Normal ) ) . rgb ; } ENDCG } Fallback "Diffuse" }
注解: 这两段都是加一个cubemap的反射。第二段相比之下是在有normal反射的基础上加的。Cubemap这东西,可设置几种面的不能渲染图,这方面可用于做天空盒。因为这样可以从各个角度看过去以显示不同的渲染效果。 以下说明:
  1. worldRefl:即为世界空间的反射向量。
  2. texCUBE:将反射向量一个个的往_Cube反射盒上找出然后做为Emission反射出来。
  3. 第二个例子只是将其用在Normal反射后,这样一定要多添加一个INTERNAL_DATA的属性,另外也需用到WorldReflectionVectore方法取其利用Normal后的反射向量值。 类似于的效果,可见官网中的。我这也有一个,有点像打了光的样子。
这是一个贴了法线贴图(normal map)的有光泽的士兵。

Slices via World Space Position 通过世界空间位置进行切割

在这个着色器里,被"切割" 所抛弃的像素(pixels)形状是几乎与水平位置平行的环状。它是基于世界位置的像索(pixel)通过使用Cg/HLSL语言的 clip()函数实现。我们将使用表面着色器(surface shader)内置的worldPos变量。
    
    
Shader "Example/Slices" { Properties { _MainTex ( "Texture" , 2D ) = "white" { } _BumpMap ( "Bumpmap" , 2D ) = "bump" { } } SubShader { Tags { "RenderType" = "Opaque" } Cull Off CGPROGRAM # pragma surface surf Lambert struct Input { float2 uv_MainTex ; float2 uv_BumpMap ; float3 worldPos ; } ; sampler2D _MainTex ; sampler2D _BumpMap ; void surf ( Input IN , inout SurfaceOutput o ) { clip ( frac ( ( IN . worldPos . y + IN . worldPos . z * 0.1 ) * 5 ) - 0.5 ) ; o . Albedo = tex2D ( _MainTex , IN . uv_MainTex ) . rgb ; o . Normal = UnpackNormal ( tex2D ( _BumpMap , IN . uv_BumpMap ) ) ; } ENDCG } Fallback "Diffuse" }
注解: frac是取小数的函数,如1.23 取出来是 0.23。clip函数用于清Pixel的,负值情况下才进行清pixel。且越小,即绝对值越大则清越多。 这里注意那个 5,仔细一想,如果frac出来的值越大,-0.5值就越大,绝对值就越小,因此这样清掉的pixel越少,所以就可以间接的增加分段的次数。那为什么要+IN.worldPos.z 0.1呢,主要原因就是空开的断添加一个倾斜角度,可以用空间思想想下。

Normal Extrusion with Vertex Modifier 法线挤压与顶点修改

它可以在顶点着色器(vertex shader)中使用"顶点修改(vertex modifier)"函数修改传入的顶点(vertex)数据。它能作用在程序动画上。比如顺着法线挤压等等。表面着色器(surface shader)是通过编译vertex:functionName函数指令来使用它。这个函数传入的参数是 inout appdata_full 。
这个着色器它在材质(material)里面顶点是随着法线变化的:
    
    
Shader "Example/Normal Extrusion" { Properties { _MainTex ( "Texture" , 2D ) = "white" { } _Amount ( "Extrusion Amount" , Range ( - 1 , 1 ) ) = 0.5 } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM # pragma surface surf Lambert vertex:vert struct Input { float2 uv_MainTex ; } ; float _Amount ; void vert ( inout appdata_full v ) { UNITY_INITIALIZE_OUTPUT ( Input , data ) ; v . vertex . xyz += v . normal * _Amount ; } sampler2D _MainTex ; void surf ( Input IN , inout SurfaceOutput o ) { o . Albedo = tex2D ( _MainTex , IN . uv_MainTex ) . rgb ; } ENDCG } Fallback "Diffuse" }
注解: 这是个自定义vertex的例子,效果可以实现点坐标的放大缩小,以形成肥仔与瘦棍的效果,哈哈。 添加一个可选参数为vertex,主要是为了给其添加一个函数vert。 _Amount对应开头的那个属性_Amount。具体是个Range值,可在shader界面外通过滑动条改变这个值。默认为0.5。 v.vertex.xyz += v.normal * _Amount;就是为个点,换当前法线向量的指定倍数进行扩展。 这里除了之前学过的东西外,多了个appdata_full的结构体。这里面的结构(载自UNITY官方论坛)如下:
    
    
struct appdata_full { float4 vertex : POSITION ; float4 tangent : TANGENT ; float3 normal : NORMAL ; float4 texcoord : TEXCOORD0 ; float4 texcoord1 : TEXCOORD1 ; fixed4 color : COLOR ; } ;
顶点随着它的法线(normal)变化后生成一个浮肿的士兵:

Custom data computed per-vertex 用自定义数据计算每个顶点变化

在一个顶点着色器(vertex shader)中通过计算自定义数据也可以实现上面的顶点修改函数(vertex modifier function)的效果。我们这就通过表面着色器函数(surface shader function)来计算每个顶点(per-vertex)变化。同样需要使用编译vertex:functionName这个函数指令。但是这次我们传入两个参数: inout appdata_full 和 out Input o 。在这里你能传入任何一个属于输入结构(input structure)的成员,而不是内置的值。
下面的例子定义了一个自定义的float3 customColor成员,它将在顶点函数(vertex function)中运算:
    
    
Shader "Example/Custom Vertex Data" { Properties { _MainTex ( "Texture" , 2D ) = "white" { } } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM # pragma surface surf Lambert vertex:vert struct Input { float2 uv_MainTex ; float3 customColor ; } ; void vert ( inout appdata_full v , out Input o ) { UNITY_INITIALIZE_OUTPUT ( Input , data ) ; o . customColor = abs ( v . normal ) ; } sampler2D _MainTex ; void surf ( Input IN , inout SurfaceOutput o ) { o . Albedo = tex2D ( _MainTex , IN . uv_MainTex ) . rgb ; o . Albedo *= IN . customColor ; } ENDCG } Fallback "Diffuse" }
注解: 这个例子是用来渲染颜色的: 取一个颜色值,float3,对应RGB。 较前个例子,多一个Input类型的参数,只为输出使用。 RGB颜色值当然只能为正值,所以使用绝对值去取normal的值。 在原先已经渲染上texture颜色值的基础上,加上这层自定义的颜色值。
在这个例子里customColor值设置的是法线(normal)的绝对值。
计算所有每个顶点(per-vertex)数据可以有更多的用途。它不是提供内置的输入结构(input structure)内的变量,或者优化着色器计算。例如:它可以计算在对象的顶点((vertex)上计算边缘光照(Rim lighting),而不是在表面着色器(surface shader)的每个像索(per-pixel)内做。

Final Color Modifier 最终颜色修改

这可以使用了一个"最终颜色修改(final color modifier)"函数,这个函数将通过着色器计算变化的最终颜色。为了实现它要使用finalcolor:functionName这个表面着色器(surface shader)编译命令。这个函数传入的参数是Input IN, SurfaceOutput o, inout fixed4 color 。
下面是一个简单的着色器(shader),它适用于给最终颜色着色。这是一个特别的仅适用于给表面反射率的颜色(surface Albedo color)着色。 这个色调也会影响任何颜色的光照贴图(lightmap), 光照探测(light probes)和类似的特别资源。
    
    
Shader "Example/Tint Final Color" { Properties { _MainTex ( "Texture" , 2D ) = "white" { } _ColorTint ( "Tint" , Color ) = ( 1.0 , 0.6 , 0.6 , 1.0 ) } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM # pragma surface surf Lambert finalcolor:mycolor struct Input { float2 uv_MainTex ; } ; fixed4 _ColorTint ; void mycolor ( Input IN , SurfaceOutput o , inout fixed4 color ) { color *= _ColorTint ; } sampler2D _MainTex ; void surf ( Input IN , inout SurfaceOutput o ) { o . Albedo = tex2D ( _MainTex , IN . uv_MainTex ) . rgb ; } ENDCG } Fallback "Diffuse" }
注解: 这个例子是跟上面例子的对比,前种使用普通反射进行叠加上颜色,此处则是直接使用finalcolor对其颜色进行处理,这种可以处理整个模型的固定颜色值的渲染。以下做简要的分析: 1.finalcolor:mycolor :这个是另一种可选参数,就是用户自定义的颜色处理函数。函数名为mycolor. 2.mycolor函数:注意到函数除了有surf的两个参数外,还多了个颜色参数,这个颜色参数就是当前模型上颜色对象,对他的更改将直接影响全部来自于lightmap,light probe和一些相关资源的颜色值。

Custom Fog with Final Color Modifier 自定义雾效与最终颜色修改

在共用最终颜色修改(final color modifier)的情况下将完全实现自定义雾效。雾效需要受到最终计算像索的着色器颜色的影响。这正是finalcolor做的。
下面这个着色器适用于给基于屏幕中心距离远近的雾效来着色。这结合了顶点修改(vertex modifier)与自定义顶点数据(custom vertex data)(雾效)还有最终颜色修改(final color modifier)。当附加到正向渲染(forward render)通道(pass)中使用时,雾效(Fog)需要淡入淡出黑色。在这个例子中控制它还不如检查 UNITY_PASS_FORWARDADD。
    
    
Shader "Example/Fog via Final Color" { Properties { _MainTex ( "Texture" , 2D ) = "white" { } _FogColor ( "Fog Color" , Color ) = ( 0.3 , 0.4 , 0.7 , 1.0 ) } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM # pragma surface surf Lambert finalcolor:final vertex:vert struct Input { float2 uv_MainTex ; half fog ; } ; void vert ( inout appdata_full v , out Input data ) { UNITY_INITIALIZE_OUTPUT ( Input , data ) ; float4 hpos = mul ( UNITY_MATRIX_MVP , v . vertex ) ; data . fog = min ( 1 , dot ( hpos . xy , hpos . xy ) * 0.1 ) ; } sampler2D _MainTex ; void surf ( Input IN , inout SurfaceOutput o ) { o . Albedo = tex2D ( _MainTex , IN . uv_MainTex ) . rgb ; } fixed4 _FogColor ; void final ( Input IN , SurfaceOutput o , inout fixed4 color ) { fixed3 fogColor = _FogColor . rgb ; #ifdef UNITY_PASS_FORWARDADD fogColor = 0 ; # endif color . rgb = lerp ( color . rgb , fogColor , IN . fog ) ; } ENDCG } Fallback "Diffuse" }
注解: mul是矩阵相乘的函数。UNITY_MATRIX_MVP是model、view、projection三个矩阵相乘出来的4x4的 矩阵。v.vertex是一个float4的变量,可理解成4x1的矩阵,两者相乘,则得出一个float4,这个值就是视角窗口的坐标值,这个坐标就跟 camera的关联了。 这个fog的浮点值就是其强度,范围一般在-1到1之间,说一般,只是我个人建议的值,设成其他也行,只是没多大意义。越负就越黑。再 看后面这个点积,这个仔细一想,不难理解,其实就是为了达到一种扩散的效果,因此两个一样的向量相乘,其实就是直接对坐标做平方扩展,这样fog就更有雾 的感觉。 这个宏不好找,就看官方对这个例子的解释为正向渲染时的额外通道。字面不好理解,多多尝试过可以有所发现,其实就是在雾气渐渐消失处那 块额外的渲染区。可以将fogColor = 0; 改成fogColor = fixed3(1,0,0)。外面雾气颜色再选成白色,雾气改成绿色后,效果则如下. lerp函数是个有趣的函数。第一个参数是左边界,第二个参数是右边界,第三个相当于一个值介于0到1之间的游标。游标为0,则为左边 界,为1为右边界,取中间值则是以此类推,取插值。其实也可以把它看成百分比。这里的fog则可以看来那个游标,值越大,则越接近fogColor,越小 越接近原色。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值