4.基础纹理

  • 纹理的目的:使用一张图片来控制模型的外观
  • 纹理映射技术:把一张图“黏”在模型表面,逐纹素(与像素不同)地控制模型颜色
  • 通常在建模软件中利用纹理展开技术实现,把纹理映射坐标存储在每个顶点上
  • 纹理映射坐标:定义了该顶点在纹理中对应的2D坐标
  • 纹理映射坐标也称UV坐标用二维变量(u,v)表述,u代表横坐标 v代表纵坐标
  • 纹理的大小多种多样,但顶点UV坐标中被归一化到[0,1]范围
  • 纹理采样时使用的纹理坐标不一定在[0,1]
  • OpenGL与DirectX的纹理空间坐标不同,unity中符合OpenGL,原点在左下角

一、单张纹理

纹理类型中需要声明一个float4类型的变量_MainTex_ST,其中_MainTex_ST的名字不是任意起的,在unity中,需要使用纹理名_ST的方式来声明某个纹理的属性。

ST是缩放(scale)和平移(translation)的缩写

_MainTex_ST可以让我们得到该纹理的缩放和平移(偏移)值

_MainTex_ST.xy存储的是缩放值,_MainTex_ST.zw存储的是偏移值

这些值可以在材质面板的纹理属性中调节

1.实践

效果如下:

全部代码:

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Shader/SingleTexture"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color ("Color", Color) = (1,1,1,1)
        _Specular ("Specular", Color) = (1,1,1,1)
        _Gloss ("Gloss", Range(8.0, 256)) = 20 
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Tags{"LightMode"="ForwardBase"}//LightMode标签是Pass标签中的一种,用于定义该Pass在unity的光照流水线中的角色
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Color;
            fixed4 _Specular;
            float _Gloss;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.uv = v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo *
                                 max(0, dot(worldNormal, worldLightDir));

                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * 
                                  pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                return fixed4(ambient + diffuse + specular, 1.0);
            }
            ENDCG
        }
    }
    Fallback "Specular"
}

2.纹理属性

我们向unity中导入一张纹理资源,其材质面板上的参数如下,我们可以调节他们

Texture Type 纹理类型:选择合适的纹理类型,才能让unity知道我们的意图,传递正确的纹理,或对纹理进行优化

Wrap Mode:决定了当纹理坐标超过[0,1]范围后如何被平铺。

Wrap Mode有两种模式:

Repeat,如果纹理超过1,那么它的整数部分将会被舍弃,直接使用小数部分采样。这样的结果是纹理将会不断重复。

Clamp,如果纹理大于1,那么将会截取到1,如果小于0,那么就会截取到0

Filter Mode:决定了当纹理由于变化而产生拉伸时将会采用哪种滤波模式

Filter Mode有三种模式:Point、Bilinear、Trilinear。它们得到的图片滤波效果依次提升,但耗费的性能也依次增大。纹理滤波会影响放大或缩小纹理时得到的图片质量。

如:把64x64大小的纹理贴在512x512大小的平面上时,就需要放大纹理

mip map:多级渐远纹理:在一个很小的空间中有许多东西

多级渐远纹理技术将原纹理提前用滤波处理来得到很多更小的图像,形成了一个图像金字塔,每一层都是对上一层图像采样的结果。这样实时运行,就可以快速得到结果像素。

如当物体原理相机时,就用较小的纹理

缺点是需要一定空间存储,通常会多占用33%的内存空间

 

  •  Point:最邻近滤波。在放大或缩小时,它的采样数目通常只有一个,因此图像会看起来有种像素风格
  • Bilinear:线性滤波。对于每个目标像素,会找到4个临近像素,然后对它们进行线性插值混合得到最终像素,因此图像看起来像被模糊了
  • Trilinear:几乎和Bilinear一样,只是Trilinear还会在多级渐远纹理之间进行混合。如果一张纹理没有使用多级渐远纹理技术,那么Trilinear得到的结果和Bilinear一样

一般情况下选择Bilinear模式,注意,有时我们不希望纹理看起来是模糊的,例如对于一些类似棋盘的纹理,这是就希望是像素风,那么就用Point模式

不同平台发布游戏时,需要考虑目标平台的纹理和质量问题。

如果导入的纹理超出最大尺寸,那么unity会把该纹理缩放为这个最大分辨率。

理想情况下导入的纹理可以是非正方形的,但长宽的大小应该是2的幂。2、4、8、16、32、64

如果使用非2的幂大小(NPOT),那么纹理会占用跟多的内存空间,而且GPU读取该纹理的速度也会有所下降

一些平台不支持NPOT纹理,unity会在内部将它缩放为2的幂,为了性能,请尽量使用2的幂大小

Format:决定了unity内部用哪种格式存储该纹理。使用的纹理格式精度越高,占用的内存空间越大,得到的效果越好。

在最下方可以查看存储该纹理需要占用的内存空间(若开启多级渐远纹理技术,也会增加纹理的内存占用)。

二、凹凸纹理(bump mapping)

凹凸纹理:使用一种纹理来修改模型表面的法线,以便为模型提供更多细节。

