3.python opengl 繪制三角形

在OpenGL中,任何事物都在3D空間中,而屏幕和窗口卻是2D像素數組,這導致OpenGL的大部分工作都是關于把3D坐標轉變?yōu)檫m應你屏幕的2D像素。3D坐標轉為2D坐標的處理過程是由OpenGL的圖形渲染管線(Graphics Pipeline,大多譯為管線,實際上指的是一堆原始圖形數據途經一個輸送管道,期間經過各種變化處理最終出現(xiàn)在屏幕的過程)管理的。圖形渲染管線可以被劃分為兩個主要部分:第一部分把你的3D坐標轉換為2D坐標,第二部分是把2D坐標轉變?yōu)閷嶋H的有顏色的像素。這個教程里,我們會簡單地討論一下圖形渲染管線,以及如何利用它創(chuàng)建一些漂亮的像素。
圖形渲染管線接受一組3D坐標,然后把它們轉變?yōu)槟闫聊簧系挠猩?D像素輸出。圖形渲染管線可以被劃分為幾個階段,每個階段將會把前一個階段的輸出作為輸入。所有這些階段都是高度專門化的(它們都有一個特定的函數),并且很容易并行執(zhí)行。正是由于它們具有并行執(zhí)行的特性,當今大多數顯卡都有成千上萬的小處理核心,它們在GPU上為每一個(渲染管線)階段運行各自的小程序,從而在圖形渲染管線中快速處理你的數據。這些小程序叫做著色器(Shader)。
有些著色器允許開發(fā)者自己配置,這就允許我們用自己寫的著色器來替換默認的。這樣我們就可以更細致地控制圖形渲染管線中的特定部分了,而且因為它們運行在GPU上,所以它們可以給我們節(jié)約寶貴的CPU時間。OpenGL著色器是用OpenGL著色器語言(OpenGL Shading Language, GLSL)寫成的,在下一節(jié)中我們再花更多時間研究它。


image.png

如你所見,圖形渲染管線包含很多部分,每個部分都將在轉換頂點數據到最終像素這一過程中處理各自特定的階段。我們會概括性地解釋一下渲染管線的每個部分,讓你對圖形渲染管線的工作方式有個大概了解。

首先,我們以數組的形式傳遞3個3D坐標作為圖形渲染管線的輸入,用來表示一個三角形,這個數組叫做頂點數據(Vertex Data);頂點數據是一系列頂點的集合。一個頂點(Vertex)是一個3D坐標的數據的集合。而頂點數據是用頂點屬性(Vertex Attribute)表示的,它可以包含任何我們想用的數據,但是簡單起見,我們還是假定每個頂點只由一個3D位置(譯注1)和一些顏色值組成的吧。
當我們談論一個“位置”的時候,它代表在一個“空間”中所處地點的這個特殊屬性;同時“空間”代表著任何一種坐標系,比如x、y、z三維坐標系,x、y二維坐標系,或者一條直線上的x和y的線性關系,只不過二維坐標系是一個扁扁的平面空間,而一條直線是一個很瘦的長長的空間。
為了讓OpenGL知道我們的坐標和顏色值構成的到底是什么,OpenGL需要你去指定這些數據所表示的渲染類型。我們是希望把這些數據渲染成一系列的點?一系列的三角形?還是僅僅是一個長長的線?做出的這些提示叫做圖元(Primitive),任何一個繪制指令的調用都將把圖元傳遞給OpenGL。這是其中的幾個:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。
圖形渲染管線的第一個部分是頂點著色器(Vertex Shader),它把一個單獨的頂點作為輸入。頂點著色器主要的目的是把3D坐標轉為另一種3D坐標(后面會解釋),同時頂點著色器允許我們對頂點屬性進行一些基本處理。

圖元裝配(Primitive Assembly)階段將頂點著色器輸出的所有頂點作為輸入(如果是GL_POINTS,那么就是一個頂點),并所有的點裝配成指定圖元的形狀;本節(jié)例子中是一個三角形。

圖元裝配階段的輸出會傳遞給幾何著色器(Geometry Shader)。幾何著色器把圖元形式的一系列頂點的集合作為輸入,它可以通過產生新頂點構造出新的(或是其它的)圖元來生成其他形狀。例子中,它生成了另一個三角形。

