OpenGL ES案例03 - 使用GLSL完成紋理圖片加載

案例:根據(jù)對GLSL語言的理解,自定義一個頂點(diǎn)著色器和一個片元著色器,使用著色器API完成紋理的加載。
進(jìn)階:解決紋理倒置問題。
效果如下:


翻轉(zhuǎn)前效果

翻轉(zhuǎn)后效果

準(zhǔn)備工作

  1. 新建iOS應(yīng)用工程,修改當(dāng)前controller的view。將原來的view繼承于UIView改成繼承于HView。
  2. 自定義一個HVIew類,后續(xù)繪制圖片在該類中完成。
  3. 新建頂點(diǎn)著色器文件和片元著色器文件。
    3.1 command + N,開始新建文件。
    3.2 選擇iOS->Other->Empty,新建兩個空文件,分別命名為:shaderv.vsh、shaderf.fsh

至此準(zhǔn)備工作完成,接下來就開始編碼工作。

自定義著色器

自定義著色器本質(zhì)上其實(shí)是一個字符串,但是在Xcode的編寫過程沒有任何錯誤提示,因此,在編寫過程中需要格外仔細(xì)。

  1. 頂點(diǎn)著色器shaderv.vsh
  • 定義兩個attribute修飾符修飾的變量,分別表示頂點(diǎn)坐標(biāo)和紋理坐標(biāo)
  • 定義一個varying修飾符修飾的變量,用于將紋理坐標(biāo)從頂點(diǎn)著色器傳遞給片元著色器
  • main函數(shù),在該函數(shù)內(nèi)給內(nèi)建變量gl_Position賦值。若頂點(diǎn)坐標(biāo)不需要變換,則直接將頂點(diǎn)坐標(biāo)賦值給內(nèi)建變量gl_Position。若頂點(diǎn)坐標(biāo)需要進(jìn)行變換,則將變換后的結(jié)果賦值給內(nèi)建變量gl_Position。
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;
void main(){
    varyTextCoord = textCoordinate;
    gl_Position = position;
}

2.片元著色器shaderf.fsh

  • 指定片元著色器中float類型的精度,如果不寫,可能會報一些異常錯誤
  • 定義一個與頂點(diǎn)著色器橋接的紋理坐標(biāo),寫法必須同在頂點(diǎn)著色器寫法一致,否則將無法收到從頂點(diǎn)著色器傳遞過來的數(shù)據(jù)
  • 定義一個unifom修飾符修飾的變量,用于獲取紋理坐標(biāo)上每個像素點(diǎn)的紋素。
  • main函數(shù),在函數(shù)內(nèi)給內(nèi)建變量gl_FragColor賦值。通過texture2D內(nèi)建函數(shù)獲取當(dāng)前顏色值,它有兩個參數(shù):參數(shù)1:紋理圖片;參數(shù)2:紋理坐標(biāo),返回值:vec4類型的顏色值。當(dāng)顏色不需要進(jìn)行修改時,可直接將vec4類型的顏色值賦值給內(nèi)建變量gl_FragColor。當(dāng)顏色需要修改時,將最終修改的結(jié)果賦值給內(nèi)建變量gl_FragColor。
precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main(){
    gl_FragColor = texture2D(colorMap, varyTextCoord);
}

初始化

1. 創(chuàng)建圖層

1.1 圖層主要是顯示OpenGL ES繪制內(nèi)容的載體。它的創(chuàng)建有兩種方式:
  • 直接使用當(dāng)前view的layer。但是view的layer是繼承于CALayer,需要重寫類方法layerClass,使其繼承于CAEAGLLayer。
  • 直接使用[[CAEAGLLayer alloc] init]方法創(chuàng)建一個CAEAGLLayer類型的圖層,并將新創(chuàng)建的圖層添加到當(dāng)前圖層上。
self. myEagLayer = (CAEAGLLayer*)self.layer;
+ (Class)layerClass{
    return [CAEAGLLayer class];
}
1.2 設(shè)置scale,這里設(shè)置當(dāng)前view的scale與屏幕的scale一樣大
[self setContentScaleFactor:[[UIScreen mainScreen] scale]];
1.3 設(shè)置描述屬性,這里設(shè)置不維持渲染內(nèi)容以及顏色格式為RGBA8
  • kEAGLDrawablePropertyRetainedBacking:表示繪圖表面顯示后,是否保留其內(nèi)容,true-保留,false-不保留
  • kEAGLDrawablePropertyColorFormat:可繪制表面的內(nèi)部顏色緩存區(qū)格式,這個key對應(yīng)的值是一個NSString指定特定顏色緩存區(qū)對象。默認(rèn)是kEAGLColorFormatRGBA8;
