Unity Shader - Simple Toon Shading - 简单卡通渲染

141 篇文章 33 订阅


使用的是Unity内置管线,后面有时间再学习:LWRP(URP),还有HDRP,学习任务有点多,一步一步来

本来想弄个资源来学习:后处理实现:深度+法线描边的
但现在有个适合的资源,还是先处理一下简单的卡通渲染效果吧
而且弄好后,后面做其他的效果也有个比较好的模型来做实验

最终效果 - Final Effect

刀的法线是有问题的,可能是建模的同学法线没处理好
子模型、Mask纹理都还不够细分,否则某些部位的光影可以控制得很完美
在这里插入图片描述
下面一步步来显示

无光照,只有纹理与主色调

在这里插入图片描述
可以看到纹理中部分的边缘信息也话上去了,如:白色丝绸的边缘,有还超短裙上的黑边条纹。
这些一般不是高级超模的几何体模型,都会画在纹理上。

Shader

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;
            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                fixed4 col = tex2D(_MainTex, i.uv);
                col.rgb *= _MainColor;
                return col;
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

加描边 - Outline

在这里插入图片描述
加上顶点按法线方向挤出后的背面绘制来描边的效果。
比之前没有描边的好很多,最明显的是,大腿之间的相同颜色的线条、头发与天空盒的线条

GIF

在这里插入图片描述
上面使用的描边方式比较简单:

  • 两个pass
  • 第一个绘制本体
  • 第二个将顶点想法线方向挤出,再绘制本体的背面

具体还有很多种描边,这里只简单介绍这种

Shader

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _OutLineWidth ("OutlLineWidth", Range(0, 0.1)) = 0.002
        _OutLineColor ("OutLineColor", Color) = (0,0,0,1)
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;
            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                fixed4 col = tex2D(_MainTex, i.uv);
                col.rgb *= _MainColor;
                return col;
            }
            ENDCG
        }
        Pass { // outline
            Name "Outline"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            fixed _OutLineWidth;
            fixed4 _OutLineColor;
            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                return UnityObjectToClipPos(vertex + normal * _OutLineWidth);
            }
            fixed4 frag () : SV_Target { return _OutLineColor; }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

添加光影 - RecieveShadow

首先是阴影

自身接收阴影

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Shader

shader中添加了阴影的注释

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _OutLineWidth ("OutlLineWidth", Range(0, 0.1)) = 0.002
        _OutLineColor ("OutLineColor", Color) = (0,0,0,1)
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础pass
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"                              // shadow需要,宏UNITY_LIGHTING_COORDS需要
            #pragma multi_compile_fwdbase_fullshadows               // shadow需要
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };
            struct v2f {
                // 变量名必须为pos,因为光影宏:TRANSFER_VERTEX_TO_FRAGMENT中有些嵌套宏有固化这个变量名称来处理
                float4 pos : SV_POSITION;                           // shadow需要
                float2 uv : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                // UNITY_LIGHTING_COORDS(3,4)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) SHADOW_COORDS(idx2)
                // #   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
                // DECLARE_LIGHT_COORDS(idx1)会声明,float[2~4] _LightCoord : TEXCOORD##idx;光源坐标的变量
                // SHADOW_COORDS(idx1) float[3~4] _ShadowCoord : TEXCOORD##idx1;阴影坐标变量
                
                // 所以可以理解该宏是:声明光源、阴影纹理采样坐标的
                LIGHTING_COORDS(3,4)                                // shadow需要
            };
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;
            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                // TRANSFER_VERTEX_TO_FRAGMENT(a)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define TRANSFER_VERTEX_TO_FRAGMENT(a) COMPUTE_LIGHT_COORDS(a) TRANSFER_SHADOW(a)
                // #   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
                // #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));

                // 所以可以理解该宏是:
                // - 将光源坐标通过unity_WorldToLight矩阵变换到光源空间下
                // - 将阴影坐标通过unity_WorldToShadow[](或其他矩阵,依不同光源类型)矩阵变换到阴影空间(其实就是对应的光源空间)下
                // - 注意阴影坐标转回也会根据算法类型来计算,如:使用ScreenSpace Shadow来处理会将阴影坐标转为屏幕空间坐标即可
                TRANSFER_VERTEX_TO_FRAGMENT(o)                      // shadow需要
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                i.worldNormal = normalize(i.worldNormal);

                //viewDir后面高光用
                //float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                // 光衰减atten
                // 有采样光源空间深度图,将光源空间下的坐标与深度图比较是否在于深度图
                // 大于返回光影数据值作为系数衰减,否则返回1.0系数
                UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * UNITY_LIGHTMODEL_AMBIENT.a;

                atten = atten * 0.5 + 0.5;
                // diffuse
                fixed LdotN = dot(lightDir, i.worldNormal);
                fixed halfLambert = LdotN * 0.5 + 0.5;
                fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _MainColor * halfLambert * atten;
                return fixed4(ambient + diffuse, 1);
            }
            ENDCG
        }
        Pass { // outline
            Name "Outline"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            fixed _OutLineWidth;
            fixed4 _OutLineColor;
            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                return UnityObjectToClipPos(vertex + normal * _OutLineWidth);
            }
            fixed4 frag () : SV_Target { return _OutLineColor; }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