幾何著色器的輸出會被傳入光柵化階段(Rasterization Stage),這里它會把圖元映射為最終屏幕上相應的像素,生成供片段著色器(Fragment Shader)使用的片段(Fragment)。在片段著色器運行之前會執(zhí)行裁切(Clipping)。裁切會丟棄超出你的視圖以外的所有像素,用來提升執(zhí)行效率。
OpenGL中的一個片段是OpenGL渲染一個像素所需的所有數據。
片段著色器的主要目的是計算一個像素的最終顏色,這也是所有OpenGL高級效果產生的地方。通常,片段著色器包含3D場景的數據(比如光照、陰影、光的顏色等等),這些數據可以被用來計算最終像素的顏色。

在所有對應顏色值確定以后,最終的對象將會被傳到最后一個階段,我們叫做Alpha測試和混合(Blending)階段。這個階段檢測片段的對應的深度(和模板(Stencil))值(后面會講),用它們來判斷這個像素是其它物體的前面還是后面,決定是否應該丟棄。這個階段也會檢查alpha值(alpha值定義了一個物體的透明度)并對物體進行混合(Blend)。所以,即使在片段著色器中計算出來了一個像素輸出的顏色,在渲染多個三角形的時候最后的像素顏色也可能完全不同。

可以看到,圖形渲染管線非常復雜,它包含很多可配置的部分。然而,對于大多數場合,我們只需要配置頂點和片段著色器就行了。幾何著色器是可選的,通常使用它默認的著色器就行了。

在現(xiàn)代OpenGL中,我們必須定義至少一個頂點著色器和一個片段著色器(因為GPU中沒有默認的頂點/片段著色器)。出于這個原因,剛開始學習現(xiàn)代OpenGL的時候可能會非常困難,因為在你能夠渲染自己的第一個三角形之前已經需要了解一大堆知識了。在本節(jié)結束你最終渲染出你的三角形的時候,你也會了解到非常多的圖形編程知識。

頂點輸入
開始繪制圖形之前,我們必須先給OpenGL輸入一些頂點數據。OpenGL是一個3D圖形庫,所以我們在OpenGL中指定的所有坐標都是3D坐標(x、y和z)。OpenGL不是簡單地把所有的3D坐標變換為屏幕上的2D像素;OpenGL僅當3D坐標在3個軸(x、y和z)上都為-1.0到1.0的范圍內時才處理它。所有在所謂的標準化設備坐標(Normalized Device Coordinates)范圍內的坐標才會最終呈現(xiàn)在屏幕上(在這個范圍以外的坐標都不會顯示)。

由于我們希望渲染一個三角形,我們一共要指定三個頂點,每個頂點都有一個3D位置。我們會將它們以標準化設備坐標的形式(OpenGL的可見區(qū)域)定義為一個float數組。

side = 1.0
s = side/2
vertices = [
-s, -s, 0, 
 s, -s, 0,
 0, s, 0
]

由于OpenGL是在3D空間中工作的,而我們渲染的是一個2D三角形,我們將它頂點的z坐標設置為0.0。這樣子的話三角形每一點的深度(Depth,譯注2)都是一樣的,從而使它看上去像是2D的。
標準化設備坐標(Normalized Device Coordinates, NDC)

一旦你的頂點坐標已經在頂點著色器中處理過,它們就應該是標準化設備坐標了,標準化設備坐標是一個x、y和z值在-1.0到1.0的一小段空間。任何落在范圍外的坐標都會被丟棄/裁剪,不會顯示在你的屏幕上。下面你會看到我們定義的在標準化設備坐標中的三角形(忽略z軸):


image.png

與通常的屏幕坐標不同,y軸正方向為向上,(0, 0)坐標是這個圖像的中心,而不是左上角。最終你希望所有(變換過的)坐標都在這個坐標空間中,否則它們就不可見了。

你的標準化設備坐標接著會變換為屏幕空間坐標(Screen-space Coordinates),這是使用你通過glViewport函數提供的數據,進行視口變換(Viewport Transform)完成的。所得的屏幕空間坐標又會被變換為片段輸入到片段著色器中。