顏色緩沖區(qū)格式 描述
kEAGLColorFormatRGBA8 32位RGBA的顏色,4*8=32位
kEAGLColorFormatRGB565 16位RGB的顏色
kEAGLColorFormatSRGBA8 sRGB代表了標(biāo)準(zhǔn)的紅、綠、藍(lán),即CRT顯示器、LCD顯示器、投影機(jī)、打印機(jī)以及其他設(shè)備中色彩再現(xiàn)所使用的三個基本色素。sRGB的色彩空間基于獨(dú)立的色彩坐標(biāo),可以使色彩在不同的設(shè)備使用傳輸中對應(yīng)于同一個色彩坐標(biāo)體系,而不受這些設(shè)備各自具有的不同色彩坐標(biāo)的影響。
self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false, kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatSRGBA8, kEAGLDrawablePropertyColorFormat, nil];

2. 創(chuàng)建上下文

上下文主要用來保存OpenGL ES的狀態(tài),是一個狀態(tài)機(jī),不論GLKit還是GLSL,都需要使用context。
2.1 創(chuàng)建上下文,并指定OpenGL ES渲染API的版本號

self.myContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];

2.2 設(shè)置當(dāng)前上下文

[EAGLContext setCurrentContext:self.myContext];

3. 清空緩沖區(qū)

清除緩沖區(qū)的殘留數(shù)據(jù),防止其它無用數(shù)據(jù)對繪制效果造成影響

//清空渲染緩存區(qū)
glDeleteBuffers(1, &_myColorRenderBuffer);
self.myColorRenderBuffer = 0;
    
//清空幀緩存區(qū)
glDeleteBuffers(1, &_myColorFrameBuffer);
self.myColorFrameBuffer = 0;

4. 設(shè)置緩沖區(qū)

設(shè)置緩沖區(qū)包括設(shè)置RenderBuffer和FrameBuffer。

  1. RenderBuffer:是一個通過應(yīng)用分配的2D圖像緩沖區(qū),需要附著在FrameBuffer上。
    1.1 RenderBuffer有3種緩沖區(qū)

    • 深度緩沖區(qū)(Depth Buffer):存儲深度值等
    • 紋理緩沖區(qū)(Depth Buffer):存儲紋理坐標(biāo)中對應(yīng)的紋素、顏色值等
    • 模板緩沖區(qū)(Stencil Buffer):存儲模板等

    1.2 設(shè)置RenderBuffer

    • 定義一個緩存區(qū)ID
    • 申請一個緩沖區(qū)標(biāo)志
    • 將緩沖區(qū)標(biāo)識綁定到GL_RENDERBUFFER
    • 綁定一個可繪制對象(layer)的存儲到一個OpenGL ES RenderBuffer對象
