图形学基础

图形学基础

图形学基础|景深效果(Depth of Field/DOF)

文章目录

图形学基础|景深效果(Depth of Field/DOF)一、前言二、景深效果2.1 物理原理2.2 弥散圆量化

三、景深实现3.1 非物理的简单实现3.2 基于物理的景深效果3.2.1 CoC计算3.2.2 下采样和预滤波3.2.3 散景滤波3.2.4 后滤波3.2.5 混合

参考博文

一、前言

景深(DOF) 是模拟摄像机镜头对焦特性的一种常见的后处理效果。

在现实生活中,相机只能对特定距离内的物体进行锐利的聚焦,离相机较近或较远的物体会有些失焦。

模糊不仅提供了一个关于物体距离的视觉提示,还引入了焦外成像(Bokeh,散景)。

Bokeh,亦称焦外,是一个摄影术语,一般表示在景深较浅的摄影成像中,落在景深以外的画面,会有逐渐产生松散模糊的效果。

景深示意图:

散景示意图:

二、景深效果

2.1 物理原理

景深,指的是相机对焦点前后相对清晰的成像范围。在景深之内的图像比较清楚,在这个范围之前或是之后的图像则比较模糊。

相机的最简单形式是完美的针孔相机,它有一个用于记录光的图像平面。在此平面之前是一个被称为光圈(aperture) 的小孔:仅允许一个光束通过。

相机前面的物体会向多个方向发射或反射光,从而产生大量光线。对于每个点,只有一条光线能够穿过孔并被记录。

如下图,记录了3个点。

因为每个点只会产生一个光束,因此记录是产生的图像总是锐利的。

但单一的光束不够亮,需要很长的时间积累(这意味着摄像机需要一个长曝光时间)才能得到一张清晰的图像。

为减少曝光时间,光需要积累得足够快。唯一的方法是同时记录多个光线。这能通过增加光圈的半径来实现。

假设光圈为圆形,这意味着每一个点都会在图片平面上投影光束,而不是光线。所以我们会接收更多的光线,但不再是一个点的,而是一个圆盘的。

如下图,使用更大的光圈。

为了重新聚焦光线,我们需要将光束还原为一个点,这可以通过在光圈前加一个镜头来实现。镜头可以弯曲光线,使其重新聚焦。这能形成一个亮且锐利的成像,但是只在一个范围内。

对于远处的点,聚焦不够,而对于近处的点,聚焦又过了。两者都让成像变为了光束,也就变模糊了,而此模糊投影正是弥散圆(circle of confusion),简称为CoC。

2.2 弥散圆量化

当成像平面不在焦点上时,光线无法聚焦在一点,就会朝四周分散开。散开的范围被称为Circle Of Confusion,简称COC。

如图所示,展示了不同半径的弥散圆。

那么如何量化这个弥散圆的大小呢?

根据下图:

镜头(光圈直径为

A

A

A)和成像平面位置( 成像平面距光圈的距离为

I

I

I)。

在距镜头距离为

P

P

P的点,刚好可以落在成像平面上。

根据凸透镜成像公式:

1

u

+

1

v

=

1

F

\frac{1}{u} + \frac{1}{v} = \frac{1}{F}

u1​+v1​=F1​

其中,

u

u

u表示物距,

v

v

v表示像距,

F

F

F表示焦距。

则有:

1

P

+

1

I

=

1

F

\frac{1}{P} + \frac{1}{I} = \frac{1}{F}

P1​+I1​=F1​

在距镜头距离为

D

D

D的物体,在成像平面上形成直径为

C

C

C的弥散圆,其经凸透镜聚焦为投影点,假设其距离光圈的距离为

X

X

X。

1

D

+

1

X

=

1

F

\frac{1}{D} + \frac{1}{X} = \frac{1}{F}

D1​+X1​=F1​

如下图所示,根据相似三角形有:

C

A

=

X

I

X

\frac{C}{A} = \frac{X-I}{X}

AC​=XX−I​

C

=

A

X

I

X

=

A

D

F

D

F

P

F

P

F

D

F

D

F

=

A

F

(

P

D

)

D

(

P

F

)

\begin{aligned} C &= A \cdot \frac{X-I}{X} \\ &= A \cdot \frac{\frac{DF}{D-F}-\frac{PF}{P-F}}{\frac{DF}{D-F}} \\ &= \left | A \frac{F(P-D)}{D(P-F)} \right | \end{aligned}

