在OpenGL中所有的事物都是在3D空間中,但是我們所看到的螢屏成像卻是2D的像素陣列,這導致OpenGL的大部分作業就是把得到的3D坐標轉換為適應螢屏的2D影像,轉換的整個處理程序是由OpenGL的圖形渲染管線管理的,
OpenGL圖形渲染管線:
1> 指的是一堆原始圖形資料途經一個輸送管道,期間經過各種變化處理最終出現在螢屏的程序
2> 圖形渲染管線可以被劃分為兩個主要部分:第一部分把你的3D坐標轉換為2D坐標,第二部分是把2D坐標轉變為實際的有顏色的像素,
3> 圖形渲染管線可以劃分為幾個階段,每個階段都是把上個階段的輸出當做輸入來處理的,由于每隔階段的處理都是高度專門化的,所以很容易并行執行,所以在GPU中可用成千上萬的小處理核心可以為每個管線階段的著色器并行處理,
4> OpenGL的渲染管線大致可分為以下幾個階段:
定點資料輸入——> >圖元(片元)裝配——>幾何著色器——>光柵化——>片段著色器——>Alpha測驗與顏色混合
5> 有些著色器語序可以通過開發者自己實作,從而達到更細致的控制影像的渲染結果和速度,其中頂點著色器,幾何著色器,片段著色器是我們可以重新實作的部分,
簡略概括:
頂點資料輸入: 首先將頂點資料以陣列的形式將n個3D坐標作為整個圖形渲染管線的輸入,頂點資料是一系列頂點的集合,一個頂點是一個3D坐標的資料的集合,在OpenGL中頂點資料是用頂點屬性表示的,他可以包含任何可能呢會用到的資料,
頂點著色器: 接受一個單獨的頂點進行輸入,主要作業是將3D坐標變為另一種3D坐標,他接受開發者自己實作,
圖元(片元)裝配:
1> OpenGL需要去指定我們想要繪制的影像的坐標和顏色構成,是一系列的點,或是一系列的三角形,還是一系列的線,這些最基礎的構成被稱為圖元(片元), // TODO eg:
2> 圖元裝配將頂點著色器的所有輸出作為輸入,(注意,如果圖元型別為GL_PIONTS,則只接受一個頂點),然后將所有的點裝配成指定好的圖元形狀,
幾何著色器: 接受圖元形式的一系列頂點作為輸入,通過產生新頂點構造出新的圖元生成其他圖元,
光柵化: 將圖元映射為最終螢屏上相應的像素,生成供片段著色器使用的片段,并進行裁切,將超出你的視圖以外的所有像素丟棄,用來提升執行效率,
片段著色器:
1> 這里的片段是指OpenGL渲染一個像素所需的所有資料,
2> 片段著色器將計算一個像素的最終顏色,這也是所有OpenGL高級效果產生的地方,通常,片段著色器包含3D場景的資料(如光照、陰影、光的顏色等),這些資料可以被用來計算該片段最終像素的顏色,
Alpha測驗與顏色混合: 該階段會檢測對應片段對應的深度值,用來判斷這個片段是否存在于其他的片段之前或者之后,從而判斷該片段是否要被丟棄,同時也會檢查Alpha(透明度)值并對片段進行顏色混合
詳細程序及代碼實作:
定義頂點資料
float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f };
(OpenGL僅當3D坐標在3個軸(x、y和z)上都為-1.0到1.0的范圍內時才處理它,)
將定義的定點資料作為輸入發送給頂點著色器,頂點著色器在GPU上創建記憶體用于儲存頂點資料,還要配置OpenGL如何解釋這些記憶體,并且指定其如何發送給顯卡,頂點著色器接著會處理我們在記憶體中指定好數量的頂點,
這個時候如果一個頂點一個頂點的發送,其缺點是顯而易見的,將頂點資料從CPU發送至GPU的速度相對較慢,使用頂點緩沖物件來管理這些頂點的記憶體則可以一次性發送盡可能多的資料,當頂點資料發送至顯存中后,頂點著色器幾乎能立即訪問頂點,
創建一個頂點緩沖物件的程序如下:
首先生成一個緩沖區物件
// generate buffer object names void glGenBuffers(GLsizei n, GLuint * buffers);
第一個引數是要生成的緩沖物件的數量,第二個是要輸入用來存盤緩沖物件名稱的長度為n的id陣列,如果 n == 1, 則只需要一個id,
該函式僅僅生成一個緩沖物件的名稱,這個緩沖物件并不具備任何意義,即它僅僅是個緩沖物件,還不是頂點緩沖物件,他就像C中的一個指標變數,可以給他分配記憶體物件并且用它的名稱來參考這個記憶體物件,
unsigned int VBO; glGenBuffers(1, &VBO);
頂點緩沖物件的緩沖區型別(緩沖區ID)為 GL_ARRAY_BUFFER,
要指定緩沖物件的型別則需要用到下面的函式
// bind a named buffer object void glBindBuffer(GLenum target, GLuint buffer);
一個引數是緩沖物件的型別,第二個引數是已經創建的緩沖物件的名稱,使用該函式將創建好的VBO物件系結到OpenGL的背景關系環境中,如果系結的buffer值為零,那么OpenGL將不再對當前target使用任何物件,
需要注意的是函式的第二個引數雖然是Gluint型的,
OpenGL允許我們同一同時系結多個不同型別的緩沖物件,但是不能系結兩個相同的緩沖物件,這就是背景關系之所以用背景關系來理解它的意義,
緩沖物件只是OpenGL物件的一種,使用其他物件的思路跟緩沖物件是差不多的,都是創建物件——>系結背景關系——>設定資料
// 創建并初始化緩沖區物件的資料存盤 void glBufferData (GLenum target, GLsizeiptr size, const GLvoid * data, GLenum usage);
該函式將洗掉之前存在于指定型別緩沖區的資料,為當前已系結的指定型別的緩沖區物件創建一個新的資料存盤,第一個引數是指定目標緩沖區型別,有GL_ARRAY_BUFFER(VBO)、GL_ELEMENT_ARRAY_BUFFER(EBO)兩種,第二個引數用來指定緩沖區物件的新資料需要的記憶體大小,第三個引數是我們希望發送的實際資料的指標,第四個引數用來指定給GPU資料的預期使用方式,這使得GL的實作能夠做出更明智的選擇,可能會影響緩沖區物件的性能,它不會限制資料存盤的實際使用,usage可分解為兩個部分理解,第一,訪問的頻率,第二,訪問的性質:
訪問頻率:
STREAM // 資料存盤內容將被修改一次并最多使用幾次,
STATIC // 資料存盤內容將被修改一次并多次使用,
DYNAMIC // 資料存盤內容將被重復修改并多次使用,
訪問的性質
DRAW // 資料存盤內容由應用程式修改,并用作GL繪圖和影像規范命令的源,
和在一起則是以下三個
GL_STATIC_DRAW // 資料不會或幾乎不會改變,
GL_DYNAMIC_DRAW // 資料會被改變很多,
GL_STREAM_DRAW // 資料每次繪制時都會改變,
到此頂點資料已經被存在GPU記憶體中了,
頂點著色器:
頂點著色器就要使用著色器語言GLES,用字串存盤,發送給GL,然后編譯這個著色器,eg:
#version 330 core layout (lcoation = 0) in vec3 aPos; void main() { //注意,如果圖元型別為GL_PIONTS,則只接受一個頂點 sition = vec4(aPos.x, aPos.y, aPos.z, 1.0); }
每個著色器都起始與一個版本宣告,同時說明我們使用的渲染模式是核心模式,
in 關鍵字用來頂點著色器中宣告的所有頂點屬性,(上面的只有一個頂點位置屬性),layout (lcoation = 0) 代表輸入變數的位置值,
在主函式中設定著色器的雙輸出值,這里將gl_Position頂點位置屬性進行更改,
然后是編譯著色器:
// 創建一個著色器物件 unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER); // 將著色器原始碼附加到著色器物件上 glShaderSource(vertexShader, 1, &vertexShaderSource, NULL) // 第一個引數是創建好的著色器物件,第二個引數指定的傳遞的原始碼字串數量,第三個引數是頂點著色器所需要的原始碼, // 編譯著色器 glCompileShader(vertexShader); // 檢測著色器是否編譯編譯成功,并獲取錯誤資訊, int success; char infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; }
片段著色器:
片段著色器的撰寫,編譯和頂點著色器的程序大同小異,只是型別變成了GL_FRAGMENT_SHADER,
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); } // 片段著色器只需要一個輸出變數,這個變數是一個vec4型別,他代表的是最終的 輸出顏色,使用out關鍵字宣告輸出變數, unsigned int fragmentShader; fragmentShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader);
兩個著色器寫完之后,將兩個著色器物件鏈接到一個用來渲染的著色器程式中,
著色器程式物件是多個著色器物件鏈接在一起的成果,在渲染物件的時候激活需要的著色器程式,在發送渲染呼叫的時候GL會使用當前激活的著色器程式,
當鏈接著色器至一個程式的時候,它會把每個著色器的輸出鏈接到下個著色器的輸入,當輸出和輸入不匹配的時候,你會得到一個連接錯誤,
// 創建一個著色器程式物件
// 創建一個著色器程式物件 unsigned int shaderProgram; shaderProgram = glCreateProgram(); // 將著色器物件加入著色器程式物件并鏈接 glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); // 檢查鏈接成功: int successs; char infoLog[512]; glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if(!success) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); } // 在鏈接成功后洗掉之前創建的著色器物件,釋放記憶體 glDeleteShader(vertexShader); glDeleteShader(fragmentShader); // 在需要的時候,使用著色器 glUseProgram(shaderProgram);
在glUseProgram函式呼叫之后,每個著色器呼叫和渲染呼叫都會使用這個著色器程式物件,
頂點資料已經發送給了GPU,也指示了GPU在頂點著色器和片段著色器中如何處理它,但是GPU還不知道如何處理已經存在GPU記憶體中的的頂點數據,即如何將已經在GPU中的頂點是資料以頂點著色器可以接受的方式發送給頂點著色器,頂點著色器可以接受任何以頂點屬性形式的輸入,所以頂點著色器有著很強的靈活性,但是這樣我們必須手動指定輸入資料的哪一個部分對應頂點著色器需要的的哪一個頂點屬性,所以在開始渲染之前,必選要告訴頂點著色器如何解釋頂點資料,
上文在傳入的時候,vertices是一個float型別的一維陣列,即一段連續的浮點型記憶體,在GPU中頂點緩沖資料則會被決議為以下特征的連續記憶體:
1> 每個資料會被存盤32位(4位元組)的float型別值;
2> 每個位置包含三個這樣的值;
3> 存盤的記憶體和傳入的時候一樣是連續的;
4> 資料的第一個值在緩沖開始的位置,即陣列的第0個元素,
通過既定的這個資訊,就可以使用glVertexAttribPointer函式來告訴GL如何去解釋頂點緩沖物件,eg:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0);
// 引數含義
1> 第一個引數指定配置的頂點屬性,在撰寫頂點著色器的時候,第二句中有個 layout(location = 0),該陳述句設定position頂點的屬性位置值(location)為0,如果希望把頂點資料傳遞到這一個頂點屬性中,那么這里引數傳入0;
2> 第二個引數用來指定頂點屬性的大小,因為用到的頂點位置資料為vec3,由三個值組成,大小為3;
3> 第三個引數指定資料的型別;
4> 第四個引數指定是否希望資料被標準化,如果為GL_TRUE,所有傳入的資料會被映射到0到1(有符號輸為-1到1)之間,
5> 第五個引數可以用“步長”來解釋,他用來告訴GL整個頂點屬性第二次出現的地方到整個陣列0位置之間有多少位元組,也可以傳入0讓GL自己檢測,但是只能在傳入連續記憶體(即陣列)的情況下使用,
6> 第六個引數用來表示位置資料在緩沖中起始位置的偏移量(Offset),但要注意,改引數的型別為void*,所以要講型別強制轉換為void*,
而 glEnableVertexAttribArray 則以頂點屬性位置值作為引數,啟用頂點屬性,因為頂點屬性默認是禁用的, 整套流程下來大概會是這樣:
// 創建并系結頂點緩沖物件 float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f }; glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 設定頂點屬性指標 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 渲染時,使用著色器程式 glUseProgram(shaderProgram); // 繪制物體 someOpenGLFunctionThatDrawsOurTriangle();
在繪制時,每繪制一個物體的時候都必須重復這一程序,這個時候系結正確的緩沖物件,為每次繪制配置正確的頂點屬性就變的比較麻煩,為了解決這個麻煩,可以使用頂點陣列物件,將所有繪制需要的狀態資訊儲存在一個物件中,
頂點陣列物件可以像頂點緩沖物件一樣被系結,任何隨后的頂點屬性呼叫都會被儲存著這個VAO物件中,這樣就等于說是在配置頂點屬性指標時,只需要將之前的呼叫執行一次,在繪制不同的物體時,只需要系結不同的VAO就可以了,(OPGL的核心渲染模式指定使用VAO物件,如果系結VAO物件失敗,OPGL會拒絕渲染)
一個頂點陣列物件將會存盤以下內容:
1> glEnableVertexAttribArray和glDisableVertexAttribArray的呼叫,
2> 通過glVertexAttribPointer設定的頂點屬性配置,(也就是資料放在哪里,和如何讀取資料)
3> 通過glVertexAttribPointer呼叫與頂點屬性關聯的頂點緩沖物件,
在記憶體中大概就是這個意思
VBO1 = {pos[0], pos[1], pos[2], ...}
VBO2 = {pos2[0], pos2[1], pos2[2], ...}
VAO1 = {*pos[0]}
VBO3 = {pos[0], color[0], pos[1], color[1], pos[2], color[2], ...}
VAO2 = {*pos[0], *color[0]}
創建一個VAO物件跟創建一個VBO物件相似:
unsigned int VAO; glGenVertexArrays(1, &VAO);
在使用glBindVertexArray系結VAO之后,就可以對VAO物件進行操作,在系結之后,可把系結和配置對應的VBO和屬性指標然后解綁,供之后使用,在繪制物體的時候只需要把VAO物件系結到希望使用的設定上,新的使用VAO繪制的流程應該如下:
// 系結VAO glBindVertexArray(VAO); // 將BO復制到GL緩沖區中供GL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 設定VBO物件 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 繪制 glUseProgram(shaderProgram); glBindVertexArray(VAO); someOpenGLFunctionThatDrawsOurTriangle(); // 實際繪制時(在回圈中)使用的代碼應如下 glUseProgram(shaderProgram); glBindVertexArray(VAO); //系結我們需要的VAO,會導致上面所有VAO保存的設定自動設定完成 glDrawArrays(); // 最后記得解綁不會再使用的VAO物件 glBindVertexArray(0); //解綁VAO
索引緩沖物件(EBO):
如果是繪制一個矩形,可以使用繪制兩個三角形的辦法繪制,這會生成下面兩個索引緩沖物件:
float vertices[] = { // 第一個三角形 0.5f, 0.5f, 0.0f, // 右上角 0.5f, -0.5f, 0.0f, // 右下角 -0.5f, 0.5f, 0.0f, // 左上角 // 第二個三角形 0.5f, -0.5f, 0.0f, // 右下角 -0.5f, -0.5f, 0.0f, // 左下角 -0.5f, 0.5f, 0.0f // 左上角 }
可以看到,有兩對一模一樣的頂點,但實際上,一個矩形只有四個頂點這就會產生50%的額外開銷,如果模型越來愈大,這個問題會更加糟糕,會產生數不清的浪費,好的解決方案是存盤不同的頂點,并設定繪制這些頂點的順序,這就是索引緩沖物件的(EBO)作業方式和頂點緩沖物件一樣EBO是一個緩沖,他專門儲存索引,OpenGL呼叫這些頂點的索引來決定繪制哪個點,
1> 首先,我們要定義不懂位置的頂點,和繪制矩形所需要啊的索引:
float vertices[] = { 0.5f, 0.5f, 0.0f, // 右上角 0.5f, -0.5f, 0.0f, // 右下角 -0.5f, 0.5f, 0.0f, // 左上角 -0.5f, -0.5f, 0.0f, // 左下角 } unsigned int indices[] = { // 注意索引從0開始! 0, 1, 3, // 第一個三角形 1, 2, 3 // 第二個三角形 };
2> 創建索引緩沖物件:
ussigned int EBO; glGenBuffers(1, &EBO);
3> 系結EBO然后用將索引復制到緩沖中
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
4> 用glDrawElements來替換glDrawArrays函式,來指明我們從索引緩沖渲染,
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
第一個引數指定了我們繪制的模式;
第二個引數是我們打算繪制頂點的個數,這里填6;
第三個引數是索引的型別;
最后一個引數里我們指定EBO中的偏移量,
如果整套流程下來(使用VAO、VBO、EBO)整個三角形的位置代碼會是這樣:
#include <iostream> #include <glad/glad.h> #include <GLFW/glfw3.h> #include "config.h" using namespace std; const char *strvertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" "}\0"; const char *strfragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "}\n\0"; int main() { glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow *window = glfwCreateWindow(WIND_WIGHT, WIND_HEIGHT, "window", NULL, NULL); if (window == NULL) { std::cout << "create window failed!" << endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "init glad failed!" << endl; return -1; } // 創建一個著色器,著色器型別為 GL_VERTEX_SHADER(頂點著色器) unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); // 把著色器原始碼附加到著色器物件 glShaderSource(vertexShader, 1, &strvertexShaderSource, NULL); // 編譯著色器 glCompileShader(vertexShader); int success; char infoLog[512]; // 判斷編譯結果并列印出錯誤 glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std:cout << "VERTEX SHADER COMPILATION_FAILED" << infoLog << std::endl; } // 創建一個著色器,著色器型別為 GL_FRAGMENT_SHADER (片段著色器) unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &strfragmentShaderSource, NULL); glCompileShader(fragmentShader); // 判斷編譯結果并列印出錯誤 glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); std::cout << "FRAGMENT SHADER COMPLATION_FAILED" << infoLog << std::endl; } // 鏈接著色器物件 int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); // 判斷鏈接結果并列印出錯誤 if (!success) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); std::cout << "LINK SHADER FAILED" << infoLog << std::endl; } // 鏈接成功后就可以將shader洗掉 glDeleteShader(vertexShader); glDeleteShader(fragmentShader); // 將頂點資料傳入頂點著色器 // 定義一個頂點陣列 float vertices[] = { 0.5f, 0.5f, 0.0f, // 右上角 0.5f, -0.5f, 0.0f, // 右下角 -0.5f, -0.5f, 0.0f, // 左下角 -0.5f, 0.5f, 0.0f // 左上角 }; // 定義一個索引陣列 unsigned int indices[] = { 0, 1, 3, 1, 2, 3 }; // 創建一個VAO物件,一個VBO物件,一個EBO物件 unsigned int VAO, VBO, EBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); // 系結VAO物件 glBindVertexArray(VAO); // 將VBO復制到GL緩沖區中供GL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); // 復制頂點陣列到緩沖中供OpenGL使用 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 告訴GL如何去解釋頂點緩沖物件(VBO) glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); // 將EBO復制到GL緩沖區中供GL使用 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 將頂點索引復制到緩沖中 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 告訴GL啟用頂點屬性 glEnableVertexAttribArray(0); // 解除背景關系系結VAO,這樣其他VAO呼叫不會意外地修改此VAO(通常情況下是不必要的) // glBindVertexArray(0); while (!glfwWindowShouldClose(window)) { glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 使用鏈接好的shader glUseProgram(shaderProgram); // 系結VAO物件(如果有操作解綁了VAO物件) // glBindVertexArray(VAO); // 指明從索引緩沖渲染 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); glfwSwapBuffers(window); glfwPollEvents(); } // GC glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); glDeleteProgram(shaderProgram); glfwTerminate(); return 0; }
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/562.html
標籤:其他
上一篇:復雜的光照與更復雜的陰影實作程序——ShaderCP9
下一篇:three.js中的矩陣計算
