
如果 PDF 是本地文件,还多一层限制。
即便你的扩展本来支持 file:///,Chrome 也要求用户在扩展详情页手动开启"允许访问文件网址(Allow access to file URLs)",否则扩展对本地 PDF 也不能工作。
浏览器把"网页内容"和"浏览器自身/扩展自身的受信 UI"刻意隔离开了。
你可以把它理解成三层:
第一层是普通网页,比如 https://...。
内容脚本本来就是为这层设计的:它运行在网页上下文里,可以读 DOM、改 DOM、响应点击。Chrome 官方对 content scripts 的定义就是"运行在 web pages 的上下文中"。
第二层是本地文件,比如 file:///...。
这层已经比普通网页更敏感,因为可能涉及你磁盘上的文档。Chrome 因此不默认放开,要求用户单独开启"Allow access to file URLs"。也就是说,即使扩展声明了权限,浏览器仍然要用户再做一次显式授权。
内置 PDF Viewer,本质上就是浏览器/扩展提供的专用界面,不是普通网页。若任意第三方扩展都能把脚本注入进去,就会出现非常危险的能力升级:它可能读取你在浏览器受信界面中的内容、劫持点击、篡改 UI、伪造权限提示,甚至干扰别的扩展页面。Chrome 用扩展页 CSP、扩展页与内容脚本分离、以及受限的注入模型,目的就是防止这种"扩展之间"或"网页到浏览器 UI"的越权。Chrome for Developers+2Chrome for Developers+2
浏览器自己的界面、权限页、PDF 查看器、别的扩展页,如果允许随便注入,攻击者可以把按钮、菜单、文案做得和系统 UI 一样,诱导用户点错。这类风险比改普通网页严重得多,因为用户天然更信任浏览器原生界面。这个动机和 CSP 的设计目标是一致的:尽量减少脚本注入面、降低 XSS/篡改风险。Chrome for Developers+1
对应到你这个 PDF Viewer 场景,本质上是两道门:
一是这个 PDF 可能来自 file:///,所以先受"文件访问"限制。
二是显示 PDF 的壳子又是 chrome-extension://... 的 PDF Viewer,所以还受"不能把普通 content script 注入受信扩展页"的限制。
这就是为什么你会看到"插件不能读取或更改此站点的数据"这一类提示。前者是数据来源敏感,后者是宿主页面本身敏感。
URL 拦截 / 打开方式 → 扩展 viewer → PDF.js 渲染 → text layer 点击取词 → 后台翻译服务。
重点是换入游览器的js形式----算了,右键也是一样