调整阴影 - Adjusting Shadow Params

当然了,这个光影效果,不是我们卡通渲染需要的。
卡通渲染的光影过渡是比较硬的,我们可以使用一张1D的纹理过渡图来处理阴影的过渡
该纹理尺寸一般只要:256x1就够了
但是我为了纹理方便查看,我就使用了256x16

下面使用GIMP来绘制(我的游戏本上没有安装PS,因为PS都要收费,破解的又不想安装在游戏本中,因为一般破解程序都有木马,所以我就使用了免费、开源的GIMP,但是肯定没有PS好用,T^T)

我用填充工具随便填个渐变色图

  • 皮肤的在这里插入图片描述
  • 非皮肤在这里插入图片描述
    在这里插入图片描述
    效果不是很理想
    特别在头发,脸部的光影

其实这些可以使用额外的纹理来mask或是系数控制

在做光影时,发现模型制作不是很规范
(胸部部分竟然做到了衣服的子模型里)
所以导致胸部的部分光影不对
硬是要解决就是用mask texture来处理,但没必要了,以后再找找看有没更简单的模型,方便测试的
在这里插入图片描述

Shader

只有ambient+diffuse的光影
思路:

  • 使用diffuse的LdotN系数来控制对GradientTex阴影梯度纹理采样
  • GradientTex纹理主要是控制阴影梯度的,可给外部提供灵活的控制方式

当然这只是其一一种方式
也可以使用:

  • ShadowColor 阴影颜色
  • GradientGrayTex 阴影亮度剔除

然后:

fixed g = tex2D(GradientGrayTex, LdotN).r;
// g *= atten // 阴影衰减
// g *= specular // 高光
fixed3 shadow = ShadowColor * g;
fixed3 combined = ambient + diffuse + specular;

combined = lerp(combined, shadow, _ShadowItensity);

