Unity透明效果原理与实现

一、前提知识

(1)深度缓存

它的基本思想是:根据深度缓存中的值来判断该片元距离摄像机的距离,当渲染一个片元时,需要把它的深度值和已经存在于深度缓冲中的值进行比较(如果开启了深度测试),如果它的值距离摄像机更远,那么说明这个片元不应该被渲染到屏幕上(有物体挡住了它);否则,这个片元应该覆盖掉此时颜色缓冲中的像素值,并把它的深度值更新到深度缓冲中(如果开启了深度写入)。

(2)颜色缓存

需要渲染的场景的每一个像素都最终写入该缓冲区,然后由他渲染到屏幕上显示。

(3)渲染队列

Unity为了解决渲染顺序的问题提供了渲染顺序这一解决方案。我们可以使用SubShader的Queue标签来决定我们的模型属于哪一个渲染队列。Unity内部使用一系列的整数索引来表示每一个渲染队列。且索引号越小表示越早被渲染。


unity内置的渲染队列.png

二、透明度测试

(1)概念

它采用一种“霸道极端”的机制,只要一个片元的透明度不满足条件(通常是小于某个阈值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它,即进行深度测试、深度写入等

(2)效果
透明度测试.png
(3)实现
Shader "Unity Shaders Book/Chapter 8/Alpha Test"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color ("Main Tint",Color) = (1,1,1,1)
        //透明度参考值
        _Cutoff ("Alpha Cutoff",Range(0,1)) = 0.5
    }
    SubShader
    {
        // Queue 设置渲染队列为透明度测试队列 IgnoreProjector 表示是否受投影器(Projectors)的影响 TransparentCutout提前定义的组
        Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" }

        Pass
        {
            Tags {"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed _Cutoff;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

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

            v2f vert (a2v 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.texcoord,_MainTex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed4 texColor = tex2D(_MainTex,i.uv);

                //透明度测试
                clip(texColor.a - _Cutoff);
                //函数实现
                //if (texColor.a - _Cutoff < 0.0)
                //{
                //  discard;
                //  }
                 
                 //实现光照
                 //吸收系数
                 fixed3 albedo = texColor.rgb * _Color.rgb;
                 //环境光
                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                 //漫反射
                 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldNormal,worldLightDir));
                 return fixed4(ambient + diffuse,1.0);
            }
            ENDCG
        }
    }
    Fallback "Transparent/Cutout/VertexLit"
}

三、透明度混合

(1)概念

这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。注意事项:渲染顺序,关闭深度写入

(2)为什么要关闭深度写入?

如果不关闭深度写入,一个半透明表面背后的表面本来可以透过它被我们看到的,但由于深度测试时判断结果是该半透明表面距离摄像机更近,导致后面的表面会被剔除,我们也就无法透过半透明表面看到后面的物体了。

(2)为什么渲染顺序很重要?
A为透明物体B为不透明物体.png

AB都为透明物体.png

第一种情况:先渲染B,再渲染A。那么由于不透明物体开启了深度测试和深度检验,而此时深度缓冲中没有任何有效数据,因此B首先会写入颜色缓冲和深度缓冲。随后我们渲染A,透明物体仍然会进行深度测试,因此我们发现和B相比A距离摄像机更近,因此,我们会使用A的透明度来和颜色缓冲中的B的颜色进行混合,得到正确的半透明效果。

第二种情况:先渲染A,再渲染B。渲染A时,深度缓冲区中没有任何有效数据,因此A直接写入颜色缓冲,但由于对半透明物体关闭了深度写入,因此A不会修改深度缓冲。等到渲染B时,B会进行深度测试,它发现“咦,深度缓冲中还没有人来过,那我就放心地写入颜色缓冲了!”,结果就是B会直接覆盖A的颜色。从视觉上来看,B就出现了A的前面,这是错误的。

(3)渲染顺序结论

(1)先渲染所有不透明的物体,并开启它们的深度测试和深度写入。
(2)把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入。
(3)以上两种是unity渲染顺序的基本常识,为了解决更多更复杂的渲染顺序问题,我们应该使用unity为我们提供的渲染队列(一、前提知识-(3)渲染队列)。