WebRTC 不是"网站都默认在用"的通用传输层。它主要用于实时双向 场景:浏览器里的音视频通话、屏幕共享、实时语音、以及低延迟双向数据通道。技术上通常会看到 RTCPeerConnection、getUserMedia()、RTCDataChannel 这些接口;如果站点没有建立对等连接或采集媒体,检测工具通常就会显示"未使用 WebRTC"。MDN Web Docs+3MDN Web Docs+3MDN Web Docs+3
所以"哪些网站可以用 WebRTC"要分成两类看。第一类是明确需要实时互动 的网站,例如 Google Meet、Discord 网页版、Zoom Web、Slack Huddle/Call、一些在线客服语音/视频页面、浏览器云游戏/远控/白板协作、文件点对点传输工具等。这类页面通常真的会建立 WebRTC 会话。第二类是平时不用,只有某个功能触发时才用 的网站;你不开那个功能,检测页就会显示没有 WebRTC 活动。这个现象本身是正常的,因为 WebRTC 连接不是"网页一打开就必须存在"。MDN Web Docs+2MDN Web Docs+2
YouTube 普通看视频基本不属于 WebRTC 的典型使用场景。WebRTC 设计重点是浏览器到浏览器或浏览器到实时服务的低延迟双向通信;而 YouTube 常规点播是大规模分发的视频流业务,不需要浏览器和服务端之间建立那种实时对等连接。Google 官方公开的 YouTube Live 文档侧重的是直播事件和视频流管理,并没有把普通观看页面描述成 WebRTC 应用;帮助页讲的是"直播"这一产品能力,而不是网页观看端一定通过 WebRTC 传输。你在 YouTube 页面上看到"未使用 WebRTC",通常是符合预期的。Google Help+3MDN Web Docs+3Google for Developers+3
ChatGPT 也是同理。普通文本聊天 不需要 WebRTC,所以检测工具显示未使用,很正常。OpenAI 官方文档明确写的是:浏览器里的 Realtime/voice agent 场景推荐通过 WebRTC 连接实时模型;也就是说,WebRTC 主要对应的是实时语音/音频交互 ,不是普通文本问答页面本身。OpenAI 还单独提供了 "Realtime API with WebRTC" 文档,这进一步说明 WebRTC 是某些实时能力的传输选项,而不是所有 ChatGPT 页面都会启用的底层。OpenAI+3OpenAI Developers+3OpenAI Developers+3
你检测时之所以常看到"没用",还有一个常见原因:很多检测扩展看的是当前页面有没有活跃的 RTCPeerConnection / ICE candidate / 媒体采集 。如果页面只是加载了相关脚本、预留了通话功能、或者代码里有 WebRTC 能力但当前没拨号、没开麦、没共享屏幕,它也会显示未使用。这不代表网站"永远不用",只代表你当前这个标签页、当前这个时刻 没有在跑 WebRTC 会话。MDN Web Docs+2MDN Web Docs+2
判断一个网站到底有没有在用 WebRTC,最可靠的方法不是只看"这个域名",而是看你触发了什么功能 。例如:进入语音通话、视频会议、网页端实时客服、屏幕共享、浏览器内对讲,这时更可能出现 WebRTC;只是看文章、看普通视频、刷信息流、文本聊天,通常不会。MDN Web Docs+2MDN Web Docs+2
你可以自己做一个很直接的验证:打开 Chrome DevTools,在 Console 里执行 window.RTCPeerConnection 看浏览器是否支持;再在 Network 之外结合扩展观察,只有当你真正发起语音/视频/屏幕共享时,才更可能看到候选地址收集和连接建立。单纯停留在 YouTube 播放页或 ChatGPT 文本页,看到"未使用"基本就是正常结果。MDN Web Docs+1