C​=A⋅XX−I​=A⋅D−FDF​D−FDF​−P−FPF​​=∣∣∣∣​AD(P−F)F(P−D)​∣∣∣∣​​

其中,图中的各个符号为:

C

C

C:弥散圆直径;

在计算中通常使用用CoC表示弥散圆半径。

A

A

A:光圈直径;

F

F

F:焦距;

焦距属于镜头的固有属性,不因其他因素的变化而变化,是平行光入射镜头后聚焦的位置与镜头中心的距离。

P

P

P :聚焦位置;

指的是一个特定的物距,指在确定了镜头和成像平面位置之后,镜头前有一个刚好可以在成像面上聚焦的位置,我们称这个位置与镜头中心的距离为

P

P

P。图上的Plane in focus直译可能会有些误导,其实图上的本意是站在一个空间的角度来看,能够在镜头前聚焦的位置应该是一个平行于镜头、距离固定的平面,平面上每个点反射的光都可以在成像面上汇聚成一个点

D

D

D:物距;

物体距离镜头的距离,即要求这个距离的物体在镜头上的COC弥散圆。

I

I

I:成像面距离;

成像面与镜头中心的距离。

P

P

P可以通过

I

I

I和

F

F

F确定,在镜头和成像面确定的情况下也可以称之为常量。

因此,我们就得到了一个以

C

C

C为因变量,

D

D

D为自变量的函数,这个函数的图像(不加绝对值)是这样的:

MaxBgdCoC,指背景最大弥散圆半径,可以通过左边的这个算式得到。

在函数图像中,z即为上面公式的

D

D

D,也就是自变量,纵轴即为CoC值,可以看到在P点,弥散圆半径为0。

前景后景随着距离增大,弥散圆半径逐渐增大,而且前景的变化趋势更为陡峭。

至此,我们就可以描述整个屏幕上每一个点成像的弥散范围了。

这也就是我们后续进行屏幕后处理计算的基础:获取屏幕上的点,以及它距离摄像机的距离,其他常量都可以通过参数进行调整。(成像面距离不要小于焦距,否则无法聚焦)。

三、景深实现

3.1 非物理的简单实现

这个部分比较简单,笔者就没有实现。

最简单的实现如下图所示,从摄像机位置开始,按照距离分为三个部分:

近距离模糊;焦点范围清晰;远距离模糊;

渲染的时候按深度(即距离)进行判断,在焦点范围内则是清晰的,否则就进行模糊处理。

整个过程共分为三个Pass:

将场景渲染到一个RenderTarget,作为清晰版;将上一步得到的RenderTarget进行模糊处理,得到BluredRT(模糊版);合成!跟据距离来判断是否应该模糊,如果不在焦点范围内则绘制BluredRT,否则就绘制RenderTarget。

示例Shader代码:

sampler RenderTarget;

sampler BluredRT;

// 焦点范围

float fNearDis;

float fFarDis;

float4 ps_main( float2 TexCoord : TEXCOORD0 ) : COLOR0

{

float4 color = tex2D( RenderTarget, TexCoord );

if( color.a > fNearDis && color.a < fFarDis )

return color;

else

return tex2D( BluredRT, TexCoord );

}

效果如下:

可以看出,上图看起来很不自然。

原因就是DOF在清晰与模糊的交界处过渡太生硬。

可以通过增加两个过渡带实现一个简单的渐变。

示例Shader代码:

sampler RenderTarget;

sampler BluredRT;

// 焦点范围

float fNearDis;

float fFarDis;

float fNearRange;

float fFarRange;

float4 ps_main( float2 TexCoord : TEXCOORD0 ) : COLOR0

{

float4 sharp = tex2D( RenderTarget, TexCoord );

float4 blur= tex2D( BluredRT, TexCoord );

// sharp.a存储的

float percent = max(saturate(sharp.a-fNearDis)/fNearRange),saturate((sharp.a-(fFarDis-fFarRange))/fFarRange));

return lerp( sharp, blur, percent );

}

效果如下:

3.2 基于物理的景深效果

3.1 仅通过不加区分的模糊操作来实现景深DOF效果,效果比较一般。

比较符合物理的做法,是基于2.2推导的弥散圆Circle Of Confusion(COC),将某点的像素值均匀地沿着弥散圈扩散。