-(void)setupRenderBuffer{
    //1.定義一個緩存區(qū)ID
    GLuint buffer;
    //2.申請一個緩存區(qū)標(biāo)志
    glGenRenderbuffers(1, &buffer);
    
    self.myColorRenderBuffer = buffer;
    
    glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
    
    [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
}
  1. FrameBuffer:是一個收集顏色、深度、模板緩沖區(qū)的附著點(diǎn),簡稱FBO,即是一個管理者,用來管理RenderBuffer,且FrameBuffer沒有實(shí)際的存儲功能,真正實(shí)現(xiàn)存儲的是RenderBuffer。
    2.1 FrameBuffer有3個附著點(diǎn)

    • 顏色附著點(diǎn)(Color Attachment):管理紋理、顏色緩沖區(qū)
    • 深度附著點(diǎn)(depth Attachment):管理深度緩沖區(qū),會根據(jù)當(dāng)前深度緩沖中的值修改顏色緩沖中的內(nèi)容
    • 模板附著點(diǎn)(Stencil Attachment):管理模板緩沖區(qū)

    2.2 設(shè)置FrameBuffer

    • 定義一個緩存區(qū)ID
    • 申請一個緩沖區(qū)標(biāo)志
    • 將緩沖區(qū)標(biāo)識綁定到GL_FRAMEBUFFER
    • 通過FrameBuffer來管理RenderBuffer,將RenderBuffer附著到FrameBuffer的GL_COLOR_ATTACHMENT0附著點(diǎn)上。
-(void)setupFrameBuffer{
    GLuint buffer;
    glGenFramebuffers(1, &buffer);
    
    self.myColorFrameBuffer = buffer;
    
    glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
    
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
}

注意點(diǎn):綁定renderBuffer和FrameBuffer是有順序的,先有RenderBuffer,才有FrameBuffer。

開始繪制

初始化

清除屏幕顏色,清空顏色緩沖區(qū),設(shè)置視口大小。

//設(shè)置清屏顏色
glClearColor(0.3, 0.45, 0.5, 1.0);
    
//清除屏幕
glClear(GL_COLOR_BUFFER_BIT);

//1.設(shè)置視口大小
CGFloat scale = [[UIScreen mainScreen] scale];
glViewport(self.frame.origin.x * scale, self.frame.origin.y * scale, self.frame.size.width * scale, self.frame.size.height * scale);

加載自定義著色器

1. 讀取并編譯頂點(diǎn)著色程序、片元著色程序

1.1 創(chuàng)建一個頂點(diǎn)/片元著色器

*shader = glCreateShader(type);

1.2 以字符串的形式將著色器源碼讀取出來,并將著色器源碼加載到著色器對象上

NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
const GLchar* source = (GLchar*)content.UTF8String;
glShaderSource(*shader, 1, &source, NULL);

1.3 編譯著色器,把著色器源代碼編譯成目標(biāo)代碼。此時得到一個可附著到程序的著色器對象

glCompileShader(*shader);
2. 加載著色器

2.1 創(chuàng)建program

GLint program = glCreateProgram();

2.2 將編譯好的著色器對象附著到程序中

glAttachShader(program, verShader);
glAttachShader(program, fragShader);

2.3 釋放不需要的著色器對象

glDeleteShader(verShader);
glDeleteShader(fragShader);
  1. 鏈接program
    在鏈接之后可調(diào)用glGetProgramiv函數(shù)判斷當(dāng)前是否鏈接成功
glLinkProgram(self.myPrograme);
  1. 使用program
glUseProgram(self.myPrograme);

設(shè)置并處理頂點(diǎn)數(shù)據(jù)

  1. 設(shè)置頂點(diǎn)數(shù)據(jù)
GLfloat attrArr[] ={
    0.5f, -0.5f, -1.0f,     1.0f, 0.0f,
    -0.5f, 0.5f, -1.0f,     0.0f, 1.0f,
    -0.5f, -0.5f, -1.0f,    0.0f, 0.0f,
        
    0.5f, 0.5f, -1.0f,      1.0f, 1.0f,
    -0.5f, 0.5f, -1.0f,     0.0f, 1.0f,
    0.5f, -0.5f, -1.0f,     1.0f, 0.0f,
};
  1. 申請一個頂點(diǎn)緩沖區(qū)ID,并將它綁定到GL_ARRAY_BUFFER標(biāo)識符上
GLuint attrBuffer;
glGenBuffers(1, &attrBuffer);
glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
  1. 把頂點(diǎn)數(shù)據(jù)從CPU拷貝到GPU
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
  1. 打開頂點(diǎn)/片元著色器屬性通道
  • 通過glGetAttribLocation函數(shù)獲取頂點(diǎn)屬性入口,它需要兩個參數(shù),參數(shù)1:program;參數(shù)2:自定義著色器文件中變量名稱的字符串,重點(diǎn):這里的字符串必須同自定義著色器文件中變量名稱保持一致。
  • 通過glEnableVertexAttribArray函數(shù)打開著色器的屬性通道
  • 通過glVertexAttribPointer函數(shù)設(shè)置讀取方式
//設(shè)置頂點(diǎn)坐標(biāo)
GLuint position = glGetAttribLocation(self.myPrograme, "position");
glEnableVertexAttribArray(position);
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), NULL);

//設(shè)置紋理坐標(biāo)
GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
glEnableVertexAttribArray(textCoor);
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), (float *)NULL + 3);

加載紋理

加載紋理的過程是將png/jpg圖片解壓縮成位圖,并通過自定義著色器讀取每個像素點(diǎn)的紋素。

  1. 解壓縮png/jpg圖片,將UIImage轉(zhuǎn)換為CGImageRef。
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
  1. 根據(jù)CGImageRef屬性獲取圖片的寬和高,并開辟一段空間用于存放解壓縮后的位圖信息。位圖數(shù)據(jù)的大小為寬4。為什么是寬4?因?yàn)閳D片共有寬高個像素點(diǎn),每個像素點(diǎn)有4個字節(jié),即RGBA,因此共有寬高*4大小的空間。