不同 UI 元素的三角面数量情况
-
基础矩形 UI(Image/Button,无特殊效果)
这类最常规的矩形 UI,确实是由 两个三角面组成(拼成一个矩形),这是最基础的情况。
2. 带复杂形状的 UI- 圆角矩形:圆角部分需要用多个小三角面来逼近圆弧,圆角越平滑,三角面数量越多。
- 不规则图形(如多边形、自定义 Sprite):根据图形的顶点数决定三角面数,比如一个正六边形需要 6 个三角面拼接。
- 文本(Text/TMP):每个字符的轮廓由字体的矢量数据转化而来,会生成大量三角面(尤其是复杂字体或大字号),并非简单的两个三角面。
- 带特效 / 遮罩的 UI
- 遮罩(Mask):若遮罩是不规则形状,会额外生成用于裁剪的三角面,或通过 Stencil Buffer 实现但不增加三角面,不过复杂遮罩的计算仍会间接关联几何数据。
- 渐变 / 描边 / 阴影:部分特效会通过扩展顶点生成额外三角面(如描边需要在原图形外再生成一圈三角面)。
简单说,只有 纯矩形、无任何特殊形状和特效的基础 UI,才会保持两个三角面的最简结构;一旦形状或效果变复杂,三角面数量会显著增加。
4 个顶点的采样插值为何不会让 UI 图片模糊?
UI 图片的采样清晰度 不取决于顶点数量 ,而是由 纹理采样方式 和 UV 坐标的映射逻辑决定,核心原因有两点:
- UV 坐标的线性插值特性
GPU 在渲染时,会根据顶点的 UV 坐标,对像素级别的纹理位置做线性插值 。矩形的 4 个顶点对应纹理的(0,0)、(1,0)、(1,1)、(0,1)四个 UV 点,GPU 会在这四个点之间,为每个像素计算出精确的纹理采样坐标,而非只在顶点处采样。哪怕只有 4 个顶点,也能覆盖整个纹理的像素信息。
UI 的纹理过滤与像素对齐
- Unity 等引擎中 UI 默认使用bilinear/trilinear 过滤,会对纹理的相邻像素做插值采样,减少锯齿和模糊;
- UI 渲染时还会开启像素完美对齐(Pixel Perfect),让 UI 的像素与屏幕像素一一对应,避免因缩放、偏移导致的采样失真,这是保证 UI 清晰的关键,和顶点数量无关。
所以顶点的数量更多的是控制采样的位置合适,而不是贴图是否清晰,贴图的采样永远是非常清晰的,顶点的数量是为了让贴图贴的更"准"
过程拆解成 '几何定义' 和 '纹理填充' 两个完全独立的步骤
纹理采样是在 "填肉",且是超采样
当 GPU 要画出这个矩形的每一个像素时,它会做两件事:
- 定位 :对于屏幕上的第 i 个像素,它通过顶点数据插值算出这个像素在局部坐标系的位置。
- 取样(关键):根据这个局部位置,去贴图(Texture)上找颜色。
虽然你的几何只有 4 个点,但 纹理采样是针对屏幕上的每一个像素进行的。
- 假如你的 UI 图是 1000x1000 的,而你显示在屏幕上是 200x200 的像素。
- 虽然几何 Mesh 只有两个三角形(4 个点),但 GPU 会在这 200x200 = 40,000 个像素点上,逐个去贴图里采样颜色。
所以,绝对不会出现 "只采 4 个点" 的情况。顶点数量决定了 "架子的精度",而采样的密度取决于 "屏幕像素密度"。
另外一个防模糊的黑科技:UV 拉伸与像素完美(Pixel Perfect)
UI 系统为了让你觉得 "图贴得准",做了一个隐式操作: UV 拉伸(Stretch)。
- 你的图是 512x512。
- UI 系统在构建 Mesh 时,会把这张图的 UV 坐标 (0,0) 严格映射到顶点 0,(1,1) 严格映射到顶点 1。
- 这就意味着,贴图的每一个像素都会被拉伸到屏幕的对应像素 。如果开启了
Canvas Scaler的 Pixel Perfect,屏幕像素就会 1:1 匹配贴图像素,采样出来的结果就是绝对清晰的。
纹理的清晰程度,取决于 贴图本身的分辨率 和 采样时的拉伸比例(UV),而不在于你用了 4 个顶点还是 400 个顶点去框这个矩形。
- 逐像素纹理采样:是 GPU 对 UI 矩形区域内的每个屏幕像素,根据 UV 坐标从纹理中取色的常规操作,是 2D UI 渲染的基础,无性能额外开销。
- 超采样(SSAA) :是对渲染画面的过采样(比如用 2K 分辨率渲染再缩放到 1080P),目的是抗锯齿,会大幅增加 GPU 负载,UI 渲染中极少使用。
UI 纹理分辨率固定,放大时会因贴图像素数 < 屏幕显示像素数,导致 GPU 通过纹理过滤(如双线性)插值补色,出现模糊;缩小则可能因纹理过滤丢失细节,这是纹理本身的分辨率瓶颈,而非采样方式的问题。
Unity 的Canvas Scaler提供了Constant Pixel Size (固定像素)、Scale With Screen Size(随屏幕缩放)等模式,若选择固定像素模式,UI 在不同分辨率屏幕上缩放会失真;但选对适配模式(如按屏幕宽高比缩放),就能实现自由缩放且保持清晰。
实现 UI 自由缩放且不模糊
- 使用矢量图 / 可缩放纹理(SDF):如 TextMeshPro 的 SDF 字体、Sprite Shape 的矢量图形,缩放时不会丢失细节。
- 开启Mipmap并设置合适的纹理过滤模式(如 Trilinear),减少缩放后的纹理失真。
- 采用多分辨率纹理集:为不同屏幕分辨率准备对应尺寸的 UI 纹理,适配时调用匹配的纹理。
在Unity中,设置纹理的Filter Mode为
**Bilinear(双线性滤波)**会改变GPU采样时的原理,相比Nearest(临近采样),它会在采样时对像素进行加权平均,使纹理在放大时更平滑,消除锯齿,但这属于常规的GPU纹理采样映射机制
- 采样原理 :当纹理映射的像素点不处于纹理纹素(Texel)的中心时,Bilinear采样会选取目标点周围最近的4个纹素,根据其距离远近进行线性加权平均,得出最终颜色。
- 适用场景:适用于大多数3D场景下的纹理,提供比Point(Nearest)更平滑的视觉效果,比Trilinear(三线性滤波)计算量小。
如何设置 :选中图片资源,在Inspector窗口中找到 Filter Mode,设置为Bilinear。
3D 物体 SetActive 和 UI SetActive,性能后果天差地别。
我用最干脆的话给你讲清楚:
1. 3D 物体:SetActive 几乎不触发重建、不打断合批
- 3D 的 Mesh 是静态 / 独立的,不会合并成一个大 Mesh
- SetActive 只是把物体从渲染列表移除 / 加回
- 不会重新计算布局(3D 没有 Layout)
- 不会重建 Mesh
- 不会破坏合批(静态批处理已经预先打好,动态批是引擎临时处理,也不会因为你显隐一个物体就全炸)
所以:
3D 里频繁 SetActive,开销非常小,几乎没问题。
一句话总结区别
- 3D:SetActive = 渲染开关,轻量。
- UGUI:SetActive = 重建整个面板,重量级。
4. 延伸一个你可能马上会踩的坑
- 3D 用
enabled = false(MeshRenderer.enabled)
→ 和 SetActive 接近,轻量,只是不渲染。 - UI 用
graphic.enabled = false
→ 仍然会打断合批,比 SetActive 轻一点,但依然不推荐高频使用。
UI 想要真正轻量隐藏、不重建、不打断合批:
只有 alpha = 0 这一条路。
UI 物体:SetActive = 爆炸级开销
原因你已经知道:
- UI 所有元素共用一个大 Mesh
- 显隐一个元素 = 整个 Canvas 重新生成 Mesh
- 会触发 LayoutRebuild + GraphicRebuild
- 合批全部打断,重新 Batch
这是 UGUI 结构决定的,不是 Unity 故意坑。
合批的前提:所有东西必须是同一套渲染状态
GPU 一次 DrawCall,只能画 一套状态完全一致的东西:
- 同一个 Shader
- 同一个材质 / 纹理
- 同一个混合模式(Blend)
- 同一个模板测试 / 深度测试配置(Stencil / ZTest)
只要 任何一项不一样,GPU 就必须:
- 结束当前 DrawCall
- 切换渲染状态
- 开新的 DrawCall
不能跨状态合批,这是硬件级限制,不是软件优化问题。
Mask 本质就是强行改了 Stencil 状态
普通 UI:
- Stencil 关闭 或 固定不变
带 Mask 的 UI:
- 先写 Stencil(标记可显示区域)
- 再用 Stencil 测试(只显示区域内像素)
- 最后还要恢复 Stencil 状态
这意味着:
- 普通 UI = 状态 A
- Mask 内 UI = 状态 B
- 恢复后又 = 状态 A
状态变了 → 必须切 DrawCall → 合批直接断开。
Alpha Cut(AlphaTest / Discard)
属于片元着色器内部逻辑
- 看纹理的 alpha 值
- 如果 alpha < 阈值,直接 discard 丢弃这个像素
- 不写颜色、不写深度、不写模板
- 只是不画这个像素
关键点:
- 不改变任何 RenderState
- 不影响合批
- 同一个材质、同一个 shader,不管怎么 discard,都能正常合批
所以场景模型、树叶、草、瓦片用 AlphaCut, 完全不会打断合批。
Stencil(Mask 用的模板缓冲)
属于 GPU 固定管线状态
- 是 GPU 硬件层面的开关
- 会修改 Stencil 操作、参考值、比较函数
- 属于 RenderState
关键点:
- 只要 Stencil 状态不同 → 不能合批
- 这是硬件规则,不是 shader 逻辑
- UGUI 的 Mask 本质就是一套固定的 Stencil 状态
UI 是 "先合批,再裁剪",3D 是 "先裁剪,再合批"
这是最根本的结构差异。
- 3D 模型
每个物体独立渲染 → 自己 shader 里 discard 像素 → 不影响别人
→ Alpha Cut 完全没问题。 - UGUI
所有 UI 先合并成 同一个大 Mesh、同一个 DrawCall
→ 再一起送入 GPU 渲染
如果你在这个大 Mesh 里 某一部分想用 Alpha Cut 做遮罩:
- 你怎么告诉 GPU:
"只让这一小块区域内的像素保留,外面的 discard"?
Shader 里做不到,因为:
整个 UI 是同一个 DrawCall、同一个材质、同一个 shader 实例。
你不能让 shader 对一部分像素裁剪、另一部分不裁剪。
→ Alpha Cut 做不到局部区域裁剪,只能全局裁剪。
Alpha Cut 不能嵌套,Mask 可以
UI 经常需要:
- 滚动列表遮罩
- 窗口内遮罩
- 按钮内遮罩
- 多层嵌套遮罩
Alpha Cut 是像素丢弃,没有层级、没有标记、没有状态。
你根本实现不了多层嵌套裁剪。
但 Stencil 可以:
- 写 1
- 只允许 1 的区域显示
- 子遮罩写 2
- 只允许 2 的区域显示
- 最后恢复
这是 Alpha Cut 永远做不到的。
UI 用 Alpha Cut 会产生严重锯齿,无法修复
UI 是屏幕像素对齐的,边缘要求极干净。
Alpha Cut 是 0/1 二值丢弃,边缘必然锯齿。但 UI 遮罩需要平滑边缘、抗锯齿。
Stencil 配合矩形边缘精确采样,才能做到干净裁剪。
4. 真正的致命问题:UI 裁剪需要 "外部形状",Alpha Cut 只能看自身纹理
UGUI 的 Mask 是:用一个矩形(父物体)定义裁剪区域,子物体全部被这个区域限制。
这是外部区域约束。
Alpha Cut 是:看自己贴图的 alpha,丢弃像素。 这是自身纹理约束。
你要实现 "滚动框" 这种:
- 子物体随便动
- 超出父矩形就隐藏
Alpha Cut 完全做不到。只有 Stencil 能实现 "区域裁剪"。

