【3D數(shù)學(xué)】——透視變換與相機(jī)偏手性

書上和網(wǎng)上有很多透視變換的例子,但是其缺點(diǎn)是只討論一種情況,無法將不同的情況關(guān)聯(lián)起來,因此整理一篇文章,希望可以把透視變換的相關(guān)東西都聯(lián)系起來。

透視變換

透視變換,就是用一個(gè)矩陣(透視變換矩陣)將整個(gè)場景轉(zhuǎn)換到齊次裁剪空間之中。所謂的齊次裁剪空間,是一個(gè)特殊的空間,它的作用就是讓渲染管線可以對(duì)這個(gè)空間中一定范圍內(nèi)的物體保留,超出范圍的物體剔除,如果有截?cái)啾砻娴那闆r,還要生成新的頂點(diǎn)數(shù)據(jù)。
齊次裁剪空間通常有兩種,兩種齊次裁剪空間的x和y坐標(biāo)范圍都是[-1,1],但是z坐標(biāo)的范圍不同,一種是[0,1],一種是[-1,1]。很多書上,網(wǎng)上的教程都會(huì)說DX的齊次裁剪空間是[0,1]的那種,而OpenGL的裁剪空間是[-1,1]的那種。不過到底事實(shí)是什么樣的,我們還得試過之后才知道。
為了在計(jì)算機(jī)中模擬人眼看東西的狀態(tài),我們想出了這樣的一個(gè)模型:把人眼固定在相機(jī)空間的原點(diǎn),然后從原點(diǎn)定義一個(gè)四棱錐,表示人眼可以看到的范圍,就像下面這張圖一樣:



這個(gè)四棱錐就被稱為視景體。除此之外,對(duì)于離眼睛太近的物體我們看不到,太遠(yuǎn)的物體同樣也看不到,所以我們可以選取一定范圍內(nèi)的物體顯示出來,這樣可以節(jié)省渲染時(shí)間。那么多近的物體不顯示,多遠(yuǎn)的物體不顯示呢?這個(gè)沒有定論,經(jīng)常使用n和f表示這兩個(gè)距離,比n近的物體不顯示,比f還遠(yuǎn)的物體不顯示。這兩個(gè)面就把視景體截?cái)喑梢粋€(gè)四棱臺(tái),這個(gè)四棱臺(tái)里的物體會(huì)被繪制,而在外面的不會(huì)。

接著,我們就可以來推導(dǎo)這個(gè)透視變換矩陣了。此時(shí)我們是在相機(jī)中間中,并且假設(shè)相機(jī)空間是一個(gè)右手坐標(biāo)系。

水平視場角

下圖是從視景體的上方垂直看下去的圖,就像是三視圖一樣。圖中,三角形的頂邊就是投影平面。



參數(shù)說明:為了計(jì)算方便,我們?nèi)√厥獾耐队捌矫?,它的x坐標(biāo)區(qū)間是[-1,1],這個(gè)坐標(biāo)區(qū)間剛好可以直接當(dāng)成齊次裁剪空間的坐標(biāo)。如圖中所示,投影平面距離原點(diǎn)的距離為e,注意這是距離,不是投影平面的z坐標(biāo),這個(gè)e有時(shí)也被稱為相機(jī)的焦距。這樣,左錐面和右錐面形成的夾角為xfov,稱為水平視場角。

水平視場角,英文名是horizontal angle of view,但是通常,我們會(huì)把視場角和視野混用,視野的英文是field of view,所以我就用xfov(x-axis field of view)來表示水平視場角。

可以看到,距離e滿足e = \frac{1}{\tan(\frac{xfov}{2})}
再來看看垂直方向上:

垂直方向與水平方向類似,投影平面的縱橫比(寬除以高)用aspect表示,這個(gè)比值通常是16:9,就是我們顯示器的寬高比。所以,投影平面的y坐標(biāo)范圍為[-1/aspect, 1/aspect]。如圖中所示,垂直視場角yfov = 2\arctan(\frac{\frac{1}{aspect}}{e})

計(jì)算投影后的x坐標(biāo)


原始的頂點(diǎn)是v,投影之后的頂點(diǎn)是v',根據(jù)相似三角形原理,我們可以列出下面的等式:


求解x'得:


接著計(jì)算y'的值。



如上圖所示,同樣使用相似三角形的公式來計(jì)算:



于是

到這里,y'的計(jì)算還沒有結(jié)束,最關(guān)鍵的一步來了!上述的推導(dǎo)成立的前提是,y的投影面是[-1/aspect, 1/aspect],而齊次裁剪空間中,y的取值范圍必須是[-1,1],所以,最后一步是,將上述計(jì)算出來的y'值除以1/aspect,得出的結(jié)果才是齊次裁剪空間中真正的y值:


轉(zhuǎn)換之前的坐標(biāo)記為(x, y, z, 1),轉(zhuǎn)換之后的坐標(biāo)就是(x', y', z', 1),將之間計(jì)算的結(jié)果代入得到轉(zhuǎn)換之后的坐標(biāo)是


在齊次坐標(biāo)系統(tǒng)中,點(diǎn)(x, y, z, 1)和點(diǎn)(cx, cy, cz, c)表示的是同一個(gè)點(diǎn),如果這個(gè)c不是0的話。利用這個(gè)性質(zhì),我們可以對(duì)上述坐標(biāo)乘以一個(gè)z得到如下的形式


列出轉(zhuǎn)換矩陣表示這個(gè)轉(zhuǎn)換過程

轉(zhuǎn)換矩陣中有四個(gè)參數(shù)沒有確定,我們需要把這四個(gè)參數(shù)解出來。先來思考一下,對(duì)z進(jìn)行投影,這和x,y坐標(biāo)有沒有關(guān)系?顯然沒關(guān)系,所以m1和m2的值肯定是0。接下來考慮m3和m4,我們可以列出下面的方程

兩邊除以z得到

接著,我們說過齊次裁剪空間的z坐標(biāo)范圍有兩種,一個(gè)是[0,1],一個(gè)是[-1,1],我們先計(jì)算[0,1]。這種轉(zhuǎn)換,近裁剪面(n)轉(zhuǎn)換到0,遠(yuǎn)裁剪面(f)轉(zhuǎn)換到1,于是我們將坐標(biāo)代入(注意我們所說的裁剪面n和f是距離)得到方程組

解方程組得到的結(jié)果是

將結(jié)果代入到轉(zhuǎn)換矩陣?yán)?,我們就能得到最終的轉(zhuǎn)換矩陣:

投影矩陣有很多變種,嗯,多到頭暈的變種。比如,之前我們?yōu)榱朔奖阌?jì)算,對(duì)轉(zhuǎn)換后的坐標(biāo)值乘以了一個(gè)z得到的

顯然,我可以不乘z,如果我乘以一個(gè)-z得到的結(jié)果會(huì)是什么呢?

用這個(gè)結(jié)果去推導(dǎo)投影矩陣,得到的結(jié)果是:

很明顯,P2 = -P1。這兩個(gè)矩陣效果都一樣嗎?事實(shí)是,不一樣!P1是一個(gè)無效的透視矩陣,P2才是有效的矩陣。這里涉及到裁剪的相關(guān)原理。

透視矩陣會(huì)把所有的頂點(diǎn)都轉(zhuǎn)換到齊次裁剪空間中,為啥叫齊次裁剪空間?因?yàn)樵谶@個(gè)空間中,會(huì)發(fā)生裁剪的操作。為啥是齊次?因?yàn)檫@個(gè)空間中的點(diǎn),w坐標(biāo)不是1,而是z或者-z。裁剪空間要做的第一件事,就是把w坐標(biāo)小于0的頂點(diǎn)都剔除。而我們的z坐標(biāo),是一個(gè)小于0的值!所以,如果你用P1作為透視矩陣,那么你無法看到任何東西,因?yàn)樗鼈兌急惶蕹袅?。不信?試試~

推導(dǎo)這么復(fù)雜,寫成代碼就很簡單了,只要把結(jié)果賦值就好了。

float cFov = 1.f / tanf(xfov / 2);
result.m[0][0] = cFov;
result.m[1][1] = aspect * cFov;
result.m[2][3] = -1;
result.m[2][2] = far / (near - far);
result.m[3][2] = near * far / (near - far);