并不会真的改变模型顶点位置,只是让模型看起来凹凸不平,但在模型的轮廓处可以看出破绽

1.高度纹理

使用一张高度纹理(height map)来模拟表面位移,然后得到一个修改后的法线值,这种方法也被称为高度映射。

高度图中存储的是强度值(intensity),它用于表示模型表面局部的海拔高度。

颜色越浅,越向外凸起,颜色越深,越向内凹陷

好处:直观,可以从高度图中明确地知道每个模型的表面凹凸情况

缺点:计算更复杂,在实时计算时不能直接得到表面法线,而需要灰度值计算而得到,需要消耗更多的性能

高度图一般与法线映射一起使用,用于给出表面凹凸的额外信息。我们通常会用法线映射案例修改光照

2.法线纹理

使用一张法线纹理来直接存储表面法线,也被称为法线映射。

法线纹理中存储的就是表面的发现方向,由于法线方向的分量范围在[-1,1],而像素的分量范围为[0,1],因此我们需要做一个映射,通常的映射方法:

pixel=\frac{normal+1}{2}

在shader中对纹理法线进行纹理采样后,还需要对结果进行一次反映射的过程,以得到原先的法线方向。反映射即是映射函数的逆函数:

normal=pixel\times 2-1

模型顶点自带的法线,它们定义子啊模型空间中,因此一种直接的想法就是将修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称为模型空间的法线纹理。

切线空间:模型的每个顶点都有属于自己的切线空间,这个切线空间的原点就是该顶点本身,而z轴是顶点的法线方向(n),x轴是顶点的切线方向(t),而y轴可由法线和切线叉积而得,称为负切线空间(b)。

如果选择切线空间,则要把法线纹理中得到的法线方向的切线空间转换到世界空间(或其他空间)中

使用模型空间存储法线的优点:

  • 实现简单,更加直观。不需要模型原始法线和切线等信息,计算更少。
  • 在纹理坐标的缝合处和尖锐的边角部分,可见的突破(缝隙)较少,即可以提供平滑的边界。

使用切线空间的优点:

  • 自由的高。模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的那个模型,应用到其他模型上效果就完全错误了
  • 可以进行UV动画。
  • 可以重用法线纹理。
  • 可压缩。由于切线空间下的法线纹理中的法线的Z方向总是正方形,因此我们就可以仅存储XY方向。

切线空间很多情况下都优于模型空间,而且可以节省美术人员的工作

3.实践

在计算光照模型时,要统一各个方向矢量所在的坐标空间。由于法线纹理中存储的法线是切线空间下的方向,因此有两种选择:

  1. 在切线空间下进行光照计算,把光照方向、视角方向变换到切线空间下
  2. 在世界空间下进行光照计算,把采样到的切线方向变换到世界空间下

效率上,第一种更优:可以在顶点着色器中就完成对光照方向和视角方向的变换,第二种要先对法线纹理进行采样,所以变换过程必须在片元着色器,必须在片元着色器中进行一次矩阵操作。

通用性,第二种更优:要在世界空间下计算。如果在采样的同时进行法线映射,就需要把法线方向变换到世界空间下。

也可以选择其他坐标空间计算,如模型空间等,但切线空间和世界空间是最常见的

在切线空间下:

效果如下:

代码如下:

Shader "Shader/NormalMap"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
        _BumpMap ("Normal Map", 2D) = "bump" {}//unity内置法线纹理bump
        _BumpScale ("Bump Scale", Float) = 1//控制凹凸程度
        _Specular ("Sprcular", Color) = (1,1,1,1)
        _Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"  
            #include "Lighting.cginc"         

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float4 uv : TEXCOORD0;

            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
                float3 LightDir : TEXCOORD1;
                float3 ViewDir : TEXCOORD2;
            };

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            float _BumpScale;
            fixed4 _Specular;
            float _Gloss;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                o.uv.xy = v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                o.uv.zw = v.uv.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                TANGENT_SPACE_ROTATION;

                o.LightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;

                o.ViewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 tlDir = normalize(i.LightDir);
                fixed3 tvDir = normalize(i.ViewDir);

                fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
                fixed3 tangentNormal;

                tangentNormal = UnpackNormal(packedNormal);
                tangentNormal.xy *= _BumpScale;
                tangentNormal.z = sqrt(1.0 - 
                                  saturate(dot(tangentNormal.xy, tangentNormal.xy)));

                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * 
                                 max(0, dot(tangentNormal, tlDir));

                fixed3 halfDir = normalize(tlDir + tvDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * 
                                  pow(max(0, dot(tangentNormal, halfDir)), _Gloss);

                return fixed4(ambient + diffuse + specular, 1.0);
            }
            ENDCG
        }
    }
    Fallback "Specular"
}