//讀取圖片的大小,寬和高
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);

//獲取圖片字節(jié)數(shù) 寬*高*4(RGBA)
GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
  1. 創(chuàng)建CGContextRef上下文
/*
 參數(shù)1:data,指向要渲染的繪制圖像的內(nèi)存地址
 參數(shù)2:width,bitmap的寬度,單位為像素
 參數(shù)3:height,bitmap的高度,單位為像素
 參數(shù)4:bitPerComponent,內(nèi)存中像素的每個組件的位數(shù),比如32位RGBA,就設(shè)置為8
 參數(shù)5:bytesPerRow,bitmap的沒一行的內(nèi)存所占的比特數(shù)
 參數(shù)6:colorSpace,bitmap上使用的顏色空間  kCGImageAlphaPremultipliedLast:RGBA
 */
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
  1. 在CGContextRef上將圖片繪制出來,調(diào)用CGContextDrawImage函數(shù),使用默認(rèn)方式繪制
/*
 CGContextDrawImage 使用的是Core Graphics框架,坐標(biāo)系與UIKit 不一樣。UIKit框架的原點(diǎn)在屏幕的左上角,Core Graphics框架的原點(diǎn)在屏幕的左下角。
 CGContextDrawImage 
 參數(shù)1:繪圖上下文
 參數(shù)2:rect坐標(biāo)
 參數(shù)3:繪制的圖片
*/
CGContextDrawImage(spriteContext, rect, spriteImage);
  1. 繪制完成之后,需要將上下文釋放掉
CGContextRelease(spriteContext);
  1. 經(jīng)過重繪之后,就將jpg/png圖片轉(zhuǎn)換成了位圖得到了紋理數(shù)據(jù)。接下來就是載入紋理數(shù)據(jù)。
    6.1 綁定紋理到默認(rèn)的紋理ID
    6.2 設(shè)置紋理屬性
    6.3 載入2D紋理數(shù)據(jù)
//綁定紋理到默認(rèn)的紋理ID
glBindTexture(GL_TEXTURE_2D, 0);

//設(shè)置紋理屬性
/*
 參數(shù)1:紋理維度
 參數(shù)2:線性過濾、為s,t坐標(biāo)設(shè)置模式
 參數(shù)3:wrapMode,環(huán)繞模式
*/
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

/*
 參數(shù)1:紋理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
 參數(shù)2:加載的層次,一般設(shè)置為0
 參數(shù)3:紋理的顏色值GL_RGBA
 參數(shù)4:寬
 參數(shù)5:高
 參數(shù)6:border,邊界寬度
 參數(shù)7:format
 參數(shù)8:type
 參數(shù)9:紋理數(shù)據(jù)
*/
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);

//釋放spriteData
free(spriteData); 
  1. 設(shè)置紋理采樣器
    主要是用來獲取紋理中對應(yīng)像素點(diǎn)的顏色值,即紋素。
  • 通過glGetUniformLocation函數(shù)獲取片元著色器中uniform的入口。該函數(shù)需要傳入兩個參數(shù),參數(shù)1:program;參數(shù)2:在片元著色器中用uniform修飾的變量名字的字符串。注意,該字符串必須同片元著色器中對應(yīng)的變量名保持一致
  • 使用glUniform1i函數(shù)獲取紋素,它也有兩個參數(shù),參數(shù)1:片元著色器中uniform的入口;參數(shù)2:紋理ID,默認(rèn)為0。
glUniform1i(glGetUniformLocation(self.myPrograme, "colorMap"), 0);

繪制

開始繪制,存儲到RenderBuffer,從RenderBuffer將圖片顯示到屏幕上。

  • 調(diào)用glDrawArrays函數(shù),指定圖元連接方式進(jìn)行繪制
  • context調(diào)用presentRenderbuffer函數(shù)將繪制好的圖片渲染到屏幕上顯示
glDrawArrays(GL_TRIANGLES, 0, 6);
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];

至此,使用GLSL加載紋理已經(jīng)完成,完整代碼見Demo地址