但在GPU中实现向外扩散的操作是非常困难的。

因而,通常将该过程反过来,计算某个像素点的颜色时,根据周围像素点弥散圈的大小从周围的像素点中搜集 (gather) 像素颜色。

动视暴雪在Siggraph2014上分享了他们的Scatter As Gather方法。

搜集的过程,就是在周围的像素点中,寻找处于周围像素点弥散圈范围内的像素点,将其颜色按照一定的权重累加。

如图所示:

左边,五个点:红色和蓝色处于近处,黄色和紫色点处于远处,绿色点是正确对焦处。

右边,展示了每个像素点的弥散圈范围,弥散圈的范围越大,对其他像素点的影响越小,中间黑色的点的位置受到两个弥散圈的影响。

整个景深后处理的过程大致是这样的: 笔者参考Unity-Technologies/PostProcessing/DepthOfField和CatlikeCoding-DOF实现一个稍微有点基于物理的景深DOF效果。

先给出实现的效果图:

笔者的实现共分为5个Pass,分别为:

CoC(弥散圆计算);DownSample and Prefilter(下采样和预滤波);Bokeh Filter(散景滤波);Postfilter(后滤波);Combine(混合);

实现基于DX12的ComputeShader。

3.2.1 CoC计算

为了实现的简单方便,CoC的公式并没有选择2.2中介绍的公式。而采用了下面的一种方法。

基于一个焦点距离(FoucusDistance)一个简单的焦点区域(FoucusRange):

正如CatlikeCoding-DOF中使用的:

CoC值采用如下公式:

c

o

c

=

S

c

e

n

e

D

e

p

t

h

F

o

u

c

u

s

D

i

s

t

a

n

c

e

F

o

u

c

u

s

R

a

n

g

e

coc = \frac{SceneDepth - FoucusDistance}{FoucusRange}

coc=FoucusRangeSceneDepth−FoucusDistance​

其中,

S

c

e

n

e

D

e

p

t

h

SceneDepth

SceneDepth表示像素在相机空间的深度。

如下图所示,横轴表示深度的变化,纵轴表示Coc的量化值(这里将其截断为-1到1之间的值)。

具体的Shader代码如下:

RWTexture2D CoCBuffer : register(u0);

Texture2D DepthBuffer : register(t0);

cbuffer CB0 : register(b0)

{

float FoucusDistance;

float FoucusRange;

float2 ClipSpaceNearFar;

};

[numthreads(8, 8, 1)]

void cs_FragCoC

(

uint3 DTid : SV_DispatchThreadID

)

{

// screenPos

const uint2 ScreenST = DTid.xy;

// non-linear depth

float Depth = DepthBuffer[ScreenST];

// 转到相机空间的深度

float SceneDepth = LinearEyeDepth(Depth, ClipSpaceNearFar.x, ClipSpaceNearFar.y);

// compute simple coc

float coc = (SceneDepth - FoucusDistance) / FoucusRange;

coc = clamp(-1, 1, coc);

// 将[-1,1]转换为[0,1]

CoCBuffer[ScreenST] = saturate(coc * 0.5f + 0.5f);

}

3.2.2 下采样和预滤波

我们将在半分辨率的情况下创建散景的效果。所以需要一个下采样的Pass。

在这个Pass中,我们将获取邻近四纹素中最显著的CoC值,而非进行平均(这样并没有意义)。\

对于颜色,进行了基于亮度和coc绝对值的加权。

最后,输出的四通道中rgb存储了下采样滤波后的颜色值,alpha通道存储了coc值。

注意,这里的coc值乘以了BokehRadius(散景半径),转换为了散景距离值。

具体的Shader代码如下:

RWTexture2D PrefilterColor : register(u0);

Texture2D SceneColor : register(t0);

Texture2D CoCBuffer : register(t1);

SamplerState LinearSampler : register(s0);

cbuffer CB0 : register(b0)

{

float BokehRadius;

};

void GetSampleUV(uint2 ScreenCoord, inout float2 UV, inout float2 HalfPixelSize)

{

float2 ScreenSize;

PrefilterColor.GetDimensions(ScreenSize.x, ScreenSize.y);

float2 InvScreenSize = rcp(ScreenSize);

HalfPixelSize = 0.5 * InvScreenSize;

UV = ScreenCoord * InvScreenSize + HalfPixelSize;

}