(4)透明度混合命令,混合因子,混合操作介绍

(1)透明度混合命令( Blend )
在设置混合因子的同时也开启了混合模式。这是因为,只有开启了混合之后,设置片元的透明通道才有意义。


透明度混合命令.png

(2)透明度混合因子


透明度混合因子.png

(3)透明度混合操作


透明度混合操作.png

(4)透明度混合示例
例如: 正常(Normal),即透明度混合 Blend SrcAlpha OneMinusSrcAlpha
NewColor = SrcColor ×SrcAlpha + DstColor * (1-SrcAlpha)


正常(Normal).png

变亮(Lighten) BlendOp Max Blend One One
NewColor =Color( max(SrcColor.r,DstColor.r),max(SrcColor.g,DstColor.g),max(SrcColor.b,DstColor.b),max(SrcColor.a,DstColor.a))

变亮(Lighten) .png

(5)透明度混合效果
透明度混合效果.png
(6)透明度混合实现
Shader "Unity Shaders Book/Chapter 8/Alpha Blend"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color ("Main Tint",Color) = (1,1,1,1)
        //透明度参考值
        _AlphaScale ("Alpha Cutoff",Range(0,1)) = 1
    }
    SubShader
    {
        Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }

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

            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha

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

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed _AlphaScale;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

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

            v2f vert (a2v 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.texcoord,_MainTex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed4 texColor = tex2D(_MainTex,i.uv);

                 //实现光照
                 //吸收系数
                 fixed3 albedo = texColor.rgb * _Color.rgb;
                 //环境光
                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                 //漫反射
                 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldNormal,worldLightDir));
                 return fixed4(ambient + diffuse,texColor.a * _AlphaScale);
            }
            ENDCG
        }
    }
    Fallback "Transparent/Cutout/VertexLit"
}
(7)开启深度写入的透明度混合效果

在一些情况下,由于我们关闭了深度写入,会给我们带来各种复杂的问题。例如:当模型本身有复杂的遮挡关系或是包含了复杂的非凸网格的时候,就会有各种各样的因为排序错误而产生的错误的透明效果。如下图:
错误的透明效果.png

这是因为我们模型无法进行像素级别的深度排序。于是我们可以使用两个pass来渲染这个模型:第1个pass开启深度写入,但是不向颜色缓存中输入任何颜色,目的就是为了通过深度缓存的规则,把该模型的深度值写入深度缓存中,这样就可以将模型自身被遮挡住的片元剔除掉。第2个pass就进行正常的透明度混合就行了。

代码实现:

Shader "Unity Shaders Book/Chapter 8/Alpha Blending With ZWrite" {
    Properties {
        _Color ("Color Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Main Tex", 2D) = "white" {}
        _AlphaScale ("Alpha Scale", Range(0, 1)) = 1
    }
    SubShader {
        Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
        
        // 第一个pass的作用仅仅是开启深度写入
        Pass {
            ZWrite On
            //然后我们使用了一个新的渲染命令——ColorMask。在ShaderLab 中,ColorMask用于设置颜色通道的写掩码,为0时表示不写入任何颜色
            ColorMask 0
        }

        //普通的透明度混合实现
        Pass {
            Tags { "LightMode"="ForwardBase" }
            
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Lighting.cginc"
            
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed _AlphaScale;
            
            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };
            
            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };
            
            //顶点着色器,主要是坐标转换以及UV值
            v2f vert(a2v 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.texcoord, _MainTex);
                
                return o;
            }
             //片元着色器,主要是光照实现以及纹理采样
            fixed4 frag(v2f i) : SV_Target {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                
                fixed4 texColor = tex2D(_MainTex, i.uv);
                
                fixed3 albedo = texColor.rgb * _Color.rgb;
                
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
                
                return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
            }
            
            ENDCG
        }
    } 
    FallBack "Transparent/VertexLit"
}

(8)双面渲染的透明度混合效果