代碼的矩陣是P2矩陣的轉(zhuǎn)置,因?yàn)檫@種數(shù)據(jù)格式傳遞給渲染管線的時(shí)候計(jì)算方便。運(yùn)行測試場景,得到的結(jié)果是:


另一個(gè)齊次裁剪空間

之前我們首先的齊次裁剪空間的z范圍是[0,1],那么如果它的范圍是[-1,1],又會(huì)是什么結(jié)果呢?下面我們來嘗試推導(dǎo)[-1,1]的投影矩陣,這個(gè)不難,只有最后轉(zhuǎn)換z坐標(biāo)的時(shí)候有區(qū)別,我們以乘以-z為基礎(chǔ)來推導(dǎo)。
\begin{bmatrix}\cot({\frac{xfov}{2}}) && 0 && 0 && 0\\ 0 && aspect·(\cot{\frac{xfov}{2}}) && 0 && 0\\ m_1 && m_2 && m_3 && m_4\\ 0 && 0 && -1 && 0\end{bmatrix}·\begin{bmatrix}x \\ y \\ z \\ 1\end{bmatrix}=\begin{bmatrix} x·(\cot{\frac{xfov}{2}})\\ y·aspect·(\cot{\frac{xfov}{2}})\\ -z·z'\\ -z \end{bmatrix}
首先,m1和m2還是0,因?yàn)閤坐標(biāo)和y坐標(biāo)對(duì)z投影之后的坐標(biāo)值沒有影響。然后,列出z坐標(biāo)轉(zhuǎn)換公式:
m_3·z + m_4 = -z·z'
兩邊除以-z
-m_3 - \frac{m_4}{z} = z'
現(xiàn)在,我們要把近裁剪面轉(zhuǎn)換成-1,遠(yuǎn)裁剪面轉(zhuǎn)換成1,列出方程:
\begin{cases} -m_3 + \frac{m_4}{n} = -1\\ -m_3 + \frac{m_4}{f} = 1 \end{cases}
求得
\begin{cases} m_3 = \frac{n + f}{n - f}\\ m_4 = \frac{2nf}{n - f} \end{cases}
最后的轉(zhuǎn)換矩陣是:
\begin{bmatrix}\cot({\frac{xfov}{2}}) && 0 && 0 && 0\\ 0 && aspect·(\cot{\frac{xfov}{2}}) && 0 && 0\\ 0 && 0 && \frac{n + f}{n - f} && \frac{2nf}{n - f}\\ 0 && 0 && -1 && 0\end{bmatrix}
再用代碼實(shí)現(xiàn):

float cFov = 1.f / tanf(xfov / 2);
result.m[0][0] = cFov;
result.m[1][1] = aspect * cFov;
result.m[2][3] = -1;
result.m[2][2] = (near + far) / (near - far);
result.m[3][2] = (2 * near * far) / (near - far);

應(yīng)用這個(gè)投影矩陣,得到的結(jié)果是:



沒有半毛錢區(qū)別!

上面兩張圖是OpenGL下運(yùn)行的結(jié)果,下面兩張圖是D3D12下運(yùn)行的結(jié)果:


D3D12 z區(qū)間為[0,1]

D3D12 z區(qū)間為[-1,1]

可以看到,上面兩張圖也沒啥區(qū)別。而對(duì)比OpenGL和D3D12的圖會(huì)發(fā)現(xiàn),OpenGL中顯示的物體稍微偏上了一點(diǎn),為什么會(huì)產(chǎn)生這種效果,筆者也不清楚。

如果不用fov,還能用什么參數(shù)計(jì)算投影矩陣?