[numthreads(8, 8, 1)]

void cs_FragPrefilter(uint3 DTid : SV_DispatchThreadID)

{

float2 HalfPixelSize, UV;

GetSampleUV(DTid.xy, UV, HalfPixelSize);

// screenPos

float2 uv0 = UV - HalfPixelSize;

float2 uv1 = UV + HalfPixelSize;

float2 uv2 = UV + float2(HalfPixelSize.x, -HalfPixelSize.y);

float2 uv3 = UV + float2(-HalfPixelSize.x, HalfPixelSize.y);

// Sample source colors

float3 c0 = SceneColor.SampleLevel(LinearSampler, uv0, 0).xyz;

float3 c1 = SceneColor.SampleLevel(LinearSampler, uv1, 0).xyz;

float3 c2 = SceneColor.SampleLevel(LinearSampler, uv2, 0).xyz;

float3 c3 = SceneColor.SampleLevel(LinearSampler, uv3, 0).xyz;

float3 result = (c0 + c1 + c2 + c3) * 0.25;

// Sample CoCs

// convert [0,1] to [-1,1]

float coc0 = CoCBuffer.SampleLevel(LinearSampler, uv0, 0).r * 2 - 1;

float coc1 = CoCBuffer.SampleLevel(LinearSampler, uv1, 0).r * 2 - 1;

float coc2 = CoCBuffer.SampleLevel(LinearSampler, uv2, 0).r * 2 - 1;

float coc3 = CoCBuffer.SampleLevel(LinearSampler, uv3, 0).r * 2 - 1;

// Apply CoC and luma weights to reduce bleeding and flickering

float w0 = abs(coc0) / (Max3(c0.r, c0.g, c0.b) + 1.0);

float w1 = abs(coc1) / (Max3(c1.r, c1.g, c1.b) + 1.0);

float w2 = abs(coc2) / (Max3(c2.r, c2.g, c2.b) + 1.0);

float w3 = abs(coc3) / (Max3(c3.r, c3.g, c3.b) + 1.0);

// Weighted average of the color samples

half3 avg = c0 * w0 + c1 * w1 + c2 * w2 + c3 * w3;

avg /= max(w0 + w1 + w2 + w3, 1e-4);

// Select the largest CoC value

float cocMin = min(min(min(coc0, coc1), coc2), coc3);

float cocMax = max(max(max(coc0, coc1), coc2), coc3);

float coc = (-cocMin >= cocMax ? cocMin : cocMax) * BokehRadius;

// Premultiply CoC again

avg *= smoothstep(0, HalfPixelSize.y * 4, abs(coc));

// alpha通道存储了coc值

PrefilterColor[DTid.xy] = float4(avg, coc);

}

3.2.3 散景滤波

在半分辨率预过滤后,我们需要创建散景图。采用的方法即前面所介绍的Gather的方法。

通过圆盘采样(DiskKernel)来查看采样点的Coc是否覆盖了当前中心点。

并且分别累积前散景和后散景。

圆盘采样提供了好几个,不同的采样数量,所形成环不同。

笔者这里选择了KERNEL_LARGE。

#if defined(KERNEL_LARGE)

// rings = 4

// points per ring = 7

static const int kSampleCount = 43;

static const float2 kDiskKernel[kSampleCount] = {

float2(0,0),

float2(0.36363637,0),

float2(0.22672357,0.28430238),

float2(-0.08091671,0.35451925),

float2(-0.32762504,0.15777594),

float2(-0.32762504,-0.15777591),

float2(-0.08091656,-0.35451928),

float2(0.22672352,-0.2843024),

float2(0.6818182,0),

float2(0.614297,0.29582983),

float2(0.42510667,0.5330669),

float2(0.15171885,0.6647236),

float2(-0.15171883,0.6647236),

float2(-0.4251068,0.53306687),

float2(-0.614297,0.29582986),

float2(-0.6818182,0),

float2(-0.614297,-0.29582983),

float2(-0.42510656,-0.53306705),

float2(-0.15171856,-0.66472363),

float2(0.1517192,-0.6647235),

float2(0.4251066,-0.53306705),

float2(0.614297,-0.29582983),

float2(1,0),

float2(0.9555728,0.2947552),

float2(0.82623875,0.5633201),

float2(0.6234898,0.7818315),

float2(0.36534098,0.93087375),

float2(0.07473,0.9972038),

float2(-0.22252095,0.9749279),

float2(-0.50000006,0.8660254),

float2(-0.73305196,0.6801727),

float2(-0.90096885,0.43388382),

float2(-0.98883086,0.14904208),

float2(-0.9888308,-0.14904249),

float2(-0.90096885,-0.43388376),

float2(-0.73305184,-0.6801728),

float2(-0.4999999,-0.86602545),

float2(-0.222521,-0.9749279),

float2(0.07473029,-0.99720377),

float2(0.36534148,-0.9308736),

float2(0.6234897,-0.7818316),

float2(0.8262388,-0.56332),

float2(0.9555729,-0.29475483),

};