Canvas 内部维护了一个私有 Batch 列表。它会主动忽略场景中其他 3D 物体的合批机会,只把自己内部的 UI 元素(矩形 Mesh)合并在一起。这使得 Canvas 的渲染数据在 GPU 调度队列中是一个独立的连续块。
- 为了渲染层级(Z-Order)的绝对可控 :UI 必须盖在 3D 场景之上。如果 Canvas 尝试和 3D 物体合批,渲染顺序就会被打乱,导致 UI 被场景遮挡或者出现严重的闪烁。
- 为了重建效率:UI 的变化通常非常频繁(比如每帧变动的进度条、字体)。Canvas 内部维护私有 Batch 列表,意味着当 UI 元素变化时,它只需要重新计算这个小范围内的顶点数据,而不会触发整个 3D 场景的渲染重绘(Rebuild)。
本质上,Canvas 就是在 GPU 繁忙的任务队列里强行划出了一块"自留地",专门用来高效处理那些基于矩形的、平面化的渲染任务。
渲染管线隔离:Render Target / Frame Buffer 分离
在 Screen Space - Overlay 模式下,Canvas 拥有 独立的渲染优先级。
UI 是在 3D 场景渲染完成后,作为最后一个渲染阶段直接画在主 Frame Buffer(即 Back Buffer)之上的。
直接渲染到主缓冲(默认常用方式)
流程 :渲染 3D 场景 -> 后处理(Post-processing) -> 渲染 UI -> 显示到屏幕。
优点:省内存,减少了一次全屏贴图的内存占用和一次昂贵的读写开销(Blit)。
Canvas 体现 :Unity 的 Screen Space - Overlay 模式通常就是这种逻辑。它直接在当前 Render Target 上按顺序绘制 UI 的 Batch。
渲染到独立的 Render Texture / Frame Buffer
在某些特定需求下,引擎确实会开辟一块独立的 Buffer 来渲染 UI:
UI 需要特殊后处理 :如果你想给整个 UI 界面加一个模糊效果、描边,或者像《赛博朋克 2077》那种 UI 故障抖动效果,必须先将 UI 画在一张独立的 Render Texture 上,处理完后再和场景合并。
复杂的层级混合:当 3D 场景和 UI 需要极其复杂的遮挡关系(例如某些 UI 元素要在 3D 物体中间穿插)时,可能会使用独立的 Buffer。
异速更新(Optimization):为了节省性能,某些引擎可能会让 UI 以较低的频率(比如 30fps)渲染到一个离屏 Buffer 中,而 3D 场景保持 60fps。每一帧只需把上一帧画好的 UI 贴图直接盖上去即可。