在《3D游戲與計(jì)算機(jī)圖形學(xué)中的數(shù)學(xué)方法》一書和《Real Time Rendering 4th Edition》一書中,提供了一種不用fov的方法。這個(gè)方法是這樣的:
首先確定投影平面的位置,令投影平面正好位于近裁剪面處,它與觀察點(diǎn)之間的距離為n,所以其z坐標(biāo)就是-n。然后,這個(gè)投影平面的x范圍是[l, r],y范圍是[b, t]。先通過相似原理列出一個(gè)點(diǎn)(x, y, z, 1)投影到上述投影平面上之后的x,y坐標(biāo),令投影后的坐標(biāo)為(x', y', z', 1):
\frac{x'}{x} = \frac{z'}{z}=\frac{-n}{z}
x' = (-n)\frac{x}{z}
同理
y'=(-n)\frac{y}{z}
現(xiàn)在,我們來計(jì)算假如投影平面上有一點(diǎn)(x', y'),要將其坐標(biāo)映射到[-1,1]區(qū)間,計(jì)算公式是什么樣的?
因?yàn)橥队捌矫嫔蟲坐標(biāo)取值范圍是[l, r],我們可以通過x'到l的距離占(r - l)的比例來映射:
\frac{x - l}{r - l} \quad \text{x到l的距離占總長度之比,它的范圍是[0,1]}
要將這個(gè)范圍轉(zhuǎn)換到[-1,1],只需要將這個(gè)范圍翻倍,然后左移一個(gè)單位的距離就可以,即:
x'' = \frac{x' - l}{r- l}·2 - 1
同理
y'' = \frac{y' - b}{t- b}·2 - 1
將x'和y'的式子代入到x''和y''的公式中
x'' = \frac{(-n)\frac{x}{z} - l}{r- l}·2 - 1=\frac{(-2n)\frac{x}{z} - 2l}{r- l} - 1=\frac{-2n}{r-l}·\frac{x}{z}-\frac{2l}{r-l}-1=\frac{2n}{r-l}(-\frac{x}{z})-\frac{r+l}{r-l}
y'' = \frac{2n}{t-b}(-\frac{y}{z})-\frac{t+b}{t-b}
轉(zhuǎn)換后的點(diǎn)為
\begin{bmatrix} \frac{2n}{r-l}(-\frac{x}{z})-\frac{r+l}{r-l} \\ \frac{2n}{t-b}(-\frac{y}{z})-\frac{t+b}{t-b} \\ z''\\ 1 \end{bmatrix}
老規(guī)矩,乘以-z
\begin{bmatrix} \frac{2n}{r-l}(x)+\frac{r+l}{r-l}z \\ \frac{2n}{t-b}(y)+\frac{t+b}{t-b}z \\ (-z)·z''\\ -z \end{bmatrix}
列出矩陣
\begin{bmatrix}\frac{2n}{r-l} && 0 && \frac{r+l}{r-l} && 0\\ 0 && \frac{2n}{t-b} && \frac{t+b}{t-b} && 0\\ 0 && 0 && m_3 && m_4\\ 0 && 0 && -1 && 0\end{bmatrix}·\begin{bmatrix}x \\ y \\ z \\ 1\end{bmatrix}=\begin{bmatrix} \frac{2n}{r-l}(x)+\frac{r+l}{r-l}z \\ \frac{2n}{t-b}(y)+\frac{t+b}{t-b}z \\ (-z)·z''\\ -z \end{bmatrix}
m3和m4的計(jì)算和上面一樣,我們直接寫出結(jié)果就行。對(duì)[0,1]區(qū)間是
\begin{bmatrix}\frac{2n}{r-l} && 0 && \frac{r+l}{r-l} && 0\\ 0 && \frac{2n}{t-b} && \frac{t+b}{t-b} && 0\\ 0 && 0 && \frac{f}{n-f} && \frac{nf}{n - f}\\ 0 && 0 && -1 && 0\end{bmatrix}
對(duì)[-1,1]區(qū)間是
\begin{bmatrix}\frac{2n}{r-l} && 0 && \frac{r+l}{r-l} && 0\\ 0 && \frac{2n}{t-b} && \frac{t+b}{t-b} && 0\\ 0 && 0 && \frac{n + f}{n - f} && \frac{2nf}{n - f}\\ 0 && 0 && -1 && 0\end{bmatrix}
接著我們用代碼來實(shí)現(xiàn)這個(gè)投影矩陣:

// 注意矩陣要設(shè)置成我們推導(dǎo)結(jié)果的轉(zhuǎn)置。
void BuildPerspectiveFovRHMatrix(Matrix4f& result, const float l, const float r, float b, float t, const float n, const float f)
{
    result.Set(0);

    result.m[0][0] = 2 * n / (r - l);
    result.m[1][1] = 2 * n / (t - b);
    result.m[2][0] = (r + l) / (r - l);
    result.m[2][1] = (t + b) / (t - b);
    if (g_DepthClipSpace == DepthClipSpace::kDepthClipZeroToOne)
    {
        result.m[2][2] = f / (n - f);
        result.m[3][2] = n * f / (n - f);
    }
    else /* g_DepthClipSpace == DepthClipSpace::kDepthClipNegativeOneToOne */
    {
        result.m[2][2] = (n + f) / (n - f);
        result.m[3][2] = (2 * n * f) / (n - f);
    }

    return;
}
(fov)式投影矩陣和(l,r,b,t)式投影矩陣的聯(lián)系

這兩個(gè)的關(guān)系非常明確,把我們推導(dǎo)fov矩陣時(shí)假設(shè)的參數(shù)代入就行了。l等于-1,r等于1,b等于-1/aspect,t等于1/aspect。代入之后,(l,r,b,t)式矩陣就變成
\begin{bmatrix}n && 0 && 0 && 0\\ 0 && n·aspect && 0 && 0\\ 0 && 0 && \frac{f}{n-f} && \frac{nf}{n - f}\\ 0 && 0 && -1 && 0\end{bmatrix}
而這里的n是投影平面到原點(diǎn)的距離,也就是說
n=\cot(\frac{xfov}{2})
于是(l,r,b,t)式矩陣就和(fov)式矩陣畫上等號(hào)了。如果n = 0.1,那么這個(gè)(l,r,b,t)式矩陣的xfov值就是168.58度。來看看運(yùn)行效果:

OpenGL z范圍[0, 1]

OpenGL z范圍[-1, 1]

D3D12 z范圍[0, 1]

D3D12 z范圍[-1, 1]

OpenGL的顯示還是比D3D12偏上一些,整個(gè)物體也變小很多,因?yàn)槲覀冞@次的xfov值太大了。

到這里,我們就可以回答本節(jié)最開始的問題了,對(duì)DX和OpenGL渲染管線來說,不存在裁剪空間[0,1]還是[-1,1]的區(qū)別,而之所以印象中有這區(qū)別,是因?yàn)樵趯慏X和OpenGL代碼時(shí),所用的數(shù)學(xué)庫一個(gè)是[0,1],一個(gè)是[-1,1]。

