深度测试、Early-Z 与深度精度

深度测试、Early-Z 与深度精度:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。

学习目标

  • 能解释深度比较、深度写入与颜色写入的契约,以及驱动何时能提前执行 Early-Z
  • 能配置并验证 GL_DEPTH_TESTglDepthFuncglDepthMask 与每帧清除值
  • 能从 gl_FragCoord.z 反算线性视图空间距离,并解释 near/far 如何影响精度
  • 能区分“关闭深度测试”“GL_ALWAYS 总通过”和“关闭深度写入”三种状态
  • 能诊断 Z-fighting、上一帧深度残留、错误比较函数和 gl_FragDepth 引起的性能问题

为什么谁先画都不该决定谁挡住谁

想象一条往画布上贴贴纸的流水线:场景里每个物体被拆成无数小色块,一块接一块地送上传送带,轮到谁就把谁贴到画布对应的位置。问题来了——如果一个近处的杯子和一面远处的墙,恰好要贴到画布同一个点上,先送到的那块会先贴,后送到的又盖上去……那最后留在画布上的,到底该是杯子还是墙?

如果完全听传送带的「谁后到谁说了算」,结果就乱了:墙明明在杯子后面,却可能因为「碰巧后送上来」而盖住了杯子,近的反被远的挡住,画面穿帮。这一章要解决的就是:怎么让流水线自动认准「近的该挡住远的」,不管它们谁先谁后送上来

办法是在传送带末端加一道质检工序:每块色块贴之前,先报一下「我离镜头多远」,和这个位置上已经贴着的那块比一比——更近才准贴、更远就丢掉。有了这道工序,三维物体的前后遮挡就全自动正确了;没有它,你的场景就只是一堆按上色顺序胡乱叠起来的色块。

深度缓冲:每个像素偷偷存了一个「远近」

那道质检工序要能「比远近」,前提是画布上每个像素都记着「我现在贴的这块,离镜头多远」。存这份远近账本的地方,就是 (depth buffer,又叫 z-buffer)。

它和你上一篇认识的颜色缓冲是「孪生兄弟」:尺寸一样大、一一对应每个像素,只不过颜色缓冲存的是「这个像素什么颜色」,深度缓冲存的是「这个像素现在贴着的色块有多远」。屏幕显示的是颜色缓冲;深度缓冲不直接显示,只在幕后默默记账。它通常是 24 位精度,足够细地区分远近。

每个像素记的那个数,就是 (depth value)。本章先采用 OpenGL 教程的标准配置:0 表示最近、1 表示最远;后文提到 reversed-Z 时会明确说明状态反转。下面这张图把「一个像素 = 颜色 + 深度两份数据」,以及「靠比深度让近物盖住远物」画在一起:

为什么要专门存这份账?因为只有记下每个像素「当前贴的有多远」,新来的色块才有得比。它在管线里的位置紧贴着颜色缓冲:每写一个像素的颜色,旁边就配一个深度,供下一块色块过来时核对。

深度测试:新色块和账本比一比,近的才准贴

有了账本,那道质检工序就能干活了。(depth test)做的事,一句话:片段要写进某像素前,先拿最终深度和缓冲旧值比较;是否通过与是否写回深度由不同状态控制

具体地,对每一个片段:读出它的深度值 → 和深度缓冲里同位置那个数比较 → 若「更近」(默认即深度值更小)就通过,把颜色写进颜色缓冲、并把这个更小的深度值更新进深度缓冲;否则不通过,整个片段被丢弃,颜色缓冲和深度缓冲都不动。这样无论物体以什么顺序送进管线,留在屏幕上的永远是每个像素处最近的那块。

从规范结果看,深度测试决定片段是否能更新帧缓冲;但硬件调度不一定等片段着色器跑完。若 shader 不会用 discard 改变覆盖、不写 gl_FragDepth 改变最终深度,也没有必须保留的相关副作用,驱动可以先比较深度,把必定失败的片段提前拒绝,这叫 Early-Z,能省掉昂贵的片段着色。反之则可能退回着色后的 Late-Z。无论调度早晚,最终可见结果必须一致。