下面shader没有高光的阴影,一般卡通渲染的高光比较少,或是没有高光

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        [NoScaleOffset] _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _OutLineWidth ("OutlLineWidth", Range(0, 0.1)) = 0.002
        _OutLineColor ("OutLineColor", Color) = (0,0,0,1)
        [NoScaleOffset] _GradientTex ("GradientTex", 2D) = "white" {}       // 用于阴影梯度采样纹理,暂时就叫这个名词吧
        _GradientIntensity ("GradientIntensity", Range(0,1)) = 1            // 阴影梯度采样纹理强度
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础pass
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"                              // shadow需要,宏UNITY_LIGHTING_COORDS需要
            #pragma multi_compile_fwdbase_fullshadows               // shadow需要
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };
            struct v2f {
                // 变量为必须要pos,因为光影宏:TRANSFER_VERTEX_TO_FRAGMENT中有些嵌套宏有固化这个变量名称来处理
                float4 pos : SV_POSITION;                           // shadow需要
                float2 uv : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                // UNITY_LIGHTING_COORDS(3,4)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) SHADOW_COORDS(idx2)
                // #   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
                // DECLARE_LIGHT_COORDS(idx1)会声明,float[2~4] _LightCoord : TEXCOORD##idx;光源坐标的变量
                // SHADOW_COORDS(idx1) float[3~4] _ShadowCoord : TEXCOORD##idx1;阴影坐标变量
                
                // 所以可以理解该宏是:声明光源、阴影纹理采样坐标的
                LIGHTING_COORDS(3,4)                                // shadow需要
            };
            sampler2D _MainTex;
            fixed4 _MainColor;
            sampler2D _GradientTex;
            fixed _GradientIntensity;
            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                // TRANSFER_VERTEX_TO_FRAGMENT(a)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define TRANSFER_VERTEX_TO_FRAGMENT(a) COMPUTE_LIGHT_COORDS(a) TRANSFER_SHADOW(a)
                // #   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
                // #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));

                // 所以可以理解该宏是:
                // - 将光源坐标通过unity_WorldToLight矩阵变换到光源空间下
                // - 将阴影坐标通过unity_WorldToShadow[](或其他矩阵,依不同光源类型)矩阵变换到阴影空间(其实就是对应的光源空间)下
                // - 注意阴影坐标转回也会根据算法类型来计算,如:使用ScreenSpace Shadow来处理会将阴影坐标转为屏幕空间坐标即可
                TRANSFER_VERTEX_TO_FRAGMENT(o)                      // shadow需要
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                i.worldNormal = normalize(i.worldNormal);

                //viewDir后面高光用
                //float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                // 光衰减atten
                // 有采样光源空间深度图,将光源空间下的坐标与深度图比较是否在于深度图
                // 大于返回光影数据值作为系数衰减,否则返回1.0系数
                UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);

                // ambient
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * UNITY_LIGHTMODEL_AMBIENT.a;

                atten = atten * 0.5 + 0.5;
                // diffuse
                fixed LdotN = dot(lightDir, i.worldNormal);
                fixed halfLambert = LdotN * 0.5 + 0.5;                                  // 使用半lambert,背光不用太黑
                fixed lightShadowCoef = halfLambert * atten;                            // 乘上光影系数,应用上自身阴影
                fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _MainColor;
                fixed3 gradient = tex2D(_GradientTex, float2(lightShadowCoef, 0)).rgb;  // 使用光影系数采样梯度纹理
                diffuse = lerp(diffuse, diffuse * gradient, _GradientIntensity);        // 阴影强弱插值
                return fixed4(ambient + diffuse, 1);
            }
            ENDCG
        }
        Pass { // outline
            Name "Outline"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            fixed _OutLineWidth;
            fixed4 _OutLineColor;
            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                // projection space
                float4 pos = UnityObjectToClipPos(vertex);
                // to view space normal
                fixed3 vNormal = mul((float3x3)UNITY_MATRIX_IT_MV, normal);
				fixed2 offset = TransformViewToProjection(vNormal.xy);
                // 因为在vertex post-processing会有perspective divide,所以我们先乘上pos.w以抵消透视
                // 这样无论多远多近都可以按恒定的描边边宽来显示
                pos.xy += offset * _OutLineWidth * pos.w;
                return pos;
            }
            fixed4 frag () : SV_Target { return _OutLineColor; }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

无透视法线挤出描边

然后再改了改描边,不需要透视
不然近距离镜头时,描边会变粗,如下图
在这里插入图片描述
下面是无透视的描边
在这里插入图片描述

Shader

看vs即可

            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                // projection space
                float4 pos = UnityObjectToClipPos(vertex);
                // to view space normal
                fixed3 vNormal = mul((float3x3)UNITY_MATRIX_IT_MV, normal);
				fixed2 offset = TransformViewToProjection(vNormal.xy);
                // 因为在vertex post-processing会有perspective divide,所以我们先乘上pos.w以抵消透视
                // 这样无论多远多近都可以按恒定的描边边宽来显示
                pos.xy += offset * _OutLineWidth * pos.w;
                return pos;
            }

整体运行效果

在这里插入图片描述
在这里插入图片描述

最后我将头发颜色调整为红色,风格也挺搭的

调整材质的MainColor参数为红色即可,效果如下:
在这里插入图片描述

高光 - Specular

下面是我们正常高光
但是太平滑了,我们需要硬边过渡
在这里插入图片描述
因为需要硬边过渡,我就简单粗暴的添加一个SpecularThreshold来过滤掉一些比较小的高光值
效果如下:
在这里插入图片描述
最终高光在没个子模型的材质参数再调整一下
在这里插入图片描述
没添加高光前的对比
在这里插入图片描述
添加一个自转,不是镜头转了,方便看光影
在这里插入图片描述
描边小一些,头发黄色,角度换一下,那把刀好帅
在这里插入图片描述
头发那些高光不太理想,一般需要手绘纹理的光影mask来处理就会好很多。或是头发高模法线图也可以