Canvas 强制所有 UI 使用 Alpha Blending 且通常 ZWrite Off。
在 3D 空间中,我们靠深度缓冲区(Z-Buffer)来决定谁在前谁在后。但 UI 走的是另一套逻辑:
- 层级由顺序决定 :UI 的前后遮挡关系是由你在 Hierarchy 面板里的渲染顺序(Order in Layer / Sibling Index)决定的。
性能权衡:关闭 ZWrite 意味着 GPU 不需要去更新深度缓冲区,省下了一点带宽。
Canvas 必须开启 Alpha Blending (通常是 SrcAlpha OneMinusSrcAlpha )。
这带来的副作用:Overdraw(过度绘制)
因为 ZWrite Off + Alpha Blending ,GPU 无法进行 Early-Z 剔除(即:无法因为后面有东西挡着就不画前面的)。
这意味着:即使一个全屏的大 UI 背景盖住了后面的 3D 场景,GPU 还是会老老实实地先画 3D 场景,再把 UI 叠加上去。
- 性能瓶颈 :UI 优化中最头疼的 Overdraw 就在这里。如果你叠了 10 层透明 UI,那同一个像素点就会被着色器计算 10 次。
虽然 Canvas 默认关掉它,但在某些特殊情况下你会看到它被开启:
- 3D UI:当 UI 穿插在 3D 场景中(World Space),为了和 3D 物体有正确的遮挡关系。
- 极端优化:在某些性能极差的移动设备上,如果 UI 包含大量完全不透明(Opaque)的矩形块,开发者会自定义 Shader 开启 ZWrite 并改用不透明渲染序列,以此减少 Overdraw。
Sub-Canvas(子画布)最直接的底层物理表现就是"切断":它强制终结当前的 Mesh 合并进程 。
- 普通 Canvas:会尝试将所有子节点下的 UI 元素(Image, Text 等)扫描一遍,打乱顺序(在保持层级逻辑的前提下)合并成一个巨大的顶点缓冲区(Vertex Buffer)。
- Sub-Canvas :当扫描器遇到
Canvas组件(即使是子物体上的)时,它会立刻停止当前的合并,将 Sub-Canvas 及其所有子对象划归为一个全新的、独立的 Mesh 列表。 - 结果:父 Canvas 的 Batch 无法穿透进入子 Canvas,子 Canvas 的元素也无法与外部元素合批。
- 指令切断:独立的 Draw Call
因为 Mesh 被切断了,GPU 必须通过两个独立的绘制指令来处理它们:
-
绘制父 Canvas 的顶点数据。
-
切换状态(如果需要),绘制 Sub-Canvas 的顶点数据。
这在 Profiler 中表现为:增加了一个 Draw Call。 -
重绘切断:脏标记(Dirty Flag)的隔离
这是使用 Sub-Canvas 最核心的优化理由。
- 隔离重绘(Rebuild):在 UI 系统中,如果一个元素变动(位移、缩放、改文字),整个 Canvas 都需要重新计算网格(Rebuild Mesh)。
- 局部化 :如果你把一个频繁闪烁或移动的图标放在 Sub-Canvas 里,当它变动时,只有这个 Sub-Canvas 会触发 Rebuild。父 Canvas 及其它静态元素会保持静止,完全不受干扰。