因此更准确的管线描述是:光栅化先产生窗口深度,深度测试逻辑位于片段操作阶段、颜色写入之前;实现可以在安全时把比较提前。下面先按“着色完成后统一判定”的逻辑模型拆步骤,因为它最容易理解结果契约,再回到代码讨论状态与优化边界。

逐片段拆解:一个色块怎么过这道工序

猜一猜:场景里先画了一面远墙(深度 0.7),又画一个近杯子(深度 0.3),它们落在同一个像素上。如果把比较规则从「更近才通过」改成「更远才通过」,最后留在屏幕上的会是杯子还是墙?先想一想,再单步走一遍下面的判定。

下面用一个具体片段(深度 0.3)走完整条深度测试。每一步都画出深度缓冲那一格在这一刻长什么样——盯着那格的数字怎么变,就懂了整套逻辑:

分步1 / 4

① 逻辑模型:候选片段已有颜色 + 深度

流水线把这个片段送到质检口:它的颜色已经算好,深度值是 0.3。深度缓冲这一格当前存着上一块(远墙)留下的 0.7。还没动手,先把两个数摆出来。

走完这一遍就明白:留在屏幕上的永远是每格深度最小(最近)的那块,和谁先画无关。这就是 §2 那道「比远近」工序的全部秘密。

深度函数:那条「谁算通过」的规则可以换

第 ② 步里的「更小才通过」并不是写死的,它由 (depth function)决定,用 glDepthFunc(...) 设置。默认是 GL_LESS——「新深度严格更小才通过」,这也是标准深度配置下的“近者胜出”。

但你可以换成别的算子来玩花样。常用的几个:

深度函数含义(新片段深度 vs 缓冲深度)
GL_LESS更小才通过(默认,近的胜出)
GL_LEQUAL更小或相等才通过
GL_GREATER更大才通过(反过来:远的胜出)
GL_EQUAL相等才通过
GL_ALWAYS永远通过;若 depth mask 开启,仍写入新深度
GL_NEVER永远不通过(什么都画不出来)

GL_ALWAYS 会让颜色表现成“谁后画谁赢”,但它不等于关闭深度测试:测试仍启用且总通过,glDepthMask(GL_TRUE) 时还会把每个片段深度写回缓冲;真正关闭 GL_DEPTH_TEST 时既不比较也不写深度。八个算子里还有 GL_NOTEQUAL / GL_GEQUAL,用法同理。

深度值的精度:为什么近处看得清、远处糊

到这儿还有个反直觉的真相要点破:深度值在 0~1 之间不是均匀分布的。它是 (non-linear depth):它和真实距离的倒数 1/z 成正比,结果就是绝大部分精度被堆在了离镜头很近的地方,越往远,同样一段真实距离能分到的深度值越少、越粗。

下面这张图把这件事画得很直白——把深度值 0→1 等间隔取刻度、标回真实距离轴,你会看到近平面附近刻度挤成一簇、远平面附近稀稀拉拉:

为什么要这么设计?因为我们通常更在乎近处物体之间的遮挡是否精确(它们占了画面大头),远处糊一点无所谓。这套非线性映射是透视投影「免费附送」的副产物,具体公式 §4 会拆开讲。但它也埋了个雷:远处深度值挨得太近,两个远处的面稍微一靠近就会分不出谁前谁后——这正是下一节 Z-fighting 的温床。

顺带一提,正因为深度是非线性的,你若想在着色器里直接把 gl_FragCoord.z 当灰度输出来做 (depth visualization),会发现画面几乎一片惨白——因为非线性把值全挤到了接近 1 的高位。想看到从近到远平滑的灰阶,得先把它线性化。这两种输出的差别,就是下面 Demo 让你亲手切换的。

深度冲突:两个面贴太近,谁也压不住谁

最后一个绕不开的现象。当两个面片几乎共面(深度差小到深度缓冲分辨不出)时,会出现 (Z-fighting):同一片区域里,哪个面通过深度测试反复横跳,渲染成一道道交错的撕裂条纹,视角一动还会闪烁。