还有头发、大腿、两部的高光过渡太平滑了,我们将其参数调整一下,效果会更好
在这里插入图片描述
在这里插入图片描述
在此基础上,如果对头发、衣服黄金色纹理,纽扣,铠甲金属,如果再细分一下纹理分通道来控制高光系数纹理图的话,可以制作得非常好的效果,但没有资源。

Shader

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        [NoScaleOffset] _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _OutLineWidth ("OutlLineWidth", Range(0, 0.1)) = 0.002
        _OutLineColor ("OutLineColor", Color) = (0,0,0,1)
        [NoScaleOffset] _GradientTex ("GradientTex", 2D) = "white" {}       // 用于阴影梯度采样纹理,暂时就叫这个名词吧
        _GradientIntensity ("GradientIntensity", Range(0,1)) = 1            // 阴影梯度采样纹理强度
        _SpecularPower ("SpecularPower", Range(1,100)) = 80                 // 高光平滑度
        _SpecularIntensity ("SpecularItensity", Range(0,1)) = 1             // 高光强度
        _SpecularThreshold ("SpecularThreshold", Range(0,1)) = 0.3          // 高光阈值
        _SpecularBrightness ("SpecularBrightness", Range(0,1)) = 0.1        // 高光添加的亮度量
        _SpecularValue ("SpecularValue", Range(0,1)) = 1                    // 高光值
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础pass
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"                              // shadow需要,宏UNITY_LIGHTING_COORDS需要
            #pragma multi_compile_fwdbase_fullshadows               // shadow需要
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                half3 normal : NORMAL;
            };
            struct v2f {
                // 变量名必须为pos,因为光影宏:TRANSFER_VERTEX_TO_FRAGMENT中有些嵌套宏有固化这个变量名称来处理
                float4 pos : SV_POSITION;                           // shadow需要
                float2 uv : TEXCOORD0;
                half3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                // UNITY_LIGHTING_COORDS(3,4)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) SHADOW_COORDS(idx2)
                // #   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
                // DECLARE_LIGHT_COORDS(idx1)会声明,float[2~4] _LightCoord : TEXCOORD##idx;光源坐标的变量
                // SHADOW_COORDS(idx1) float[3~4] _ShadowCoord : TEXCOORD##idx1;阴影坐标变量
                
                // 所以可以理解该宏是:声明光源、阴影纹理采样坐标的
                UNITY_LIGHTING_COORDS(3,4)                                // shadow需要
            };
            sampler2D _MainTex;
            fixed4 _MainColor;
            sampler2D _GradientTex;
            fixed _GradientIntensity;
            fixed _SpecularPower;
            fixed _SpecularIntensity;
            fixed _SpecularThreshold;
            fixed _SpecularBrightness;
            fixed _SpecularValue;
            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                // TRANSFER_VERTEX_TO_FRAGMENT(a)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define TRANSFER_VERTEX_TO_FRAGMENT(a) COMPUTE_LIGHT_COORDS(a) TRANSFER_SHADOW(a)
                // #   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
                // #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));

                // 所以可以理解该宏是:
                // - 将世界空间光源坐标通过unity_WorldToLight矩阵变换到光源空间下
                // - 将世界空间阴影坐标通过unity_WorldToShadow[](或其他矩阵,依不同光源类型)矩阵变换到阴影空间(其实就是对应的光源空间)下
                // - 注意阴影坐标转回也会根据算法类型来计算,如:使用ScreenSpace Shadow来处理会将阴影坐标转为屏幕空间坐标即可
                TRANSFER_VERTEX_TO_FRAGMENT(o)                      // shadow需要
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                i.worldNormal = normalize(i.worldNormal);

                //viewDir后面高光用
                half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
                half3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                // 光衰减atten
                // 采样光源空间深度图,将光源空间下的坐标与深度图比较
                // 大于深度图的返回光影数据值作为系数衰减,小于深度图的则返回1.0系数
                UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
                atten = atten * 0.5 + 0.5;

                // ambient
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                
                fixed LdotN = dot(lightDir, i.worldNormal);
                fixed halfLambert = LdotN * 0.5 + 0.5;                                      // 使用半lambert,背光不用太黑
                fixed lightShadowCoef = halfLambert * atten;                                // 乘上光影系数,应用上自身阴影
                
                // diffuse
                fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _MainColor;
                fixed3 dGradient = tex2D(_GradientTex, float2(lightShadowCoef, 0)).rgb;     // 使用光影系数采样梯度纹理
                //return fixed4(dGradient,1);
                diffuse = lerp(diffuse,  diffuse * dGradient, _GradientIntensity);          // 漫反射光影强弱插值

                // specular
                half3 hDir = normalize(viewDir + lightDir);
                fixed HdotN = max(0, dot(hDir, i.worldNormal));
                fixed specular = pow(HdotN, _SpecularPower) * _SpecularIntensity;
                specular *= atten;                                                          // 阴影衰减对高光有些影响
                specular = step(_SpecularThreshold, specular) * _SpecularValue;             // 大于阈值的才有效
                //return specular;

                // 高光这儿,我们是源diffuse的高光部分叠加,还有高光亮度叠加
                return fixed4(ambient + diffuse + diffuse * specular + specular * _SpecularBrightness, 1);
            }
            ENDCG
        }
        Pass { // outline
            Name "Outline"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            fixed _OutLineWidth;
            fixed4 _OutLineColor;
            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                // projection space
                float4 pos = UnityObjectToClipPos(vertex);
                // to view space normal
                fixed3 vNormal = mul((float3x3)UNITY_MATRIX_IT_MV, normal);
				fixed2 offset = TransformViewToProjection(vNormal.xy);
                // 因为在vertex post-processing会有perspective divide,所以我们先乘上pos.w以抵消透视
                // 这样无论多远多近都可以按恒定的描边边宽来显示
                pos.xy += offset * _OutLineWidth * pos.w;
                return pos;
            }
            fixed4 frag () : SV_Target { return _OutLineColor; }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