定義這樣的頂點數據以后,我們會把它作為輸入發(fā)送給圖形渲染管線的第一個處理階段:頂點著色器。它會在GPU上創(chuàng)建內存用于儲存我們的頂點數據,還要配置OpenGL如何解釋這些內存,并且指定其如何發(fā)送給顯卡。頂點著色器接著會處理我們在內存中指定數量的頂點。

我們通過頂點緩沖對象(Vertex Buffer Objects, VBO)管理這個內存,它會在GPU內存(通常被稱為顯存)中儲存大量頂點。使用這些緩沖對象的好處是我們可以一次性的發(fā)送一大批數據到顯卡上,而不是每個頂點發(fā)送一次。從CPU把數據發(fā)送到顯卡相對較慢,所以只要可能我們都要嘗試盡量一次性發(fā)送盡可能多的數據。當數據發(fā)送至顯卡的內存中后,頂點著色器幾乎能立即訪問頂點,這是個非常快的過程。

頂點緩沖對象是我們在[OpenGL]教程中第一個出現(xiàn)的OpenGL對象。就像OpenGL中的其它對象一樣,這個緩沖有一個獨一無二的ID,所以我們可以使用glGenBuffers函數和一個緩沖ID生成一個VBO對象:

vbo = glGenBuffers(1)

OpenGL有很多緩沖對象類型,頂點緩沖對象的緩沖類型是GL_ARRAY_BUFFER。OpenGL允許我們同時綁定多個緩沖,只要它們是不同的緩沖類型。我們可以使用glBindBuffer函數把新創(chuàng)建的緩沖綁定到GL_ARRAY_BUFFER目標上:

glBindBuffer(GL_ARRAY_BUFFER, vbo)

從這一刻起,我們使用的任何(在GL_ARRAY_BUFFER目標上的)緩沖調用都會用來配置當前綁定的緩沖(VBO)。然后我們可以調用glBufferData函數,它會把之前定義的頂點數據復制到緩沖的內存中:

glBufferData(GL_ARRAY_BUFFER, 4*len(vertexData), vertexData, GL_STATIC_DRAW)

glBufferData是一個專門用來把用戶定義的數據復制到當前綁定緩沖的函數。它的第一個參數是目標緩沖的類型:頂點緩沖對象當前綁定到GL_ARRAY_BUFFER目標上。第二個參數指定傳輸數據的大小(以字節(jié)為單位);用一個簡單的sizeof計算出頂點數據大小就行。第三個參數是我們希望發(fā)送的實際數據。

第四個參數指定了我們希望顯卡如何管理給定的數據。它有三種形式:

GL_STATIC_DRAW :數據不會或幾乎不會改變。
GL_DYNAMIC_DRAW:數據會被改變很多。
GL_STREAM_DRAW :數據每次繪制時都會改變。
三角形的位置數據不會改變,每次渲染調用時都保持原樣,所以它的使用類型最好是GL_STATIC_DRAW。如果,比如說一個緩沖中的數據將頻繁被改變,那么使用的類型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,這樣就能確保顯卡把數據放在能夠高速寫入的內存部分。

現(xiàn)在我們已經把頂點數據儲存在顯卡的內存中,用VBO這個頂點緩沖對象管理。下面我們會創(chuàng)建一個頂點和片段著色器來真正處理這些數據?,F(xiàn)在我們開始著手創(chuàng)建它們吧。

頂點著色器
頂點著色器(Vertex Shader)是幾個可編程著色器中的一個。如果我們打算做渲染的話,現(xiàn)代OpenGL需要我們至少設置一個頂點和一個片段著色器。我們會簡要介紹一下著色器以及配置兩個非常簡單的著色器來繪制我們第一個三角形。下一節(jié)中我們會更詳細的討論著色器。

我們需要做的第一件事是用著色器語言GLSL(OpenGL Shading Language)編寫頂點著色器,然后編譯這個著色器,這樣我們就可以在程序中使用它了。下面你會看到一個非?;A的GLSL頂點著色器的源代碼:

strVS = """
#version 330 core
layout(location = 0) in vec3 vertexPosition_modelspace;
void main(){
    gl_Position.xyz = vertexPosition_modelspace;
    gl_Position.w = 1.0;
    }
"""