成因正是上一节那个雷:深度值在远处(或两面太近时)精度不够,两个面的深度值被四舍五入成了同一个数,深度测试只能「随机」二选一,于是斑驳撕裂。怎么治?§7 会给三种实用修法。

公式:深度为什么非线性,以及怎么线性化回去

本节两件事:写出深度的非线性映射、给出把它线性化还原成真实距离的反算公式。先约定符号:

  • zz:片段在视图空间里离镜头的真实距离(沿镜头朝向,正值)
  • near\text{near}:近平面距离(视锥体最近的那张面,坐标系统章见过)
  • far\text{far}:远平面距离
  • zdepthz_\text{depth}:写进深度缓冲的那个 010\sim1 的非线性深度值
  • zndcz_\text{ndc}:标准化设备坐标(NDC)里的深度,范围 11-1\sim1

非线性映射。透视投影会把视图空间的 zz 按下面这种「与 1/z1/z 成正比」的方式压进 010\sim1

zdepth=1/z1/near1/far1/nearz_\text{depth} = \frac{1/z - 1/\text{near}}{1/\text{far} - 1/\text{near}}

这个式子在说:深度值不是直接拿真实距离 zz 来线性缩放,而是拿它的倒数 1/z1/z 来缩放。因为 1/z1/zzz 小(近)时变化剧烈、在 zz 大(远)时变化迟缓,所以近处一点点距离变化就能让 zdepthz_\text{depth} 明显挪动(精度高),远处挪很大距离 zdepthz_\text{depth} 才动一点(精度低)——这就是「近密远疏」的来源,也对上了上面那张精度图。

线性化反算。深度可视化时,我们想把这个非线性的 zdepthz_\text{depth} 还原回正比于真实距离的线性值。第一步,把 010\sim1 的深度值映射回 11-1\sim1 的 NDC 深度:

zndc=2zdepth1z_\text{ndc} = 2 \cdot z_\text{depth} - 1

这个式子在说:深度缓冲存的是 010\sim1,而 NDC 约定 11-1\sim1,所以乘 2 减 1 把区间拉伸平移过去——纯粹的区间换算。第二步,套透视投影的逆运算,从 NDC 深度反解出视图空间的真实距离 zz

z=2nearfarfar+nearzndc(farnear)z = \frac{2 \cdot \text{near} \cdot \text{far}}{\text{far} + \text{near} - z_\text{ndc} \cdot (\text{far} - \text{near})}

这个式子在说:把上一步得到的 zndcz_\text{ndc} 代进去,就能解出这个片段在视图空间里的真实距离 zz(结果落在 nearfar\text{near}\sim\text{far} 之间)。最后若想再画成 010\sim1 的灰度,把 zz 除以 far\text{far} 归一即可。下面 Demo 里的「线性化」开关,切的就是「直接显示 zdepthz_\text{depth}」与「跑完这两步再显示」的区别。

动手:把深度画成灰度,亲眼看非线性

猜一猜:下面这块画布把三个前后交叠的圆,按各自的「深度」涂成灰度——越近越黑、越远越白。把「线性化」开关从关到开,靠近你的那个圆的灰度会变深还是变浅?非线性那档下,三个圆的灰度差为什么那么不明显?先拖一下再看。

这块画布没有真正的 3D 深度缓冲(ShaderDemo 只是跑一个全屏片段着色器)。我们在着色器里手动摆了三个交叠的圆,各给它一个 01 的「伪深度」,再用深度测试的逻辑挑出每个像素最近的那个圆,把它的深度按你选的方式涂成灰度——这正是「深度可视化」做的事。uLinearize 关时显示这些圆在透视投影下的非线性深度(远处几个被挤到一起、偏白难分),开时按真实距离线性显示(即 §4 线性化想还原的那种在 nearfar 间均匀拉开的灰阶):

可交互

实时演示加载中…

把「线性化」关掉时,三个圆灰度都偏白、彼此几乎一个色——因为非线性深度把它们的值全挤到了高位(远处精度差);打开后,三圆的灰阶才在 near~far 之间均匀拉开、一眼分出远近。拖 near / far 改变可视范围,能进一步看出 near 设得越小、精度越往近处堆。这就是 §4 那两条公式在「看见深度」上的直观差别。

