GAMES101筆記(3)——Shading

課程鏈接:GAMES101-現(xiàn)代計算機(jī)圖形學(xué)入門-閆令琪
課程講師:閆令琪
本系列筆記為本人根據(jù)學(xué)習(xí)該門課程的筆記,僅分享出來供大家交流,希望大家多多支持GAMES相關(guān)講座及課程,如涉及侵權(quán)請聯(lián)系我刪除:albertlidesign@gmail.com

關(guān)于光柵化還有一點關(guān)于Z-Buffering的內(nèi)容,我們在這里先做一個補(bǔ)充。前面的課程我們知道了屏幕就是一堆像素,如何光柵化一個三角形,一些采樣理論的知識和如何進(jìn)行反采樣,就是先做模糊再做采樣。但是上一講我們只說了頻域分析,介紹了先模糊再采樣是正確的,但是沒有講為什么先采樣再模糊是錯的。這里給大家一個提示:先采樣就是把信號的頻譜進(jìn)行搬移,它會有頻譜的混疊,然后模糊就是在混疊之后截斷信號,這里就會發(fā)現(xiàn)混疊的信號截斷了還是混疊的。(采樣=頻譜搬移,模糊=截斷)

Z-Bufferring

眾所周知,空間中會有好多三角形,三角形各自離相機(jī)的距離也不一樣,那么我們該如何將這些三角形畫在屏幕上并且它們的遮擋關(guān)系是對的呢?近處的永遠(yuǎn)要遮擋遠(yuǎn)處的。在這里我們使用的方法就是深度緩存(Z-Buffering)。

Painter's Algorithm

場景中有很多不同的物體,既然要把這些物體放在屏幕上就要涉及到順序的問題,以保證這張圖是對的。一個比較直觀的方法是像畫水粉、油畫一樣,先把最遠(yuǎn)的物體繪制出來,再把近的物體覆蓋在遠(yuǎn)的物體上,層層疊加來形成最終的結(jié)果。