边缘光 - Rim

一般卡通渲染也是不需要边缘光的,下面我们就下一丢丢的边缘光好了,不多
在这里插入图片描述
合成后
在这里插入图片描述

Shader

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        [NoScaleOffset] _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _OutLineWidth ("OutlLineWidth", Range(0, 0.1)) = 0.002
        _OutLineColor ("OutLineColor", Color) = (0,0,0,1)
        [NoScaleOffset] _GradientTex ("GradientTex", 2D) = "white" {}       // 用于阴影梯度采样纹理,暂时就叫这个名词吧
        _GradientIntensity ("GradientIntensity", Range(0,1)) = 1            // 阴影梯度采样纹理强度
        _SpecularPower ("SpecularPower", Range(1,100)) = 80                 // 高光平滑度
        _SpecularIntensity ("SpecularItensity", Range(0,1)) = 1             // 高光强度
        _SpecularThreshold ("SpecularThreshold", Range(0,1)) = 0.3          // 高光阈值
        _SpecularBrightness ("SpecularBrightness", Range(0,1)) = 0.1        // 高光添加的亮度量
        _SpecularValue ("SpecularValue", Range(0,1)) = 1                    // 高光值
        _RimIntensity ("RimIntensity", Range(0,5)) = 1                      // 边缘光
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础pass
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"                              // shadow需要,宏UNITY_LIGHTING_COORDS需要
            #pragma multi_compile_fwdbase_fullshadows               // shadow需要
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                half3 normal : NORMAL;
            };
            struct v2f {
                // 变量名必须为pos,因为光影宏:TRANSFER_VERTEX_TO_FRAGMENT中有些嵌套宏有固化这个变量名称来处理
                float4 pos : SV_POSITION;                           // shadow需要
                float2 uv : TEXCOORD0;
                half3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                // UNITY_LIGHTING_COORDS(3,4)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) SHADOW_COORDS(idx2)
                // #   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
                // DECLARE_LIGHT_COORDS(idx1)会声明,float[2~4] _LightCoord : TEXCOORD##idx;光源坐标的变量
                // SHADOW_COORDS(idx1) float[3~4] _ShadowCoord : TEXCOORD##idx1;阴影坐标变量
                
                // 所以可以理解该宏是:声明光源、阴影纹理采样坐标的
                UNITY_LIGHTING_COORDS(3,4)                                // shadow需要
            };
            sampler2D _MainTex;
            fixed4 _MainColor;
            sampler2D _GradientTex;
            fixed _GradientIntensity;
            fixed _SpecularPower;
            fixed _SpecularIntensity;
            fixed _SpecularThreshold;
            fixed _SpecularBrightness;
            fixed _SpecularValue;
            fixed _RimIntensity;
            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                // TRANSFER_VERTEX_TO_FRAGMENT(a)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define TRANSFER_VERTEX_TO_FRAGMENT(a) COMPUTE_LIGHT_COORDS(a) TRANSFER_SHADOW(a)
                // #   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
                // #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));

                // 所以可以理解该宏是:
                // - 将世界空间光源坐标通过unity_WorldToLight矩阵变换到光源空间下
                // - 将世界空间阴影坐标通过unity_WorldToShadow[](或其他矩阵,依不同光源类型)矩阵变换到阴影空间(其实就是对应的光源空间)下
                // - 注意阴影坐标转回也会根据算法类型来计算,如:使用ScreenSpace Shadow来处理会将阴影坐标转为屏幕空间坐标即可
                TRANSFER_VERTEX_TO_FRAGMENT(o)                      // shadow需要
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                i.worldNormal = normalize(i.worldNormal);

                //viewDir后面高光用
                half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
                half3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                // 光衰减atten
                // 采样光源空间深度图,将光源空间下的坐标与深度图比较
                // 大于深度图的返回光影数据值作为系数衰减,小于深度图的则返回1.0系数
                UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
                atten = atten * 0.5 + 0.5;

                // ambient
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                
                fixed LdotN = dot(lightDir, i.worldNormal);
                fixed halfLambert = LdotN * 0.5 + 0.5;                                      // 使用半lambert,背光不用太黑
                fixed lightShadowCoef = halfLambert * atten;                                // 乘上光影系数,应用上自身阴影
                
                // diffuse
                fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _MainColor;
                fixed3 dGradient = tex2D(_GradientTex, float2(lightShadowCoef, 0)).rgb;     // 使用光影系数采样梯度纹理
                //return fixed4(dGradient,1);
                diffuse = lerp(diffuse,  diffuse * dGradient, _GradientIntensity);          // 漫反射光影强弱插值

                // specular
                half3 hDir = normalize(viewDir + lightDir);
                fixed HdotN = max(0, dot(hDir, i.worldNormal));
                fixed specular = pow(HdotN, _SpecularPower) * _SpecularIntensity;
                specular *= atten;                                                          // 阴影衰减对高光有些影响
                specular = step(_SpecularThreshold, specular) * _SpecularValue;             // 大于阈值的才有效
                //return specular;

                // rim
                fixed rimFactor = (1 - dot(viewDir, i.worldNormal)) * _RimIntensity;        // 边缘光
                rimFactor *= atten;                                                         // 应用上光影衰减系数
                rimFactor = step(_SpecularThreshold, rimFactor) * _SpecularValue;           // 阈值使用高光的
                //return rimFactor;

                specular = max(specular, rimFactor);                                        // 这里简单处理:高光与边缘光哪个亮取哪个

                // 高光这儿,我们是源diffuse的高光部分叠加,还有高光亮度叠加
                return fixed4(
                    ambient + 
                    diffuse + 
                    diffuse * specular + specular * _SpecularBrightness
                    , 1);
            }
            ENDCG
        }
        Pass { // outline
            Name "Outline"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            fixed _OutLineWidth;
            fixed4 _OutLineColor;
            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                // projection space
                float4 pos = UnityObjectToClipPos(vertex);
                // to view space normal
                fixed3 vNormal = mul((float3x3)UNITY_MATRIX_IT_MV, normal);
				fixed2 offset = TransformViewToProjection(vNormal.xy);
                // 因为在vertex post-processing会有perspective divide,所以我们先乘上pos.w以抵消透视
                // 这样无论多远多近都可以按恒定的描边边宽来显示
                pos.xy += offset * _OutLineWidth * pos.w;
                return pos;
            }
            fixed4 frag () : SV_Target { return _OutLineColor; }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