代码对照:开启深度测试这几行

深度测试的代码出奇地短——绝大部分活 GPU 自动干了,你只要「开开关、每帧清账本」。先看最关键的两件事:启用深度测试 + 每帧清深度缓冲

// 初始化时开一次:默认是关的,不开就没有遮挡判定
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glClearDepth(1.0);  // 标准深度:1 是最远,和 GL_LESS 配套
 
// 渲染循环里每帧开头:颜色缓冲 + 深度缓冲一起清
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

两端逻辑一字对应:标准深度配置用 LESS + clearDepth(1),把账本重置成最远值后,新一帧较近片段才能逐步覆盖。不要只记“清成 1”:清除值必须与比较方向成对;采用 reversed-Z 时通常改成 GREATER + clearDepth(0),并配套反转投影深度映射。状态只改一半会让整帧全部失败或遮挡反转。

想换比较规则、或只读深度不写,再加这两行。glDepthFunc 换的就是 §3 那张表里的算子;glDepthMask(GL_FALSE) 让深度缓冲变只读(仍参与比较,但通过的片段不更新它)——画半透明物体时常用,避免它们互相挡掉。

glDepthFunc(GL_LESS);      // 设比较规则(默认就是 GL_LESS,可改 GL_LEQUAL 等)
 
glDepthMask(GL_FALSE);     // 深度缓冲只读:参与比较但通过的片段不写回深度
// …画完需要只读深度的物体后,记得 glDepthMask(GL_TRUE) 恢复可写…

depthMask(false) 只在深度测试已启用时才有意义。透明物体通常仍要读取已有不透明深度,却不应抢先写深度挡住后续透明层,因此常见流程是:先画不透明物体并写深度,再按远到近画透明物体且关闭深度写入,结束后恢复。它不能替代透明排序,这会在混合章完整展开。

若片段 shader 写 gl_FragDepth,驱动通常必须等它给出最终深度才能完成比较;大量 discard 也可能限制 Early-Z。性能诊断时可对比“不写自定义深度/不用 discard”的版本,观察片段着色调用或 GPU 时间是否显著下降。不要为了理论上的提前测试擅自删除正确性所需逻辑,而应先用 profiler 证明这里确实是过度绘制热点。

容易踩的坑

小结

  • 深度缓冲为每个像素存窗口深度;标准配置是 DEPTH_TEST + LESS + clearDepth(1),状态与投影映射必须成套
  • 深度测试决定片段最终能否写入;驱动可在结果不变时用 Early-Z 提前拒绝遮挡片段,discard/gl_FragDepth 可能限制优化
  • 深度比较和深度写入独立:关闭测试、GL_ALWAYSdepthMask(false) 三者行为不同
  • 深度值是非线性的(与 1/z 成正比),精度堆在近处;要看清远近得先线性化还原
  • 两面几乎共面会 Z-fighting(撕裂闪烁):别共面 / 拉远 near 平面 / 用更高精度深度缓冲

练习

问题 1(改 Demo 代码 / 观察题) 在上面的深度可视化 Demo 里,把「线性化」开关在关、开之间来回切几次,并把 near 拖到最小(0.1)、far 拖到最大(50)。回答:① 非线性档下三个圆为什么灰度都偏白、彼此难分?② 把 near 调到最小后,画面的灰度对比是更明显还是更糊?为什么?

问题 2(问答题) 有人说「深度测试就是按物体离镜头的远近排个序,远的先画、近的后画盖上去」。这个说法对吗?深度测试到底是不是靠「控制绘制顺序」来实现遮挡的?

问题 3(问答题) 你做了一张「墙 + 贴在墙上的海报」,海报和墙严丝合缝地共面,结果海报边缘满是闪烁的撕裂条纹。这是什么现象?给出至少两种不改硬件就能缓解的办法,并各说一句为什么管用。

名词解释

本章出现的专业名词,用大白话再讲一遍。

深度缓冲