在世界空间下:

 

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Shader/NormalMap"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
        _BumpMap ("Normal Map", 2D) = "bump" {}//unity内置法线纹理bump
        _BumpScale ("Bump Scale", Float) = 1//控制凹凸程度
        _Specular ("Sprcular", Color) = (1,1,1,1)
        _Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"  
            #include "Lighting.cginc"         

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float4 uv : TEXCOORD0;

            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
                float4 TtoW0 : TEXCOORD1;
                float4 TtoW1 : TEXCOORD2;
                float4 TtoW2 : TEXCOORD3;
            };

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            float _BumpScale;
            fixed4 _Specular;
            float _Gloss;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                o.uv.xy = v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                o.uv.zw = v.uv.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

                o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
                fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
                bump.xy *= _BumpScale;
                bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));

                bump = normalize(half3(dot(i.TtoW0.xyz, bump), 
                                       dot(i.TtoW1.xyz, bump), 
                                       dot(i.TtoW2.xyz, bump)));

                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * 
                                 max(0, dot(bump, lightDir));

                fixed3 halfDir = normalize(lightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * 
                                  pow(max(0, dot(bump, halfDir)), _Gloss);

                return fixed4(ambient + diffuse + specular, 1.0);
            }
            ENDCG
        }
    }
    Fallback "Specular"
}

4.unity中法线纹理类型

把法线纹理的纹理类型标识成Normal map时,可以使用Unity的内置函数UnpackNormal来得到正确的法线方向

当我们使用那些包含了法线映射的内置的UnityShader时,必须把使用的法线纹理按上面的方式标识成Normal map,才能得到正确结果

三、渐变纹理

纹理可以用于储存任何表面属性。如使用渐变纹理来控制漫反射光照的结果。这样可以更灵活的控制光照结果

基于冷暖色调的着色技术,这种技术可以保证物体的轮廓线相比于之前使用的传统漫反射光照更加明显,而且能够提供多种色调变化。

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Shader/RampTexture"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _RampTex ("Ramp Tex", 2D) = "white" {}
        _Specular ("Sprcular", Color) = (1,1,1,1)
        _Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 uv     : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };

            fixed4 _Color;
            sampler2D _RampTex;
            float4 _RampTex_ST;
            fixed4 _Specular;
            float _Gloss;


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

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
                fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb 
                                      * _Color.rgb;

                fixed3 diffuse = _LightColor0.rgb * diffuseColor;

                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 halfDir = normalize(worldNormal + worldLightDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * 
                                  pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                return fixed4(ambient + diffuse + specular, 1.0);
            }
            ENDCG
        }
    }
}

注意:要把渐变纹理的Warp设为Clamp,以防止对纹理进行采样时由于浮点数精度而造成的问题

四、遮罩纹理

遮罩允许我们可以保护某些区域,使他们免于某些修改。更细腻的去控制光照。制作地形材质时混合多张图片,使用遮罩可以控制任何混合这些纹理。

使用遮罩的流程:

  1. 通过采样得到遮罩纹理的纹素值
  2. 然后使用其中某个(或几个)通道值(如texel.r)来与某种表面属性进行相乘
  3. 这样当该通道值为0时,可以保护该表面不是该属性的影响

总之,使用遮罩可以让美术人员更加精准(像素级别)地控制模型表面的各种性质

1.实践

Shader "Shader/MaskTexture"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
        _BumpMap ("Normal Map", 2D) = "bump" {}//unity内置法线纹理bump
        _BumpScale ("Bump Scale", Float) = 1//控制凹凸程度
        _SpecularMask ("Specular Mask", 2D) = "white" {}//高光反射遮罩纹理
        _SpecularScale ("Specular Scale", Float) = 1.0//控制遮罩影响度的系数
        _Specular ("Sprcular", Color) = (1,1,1,1)
        _Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float4 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 lightDir : TEXCOORD1;
                float3 viewDir : TEXCOORD2;
            };

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BumpMap;
            float _BumpScale;
            sampler2D _SpecularMask;
            float _SpecularScale;
            fixed4 _Specular;
            float _Gloss;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                o.uv.xy = v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;

                TANGENT_SPACE_ROTATION;

                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
                o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 tangentLightDir = normalize(i.lightDir);
                fixed3 tangentViewDir = normalize(i.viewDir);

                fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv));
                tangentNormal.xy *= _BumpScale;
                tangentNormal.z = sqrt(1.0 - 
                                  saturate(dot(tangentNormal.xy, tangentNormal.xy)));
                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                fixed3 diffuse = _LightColor0.rgb * albedo * 
                                 max(0, dot(tangentNormal, tangentLightDir));

                fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);

                fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;

                fixed specular = _LightColor0.rgb * _Specular.rgb * pow(max(
                                0, dot(tangentNormal, halfDir)), _Gloss) * specularMask;

                return fixed4(ambient + diffuse + specular, 1.0);
            }
            ENDCG
        }
    }
}

2.其他纹理遮罩

遮罩纹理不止限于保护某些区域使它们免于某些修改,而是可以存储任何我们希望逐像素控制的表面属性。

通常我们会充分利用一张纹理的RGBA四个通道,用于存储不同的属性。

例如,把高光反射的强度存储在R通道,把边缘光照的强度存储在G通道,把高光反射指数部分存储在B通道,最后把自发光存储在A通道

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值