控制边缘光在背光时才显示 - Rim Show At Back To the Lighting

在这里插入图片描述

	[Toggle] _RimShowAtBackToLight ("RimShowAtBackToLight", Float) = 0  // 边缘光是否被光是才显示
...
	rimFactor = lerp(rimFactor, rimFactor * max(0, dot(-lightDir, viewDir)), _RimShowAtBackToLight);   // 视线越背光,边缘光应该越亮

在这里插入图片描述
Shader Properties中可调整
在这里插入图片描述

Shader

// jave.lin 2019.08.25
Shader "Test/Toon" {
    Properties {
        [NoScaleOffset] _MainTex ("MainTex", 2D) = "white" {}
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _OutLineWidth ("OutlLineWidth", Range(0, 0.1)) = 0.002
        _OutLineColor ("OutLineColor", Color) = (0,0,0,1)
        [NoScaleOffset] _GradientTex ("GradientTex", 2D) = "white" {}       // 用于阴影梯度采样纹理,暂时就叫这个名词吧
        _GradientIntensity ("GradientIntensity", Range(0,1)) = 1            // 阴影梯度采样纹理强度
        _SpecularPower ("SpecularPower", Range(1,100)) = 80                 // 高光平滑度
        _SpecularIntensity ("SpecularItensity", Range(0,1)) = 1             // 高光强度
        _SpecularThreshold ("SpecularThreshold", Range(0,1)) = 0.3          // 高光阈值
        _SpecularBrightness ("SpecularBrightness", Range(0,1)) = 0.1        // 高光添加的亮度量
        _SpecularValue ("SpecularValue", Range(0,1)) = 1                    // 高光值
        _RimIntensity ("RimIntensity", Range(0,5)) = 1                      // 边缘光
        [Toggle] _RimShowAtBackToLight ("RimShowAtBackToLight", Float) = 0  // 边缘光是否被光是才显示
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass { // solid
            Name "Solid"
            Tags { "LightMode"="ForwardBase" }                      // shadow需要,正向渲染光照基础pass
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"                              // shadow需要,宏UNITY_LIGHTING_COORDS需要
            #pragma multi_compile_fwdbase_fullshadows               // shadow需要
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                half3 normal : NORMAL;
            };
            struct v2f {
                // 变量名必须为pos,因为光影宏:TRANSFER_VERTEX_TO_FRAGMENT中有些嵌套宏有固化这个变量名称来处理
                float4 pos : SV_POSITION;                           // shadow需要
                float2 uv : TEXCOORD0;
                half3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                // UNITY_LIGHTING_COORDS(3,4)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) SHADOW_COORDS(idx2)
                // #   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
                // DECLARE_LIGHT_COORDS(idx1)会声明,float[2~4] _LightCoord : TEXCOORD##idx;光源坐标的变量
                // SHADOW_COORDS(idx1) float[3~4] _ShadowCoord : TEXCOORD##idx1;阴影坐标变量
                
                // 所以可以理解该宏是:声明光源、阴影纹理采样坐标的
                UNITY_LIGHTING_COORDS(3,4)                                // shadow需要
            };
            sampler2D _MainTex;
            fixed4 _MainColor;
            sampler2D _GradientTex;
            fixed _GradientIntensity;
            fixed _SpecularPower;
            fixed _SpecularIntensity;
            fixed _SpecularThreshold;
            fixed _SpecularBrightness;
            fixed _SpecularValue;
            fixed _RimIntensity;
            fixed _RimShowAtBackToLight; 
            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                // TRANSFER_VERTEX_TO_FRAGMENT(a)宏在:AutoLight.cginc可以看到,所以需要 include "AutoLight.cginc"
                // #define TRANSFER_VERTEX_TO_FRAGMENT(a) COMPUTE_LIGHT_COORDS(a) TRANSFER_SHADOW(a)
                // #   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
                // #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));

                // 所以可以理解该宏是:
                // - 将世界空间光源坐标通过unity_WorldToLight矩阵变换到光源空间下
                // - 将世界空间阴影坐标通过unity_WorldToShadow[](或其他矩阵,依不同光源类型)矩阵变换到阴影空间(其实就是对应的光源空间)下
                // - 注意阴影坐标转回也会根据算法类型来计算,如:使用ScreenSpace Shadow来处理会将阴影坐标转为屏幕空间坐标即可
                TRANSFER_VERTEX_TO_FRAGMENT(o)                      // shadow需要
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                i.worldNormal = normalize(i.worldNormal);

                //viewDir后面高光用
                half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
                half3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                // 光衰减atten
                // 采样光源空间深度图,将光源空间下的坐标与深度图比较
                // 大于深度图的返回光影数据值作为系数衰减,小于深度图的则返回1.0系数
                UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
                atten = atten * 0.5 + 0.5;

                // ambient
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                
                fixed LdotN = dot(lightDir, i.worldNormal);
                fixed halfLambert = LdotN * 0.5 + 0.5;                                      // 使用半lambert,背光不用太黑
                fixed lightShadowCoef = halfLambert * atten;                                // 乘上光影系数,应用上自身阴影
                
                // diffuse
                fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _MainColor;
                fixed3 dGradient = tex2D(_GradientTex, float2(lightShadowCoef, 0)).rgb;     // 使用光影系数采样梯度纹理
                //return fixed4(dGradient,1);
                diffuse = lerp(diffuse,  diffuse * dGradient, _GradientIntensity);          // 漫反射光影强弱插值

                // specular
                half3 hDir = normalize(viewDir + lightDir);
                fixed HdotN = max(0, dot(hDir, i.worldNormal));
                fixed specular = pow(HdotN, _SpecularPower) * _SpecularIntensity;
                specular *= atten;                                                          // 阴影衰减对高光有些影响
                specular = step(_SpecularThreshold, specular) * _SpecularValue;             // 大于阈值的才有效
                //return specular;

                // rim
                fixed rimFactor = (1 - dot(viewDir, i.worldNormal)) * _RimIntensity;        // 边缘光
                rimFactor = lerp(rimFactor, rimFactor * max(0, dot(-lightDir, viewDir)), _RimShowAtBackToLight);   // 视线越背光,边缘光应该越亮
                rimFactor *= atten;                                                         // 应用上光影衰减系数
                rimFactor = step(_SpecularThreshold, rimFactor) * _SpecularValue;           // 阈值使用高光的
                //return rimFactor;

                specular = max(specular, rimFactor);                                        // 这里简单处理:高光与边缘光哪个亮取哪个

                // 高光这儿,我们是源diffuse的高光部分叠加,还有高光亮度叠加
                return fixed4(
                    ambient + 
                    diffuse + 
                    diffuse * specular + specular * _SpecularBrightness
                    , 1);
            }
            ENDCG
        }
        Pass { // outline
            Name "Outline"
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma fragmentoption ARB_precision_hint_fastest       // 最快速精度
            fixed _OutLineWidth;
            fixed4 _OutLineColor;
            float4 vert (float4 vertex : POSITION, float3 normal : NORMAL) : SV_POSITION {
                // projection space
                float4 pos = UnityObjectToClipPos(vertex);
                // to view space normal
                fixed3 vNormal = mul((float3x3)UNITY_MATRIX_IT_MV, normal);
				fixed2 offset = TransformViewToProjection(vNormal.xy);
                // 因为在vertex post-processing会有perspective divide,所以我们先乘上pos.w以抵消透视
                // 这样无论多远多近都可以按恒定的描边边宽来显示
                pos.xy += offset * _OutLineWidth * pos.w;
                return pos;
            }
            fixed4 frag () : SV_Target { return _OutLineColor; }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

Project

TestNPR_ToonShading_卡通渲染_ambient_diffuse_specular_rim

References

收集了一些资料,后面进一步了解卡通渲染再去看看,上面的是之前理解的很少一部分内容总结写出来的。

  • 11
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
Unity Shader是一种用于在Unity引擎中创建和控制图形渲染效果的编程语言。通过使用Unity Shader,开发人员可以自定义游戏中各种物体的外观和行为,从而实现更加逼真和出色的视觉效果。 而热图(Heatmap)是一种用于显示某个区域内物体热度分布的视觉化工具。在游戏开发中,热图通常用于统计和分析玩家在游戏中的行为和偏好,以便开发人员可以根据这些数据进行游戏优化和改进。 为了创建一个热图效果,我们可以使用Unity Shader来实现。首先,我们需要将游戏中各个物体按照玩家与其的互动情况和频率进行区分,不同的行为和频率可以对应不同的颜色或者纹理。接着,我们可以在Shader中根据这些信息来着色和渲染物体,以展示物体的热度分布。 在Shader中,我们可以通过为物体添加一张热图纹理,并使用该纹理来表示物体的热度值。热图纹理可以是一张灰度图,不同的灰度值对应不同的热度。然后,我们可以使用纹理坐标和采样操作来获取每个像素对应的热度值,并根据这些值来着色和渲染物体。 除了使用纹理来表示热度分布,我们还可以使用其他的技术和效果来增强热图的可视化效果。例如,我们可以使用颜色渐变和透明度来形成平滑的过渡效果,以更好地显示物体的热度变化。我们还可以添加动画效果,使热图效果更加生动和有趣。 总结而言,Unity Shader可以用于创建热图效果,通过着色和渲染来展示物体的热度分布。这样的热图可以帮助开发人员分析游戏中玩家的行为和偏好,从而优化和改进游戏的设计和内容。这些热图效果能够增强游戏的可视化效果,并提供有价值的数据供开发人员参考。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值