可以看到,GLSL看起來很像C語言。每個著色器都起始于一個版本聲明。OpenGL 3.3以及和更高版本中,GLSL版本號和OpenGL的版本是匹配的(比如說GLSL 420版本對應于OpenGL 4.2)。我們同樣明確表示我們會使用核心模式。

下一步,使用in關鍵字,在頂點著色器中聲明所有的輸入頂點屬性(Input Vertex Attribute)?,F(xiàn)在我們只關心位置(Position)數據,所以我們只需要一個頂點屬性。GLSL有一個向量數據類型,它包含1到4個float分量,包含的數量可以從它的后綴數字看出來。由于每個頂點都有一個3D坐標,我們就創(chuàng)建一個vec3輸入變量aPos。我們同樣也通過layout (location = 0)設定了輸入變量的位置值(Location)你后面會看到為什么我們會需要這個位置值。
向量(Vector)

在圖形編程中我們經常會使用向量這個數學概念,因為它簡明地表達了任意空間中的位置和方向,并且它有非常有用的數學屬性。在GLSL中一個向量有最多4個分量,每個分量值都代表空間中的一個坐標,它們可以通過vec.x、vec.y、vec.z和vec.w來獲取。注意vec.w分量不是用作表達空間中的位置的(我們處理的是3D不是4D),而是用在所謂透視除法(Perspective Division)上。我們會在后面的教程中更詳細地討論向量。

為了設置頂點著色器的輸出,我們必須把位置數據賦值給預定義的gl_Position變量,它在幕后是vec4類型的。在main函數的最后,我們將gl_Position設置的值會成為該頂點著色器的輸出。由于我們的輸入是一個3分量的向量,我們必須把它轉換為4分量的。我們可以把vec3的數據作為vec4構造器的參數,同時把w分量設置為1.0(我們會在后面解釋為什么)來完成這一任務。

當前這個頂點著色器可能是我們能想到的最簡單的頂點著色器了,因為我們對輸入數據什么都沒有處理就把它傳到著色器的輸出了。在真實的程序里輸入數據通常都不是標準化設備坐標,所以我們首先必須先把它們轉換至OpenGL的可視區(qū)域內。

片段著色器
片段著色器(Fragment Shader)是第二個也是最后一個我們打算創(chuàng)建的用于渲染三角形的著色器。片段著色器所做的是計算像素最后的顏色輸出。為了讓事情更簡單,我們的片段著色器將會一直輸出橘黃色。

在計算機圖形中顏色被表示為有4個元素的數組:紅色、綠色、藍色和alpha(透明度)分量,通??s寫為RGBA。當在OpenGL或GLSL中定義一個顏色的時候,我們把顏色每個分量的強度設置在0.0到1.0之間。比如說我們設置紅為1.0f,綠為1.0f,我們會得到兩個顏色的混合色,即黃色。這三種顏色分量的不同調配可以生成超過1600萬種不同的顏色!

strFS = """
#version 330 core
out vec3 color;
void main(){
    color = vec3(1,0,0);
    }
"""

著色器程序
著色器程序對象(Shader Program Object)是多個著色器合并之后并最終鏈接完成的版本。如果要使用剛才編譯的著色器我們必須把它們鏈接(Link)為一個著色器程序對象,然后在渲染對象的時候激活這個著色器程序。已激活著色器程序的著色器將在我們發(fā)送渲染調用的時候被使用。

當鏈接著色器至一個程序的時候,它會把每個著色器的輸出鏈接到下個著色器的輸入。當輸出和輸入不匹配的時候,你會得到一個連接錯誤。

創(chuàng)建一個程序對象很簡單:

program = glCreateProgram()

得到的結果就是一個程序對象,我們可以調用glUseProgram函數,用剛創(chuàng)建的程序對象作為它的參數,以激活這個程序對象:

glUseProgram(program)

在glUseProgram函數調用之后,每個著色器調用和渲染調用都會使用這個程序對象(也就是之前寫的著色器)了。

對了,在把著色器對象鏈接到程序對象以后,記得刪除著色器對象,我們不再需要它們了:

 glDeleteShader(vs)
 glDeleteShader(fs)