#endif

具体的Shader代码如下:

#define KERNEL_LARGE

#include "DiskKernels.hlsl"

RWTexture2D BokehColor : register(u0);

Texture2D PrefilterColor : register(t0);

SamplerState LinearSampler : register(s0);

cbuffer CB0 : register(b0)

{

float BokehRadius;

};

//------------------------------------------------------- HELP FUNCTIONS

void GetSampleUV(uint2 ScreenCoord, inout float2 UV, inout float2 PixelSize)

{

float2 ScreenSize;

BokehColor.GetDimensions(ScreenSize.x, ScreenSize.y);

float2 InvScreenSize = rcp(ScreenSize);

PixelSize = InvScreenSize;

UV = ScreenCoord * InvScreenSize + 0.5 * InvScreenSize;

}

//------------------------------------------------------- ENTRY POINT

// Bokeh filter with disk-shaped kernels

[numthreads(8, 8, 1)]

void cs_FragBokehFilter(uint3 DTid : SV_DispatchThreadID)

{

float2 PixelSize, UV;

GetSampleUV(DTid.xy, UV, PixelSize);

float4 center = PrefilterColor.SampleLevel(LinearSampler, UV, 0);

float4 bgAcc = 0.0; // Background: far field bokeh

float4 fgAcc = 0.0; // Foreground: near field bokeh

for (int k = 0; k < kSampleCount; ++k)

{

float2 offset = kDiskKernel[k] * BokehRadius;

float dist = length(offset);

offset *= PixelSize;

float4 samp = PrefilterColor.SampleLevel(LinearSampler, UV + offset, 0);

// BG: Compare CoC of the current sample and the center sample and select smaller one.

float bgCoC = max(min(center.a, samp.a), 0.0);

// Compare the CoC to the sample distance. Add a small margin to smooth out.

const float margin = PixelSize.y * 2;

float bgWeight = saturate((bgCoC - dist + margin) / margin);

// Foregound's coc is negative

float fgWeight = saturate((-samp.a - dist + margin) / margin);

// Cut influence from focused areas because they're darkened by CoC premultiplying. This is only needed for near field.

// 减少聚焦区域的影响,它们因CoC预乘而变暗。这仅适用于近场。

fgWeight *= step(PixelSize.y, -samp.a);

// Accumulation

bgAcc += half4(samp.rgb, 1.0) * bgWeight;

fgAcc += half4(samp.rgb, 1.0) * fgWeight;

}

// Get the weighted average.

bgAcc.rgb /= bgAcc.a + (bgAcc.a == 0.0); // zero-div guard

fgAcc.rgb /= fgAcc.a + (fgAcc.a == 0.0);

// FG: Normalize the total of the weights.

// 归一化前散景权重

fgAcc.a *= 3.14159265359 / kSampleCount;

// Alpha premultiplying

float alpha = saturate(fgAcc.a);

// 前散景和后散景融合

float3 rgb = lerp(bgAcc.rgb, fgAcc.rgb, alpha);

// alpha存储的是前散景的权重

BokehColor[DTid.xy] = float4(rgb, alpha);

}

3.2.4 后滤波

在创建散景效果之后再添加一个额外的模糊pass,这是一个后处理pass。采用3x3被称为帐篷滤波 (tent filter) 的卷积核。

通过一个半纹素偏移的方形滤波器,基于GPU的双线性插值,实现了一个小高斯模糊。

具体的Shader代码如下:

RWTexture2D PostfilterColor : register(u0);

Texture2D BokehColor : register(t0);