显存里一块和颜色缓冲一样大的内存,为每个像素额外存一个 0~1 的「深度值」(离镜头的远近)。它和颜色缓冲是孪生兄弟:颜色缓冲存「什么颜色」、深度缓冲存「多远」。屏幕显示颜色缓冲,深度缓冲只在幕后记账供深度测试比较,常见精度 24 位。又叫 z-buffer。详见本章「深度缓冲」一节。

深度值

深度缓冲里每个像素存的窗口深度,通常范围 0~1。标准投影把近平面映射到 0、远平面映射到 1,并配 GL_LESS;reversed-Z 会反转映射、清除值与比较方向。片段着色器可从 gl_FragCoord.z 读取当前片段深度。详见本章「深度缓冲」一节。

深度测试

GPU 对候选片段最终深度做的逐像素比较。规范结果决定片段是否能写颜色,并在 depth mask 允许时更新深度;驱动可以在不改变结果时把比较提前成 Early-Z。它让遮挡与物体提交先后无关。详见本章「深度测试」一节。

深度函数

决定深度测试「怎样算通过」的比较规则,用 glDepthFunc 设置。默认 GL_LESS(新深度更小才通过,近的胜出);还有 GL_LEQUAL(更小或相等)、GL_GREATER(反向比较)、GL_ALWAYS(总通过但仍可能写深度)等八种算子。详见本章「深度函数」一节。

非线性深度

深度值随真实距离不是均匀变化的,而是与 1/z 成正比——绝大部分精度集中在离镜头很近的范围,越远精度越粗。它是透视投影的副产物,好处是近处遮挡判定特别精确,代价是远处易出深度冲突。详见本章「深度值的精度」一节与 §4。

线性化深度

把非线性的深度值还原成正比于真实距离的线性值的过程:先把 01 的深度映射回 -11 的 NDC 深度,再套透视投影的逆运算解出视图空间里的真实距离。深度可视化时必须先线性化,画面才能看出从近到远平滑的灰度渐变。详见本章「深度值的精度」一节与 §4。

深度可视化

把片段的深度值当成灰度颜色输出到屏幕,用来「看见」深度缓冲长什么样。直接输出非线性的 gl_FragCoord.z 会发现画面几乎全白(精度堆在近处);先线性化再输出,才能看到从近到远平滑的灰阶。详见本章「深度值的精度」一节。

深度冲突

两个表面靠得极近、深度值小到深度缓冲分辨不出谁前谁后时,同一片区域里两个面交替胜出、渲染成一道道交错闪烁的撕裂条纹的现象(又称 Z-fighting)。摄像机一动还会闪。常见于贴在墙上的海报、地面贴花等共面情形。三种治法:别共面 / 拉远近平面 / 用更高精度深度缓冲。详见本章「深度冲突」一节与 §7。

版本、来源与运行边界

本章以 Joey de Vries 的 LearnOpenGL 原章 为授权改编依据,教学运行基线是 OpenGL 3.3 Core Profile。Khronos 当前发布的规范参照是 OpenGL 4.6 Core Profile;这里用 4.6 规范核查术语和状态合同,但不把 4.6 API 偷偷倒填为原教程内容。GLFW、GLAD、Assimp 与驱动版本都属于运行环境,不能拿“编译通过”替代对 context、资源和 framebuffer 结果的验证。

正式概念与状态责任

  • depth test:在“深度测试、Early-Z 与深度精度”中由framebuffer 的 depth attachment 与 GL depth-test state负责解释其输入、受控状态和可观察结果;运行时以depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图定位它的第一处变化。
  • depth buffer:在“深度测试、Early-Z 与深度精度”中由framebuffer 的 depth attachment 与 GL depth-test state负责解释其输入、受控状态和可观察结果;运行时以depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图定位它的第一处变化。
  • depth function:在“深度测试、Early-Z 与深度精度”中由framebuffer 的 depth attachment 与 GL depth-test state负责解释其输入、受控状态和可观察结果;运行时以depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图定位它的第一处变化。
  • linearize:在“深度测试、Early-Z 与深度精度”中由framebuffer 的 depth attachment 与 GL depth-test state负责解释其输入、受控状态和可观察结果;运行时以depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图定位它的第一处变化。

