简介:该资源为Unity3D开发者提供一套完整的动态屏幕遮罩Shader实现方案,适用于需要实现跟随目标物体的可视范围遮罩、边缘渐变、遮罩颜色自定义等效果的游戏或交互项目。压缩包仅含1个PDF文档,大小约66KB,文档内完整展示了Shader源码、C#调用代码以及效果图与控制面板示意,并梳理了CGPROGRAM、Vertex/Fragment Shader、Properties、SubShader、Cull Off、ZWrite Off等核心知识点,方便开发者理解关键原理并快速移植到自己的项目中。目前已有2394人学习下载,适合具备一定Unity基础、希望深入掌握Shader屏幕特效实现的开发者参考学习。
1. 动态屏幕遮罩:先做对UV,才谈得上“动起来”
做技能范围提示的时候,美术给了一张圆环贴图,我按老办法放到UI上,结果角色一动遮罩就穿帮,多分辨率下还容易对不齐。换成Unity3D Shader实现动态屏幕遮罩后,一个全屏Pass就把问题收掉了:遮罩中心、半径、柔和度全部做成参数,还能随时间呼吸或跟随鼠标。核心其实就两步:屏幕空间的UV变换,加一个mask权重,没有任何黑科技。适合刚开始碰后处理和Shader的Unity开发者,也适合不想被UI贴图反复调精度的开发者。下面我会拆一个能跑的OnRenderImage方案,讲清楚原理、参数和翻车点。
2. 屏幕UV与距离场:动态屏幕遮罩的原理和选型理由
一个全屏遮罩,本质上是在相机渲染完成之后,对每一帧屏幕颜色再做一次重新着色。这个过程在Unity里最常见的行为是OnRenderImage(source, destination),其中source是当前相机渲染结果,destination是最终输出。Shader要做的事情只有一个:把source当纹理,按屏幕坐标采样,再用一个权重把采样结果和遮罩颜色混合。这个权重,就是遮罩。
2.1 为什么是后处理而不是UI贴图
UI方案也做遮罩,但那是把遮罩图按UI树的层级叠加在界面上面,依赖Canvas的合批逻辑。如果你要做Boss出场时屏幕中央一个圆孔逐渐扩大,并且圆孔中心要跟着Boss走,UI方案往往需要四张边缘图拼框,或者用MaskableGraphic做反向镂空,维护成本不低。屏幕后处理没有这些概念,它拿到的是整张屏幕画面,叫你显示什么就显示什么,像素级精确。
而且UI贴图在Canvas缩放和多分辨率适配下容易出现边缘错位。后处理方案直接工作在屏幕UV上,中心点和半径都是归一化坐标,换分辨率不会出现“模糊的圆跑偏”这种问题。我改造这个效果时,把原来的8个UI节点删到只剩一个脚本和一个材质,DrawCall也直接少了几条。
这里有必要区分三种做法,选型基本可以照抄:
| 方案 | 实现方式 | 优点 | 主要限制 |
|---|---|---|---|
| UI贴图 | Canvas下放Alpha贴图 | 直观、易编辑 | 多分辨率适配麻烦、批次多、难做像素级动态 |
| SpriteMask | 配合SpriteRenderer | 可以遮Sprite | 只对Sprite有效,UI和不透明物体不好办 |
| 后处理Shader | OnRenderImage + 材质 | 像素级控制、开销稳定 | 需要渲染管线支持,无法单独作用于UI层 |
如果你只是给某个UI按钮加一个点击反馈遮罩,那用UI贴图就够了;但凡是“全屏范围内、位置会变、形状会变”的动态屏幕遮罩,后处理Shader是更省的方案。
2.2 遮罩形状的本质:用距离函数生成圆形遮罩
后处理Shader拿到每个像素的UV坐标后,先做一个宽高比修正。Unity的屏幕UV是左下角(0,0)、右上角(1,1),但实际屏幕不一定是正方形。如果直接算distance(i.uv, _Center),横向和纵向的像素数不一致,会把一个半径0.25的圆拉伸成椭圆。修正方法是用屏幕宽高比把UV的x方向缩放,或者缩放y方向,这里用一个常用写法:
// 屏幕UV下圆形遮罩的雏形 float2 aspect = float2(_ScreenParams.x / _ScreenParams.y, 1.0); float2 p = (i.uv - _Center) * aspect; // 中心偏移后再做宽高比修正 float d = length(p); // d = 距离中心的欧氏距离 float mask = 1.0 - smoothstep(_Radius, _Radius + _Softness, d);这段代码里,_Center是归一化的屏幕中心,_Radius是圆形半径,_Softness是边缘柔化宽度。smoothstep(a, b, x)在x小于a时返回0,大于b时返回1,中间按平滑曲线过渡;前面加一个1 -是为了让mask在圆内为1、圆外为0。mask为1时显示原始屏幕,mask为0时显示遮罩颜色,中间过渡就是半影。边缘柔化宽度如果太小,就会出现锯齿;如果太大,看起来就像水墨晕开,需要按实际分辨率调整。
这里“距离函数”是整套动态遮罩的地基。圆形用distance,如果要椭圆,修改aspect的系数;如果要扇形,再加一个角度判断;如果要条带,用abs(p.x)。总之都是对UV做数学运算,最后产生一个0到1的灰度值。The Book of Shaders 里有很多2D的SDF和噪声练习,本质上就是这套思路,搬到Unity后处理里就是现成的动态屏幕遮罩。
如果你想做反向遮罩——中央隐藏、四周显示,直接把mask取反:
float mask = 1.0 - mask;或者把lerp的第二个颜色和第三个颜色对调。这个操作在需求里经常会遇到,别在Shader里写两套,统一用同一个距离函数加一个_Invert开关最省事。
2.3 动态遮罩的三种来源:数值计算、采样贴图、外部RTT
遮罩的mask不一定非要靠公式算出来。按项目需求,可以分成三种输入来源:
第一是数值计算。距离、角度、时间、噪声、网格,这些都能直接生成mask。适合规则形状,比如Boss技能的范围预警圆、屏幕扫描线、马赛克烧屏。优点是每帧开销低、参数可控;缺点是形状一旦复杂,Shader代码里要堆很多数学公式。
第二是采样贴图。美术画一张带Alpha的遮罩图,在Shader里用tex2D(_MaskTex, i.uv).a作为mask。这种方式适合复杂美术轮廓,比如地图迷雾的边界、血迹飞溅的斑块、二次元NPR卡通渲染里常用的装饰性暗角。缺点是纹理分辨率要和屏幕适配,放大会糊,而且美术多一张资源图要维护。
第三是外部RTT。把另一个相机、或者Unity3D视频流、或者上一个后处理结果渲染到RenderTexture,再把这个RT作为遮罩纹理放进当前Shader。这样遮罩的形状每帧都在变,不受公式和静态贴图限制,适合刮奖、区域战争迷雾、视频信号故障这类效果。代价是需要额外的RenderTexture分配和一次Pass的拷贝开销。
选型时我的顺序是:规则形状用数值计算,不规则的静态形状用贴图,不规则且动态的内容用RTT。如果你只是先把一个圆形遮罩跑通,用数值计算最省事,后面的动态控制也最好接。
还有一个容易被忽略的边界:后处理的目标载体。如果你要遮罩的是Screen Space Overlay的UI画层,后处理是做不到的,因为Overlay Canvas是在相机后处理之后单独渲染的。这时可以考虑把UI切换到Screen Space Camera,或者用多个相机分层处理。这个边界决定方案选型,别等做完才后悔。
3. 用OnRenderImage跑通最小工程:完整Shader与C#脚本
在很多Unity教程里,屏幕后处理只讲两个词:OnRenderImage、Graphics.Blit。但这两个API在不同渲染管线下的行为差别很大。这里以Built-in管线为主,如果你项目用的是URP,OnRenderImage不会自动调用,得改用Renderer Feature。这也是为什么很多照抄老教程的人发现自己画面完全没有变化。
3.1 挂在相机上的C#脚本:先解决“在哪跑”的问题
后处理脚本必须挂在相机上,由相机渲染结束后调用。C#脚本本身不决定任何特效,它只负责创建材质、传递参数、触发Blit。一个最小脚本如下:
using UnityEngine; public class DynamicScreenMask : MonoBehaviour { public Shader shader; public Color maskColor = new Color(0, 0, 0, 0.8f); public Vector2 center = new Vector2(0.5f, 0.5f); public float radius = 0.25f; public float softness = 0.02f; private Material material; void OnEnable() { if (shader == null) shader = Shader.Find("Hidden/DynamicScreenMask"); material = new Material(shader); } void OnDisable() { if (material != null) DestroyImmediate(material); } void OnRenderImage(RenderTexture source, RenderTexture destination) { if (material == null) { Graphics.Blit(source, destination); return; } material.SetColor("_MaskColor", maskColor); material.SetVector("_Center", center); material.SetFloat("_Radius", radius); material.SetFloat("_Softness", Mathf.Max(0f, softness)); Graphics.Blit(source, destination, material); } }这里有一个我早期的翻车点:我在OnRenderImage里每次new Material,结果每帧都会创建一个新材质,GC开销大不说,偶尔还会让帧率抖成心电图。现在改成OnEnable时创建、OnDisable时销毁,保证整个生命周期只有一个材质。Shader.Find也是字符串查找,切换后台或热重载时有可能返回空,所以我推荐把Shader字段直接拖到Inspector,而不是靠Find。
参数含义:center和radius都在屏幕归一化坐标系里,0到1之间,取值范围和Shader里的属性对应。maskColor是遮罩颜色,但注意我们Shader里最后输出的alpha始终是1,因为屏幕后处理输出的是不透明帧;遮罩的“透明度”通过权重来决定,而不是Alpha混合。
提示:如果项目用的是URP,请确认当前相机上没有挂载Custom Render Pipeline相关组件,否则这个脚本不会产生任何效果。
3.2 最小Shader:完整代码和关键行拆解
在Project里创建一个名为DynamicScreenMask的Shader,粘贴下面的HLSL/CG代码。这是和上面C#脚本配套的例子:
Shader "Hidden/DynamicScreenMask" { Properties { _MainTex ("Screen", 2D) = "white" {} _MaskColor ("Mask Color", Color) = (0,0,0,0.8) _Center ("Center", Vector) = (0.5,0.5,0,0) _Radius ("Radius", Range(0,1)) = 0.25 _Softness ("Softness", Range(0,0.5)) = 0.02 } SubShader { Tags { "RenderType"="Opaque" } Pass { ZWrite Off Cull Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MaskColor; float2 _Center; float _Radius; float _Softness; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } fixed4 frag (v2f i) : SV_Target { // 修正宽高比:让圆形遮罩不被拉成椭圆 float2 aspect = float2(_ScreenParams.x / _ScreenParams.y, 1.0); float2 p = (i.uv - _Center) * aspect; // 距离场,得到0~1之间的mask float d = length(p); float mask = 1.0 - smoothstep(_Radius, _Radius + _Softness, d); // 用mask把屏幕原色和遮罩色做插值 float3 col = lerp(_MaskColor.rgb, tex2D(_MainTex, i.uv).rgb, mask); return fixed4(col, 1.0); } ENDCG } } Fallback Off }逻辑很简单:vert把全屏三角形铺满裁剪空间,frag针对每个像素执行一遍。_MainTex由Unity自动绑定,在OnRenderImage时它就是source纹理。_ScreenParams.x / _ScreenParams.y算的是宽高比。比如屏幕是1920x1080,aspect.x约1.78;p.x会被放大,于是原本扁的椭圆会被拉回正圆。中心点偏移在修正宽高比之前,所以中心点的相对位置不会因分辨率变化而偏移。smoothstep的三个参数都只依赖_Radius和_Softness,意味着只要调这两个Property,边缘宽度立刻变化。
很多初学Shader的开发者会问:为什么遮罩是“全屏”,而不是一个圆形物体?因为这里整个pass都是全屏三角形,每个像素都执行了frag;mask=1的像素显示原图,mask=0的像素显示遮罩色。圆只是视觉上看起来“亮区”。
3.3 挂载与调参:怎么让这个工程立刻出效果
场景搭建步骤很简单:新建一个场景,把Main Camera的Clear Flags设成Solid Color,找一个带文字或模型的画面。然后把上面DynamicScreenMask脚本挂到Main Camera上,Shader字段不填也能运行(OnEnable里会Search Find),但为了稳妥,建议在Project里创建材质,把材质拖到一个公开material字段上。由于脚本里只有shader字段,我一般会手动加一个public Material material字段,把Shader预先设置好的材质拖进去,这样可以直接在材质面板调参数。
如果你像我一样偷懒,直接在脚本面板调整center、radius、softness三个公开字段也行。以下是三个关键参数的调节经验:
| 参数 | 默认值 | 效果表现 | 调节注意 |
|---|---|---|---|
| center | (0.5,0.5) | 遮罩圆心 | 超出0~1范围会导致圆被截断,故意做边缘露光时可利用 |
| radius | 0.25 | 圆形亮区半径 | 结合aspect后是横向半径,纵向范围会小一点 |
| softness | 0.02 | 边缘过渡宽度 | 越接近0越硬边,移动端建议至少0.01以上 |
检查是否生效的方法:把radius调成0.1,center调成(0.5,0.5),你会看到屏幕中央出现一个亮圆、周围是暗边。然后把softness调到0.2,边缘会像雾气一样散开。这是最基本的动态屏幕遮罩,接下来我们就可以把radius和center做成实时变化的。
4. 让遮罩动起来:时间驱动、鼠标跟随和视频流三种实时控制
动态的本质就是让Shader的这个mask每帧都不一样。最直接的方式是脚本在每一帧修改参数,或者在Shader内部用Unity内置的时间变量。下面三种方式都值得单独落地,它们分别对应不同交互需求。
4.1 _Time驱动:呼吸、扫光和开关过渡
在Shader里,可以直接用UnityCG.cginc自带的_Time。它是一个float4,_Time.y是从场景加载起经过的秒数。做呼吸效果,只需要让半径随时间波动:
// 呼吸圆:半径在原始半径上下做正弦变化 float pulse = 0.5 + 0.5 * sin(_Time.y * 2.0); float r = _Radius * (0.8 + 0.2 * pulse); // 后面用r代替_Radius参与smoothstep这样遮罩会像心跳一样一收一放。sin(_Time.y * 2.0)的周期约3.14秒,数值范围是-1到1;先让它变成0到1的脉冲,再映射到半径的0.8到1.2倍,就能得到类似呼吸的节奏。想调快一点就把系数改成4.0,想慢就改成0.8。
如果要做扫描线式的动态遮罩,可以在C#脚本里用PingPong函数直接驱动radius:
float t = Mathf.PingPong(Time.time, 1f); float currentRadius = Mathf.Lerp(minRadius, maxRadius, t); material.SetFloat("_Radius", currentRadius);这种方式适合“技能进入CD时圆环慢慢收拢”之类的状态条。要注意的是,CPU每帧SetFloat的值很小,但尽量在Update里做,不要把这个计算放进OnRenderImage里做,避免渲染期间做不必要的时间计算。
4.2 把鼠标或角色坐标换算成屏幕UV:让遮罩跟着目标走
动态遮罩最常用的功能是“跟着目标走”,比如角色身边的探照灯。做法是把目标位置转成屏幕UV,再传给_Center。如果目标是鼠标,代码很直白:
// 鼠标坐标转屏幕UV,鼠标原点在左下角,和Shader UV一致 Vector2 mouseUV = new Vector2( Input.mousePosition.x / Screen.width, Input.mousePosition.y / Screen.height ); material.SetVector("_Center", mouseUV);如果目标是场景里的一个模型,需要先通过摄像机转成视口坐标:
Vector3 viewportPos = Camera.main.WorldToViewportPoint(target.position); // 如果相机没有翻转,这里直接就是0~1的屏幕UV material.SetVector("_Center", new Vector2(viewportPos.x, viewportPos.y));这里有一个容易翻车的点:WorldToViewportPoint返回的是视口坐标,它和屏幕UV只差一个分辨率缩放,但如果你用了Flip或镜像相机,Y轴可能是反的。另外,如果你的目标在相机背后,viewportPos的z会是负数,这时即使x,y在0~1范围内也要做遮挡判断,否则遮罩会“穿”到屏幕对面。处理办法是判断viewportPos.z < 0时直接跳过或让遮罩失效。
4.3 用Unity3D视频流或RenderTexture当遮罩源:打破公式限制
数值计算和静态贴图能覆盖大部分场景,但有些需求要求遮罩本身也在实时变化。比如做个刮刮乐,用户刮开区域就是遮罩被擦除,这用公式和贴图都没法低成本实现。常见做法是把“动态遮罩”的数据来源换成RenderTexture:另一个相机先渲染一个只画遮罩的层,或者VideoPlayer把视频流输出到RenderTexture,再把这个纹理作为_MaskTex传给Shader。
实际操作分三步:先在场景里创建RenderTexture,把VideoPlayer的targetTexture指到它,再在DynamicScreenMask脚本里加一个公开的Texture maskTex,每个时间帧把视频帧赋给材质:
public RenderTexture maskRT; maskRT = renderTexture; // 把外部RTT赋值进来 material.SetTexture("_MaskTex", maskRT);Shader里的frag对应片段如下:
// 采样遮罩纹理的红色通道作为mask float mask = tex2D(_MaskTex, i.uv).r; // 如果想边缘更利落,对mask做一次阈值 mask = smoothstep(0.1, 0.5, mask);为什么用红色通道?因为视频流或RTT往往是彩色图,如果直接拿它做混合,会出现颜色污染。只取r通道等于把灰度/Alpha信息抽出来,得到黑白遮罩。如果你渲染的是带透明通道的格式,直接采样a更好。用Unity3D视频流做遮罩的好处是,视频的每一帧天然就是一个动态时间轴,遮罩的变化根本不需要你写任何动画逻辑;坏处是RTT的分辨率、格式和内存都要额外维护,别一股脑开到1920x1080,如果只是边缘形状,用512x512足够。
到这里,遮罩已经能跟着任意参数动起来。但动态效果最容易出的不是功能问题,而是画面表现问题。下面整理几个高频翻车现场。
5. 避坑与排查:动态屏幕遮罩的4个翻车现场
写了几个月后处理,90%的问题不在Shader语法,而在坐标系、生命周期和渲染管线的边界条件。下面4条是按真实频率排的,遇到问题先对号入座。
5.1 现象:遮罩中心和鼠标/角色位置怎么都对不上
原因:坐标原点不一致。Unity的鼠标坐标Input.mousePosition和Shader的UV原点都在左下角,所以直接转换能对上;但如果你用Screen Space - Camera模式的Canvas,UI坐标经过了Canvas缩放,或者你用Camera.ScreenToViewportPoint后又在脚本里加了偏移,位置就会整体偏掉。
解决:先固定一个标准坐标系。后处理Shader使用的是视口坐标,所以从任何输入转换到视口坐标后,不要再做二次缩放。鼠标坐标除以屏幕宽高就能得到视口坐标;角色坐标用WorldToViewportPoint得到的就是视口坐标。如果这样还是偏,检查相机的targetTexture是不是存在,或者是否开启了垂直翻转。必要时在C#里打印出传入的_Center值,再用同屏渲染一个Debug小球,看小球位置和遮罩圆心是否重叠。这是最直接的定位方式。
5.2 现象:屏幕整体变成一片纯色,既看不到原图也看不到遮罩
原因:mask计算反了,导致所有像素都落在遮罩侧。最常见的是smoothstep的参数顺序写反,或者lerp的两个颜色顺序反了。比如我早期写成lerp(originalColor, maskColor, mask),圆内离原图更近,圆外显示遮罩色,虽然看起来很像反向遮罩,但至少能看到内容;如果你把smoothstep(_Radius + _Softness, _Radius, d),会让d越大mask越小,最后所有像素的mask都接近0,整个屏幕都被_MaskColor覆盖。
解决:先固定一个最小实验。把_Center设成(0.5, 0.5),_Radius设成0.2,_Softness设成0.05,然后用FrameDebugger查看这一个Pass的颜色输出。如果画面依旧纯色,把_MaskColor调成红色,你会发现整个画面变成红色,说明mask在绝大多数像素上为0,问题一定出在mask的数学表达,而不是Blit管线。用step(_Radius, d)这种简单函数替换smoothstep,先确认距离d的数值范围。
5.3 现象:动态效果一顿一顿的,像PPT
原因:CPU和GPU之间发生同步阻塞,或脚本每帧触发了高成本操作。常见行为是:在Update里每帧调用new Material、在OnRenderImage里调用RenderTexture.GetPixels,或者给Shader属性赋值时使用了字符串查找而不是Shader.PropertyToID。字符串查找在低版本Unity里确实会小幅引起GC,但更多时候是GetPixels这类回读操作把CPU停住了。
解决:把材质缓存成字段,在OnEnable创建,OnDisable销毁;所有属性ID用静态缓存的整数,比如private static readonly int CenterID = Shader.PropertyToID("_Center");。脚本这边只做SetVector/SetFloat,不做GetPixels。如果卡顿还是存在,打开Profiler的GPU帧时间看看,重点看Camera.Render里的RenderPass。大部分Unity后处理的卡顿是玄学,其实是被别的深拷贝流程拖累的。
5.4 现象:移动端边缘锯齿明显,发热还高
原因:后处理是全屏像素计算,软边宽度太小会直接表现为锯齿;同时全分辨率RT会让像素数量接近4K,发热自然上来。另一个原因是某些移动GPU对smoothstep精度不高,半影区域会出现明显的色阶断层。
解决:调整_Softness到0.03以上,如果需要非常硬边,不如用step配合fwidth做一个抗锯齿:
float aa = fwidth(d); float mask = 1.0 - smoothstep(_Radius - aa, _Radius + aa, d);这样边缘宽度会根据屏幕像素密度自适应。移动端性能方面,可以先通过RenderTexture.GetTemporary把后处理降到屏幕分辨率的0.5倍,Blit完成后再放大,视觉损失通常不明显,但发热和耗电能降一大截。这是我从多个上线项目里得到的血泪经验。
6. 进阶:用Gradient贴图柔边,再用FrameDebugger验证开销
如果把上面整套方案跑通,动态屏幕遮罩已经能应付大多数交互需求。剩下要打磨的是“边缘好不好看”和“到底值多少性能”。
6.1 用Gradient贴图代替smoothstep做柔边
smoothstep是一段固定的平滑曲线,它的问题是“所有软边看起来都一样”。很多项目中特效边缘需要露出少量噪点或羽化,直接用公式写会很痛苦。常见做法是准备一张1D的Gradient贴图,用遮罩的归一化距离查表。比如:
float t = clamp(length(p) / _Radius, 0.0, 1.0); float mask = tex2D(_EdgeGradient, float2(t, 0.5)).r;这样你可以把边缘过渡曲线完全交给美术:中部是硬边,尾部带一点虚线,都能在纹理里画出来。如果不想引入美术资源,也可以用AnimationCurve在脚本里生成一张Texture2D,这样参数仍然留在C#面板上。
6.2 用FrameDebugger验证这个Pass到底花了多少GPU时间
验证要敢于相信工具。打开Window > Analysis > Frame Debugger,点击截帧,在RenderPass列表里找到DynamicScreenMask这个Pass。如果它出现了两次,说明有额外的Blit;如果没出现,说明Shader没被正确渲染,先去检查C#脚本是不是挂在非主相机上。配合Profiler可以看到GPU总耗时,但我更推荐先用FrameDebugger看pass数量,因为后处理的开销大部分奉送给了全屏像素填充。如果你发现DrawCall数量只增加了1,但帧率有明显下降,大概率是分辨率太高或RT分配过多,先把后处理目标降一半。
6.3 一个Shader多形态:用Keyword切换三种遮罩模式
做动态屏幕遮罩时间长了,你会发现公式、贴图、RTT三种方式其实可以共用一套Shader,用Keyword切分支。#pragma multi_compile MASK_DISTANCE MASK_TEXTURE MASK_RT,C#里用Material.EnableKeyword切换,不同模式下只改mask来源,混合逻辑统一。这样同一套脚本可以服务技能范围、关卡迷雾和视频故障特效,不用为每一个需求复制一个Shader文件。
我现在的习惯是,拿到“动态屏幕遮罩”需求时,先问三个问题:什么坐标系?什么渲染管线?遮罩要不要每帧更新?这三个答案直接决定了后面代码怎么写。早期我不问,上来就改Shader,结果在URP项目里白白折腾了半个下午。希望这些排查顺序和参数经验帮到你,省下一段不必要的翻车时间。
本文还有配套的精品资源,点击获取