從效果圖上看到,圖片呈倒立顯示,這是因?yàn)?code>OpenGL要求原點(diǎn)(0,0)位于圖片的左下角,Y坐標(biāo)從下往上增加,而圖片紋理的原點(diǎn)(0,0)是位于圖片的左上角,Y坐標(biāo)從上往下增加。所以最后的照片呈上下倒置的效果。
以下是幾種解決方案:

  • 方案1:將頂點(diǎn)繞Y軸進(jìn)行翻轉(zhuǎn)。這樣可以實(shí)現(xiàn)正常顯示。
    問題:如何實(shí)現(xiàn)繞Y軸翻轉(zhuǎn)
    解決:將頂點(diǎn)坐標(biāo)與一個旋轉(zhuǎn)矩陣相乘,得到的結(jié)果就是翻轉(zhuǎn)之后的頂點(diǎn)坐標(biāo)。
    重點(diǎn):在3D課程中用的是橫向量,在OpenGL ES用的是列向量。頂點(diǎn)坐標(biāo)是一個1行4列的矩陣,因此,旋轉(zhuǎn)矩陣必須是4行4列,這樣相乘之后才能得到新的1行4列的頂點(diǎn)坐標(biāo)。另外,要實(shí)現(xiàn)翻轉(zhuǎn),只需要將該方向的坐標(biāo)數(shù)據(jù)進(jìn)行反向,如當(dāng)前需要沿X軸反向,只需要將X軸的數(shù)據(jù)全部*-1,即可將X軸的數(shù)據(jù)翻轉(zhuǎn)。
    代碼詳見方案1代碼
  • 方案2:可以解壓縮圖片的時候?qū)D片進(jìn)行翻轉(zhuǎn)。
    解決:在context繪制的圖片,對圖片進(jìn)行翻轉(zhuǎn)。
    重點(diǎn):由于翻轉(zhuǎn)之后,頂點(diǎn)數(shù)據(jù)的坐標(biāo)會發(fā)生變化,超過繪制的區(qū)域,因此在翻轉(zhuǎn)之后需要將頂點(diǎn)移至繪制區(qū)域內(nèi)。
    主要使用的函數(shù)有
//先平移至合適的位置,也可以在翻轉(zhuǎn)之后再移至繪制區(qū)域內(nèi)
CGContextTranslateCTM(context, 0, height);
//將Y軸翻轉(zhuǎn)
CGContextScaleCTM(context, 1, -1);

代碼詳見方案2代碼

  • 方案3:修改片元著色器紋理坐標(biāo),將片元著色器中的紋理坐標(biāo)在Y軸方向翻轉(zhuǎn)。
    重點(diǎn):如何獲取紋理坐標(biāo)的Y軸方向數(shù)據(jù),通過'varyTextCoord.y'即可得到Y(jié)軸數(shù)據(jù)。將1.0-varyTextCoord.y即可實(shí)現(xiàn)翻轉(zhuǎn)。
vec2 newCoord = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
gl_FragColor = texture2D(colorMap, newCoord);

代碼詳見方案3代碼

  • 方案4:修改頂點(diǎn)著色器紋理坐標(biāo),將頂點(diǎn)著色器的紋理坐標(biāo)在Y軸方向翻轉(zhuǎn)。
    該方案原理同方案3一樣,只是在不同的著色器完成紋理坐標(biāo)的翻轉(zhuǎn)。
    代碼詳見方案4代碼

  • 方案5:修改源頂點(diǎn)數(shù)據(jù)中頂點(diǎn)坐標(biāo)和紋理坐標(biāo)的映射關(guān)系。
    原理同方案3、4一致,只是直接在頂點(diǎn)數(shù)組中修改源數(shù)據(jù)。
    原頂點(diǎn)數(shù)據(jù)數(shù)組

GLfloat attrArr[] ={
    0.5f, -0.5f, -1.0f,     1.0f, 0.0f,
    -0.5f, 0.5f, -1.0f,     0.0f, 1.0f,
    -0.5f, -0.5f, -1.0f,    0.0f, 0.0f,

    0.5f, 0.5f, -1.0f,      1.0f, 1.0f,
    -0.5f, 0.5f, -1.0f,     0.0f, 1.0f,
    0.5f, -0.5f, -1.0f,     1.0f, 0.0f,
};

修改后的頂點(diǎn)數(shù)組

GLfloat attrArr[] ={
    0.5f, -0.5f, -1.0f,     1.0f, 1.0f,
    -0.5f, 0.5f, -1.0f,     0.0f, 0.0f,
    -0.5f, -0.5f, -1.0f,    0.0f, 1.0f,
        
    0.5f, 0.5f, -1.0f,      1.0f, 0.0f,
    -0.5f, 0.5f, -1.0f,     0.0f, 0.0f,
    0.5f, -0.5f, -1.0f,     1.0f, 1.0f,
};

代碼詳見方案5代碼

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

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