現(xiàn)在,我們已經把輸入頂點數據發(fā)送給了GPU,并指示了GPU如何在頂點和片段著色器中處理它。就快要完成了,但還沒結束,OpenGL還不知道它該如何解釋內存中的頂點數據,以及它該如何將頂點數據鏈接到頂點著色器的屬性上。我們需要告訴OpenGL怎么做。

鏈接頂點屬性
頂點著色器允許我們指定任何以頂點屬性為形式的輸入。這使其具有很強的靈活性的同時,它還的確意味著我們必須手動指定輸入數據的哪一個部分對應頂點著色器的哪一個頂點屬性。所以,我們必須在渲染前指定OpenGL該如何解釋頂點數據。

我們的頂點緩沖數據會被解析為下面這樣子:


image.png

位置數據被儲存為32位(4字節(jié))浮點值。
每個位置包含3個這樣的值。
在這3個值之間沒有空隙(或其他值)。這幾個值在數組中緊密排列(Tightly Packed)。
數據中第一個值在緩沖開始的位置。
有了這些信息我們就可以使用glVertexAttribPointer函數告訴OpenGL該如何解析頂點數據(應用到逐個頂點屬性上)了:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, None)
glEnableVertexAttribArray(0)

glVertexAttribPointer函數的參數非常多,所以我會逐一介紹它們:

第一個參數指定我們要配置的頂點屬性。還記得我們在頂點著色器中使用layout(location = 0)定義了position頂點屬性的位置值(Location)嗎?它可以把頂點屬性的位置值設置為0。因為我們希望把數據傳遞到這一個頂點屬性中,所以這里我們傳入0。
第二個參數指定頂點屬性的大小。頂點屬性是一個vec3,它由3個值組成,所以大小是3。
第三個參數指定數據的類型,這里是GL_FLOAT(GLSL中vec都是由浮點數值組成的)。
下個參數定義我們是否希望數據被標準化(Normalize)。如果我們設置為GL_TRUE,所有數據都會被映射到0(對于有符號型signed數據是-1)到1之間。我們把它設置為GL_FALSE。
第五個參數叫做步長(Stride),它告訴我們在連續(xù)的頂點屬性組之間的間隔。由于下個組位置數據在3個float之后,我們把步長設置為3 * sizeof(float)。要注意的是由于我們知道這個數組是緊密排列的(在兩個頂點屬性之間沒有空隙)我們也可以設置為0來讓OpenGL決定具體步長是多少(只有當數值是緊密排列時才可用)。一旦我們有更多的頂點屬性,我們就必須更小心地定義每個頂點屬性之間的間隔,我們在后面會看到更多的例子(譯注: 這個參數的意思簡單說就是從這個屬性第二次出現(xiàn)的地方到整個數組0位置之間有多少字節(jié))。
最后一個參數的類型是void,所以需要我們進行這個奇怪的強制類型轉換。它表示位置數據在緩沖中起始位置的偏移量(Offset)。由于位置數據在數組的開頭,所以這里是0。我們會在后面詳細解釋這個參數。
每個頂點屬性從一個VBO管理的內存中獲得它的數據,而具體是從哪個VBO(程序中可以有多個VBO)獲取則是通過在調用glVertexAttribPointer時綁定到GL_ARRAY_BUFFER的VBO決定的。由于在調用glVertexAttribPointer之前綁定的是先前定義的VBO對象,頂點屬性0現(xiàn)在會鏈接到它的頂點數據。

頂點數組對象
頂點數組對象(Vertex Array Object, VAO)可以像頂點緩沖對象那樣被綁定,任何隨后的頂點屬性調用都會儲存在這個VAO中。這樣的好處就是,當配置頂點屬性指針時,你只需要將那些調用執(zhí)行一次,之后再繪制物體的時候只需要綁定相應的VAO就行了。這使在不同頂點數據和屬性配置之間切換變得非常簡單,只需要綁定不同的VAO就行了。剛剛設置的所有狀態(tài)都將存儲在VAO中
OpenGL的核心模式要求我們使用VAO,所以它知道該如何處理我們的頂點輸入。如果我們綁定VAO失敗,OpenGL會拒絕繪制任何東西。
一個頂點數組對象會儲存以下這些內容:

glEnableVertexAttribArray和glDisableVertexAttribArray的調用。
通過glVertexAttribPointer設置的頂點屬性配置。
通過glVertexAttribPointer調用與頂點屬性關聯(lián)的頂點緩沖對象。


image.png

創(chuàng)建一個VAO和創(chuàng)建一個VBO很類似:

vao = glGenVertexArrays(1)
glBindVertexArray(vao)

要想使用VAO,要做的只是使用glBindVertexArray綁定VAO。從綁定之后起,我們應該綁定和配置對應的VBO和屬性指針,之后解綁VAO供之后使用。當我們打算繪制一個物體的時候,我們只要在繪制物體前簡單地把VAO綁定到希望使用的設定上就行了。這段代碼應該看起來像這樣:

import glfw
import sys
from OpenGL.GL import *
import numpy
from OpenGL.GLUT import *

side = 1.0
s = side / 1.0
vertices = [
    -s, -s, 0,
    s, -s, 0,
    0, s, 0
]

strVS = """
#version 330 core
layout(location = 0) in vec3 vertexPosition_modelspace;
void main(){
    gl_Position.xyz = vertexPosition_modelspace;
    gl_Position.w = 1.0;
    }
"""

strFS = """
#version 330 core
out vec3 color;
void main(){
    color = vec3(0,0,0);
    }
"""


#創(chuàng)建著色器
def createShader():
    #頂點著色器
    vs = glCreateShader(GL_VERTEX_SHADER)
    glShaderSource(vs, strVS)
    glCompileShader(vs)
    # 創(chuàng)建片段著色器
    fs = glCreateShader(GL_FRAGMENT_SHADER)
    glShaderSource(fs, strFS)
    glCompileShader(fs)

    program = glCreateProgram()
    glAttachShader(program, vs)
    glAttachShader(program, fs)
    glLinkProgram(program)
    glDeleteShader(vs)
    glDeleteShader(fs)
    return program


def initGlfw():
    if not glfw.init():  # 初始化glfw
        sys.exit()
    glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)  # 選擇opengl版本
    glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)  # 選擇子opengl版本
    glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)  # 選擇opengl核心模式
    glfw.window_hint(glfw.SAMPLES, 4)
    window = glfw.create_window(720, 1280, "window", None, None)  # 窗口寬度,窗口高度
    if not window:
        print("創(chuàng)建window窗口失敗")
        glfw.terminate()  # 創(chuàng)建失敗關閉窗口
    glfw.make_context_current(window)  # 創(chuàng)建窗口對象
    glViewport(0, 0, 720, 1280, )  # 窗口視圖
    return window

def makeVao(vertices):
    #vbo
    vbo = glGenBuffers(1)
    # 復制頂點數組到緩沖中供OpenGL使用
    glBindBuffer(GL_ARRAY_BUFFER, vbo)
    '''
    STATIC_DRAW:數據不會或者幾乎不會改變
    gl.DYNAMIC_DRAW:數據會被改變很多
    gl.STREAM_DRAW:數據每次繪制時都會改變
    '''
    vertexData = numpy.array(vertices, numpy.float32)
    print(vertexData)
    glBufferData(GL_ARRAY_BUFFER, 4 * len(vertexData), vertexData, GL_STATIC_DRAW)
    #vao
    vao = glGenVertexArrays(1)
    glBindVertexArray(vao)
    glEnableVertexAttribArray(0)
    # set buffers
    glBindBuffer(GL_ARRAY_BUFFER, vbo)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, None)
    # unbind VAO
    glBindVertexArray(0)
    return vao

def draw(vao,window,program):
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glClearColor(0.0, 0.0, 4.0, 0.0)
    glUseProgram(program)
    glBindVertexArray(vao)
    glDrawArrays(GL_TRIANGLES, 0, 3)
    glBindVertexArray(0)
    glfw.swap_buffers(window)
    glfw.poll_events()


window = initGlfw()
program = createShader()
vao = makeVao(vertices)
while not glfw.window_should_close(window):
    draw(vao,window,program)










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

相關閱讀更多精彩內容

友情鏈接更多精彩內容