在正常情况下,我们观察一个透明物体的时候,不仅仅可以看到透明物体的正面,也可以透过他的正面看到物体的背面,在上面的实例中不管是透明度测试还是透明度混合,我们都只能看到物体的一面,看起来物体就像只有半个。这是因为在unity中默认是剔除物体背面的渲染图元的,只渲染物体的正面图元。所以想要得到双面渲染的效果,需要我们在shader中手动的设置哪一面需要被渲染。
指令:Cull
Cull Back :背对着摄像机的渲染图元不会被渲染
Cull Front :面朝着摄像机的渲染图元不会被渲染
Cull Off : 关闭剔除功能,所有的图元都将被渲染

透明度测试的双面渲染代码:

Shader "Unity Shaders Book/Chapter 8/Alpha Test With Both Side" {
    Properties {
        _Color ("Color Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Main Tex", 2D) = "white" {}
        _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
    }
    SubShader {
        Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
        
        Pass {
            Tags { "LightMode"="ForwardBase" }
            
            // 关闭剔除功能 双面渲染
            Cull Off
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Lighting.cginc"
            
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed _Cutoff;
            
            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };
            
            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };
            
            v2f vert(a2v 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.texcoord, _MainTex);
                
                return o;
            }
            
            fixed4 frag(v2f i) : SV_Target {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                
                fixed4 texColor = tex2D(_MainTex, i.uv);
                //透明度测试
                clip (texColor.a - _Cutoff);
                
                fixed3 albedo = texColor.rgb * _Color.rgb;
                
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
                
                return fixed4(ambient + diffuse, 1.0);
            }
            
            ENDCG
        }
    } 
    FallBack "Transparent/Cutout/VertexLit"
}

透明度混合的双面渲染
对于透明度混合也是直接关闭剔除功能吗?显然是不行的,因为在透明度混合时需要关闭深度写入,所以直接关闭剔除功能,我们不能保证同一个物体正面与背面的图元的渲染顺序,有可能得到错误的半透明效果。为了确保同一个物体的正面与背面的图元以正确的渲染顺序渲染,我们选择把双面渲染的工作分成两个pass,第一个只渲染背面,第二个只渲染正面。因为在SubShader中,pass是顺序执行的。这样可以确保背面在正面之前被渲染。

代码实现:

Shader "Unity Shaders Book/Chapter 8/Alpha Blend With Both Side" {
    Properties {
        _Color ("Color Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Main Tex", 2D) = "white" {}
        _AlphaScale ("Alpha Scale", Range(0, 1)) = 1
    }
    SubShader {
        Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
        
        Pass {
            Tags { "LightMode"="ForwardBase" }
            
            // 不渲染正面图元
            Cull Front
            // 关闭深度写入
            ZWrite Off
            // 设置混合因子(设置公式更为贴切)
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Lighting.cginc"
            
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed _AlphaScale;
            
            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };
            
            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };
            
            v2f vert(a2v 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.texcoord, _MainTex);
                
                return o;
            }
            
            fixed4 frag(v2f i) : SV_Target {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                
                fixed4 texColor = tex2D(_MainTex, i.uv);
                
                fixed3 albedo = texColor.rgb * _Color.rgb;
                
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
                
                return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
            }
            
            ENDCG
        }
        
        Pass {
            Tags { "LightMode"="ForwardBase" }
            
            // 不渲染背面图元
            Cull Front
            // 关闭深度写入
            ZWrite Off
            // 设置混合因子(设置公式更为贴切)
            Blend SrcAlpha OneMinusSrcAlpha
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Lighting.cginc"
            
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed _AlphaScale;
            
            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };
            
            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };
            
            v2f vert(a2v 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.texcoord, _MainTex);
                
                return o;
            }
            
            fixed4 frag(v2f i) : SV_Target {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                
                fixed4 texColor = tex2D(_MainTex, i.uv);
                
                fixed3 albedo = texColor.rgb * _Color.rgb;
                
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
                
                return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
            }
            
            ENDCG
        }
    } 
    FallBack "Transparent/VertexLit"
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容