如果相機(jī)空間是左手坐標(biāo)系,那怎么辦?

從最初的推理開始,左手坐標(biāo)系改變了什么?投影平面的z坐標(biāo)改變了,變成了\cot\frac{xfov}{2}隨之而來的x和y坐標(biāo)的表達(dá)式也變了x'=\frac{x}{z}(\cot{\frac{xfov}{2}})
y'=\frac{y}{z}(\cot{\frac{xfov}{2}})·aspect
緊接著,我們不用再乘以-z,列出的矩陣也變了
\begin{bmatrix}\cot({\frac{xfov}{2}}) && 0 && 0 && 0\\ 0 && aspect·(\cot{\frac{xfov}{2}}) && 0 && 0\\ 0 && 0 && m_3 && m_4\\ 0 && 0 && 1 && 0\end{bmatrix}·\begin{bmatrix}x \\ y \\ z \\ 1\end{bmatrix}=\begin{bmatrix} x·(\cot{\frac{xfov}{2}})\\ y·aspect·(\cot{\frac{xfov}{2}})\\ z·z'\\ z \end{bmatrix}
求解m3和m4的方程也變了
m_3 + \frac{m_4}{z} = z'
n和f坐標(biāo)地改變,導(dǎo)致代入方程的數(shù)據(jù)也變了,先計(jì)算z范圍是[0,1]的轉(zhuǎn)換矩陣
\begin{cases} m_3 + \frac{m_4}{n} = 0\\ m_3 + \frac{m_4}{f} = 1 \end{cases}
求得的結(jié)果是
\begin{cases} m_3 = -\frac{f}{n - f}\\ m_4 = \frac{nf}{n - f} \end{cases}
最終轉(zhuǎn)換矩陣為
\begin{bmatrix}\cot({\frac{xfov}{2}}) && 0 && 0 && 0\\ 0 && aspect·(\cot{\frac{xfov}{2}}) && 0 && 0\\ 0 && 0 && -\frac{f}{n - f} && \frac{nf}{n - f}\\ 0 && 0 && 1 && 0\end{bmatrix}
計(jì)算z范圍是[-1,1]轉(zhuǎn)換矩陣的方程是
\begin{cases} m_3 + \frac{m_4}{n} = -1\\ m_3 + \frac{m_4}{f} = 1 \end{cases}
解的m3和m4的值為
\begin{cases} m_3 = -\frac{n+f}{n - f}\\ m_4 = \frac{2nf}{n - f} \end{cases}
最終的投影矩陣是
\begin{bmatrix}\cot({\frac{xfov}{2}}) && 0 && 0 && 0\\ 0 && aspect·(\cot{\frac{xfov}{2}}) && 0 && 0\\ 0 && 0 && -\frac{n+f}{n - f} && \frac{2nf}{n - f}\\ 0 && 0 && 1 && 0\end{bmatrix}
編寫代碼實(shí)現(xiàn)這個(gè)投影矩陣