无图 UI 遮罩点击优化 ,核心是 用非渲染的碰撞体 / 检测区域替代空 Image 做点击交互,彻底消除空 Image 带来的 Overdraw 开销。
为什么需要这个优化?
UGUI 中如果用 ** 空 Image(无贴图、alpha=0)** 做点击遮罩:
- 空 Image 仍会被 GPU 渲染,产生无效的 Overdraw(像素填充但无视觉效果),占用 GPU 算力;
- 即使 alpha=0,它也会参与 Canvas 的 Mesh 构建和批处理,增加 CPU 的 Rebuild 开销。
具体优化方案
- 使用
GraphicRaycaster+Collider替代空 Image
- 给 UI 父物体添加BoxCollider2D(匹配遮罩区域);
- 保留
GraphicRaycaster负责点击检测,移除空 Image 组件。 - 原理:Collider 仅参与射线检测,不生成渲染 Mesh,无 Overdraw。
- 用
UnityEvent+ 代码逻辑实现区域点击
- 直接在脚本中通过屏幕坐标判断点击区域,完全抛弃 UI 图形组件;
- 适合简单的矩形 / 规则形状点击遮罩,性能最优。
给 ScrollView 父节点添加单个碰撞体(如 BoxCollider2D),替代子元素的射线检测,减少遍历次数。
优先使用 RectMask2D 替代 Mask:RectMask2D 基于几何裁剪,不修改 Stencil 状态,不会打断合批,性能损耗远低于 Mask。
核心机制:数学剔除 vs. 模板测试
- Mask (Stencil) :利用 GPU 的 Stencil Buffer(模板缓冲)。它先画一个"隐形"的形状存入缓冲区,后续元素绘制时要逐像素比对这个缓冲区。这涉及复杂的渲染状态切换。
- RectMask2D :利用 数学运算 。它直接把遮罩矩形的四个边坐标(Left, Top, Right, Bottom)传给 Shader。Shader 在处理 UI 顶点的片元(Pixel)时,简单判断一下坐标是否在矩形外,如果在外面就直接
discard(丢弃)或设置 Alpha 为 0。
- 确保 RectMask2D 的裁剪区域与 ScrollView 视口完全匹配,减少无效裁剪计算。
对"合批"的保护:不增加 Draw Call
这是它最大的优势:
- Mask :每一个 Mask 组件都会强制开启一个 Stencil 状态。这通常会导致 Draw Call 翻倍(至少多出一个写入 Stencil 的 Call,且内部元素无法与外部合并)。
- RectMask2D :由于它只是在 Shader 里多跑了几行简单的代数运算,并没有改变渲染状态(Render State)。这意味着:同一个 Canvas 下,使用相同材质的多个 RectMask2D 内部元素,理论上依然可以合并成一个 Batch。
RectMask2D 不打断合批,但如果你的 UI 列表非常长(比如 1000 个 Item),它在 CPU 端计算"哪些元素在矩形外需要隐藏"的操作(Culling)依然会产生开销。
- CPU 端:RectMask2D 几乎没有额外开销,只是传递几个浮点数。
- GPU 端 :虽然 Shader 里多了几行代码(插值计算坐标并对比),但现代 GPU 对这种简单的数学指令(CMP/SELECT)处理速度极快,远比读写 Stencil Buffer 的带宽消耗要低。