章专属 OpenGL 状态实验

先预测“清除 depth,启用测试/写入,绘制不透明物并按需线性化观测值”发生后,framebuffer 的 depth attachment 与 GL depth-test state应怎样改变深度格式、clear value、compare func、write mask、片段深度和 early test 条件;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。

实验一:Context—资源—结果合同

选择任一正式概念与基线/单故障场景,核对它是否进入本章状态合同。正式概念只有同时出现在解释、可视状态和交付证据中才算覆盖。

Context · resource · observable result

深度测试、Early-Z 与深度精度:状态合同

解释 depth buffer、比较函数、写掩码、clear 与非线性精度如何共同决定可见片段

验证场景

官方教程正式概念

logl-18 · 基线帧

depth test固定 context、资源内容与输入事件,执行“清除 depth,启用测试/写入,绘制不透明物并按需线性化观测值”

状态所有者framebuffer 的 depth attachment 与 GL depth-test state
受控状态/资源深度格式、clear value、compare func、write mask、片段深度和 early test 条件
触发命令清除 depth,启用测试/写入,绘制不透明物并按需线性化观测值

冻结输入:depth test

framebuffer 的 depth attachment 与 GL depth-test state记录深度格式、clear value、compare func、write mask、片段深度和 early test 条件

结果:得到可重复的初始 GL 状态与资源身份

观测:depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图中的初始快照

预期:framebuffer 的 depth attachment 与 GL depth-test state得到可复查结果,并持续满足“每帧清除可写 depth buffer;透明/天空盒阶段改变状态后显式恢复”

实验二:CPU 命令到 GPU 结果的五段轨迹

逐段执行“清除 depth,启用测试/写入,绘制不透明物并按需线性化观测值”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“每帧清除可写 depth buffer;透明/天空盒阶段改变状态后显式恢复”。

CPU command · GL state · GPU result

深度测试、Early-Z 与深度精度:五段轨迹

选择一段命令—资源—结果1 / 5

当前观测:depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图中的初始快照

不变量:每帧清除可写 depth buffer;透明/天空盒阶段改变状态后显式恢复

实验三:单故障与同输入恢复

注入“上一帧把 glDepthMask 设为 false,下一帧直接 clear,深度缓冲实际未被清除”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图一起恢复才算修复。

Single fault · first divergence · replay

深度测试、Early-Z 与深度精度:反例与恢复

故障:上一帧把 glDepthMask 设为 false,下一帧直接 clear,深度缓冲实际未被清除

1. 冻结输入一致

第 1 次沿用同一 context、资源、uniform 与 draw 输入

2. 注入单故障一致

保持其余输入不变,仅注入“上一帧把 glDepthMask 设为 false,下一帧直接 clear,深度缓冲实际未被清除”

3. 定位首差一致

每帧清除可写 depth buffer;透明/天空盒阶段改变状态后显式恢复

4. 清理并重放一致

depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图

最小可重放检查

unit: logl-18
owner: framebuffer 的 depth attachment 与 GL depth-test state
state_or_resource: 深度格式、clear value、compare func、write mask、片段深度和 early test 条件
command: 清除 depth,启用测试/写入,绘制不透明物并按需线性化观测值
pass_invariant: 每帧清除可写 depth buffer;透明/天空盒阶段改变状态后显式恢复
single_fault: 上一帧把 glDepthMask 设为 false,下一帧直接 clear,深度缓冲实际未被清除
required_evidence: depth bits、func/mask 查询、片段 z、线性化值、draw 顺序与深度截图

复核者先仅依据以上合同写出预期,再运行基线、单故障和清理后重放。若两次基线的资源身份、首个状态变化或 framebuffer 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。

出处声明

本文为改编重写,改编自 Joey de Vries 的 LearnOpenGL。原文:learnopengl.com

原作及译作以 CC BY-NC 4.0 协议授权,本改编版同样遵循该协议(署名—非商业性使用)。

讨论

评论区加载中…