畫家先畫遠(yuǎn)處的山,再畫近處的草地,最后再在草地上畫上樹。這樣由遠(yuǎn)及近地對物體做光柵化的算法就稱之為畫家算法(Painter's Algortihm)。例如如果我們畫一個立方體,可以先畫最遠(yuǎn)的面,然后再畫周圍的四個面,最后繪制最前方的面,這樣就能出正確的結(jié)果。但是仔細(xì)思考會發(fā)現(xiàn),如果繪制的順序必須要非常嚴(yán)格,如果錯了會造成多余的線被繪制。因此在一定程度上說,這個算法是可以的,它要求對所有物體的深度進(jìn)行排序,排序的復(fù)雜度為O(n logn)(對于n個三角形)。但是當(dāng)三個空間三角形構(gòu)成一組互承關(guān)系時,三個兩兩之間存在了覆蓋關(guān)系,這樣就沒法定義它們之間的深度關(guān)系,也就無法對它們進(jìn)行排序。因此為了解決這個問題,人們提出了Z-Buffer。

Z-Buffer

在圖形學(xué)里,人們廣泛采用了Z-Buffer,引入了深度緩存的概念。這一算法其實就是避免空間中三角形的排序,它是從像素的角度來檢測能看到的三角形,對于每一個像素去記錄它所看見的最淺的深度的物體,也就是距離相機(jī)最近距離的物體。在圖形學(xué)中,我們不光能渲染出最后的結(jié)果(Frame Buffer),在生成這個圖像的同時,我們還能得到一個深度圖(Depth Buffer)它記錄了每個像素所看到的幾何圖形的最淺深度的信息。我們利用深度圖來維護(hù)遮擋信息。

在變換中,我們提到,我們始終假設(shè)相機(jī)是放在原點,并且看向-Z,這樣所有的點的Z坐標(biāo)都是負(fù)的,并且它們越小,說明離得越遠(yuǎn)。為了方便理解,現(xiàn)在我們換一個概念,我們把它看作是點到攝像機(jī)的距離,這樣Z就總是正的,那么Z值越小,物體越近。

如上圖所示,對于一個像素會記錄地板的深度和物體的深度,假設(shè)對于有一個能看到物體的像素,它先記錄了地板的深度,接著再記錄物體的深度,如果物體的深度比地板要小,那么意味著這個物體要遮擋住地板,這個點就要畫上看到物體的顏色,然后右側(cè)的深度圖也會做相應(yīng)的更新。這一流程的偽代碼如下:

// first step: initialize depth buffer to infinite
for (each triangle T)
  for(each sample (x,y,z) in T)
    if (z<zbuffer[x,y])              // closest sample so far
      framebuffer[x,y] = rgs;        // update color
      zbuffer[x,y] = z;              // update depth
    else
       ;                             // do nothing, this sample is occluded


注意:為無限大的一個值。初始化時要把所有的像素定義為無限大(或者足夠大的一個值)。

在使用畫家算法時,我們要把所有的三角形做一次排序,這需要花費O(n logn)的時間。而深度緩存,每個三角形都覆蓋常數(shù)個像素的話,無非就是考慮每個三角形所覆蓋的常數(shù)個數(shù)的像素,也就是O(n)的算法復(fù)雜度。它并沒有進(jìn)行排序,只是不斷地更新結(jié)果,記錄當(dāng)前所看到的最小深度值,因此只花了線性的時間。

這里要注意,不會出現(xiàn)兩個不同的三角形在同一個像素上有著相同的深度,因為深度緩存算法與順序是沒有關(guān)系的了,不管通過什么順序繪制三角形,最后的結(jié)果都會是相同的。在圖形學(xué)中,我們都是用浮點型來表示的,浮點型和浮點型判斷相等是非常困難的事,基本上可以認(rèn)為兩個浮點數(shù)是完全不會相同的。此外,這一算法可以應(yīng)用在幾乎所有的硬件中。

之前提到,為了做反走樣,我們使用了MSAA算法,對一個像素取很多采樣點,對于這些不同的采樣點,如果我們要運用深度緩存,就要對每一個采樣點檢測它所能看到的深度,因此Z-Buffer還得考慮到它不是對每個像素記錄深度,實際上是對每個采樣點做記錄。

Shading

到目前為止,我們已經(jīng)學(xué)習(xí)了如何將物體隨著相機(jī),變換到相機(jī)位于原點看向-Z方向時的位置,即View Transformation,接著我們可以將模型映射到二維的從(0,0)(w,h)的屏幕上,接著根據(jù)二維屏幕中的像素與三角形之間通過采樣求出像素的值,也就是光柵化。


目前所能做的事表現(xiàn)出來就是這樣的

而我們期待的結(jié)果是這樣的,它與我們目前做到的結(jié)果的區(qū)別在于著色。

在下面的渲染圖中,可以看到杯子中有茶、蛋撻、葡萄,不同的物體會有不同的顏色,在不同的光照下,這些物體上的顏色會有變化,這都是著色的問題。

著色 (Shading)在Merriam-Webster字典里的意思為:
shad·ing, noun, The darkening or coloring of an illustration or diagram with parallel lines or a block of color.
在我們這門課中,它的含義是:The process of applying a material to an object. 即對不同物體應(yīng)用不同材質(zhì)的過程。不同的材質(zhì)和光照會發(fā)生不同的作用效果。

Blinn-Phong

最基礎(chǔ)的著色模型為Blinn-Phong反射模型。通過下圖幾個茶杯,我們可以看到光源應(yīng)該在右上方,在每個茶杯上有高光(Specular highlights),茶杯的其他部位的變化相對不明顯,這部分叫做漫反射(Diffuse reflection)。對于最底下的茶杯的最左側(cè),光源從右上方照射,按說這個茶杯的最左側(cè)不應(yīng)該為黑色,因為這部分沒有被光直接照射到,但為什么這里有顏色呢?是因為間接光或環(huán)境光(Ambient lighting),它是由光線在其他物體之間發(fā)生反彈從而照亮這部分區(qū)域的。任何一個點都會接收到來自環(huán)境的反射光,它很復(fù)雜,我們在后面會講到。

我們看到一共有三部分,高光、漫反射和間接光,我們可以把這三部分分別做出來從而得到一個很相似的結(jié)果。在開始之前,我們需要定義一些東西。

  • 我們現(xiàn)在考慮光照是對任何一個點而言,假設(shè)這個點叫做shading point,那么這個點的著色結(jié)果是什么?我們定義在一個極小的范圍內(nèi),它是一個平面,即與它所在的曲面相切的平面
  • 既然是平面,那么就有法線\vec n垂直于平面
  • 同樣我們還可以定義一個觀測方向,我們規(guī)定從shading point到相機(jī)的方向為觀測方向\vec v
  • 同樣道理,從shading point到光源的方向稱為光照方向\vec l
  • 注意,因為我們只關(guān)心這些向量的方向,所以它們都是單位向量,長度為1
  • 我們還需要定義一些表面參數(shù),比如顏色,亮度(shininess)等


Diffuse Reflection

Blinn-Phone有三個不同的部分,我們從最簡單的漫反射開始。當(dāng)有一根光線打到物體表面上的某一點,光線會被均勻地反射到不同方向上去,這個過程叫漫反射。


當(dāng)我們考慮shading point所在表面的朝向與光照方向有一定夾角的時候,會發(fā)現(xiàn)得到的明暗是不一樣的。如圖所示,假如光是離散的,有六根光線打到表面上,每一根光線代表一個固定的能量,如果表面和光線垂直的話會接收到所有的光,但是如果表面旋轉(zhuǎn)到了某一個角度,比如,我們會發(fā)現(xiàn)只接受到了三根光線,那么就會變得暗一些。因此會發(fā)現(xiàn)表面的明暗會與光線與法線的夾角存在一定關(guān)系。為了量化,我們用shading point周圍的單位面積來定義接收到的能量,它與夾角有關(guān),這就是Lambert的余弦定律。

提到了接收能量也不得不提一下發(fā)散能量。不同的物體被光所照亮,光是一種能量,在這里我們認(rèn)為它是一個點光源,它會無時無刻地在向外輻射出不同的能量,一個很聰明的觀測方法是一個點向外發(fā)散能量,那么在某一時刻,它們一定集中在某一個球殼上。根據(jù)能量守恒定律,離中心近的球殼和離中心遠(yuǎn)的球殼的能量應(yīng)該是完全相同的能量,但是隨著球殼離中心越來越遠(yuǎn),其表面積會越變越大,那也就意味著在某一個點,它的能量會越來越少。因此我們定義在距離為的地方定義光的強(qiáng)度為,如果傳播到距離為的球殼上時,它的能量為。這就告訴了我們,光在某一時刻某一位置所能傳播的能力是與它與光源的距離的平方成反比的。光線傳播的距離越長,所能接收到的能量越小。也就是說,只要知道一個點光源,又知道shading point離光源的距離,那么就能知道有多少光傳播到了當(dāng)前的shading point。

根據(jù)前面我們又知道了有多少光能被接收,我們就得到了diffuse的表示方法了:


假設(shè)我們有一個點光源,假設(shè)它與shading point的距離為,我們定義在單位距離上它的強(qiáng)度為,那么我們就能求出它到達(dá)shading point處的能量,我們有算出了有多少光能被接收,就能算出我們最后所看到的在這一點上的能量。做一個是說,當(dāng)向量點乘為負(fù)數(shù)的時候,這是說光從背面穿過了物體達(dá)到了shading point,顯然是不可能的,因此沒有物理意義,因為我們考慮的是反射,因此當(dāng)點乘為負(fù)時就認(rèn)為是。我們再來考慮一個問題,對于shading point,它自己本身為什么會有顏色,是因為這個點會吸收顏色,或者說能量,它反射出去的是不吸收的顏色。如果我們在能量被接收后定義一個系數(shù),表示漫反射系數(shù),如果為就是最亮,如果為那么表面就是黑的,那么如果我們把它表示成一個三通道的向量就可以表達(dá)它對RGB三個顏色的反射程度。

既然光打到shading point被反射到各個方向,那就意味著無論我們從哪觀測它,所看到的結(jié)果應(yīng)該是一樣的,從公式上看也是如此,我們考慮的是光線與法線的夾角,因此漫反射跟觀測角度無關(guān)完全無關(guān)。
(可以想想,你處在一個黑屋子里,眼前一個石膏球被照亮,無論你從哪個角度看這個石膏球,除了被照亮的部分其他地方都看不到,因此漫反射與觀測角度無關(guān))

需要注意的一點是:我們說的著色是在一個點上進(jìn)行著色,如果想要得到一整張圖就要著色很多次。

Specular Term

高光有一個特點:它的反射方向非常接近鏡面反射的方向。如果是鏡面,這個物體就是無限光滑的,我們可以根據(jù)入射方向和法線來求出它的出射方向,如圖\vec R所示。如果物體是金屬,那么這個物體就沒有那么光滑,它的反射方向會沿著\vec R分布,當(dāng)我們的觀察方向和鏡面反射方向接近的時候,我們就能看到高光了,其他時候我們都看不到高光。這就告訴了我們,高光項和我們觀察的方向及鏡面反射方向有關(guān)。

Blinn-Phong模型做了一個很聰明的事,因為當(dāng)我們的觀察方向和鏡面方向接近的時候,其實就說明法線方向和半程向量(Half Vector)很接近。這是說,如果給定入射方向\vec l和出射方向\vec v,我們可以求它的角平分線方向,只需要將兩個向量相加,根據(jù)平行四邊形法則,然后再做歸一化即可得到兩向量的半程向量\vec h。如果此時\vec h\vec n接近,一定程度上就可以反映\vec v\vec R接近。這樣的話,就能根據(jù)\vec n\vec h的點乘的結(jié)果來算出高光,得出如下圖所示公式。又因為通常高光都是白色,所以高光系數(shù)k_s為白色的值。當(dāng)然,我們還要考慮有多少能量被吸收,也就需要加上一項\vec l\vec n的點乘,這里沒有考慮是因為Blinn-Phong模型是經(jīng)驗性模型,這里將其簡化掉了,其主要關(guān)注的是是否能看到高光。

那么為什么要用半程向量而不是直接用\vec v\vec R呢?當(dāng)然可以,那個模型就被稱作Phong Reflection Model,Blinn-Phong是它的一個改進(jìn)。這是因為半程向量太好算了,而反射方向就不那么好算了,計算量要大很多。除此之外,觀察上圖公式,我們在\vec n\vec h的點乘加了一個指數(shù)p,這是因為盡管向量之間的夾角余弦值能體現(xiàn)兩個向量是否足夠接近,但是容忍度太高了,比如45°時,它的余弦值仍然很大,如果我們只用夾角余弦去做高光的話會得到一個超級大的高光,看上去就很不自然,我們平常認(rèn)為高光是非常亮的并且集中在很小的區(qū)域中。所以我們要對夾角余弦加上若干個指數(shù)就能得到較為合理的結(jié)果,正常情況下我們用的指數(shù)要達(dá)到[100,200]。


下面是一個實際的例子,顯示了漫反射和高光項在一塊的效果,我們會發(fā)現(xiàn)隨著指數(shù)的增長,高光會越來越小,因此指數(shù)就是用來控制高光的大小的參數(shù)。

Ambient Term

環(huán)境光是一個非常復(fù)雜的東西,我們做一個非常大膽的假設(shè)(但是事實上不是這么回事),假設(shè)任何一個點所接收到的來自環(huán)境的光永遠(yuǎn)都是相同的,我們記作I_a,再給定一個系數(shù)k_a就可以直接近似地來得到環(huán)境光。觀察圖我們知道,環(huán)境光跟視點無關(guān),跟光源位置也無關(guān),并且和法線也沒關(guān)系,因此環(huán)境光其實是一個常數(shù),也就是某一種顏色。比如你看到一個物體,任何一個地方都有一個常數(shù)的顏色,總會得到一個“平”的結(jié)果。環(huán)境光的作用就是保證沒有地方是黑的。但實際上如果我們需要很精確地計算它,就需要全局光照的知識。

現(xiàn)在我們把所有的項都加起來就可以看到一個完整的Blinn-Phong Reflection Model。我們知道Blinn-Phong是個著色模型,它對所有的點都進(jìn)行了著色。


Shading Frequencies

接下來討論著色頻率的問題,首先我們有這三個球,這三個球有著完全相同的幾何形狀(一模一樣的模型),這從觀察邊界可以得出,那么為什么著色之后我們得到的結(jié)果各不相同呢?這就是因為不同的著色頻率,即著色運用到哪些點上。如果我們把著色運用到網(wǎng)格面上,一個平面只做了一次shading,就能得到下圖最左側(cè)的結(jié)果。中間的結(jié)果是對每一個網(wǎng)格面上的頂點進(jìn)行著色的。先求出它們的法線再對每一個頂點做一次著色,在面的內(nèi)部通過插值的方法算出來。最右側(cè)的著色是對每一個像素進(jìn)行著色的。也就是說,我們對每一個四邊形或三角形的頂點求出一個法線,然后把法線的方向在三角形內(nèi)部進(jìn)行插值,然后就得到任何一個像素自己的法線方向,并且可以做著色。也就是說,如果著色運用到像素上就可以得到非常好的結(jié)果。


下面我們對這些方法來做一個正規(guī)的定義:

  1. 每一個三角形都是平面,每個三角形的法線都非常容易求出,只需要將三角形的兩個邊做叉積,這樣就可以算出一個shading結(jié)果,但也自然在三角形內(nèi)部不會有著色的變化,也就是每個三角形面內(nèi)各點的顏色是完全一樣的。當(dāng)然這個結(jié)果不太好,但是有它自己的名字,稱為Flat Shading。

  2. 我們可以在任意一個頂點處求出它的法線,對每個頂點做一次著色,然后通過插值計算出每個三角形內(nèi)部的顏色,得到的結(jié)果要比Flat Shading要好,但是當(dāng)三角形大一點的話,高光可能就看不見了,因此它的效果也是有局限性的。這樣的著色叫Gouraud Shading

  3. 如果我們對于每一個像素,求出各三角形頂點的法線,然后在每一個像素都插值出一個法線方向,再對每一個像素進(jìn)行一次著色,就能得到相對比較好的結(jié)果,這個結(jié)果就叫做Phong Shading。注意Blinn-Phong是一種著色模型,這里的Phong Shading指的是著色頻率。

這三種著色具體的區(qū)別其實也取決于具體的模型,并不是說Flat Shading就一定會很差,下圖中,每一行的模型都是完全一樣的,每一列是不同網(wǎng)格頂點數(shù)的區(qū)別,也就是說當(dāng)我們的幾何模型相對復(fù)雜的話,其實也可以用一些相對簡單的著色模型,而且得到的結(jié)果還可以。著色頻率取決于面、點數(shù)量。當(dāng)然,Phong Shading的著色效果好,其計算量當(dāng)然也比Flat Shading大很多(但也不絕對,如果面數(shù)超過了像素數(shù)那么Phong Shading可能更?。?,所以具體用哪種著色方法要取決于具體的物體。當(dāng)面數(shù)不是特別多的情況下,Phong Shading能得到一個較好的結(jié)果。


Defining Per-Vertex Normal Vectors

在Gouraud Shading中我們要對每一個頂點求法線,那么該如何做呢?
最好最簡單的方法是,如果使用網(wǎng)格模型想擬合的模型,比如球模型,去求這幾個網(wǎng)格點所在的球模型上的法線即可。但是它的應(yīng)用情況會比較少。
第二種方法,一個頂點通常會位于多個三角形面上,即這多個三角形面共用該頂點,那么我們只需要求過該點的三角形面的法相的平均即可,也可以以為三角形面積為權(quán)重做加權(quán)平均。


Defining Per-Pixel Normal Vectors

下面一個問題是如何去定義一個逐像素的法線?我們要通過重心坐標(biāo)的方法來進(jìn)行插值,下面會詳細(xì)講,這里注意求出來的法線都要做一個歸一化的處理,以保證它們的長度是一致的。


Graphics(Real-time Rendering) Pipeline

現(xiàn)在,把前面所有的知識合在一起就已經(jīng)能夠得到一個渲染的結(jié)果了。把這所有的東西合在一塊就叫做圖形管線(Graphics Pipeline),閆老師更愿意叫做實時渲染管線。當(dāng)我們輸入三維空間中的一些點,中間經(jīng)歷了什么樣的過程,這個過程就叫Pipeline,其實就是一系列的操作。

如下圖所示,下面整個的過程就是從三維場景到最后看到的二維像素的過程。而這個過程是已經(jīng)在硬件里寫好了,顯卡所做的整個的操作就是這樣的操作。
(1)輸入三維空間中的點;
(2)投影變換,我們將三維空間中的點投影到了屏幕上;
(3)形成三角形
(4)屏幕是離散的,因此要通過光柵化來把三角形變成Fragments(OpenGL中的概念,類比于像素)
(5)著色Fragments
(6)顯示


一個小問題:為什么說我們在投影到屏幕上再連接三角形而不是一開始輸入三角形呢?其實是一樣的,因為頂點無論如何變換,其連接關(guān)系是沒有變的,因此在輸入的時候用上連接關(guān)系形成三角形還是在投影之后形成三角形沒有區(qū)別。

(1)Vertex Processing:對空間中每一個頂點做MVP變換。



(2)Rasterization:對每個像素采樣判斷是否在三角形內(nèi),即光柵化。



(3)Fragment Processing:判定像素是否可見(也可以歸為光柵化)。

(4)Shading:Shading可以發(fā)生在頂點處理上也可以發(fā)生在Fragment處理上。
注意,這兩部分是可編程的,即我們可以自己去決定如何運作,這部分代碼稱為Shader,它是控制這些頂點和像素如何著色的。



(5)Texture mapping

Shader Programs

前面提到了Shader,我們這里更詳細(xì)了解一下。現(xiàn)代的GPU允許用戶通過編程來解決頂點和像素如何做著色,這就需要用戶來自己寫Shader,Shader本質(zhì)上就是一個能在硬件上執(zhí)行的程序。OpenGL作為圖形學(xué)的API,可以用它來寫Shader,它是對每一個像素所執(zhí)行的通用的程序,因此不需要寫For-Loop。如果我們寫的是頂點的Shader,就叫做頂點著色器(Vertex-Shader),如果是對像素的操作就叫做片段(或像素)著色器(Fragment-Shader or Pixel-Shader)

下面是一個具體的例子,像素著色器是要確定像素最后的顏色,即寫清楚怎么計算像素的顏色并且輸出出去。這個例子是簡單的著色語言GLSL。

uniform sampler2D myTexture; // uniform指的是全局變量,定義了一個紋理
uniform vec3 lightDir;  // 固定的光照方向
varying vec2 uv;
varying vec3 norm;  // 插值出來的法線

void diffuseShader()
{
  vec3 kd;
  kd = texture2d(myTexture, uv);  // 每一個像素可以拿到一個漫反射系數(shù),具體操作跟紋理相關(guān),暫時忽略
  kd*=clamp(dot(-lightDir, norm), 0.0, 1.0); // 一個最最簡單的漫反射的部分,用clamp限定到[0,1]
  gl_FragColor = vec4(kd, 1.0); // 將三維向量轉(zhuǎn)四維向量,返回到gl_FragColor
}

因此 Shader能夠定義頂點、像素如何操作?,F(xiàn)在就已經(jīng)把整個實時渲染的基本思路涵蓋到了,在這個基礎(chǔ)上,就已經(jīng)可以去學(xué)習(xí)一系列圖形API了,會發(fā)現(xiàn)非常簡單,所有的矩陣都不需要自己來做,都可以借助API來生成,可以很方便的寫出來。這里推薦一個叫ShaderToy的網(wǎng)站,在這里可以只寫著色器,即頂點和像素如何著色,就可以通過這個web來執(zhí)行程序,就可以看出結(jié)果。Shader可以做到千變?nèi)f化。

隨著現(xiàn)在GPU的發(fā)展,顯卡可以同時處理大量的幾何,并且著色非常快,高度并行?,F(xiàn)在的圖形學(xué)就是向一個能夠?qū)崟r渲染超級復(fù)雜的場景發(fā)展。隨著GPU的發(fā)展,有越來越多不同的著色器產(chǎn)生,比如一種叫Geometry Shader,它可以動態(tài)的產(chǎn)生三角形。還有一種Compute Shader可以做通用的GPU計算,稱GPGPU。還需要提到,GPU分兩種,一種是獨立顯卡,另一種是集成顯卡。GPU本身可以理解為高度并行化處理器,CPU通產(chǎn)有8核、16核等,GPU的核數(shù)是CPU的很多很多倍,所以特別適合來做圖形,因為很多像素的著色方法是一樣的,這就非常利于做并行計算。

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

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