// 還是要注意我們要按照轉(zhuǎn)置的思路去設(shè)置
void BuildPerspectiveFovLHMatrix(Matrix4f& result, const float xfov, const float aspect, const float near, const float far)
{
    result.Set(0);

    float cFov = 1.f / tanf(xfov / 2.f);
    result.m[0][0] = cFov;
    result.m[1][1] = aspect * cFov;
    result.m[2][3] = 1.0f;

    if (g_DepthClipSpace == DepthClipSpace::kDepthClipZeroToOne)
    {
        result.m[2][2] = -far / (near - far);
        result.m[3][2] = near * far / (near - far);
    }
    else
    {
        result.m[2][2] = -(far + near) / (near - far);
        result.m[3][2] = (2 * near * far) / (near - far);
    }

    return;
}

修改相應(yīng)的代碼查看效果


lefthand_OpenGL_zero_to_one

lefthand_OpenGL_negative_one_to_one

lefthand_D3D12_zero_to_one

lefthand_D3D12_negative_one_to_one

不用看盒子的顯示狀況,只需看顯示的位置就可以了。因?yàn)檫@個(gè)場景是從blender導(dǎo)出的,而blender是右手坐標(biāo)系的,所以它給出的頂點(diǎn)法線都是以右手坐標(biāo)系給出的,我在這里做了一些小手腳,把攝像機(jī)放在了原點(diǎn),在它背面放了一個(gè)盒子。用了一個(gè)左手坐標(biāo)系的投影矩陣后,后面的盒子就顯示出來了。可以看到,位置和右手坐標(biāo)系及對(duì)應(yīng)的OpenGL和D3D12圖片是完全一致的,說明我們的推導(dǎo)沒有問題。

那么,不用xfov的投影矩陣怎么辦呢?

嗯,懶了,留給讀者推導(dǎo)吧。

補(bǔ)充說明

本文用于測試的程序是我在github上的Panda項(xiàng)目。項(xiàng)目的使用方法是:
(1)首先你得有一個(gè)環(huán)境,Win10+VS2017+CMake+Git+Git-Lfs+github賬號(hào)
(2)克隆項(xiàng)目到本地(克隆過程請(qǐng)耐心等待,原本github就不快,而且我項(xiàng)目里還有大文件(git-lfs更加慢得令人發(fā)指),100M以上的那種),逐個(gè)運(yùn)行build_assimp.bat,build_crossguid.bat,build_libpng.bat,build_opengex.bat,build_zlib.bat
(3)運(yùn)行build.bat
(4)打開build文件夾下,Panda.sln解決方案。
(5)運(yùn)行EditorD3D或是EditorOGL。
投影矩陣的生成函數(shù)在Numerical.cpp文件中,使用的地方在GraphicsManager.cpp的CalculateCameraMatrix()函數(shù)中。模型加載的地方是EditorLogic.cpp的Initialize()函數(shù)。上面用到的模型文件是article.dae和article_lefthand.dae,模型文件在根目錄/Asset/Scene文件夾中,你可以隨意嘗試加載文件看效果。如果有什么困難,請(qǐng)?jiān)谙路搅粞浴?/p>

參考資料

《3D游戲與計(jì)算機(jī)圖形學(xué)中的數(shù)學(xué)方法》
《Real-Time Render 4th Edition》
《3D Graphics for Game Programming》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容