SamplerState LinearSampler : register(s0);

//------------------------------------------------------- HELP FUNCTIONS

void GetSampleUV(uint2 ScreenCoord, inout float2 UV, inout float2 HalfPixelSize)

{

float2 ScreenSize;

PostfilterColor.GetDimensions(ScreenSize.x, ScreenSize.y);

float2 InvScreenSize = rcp(ScreenSize);

HalfPixelSize = 0.5 * InvScreenSize;

UV = ScreenCoord * InvScreenSize + HalfPixelSize;

}

//------------------------------------------------------- ENTRY POINT

// tent filter

/**

* 1 2 1

* 2 4 2

* 1 2 1

*/

[numthreads(8, 8, 1)]

void cs_FragPostfilter(uint3 DTid : SV_DispatchThreadID)

{

// 9 tap tent filter with 4 bilinear samples

float2 HalfPixelSize, UV;

GetSampleUV(DTid.xy, UV, HalfPixelSize);

float2 uv0 = UV - HalfPixelSize;

float2 uv1 = UV + HalfPixelSize;

float2 uv2 = UV + float2(HalfPixelSize.x, -HalfPixelSize.y);

float2 uv3 = UV + float2(-HalfPixelSize.x, HalfPixelSize.y);

float4 acc = 0;

acc += BokehColor.SampleLevel(LinearSampler, uv0, 0);

acc += BokehColor.SampleLevel(LinearSampler, uv1, 0);

acc += BokehColor.SampleLevel(LinearSampler, uv2, 0);

acc += BokehColor.SampleLevel(LinearSampler, uv3, 0);

PostfilterColor[DTid.xy] = acc / 4.0f;

}

3.2.5 混合

使用后滤波的散景图和原始的场景图进行混合。

基于coc值进行非线性的插值。

具体的Shader代码如下:

RWTexture2D CombineColor : register(u0);

Texture2D TempSceneColor : register(t0);

Texture2D CoCBuffer : register(t1);

Texture2D FilterColor : register(t2);

SamplerState LinearSampler : register(s0);

cbuffer CB0 : register(b0)

{

float BokehRadius;

};

//------------------------------------------------------- HELP FUNCTIONS

void GetSampleUV(uint2 ScreenCoord, inout float2 UV, inout float2 PixelSize)

{

float2 ScreenSize;

CombineColor.GetDimensions(ScreenSize.x, ScreenSize.y);

float2 InvScreenSize = rcp(ScreenSize);

PixelSize = InvScreenSize;

UV = ScreenCoord * InvScreenSize + 0.5 * PixelSize;

}

//------------------------------------------------------- ENTRY POINT

[numthreads(8, 8, 1)]

void cs_FragCombine(uint3 DTid : SV_DispatchThreadID)

{

// screenPos

float2 PixelSize, UV;

GetSampleUV(DTid.xy, UV, PixelSize);

float3 source = TempSceneColor.SampleLevel(LinearSampler, UV, 0);

// 采样当前片段的CoC值

float coc = CoCBuffer.SampleLevel(LinearSampler, UV, 0);

// 转换为散景距离

coc = (coc - 0.5) * 2.0 * BokehRadius;

// 采样散景

float4 dof = FilterColor.SampleLevel(LinearSampler, UV, 0);

// Convert CoC to far field alpha value.

// 对CoC正值进行插值,用于获取后散景

float ffa = smoothstep(PixelSize.y * 2.0, PixelSize.y * 4.0, coc);

// 非线性插值

float3 color = lerp(source, dof.rgb, ffa + dof.a - ffa * dof.a);

CombineColor[DTid.xy] = color;

}

参考博文

基于物理的景深效果DOF的改进算法渲染中的景深(Depth of Field/DOF)CatlikeCoding-DOF景深效果(译)Unity-DOF-ShaderUE4景深后处理效果学习笔记(一)基本原理UE4景深后处理效果学习笔记(二)效果实现

// 相关文章

平遥古城
365bet外围投注

平遥古城

⌛ 07-07 ⚠️ 7905
电脑蓝屏的原因有哪些及怎么解决
365被限制如何解决

电脑蓝屏的原因有哪些及怎么解决

⌛ 07-04 ⚠️ 9377
滑滑小子
365被限制如何解决

滑滑小子

⌛ 06-29 ⚠️ 8545