紋理,簡單一句話,就是讓圖形學呈現出的視覺效果更佳。
紋理就是將圖片貼合到圖形的過程。
紋理座標是圖形進行片段插值的依據,紋理座標的範圍爲(0,0)、(1,1)
如圖中的紋理座標的表示爲:
float texCoords[] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f // 上中
};
將一個紋理貼合到一個圖形上時,不可能每次剛剛好,在沒有剛剛好的時候,需要有紋理去補充其他的部分,這就是紋理環繞,紋理環繞有四種方式:
環繞方式 | 描述 |
---|---|
GL_REPEAT | 對紋理的默認行爲。重複紋理圖像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一樣,但每次重複圖片是鏡像放置的。 |
GL_CLAMP_TO_EDGE | 紋理座標會被約束在0到1之間,超出的部分會重複紋理座標的邊緣,產生一種邊緣被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的座標爲用戶指定的邊緣顏色。 |
效果爲:
有了方法還要講其運用到場景中,
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
如上代碼,分別規定了s,t軸的紋理環繞方式(對應x,y),如果是3D,再加上r(對應z)。
注:如果是GL_CLAMP_TO_BORDER還有規定邊緣的顏色,方法爲:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
紋理的插值方法有兩種:GL_NEAREST和GL_LINEAR,效果爲:
使用方法爲:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
多級漸遠紋理
在實際應用中,有時候需要紋理中出現遠近景的效果,這就需要多級漸遠紋理(對應圖像中的透視)。
在不同的多級漸遠紋理級別之間使用NEAREST和LINEAR過濾,爲了指定不同多級漸遠紋理級別之間的過濾方式,你可以使用下面四個選項中的一個代替原有的過濾方式:
過濾方式 | 描述 |
---|---|
GL_NEAREST_MIPMAP_NEAREST | 使用最鄰近的多級漸遠紋理來匹配像素大小,並使用鄰近插值進行紋理採樣 |
GL_LINEAR_MIPMAP_NEAREST | 使用最鄰近的多級漸遠紋理級別,並使用線性插值進行採樣 |
GL_NEAREST_MIPMAP_LINEAR | 在兩個最匹配像素大小的多級漸遠紋理之間進行線性插值,使用鄰近插值進行採樣 |
GL_LINEAR_MIPMAP_LINEAR | 在兩個鄰近的多級漸遠紋理之間使用線性插值,並使用線性插值進行採樣 |
使用方法爲:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
一個常見的錯誤是,將放大過濾的選項設置爲多級漸遠紋理過濾選項之一。這樣沒有任何效果,因爲多級漸遠紋理主要是使用在紋理被縮小的情況下的:紋理放大不會使用多級漸遠紋理,爲放大過濾設置多級漸遠紋理的選項會產生一個GL_INVALID_ENUM錯誤代碼。
加載和創建紋理
紋理就是圖像,加載紋理需要的圖像操作這裏應用stb_image.h
庫
stb_image.h
是Sean Barrett的一個非常流行的單頭文件圖像加載庫,它能夠加載大部分流行的文件格式,並且能夠很簡單得整合到你的工程之中。stb_image.h
可以在這裏下載。下載這一個頭文件,將它以stb_image.h
的名字加入你的工程,並另創建一個新的C++文件,輸入以下代碼:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
通過定義STB_IMAGE_IMPLEMENTATION,預處理器會修改頭文件,讓其只包含相關的函數定義源碼,等於是將這個頭文件變爲一個 .cpp
文件了。現在只需要在你的程序中包含stb_image.h
並編譯就可以了。
下面的教程中,我們會使用一張木箱的圖片。要使用stb_image.h
加載圖片,我們需要使用它的stbi_load函數:
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
這個函數首先接受一個圖像文件的位置作爲輸入。接下來它需要三個int
作爲它的第二、第三和第四個參數,stb_image.h
將會用圖像的寬度、高度和顏色通道的個數填充這三個變量。我們之後生成紋理的時候會用到的圖像的寬度和高度的。
生成紋理
首先創建一個ID:
unsigned int texture;
glGenTextures(1, &texture);
glGenTextures函數首先需要輸入生成紋理的數量,然後把它們儲存在第二個參數的unsigned int
數組中(我們的例子中只是單獨的一個unsigned int
),就像其他對象一樣,我們需要綁定它,讓之後任何的紋理指令都可以配置當前綁定的紋理:
glBindTexture(GL_TEXTURE_2D, texture);
現在紋理已經綁定了,我們可以使用前面載入的圖片數據生成一個紋理了。紋理可以通過glTexImage2D來生成:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
函數很長,參數也不少,所以我們一個一個地講解:
- 第一個參數指定了紋理目標(Target)。設置爲GL_TEXTURE_2D意味着會生成與當前綁定的紋理對象在同一個目標上的紋理(任何綁定到GL_TEXTURE_1D和GL_TEXTURE_3D的紋理不會受到影響)。
- 第二個參數爲紋理指定多級漸遠紋理的級別,如果你希望單獨手動設置每個多級漸遠紋理的級別的話。這裏我們填0,也就是基本級別。
- 第三個參數告訴OpenGL我們希望把紋理儲存爲何種格式。我們的圖像只有
RGB
值,因此我們也把紋理儲存爲RGB
值。 - 第四個和第五個參數設置最終的紋理的寬度和高度。我們之前加載圖像的時候儲存了它們,所以我們使用對應的變量。
- 下個參數應該總是被設爲
0
(歷史遺留的問題)。 - 第七第八個參數定義了源圖的格式和數據類型。我們使用RGB值加載這個圖像,並把它們儲存爲
char
(byte)數組,我們將會傳入對應值。 - 最後一個參數是真正的圖像數據。
當調用glTexImage2D時,當前綁定的紋理對象就會被附加上紋理圖像。然而,目前只有基本級別(Base-level)的紋理圖像被加載了,如果要使用多級漸遠紋理,我們必須手動設置所有不同的圖像(不斷遞增第二個參數)。或者,直接在生成紋理之後調用glGenerateMipmap。這會爲當前綁定的紋理自動生成所有需要的多級漸遠紋理。
生成了紋理和相應的多級漸遠紋理後,釋放圖像的內存是一個很好的習慣。
stbi_image_free(data);
生成紋理全過程:
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 爲當前綁定的紋理對象設置環繞、過濾方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加載並生成紋理
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
應用紋理
首先了解向頂點着色器裏傳輸數據的變化:
float vertices[] = {
// ---- 位置 ---- ---- 顏色 ---- - 紋理座標 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
由於我們添加了一個額外的頂點屬性,我們必須告訴OpenGL我們新的頂點格式:
上圖說明在用着色器讀取數據時訪問位置的變化過程,相應的代碼爲:
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
上面一段代碼注意glVertexAttribPointer函數參數的變化,接下來就是頂點着色器的變化:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;
out vec3 ourColor;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
TexCoord = aTexCoord;
}
接下來片段着色器接收數據,片段着色器也應該能訪問紋理對象,但是我們怎樣能把紋理對象傳給片段着色器呢?GLSL有一個供紋理對象使用的內建數據類型,叫做採樣器(Sampler),它以紋理類型作爲後綴,比如sampler1D
、sampler3D
,或在我們的例子中的sampler2D
。我們可以簡單聲明一個uniform sampler2D
把一個紋理添加到片段着色器中,稍後我們會把紋理賦值給這個uniform。
片段着色器變化爲:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
我們使用GLSL內建的texture函數來採樣紋理的顏色,它第一個參數是紋理採樣器,第二個參數是對應的紋理座標。texture函數會使用之前設置的紋理參數對相應的顏色值進行採樣。這個片段着色器的輸出就是紋理的(插值)紋理座標上的(過濾後的)顏色。
現在只剩下在調用glDrawElements之前綁定紋理了,它會自動把紋理賦值給片段着色器的採樣器:
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
如果你跟着這個教程正確地做完了,你會看到下面的圖像:
我們還可以把得到的紋理顏色與頂點顏色混合,來獲得更有趣的效果。我們只需把紋理顏色與頂點顏色在片段着色器中相乘來混合二者的顏色:
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
紋理單元
你可能會奇怪爲什麼sampler2D
變量是個uniform,我們卻不用glUniform給它賦值。使用glUniform1i,我們可以給紋理採樣器分配一個位置值,這樣的話我們能夠在一個片段着色器中設置多個紋理。一個紋理的位置值通常稱爲一個紋理單元(Texture Unit)。一個紋理的默認紋理單元是0,它是默認的激活紋理單元,所以教程前面部分我們沒有分配一個位置值。
紋理單元的主要目的是讓我們在着色器中可以使用多於一個的紋理。通過把紋理單元賦值給採樣器,我們可以一次綁定多個紋理,只要我們首先激活對應的紋理單元。就像glBindTexture一樣,我們可以使用glActiveTexture激活紋理單元,傳入我們需要使用的紋理單元:
glActiveTexture(GL_TEXTURE0); // 在綁定紋理之前先激活紋理單元
glBindTexture(GL_TEXTURE_2D, texture);
激活紋理單元之後,接下來的glBindTexture函數調用會綁定這個紋理到當前激活的紋理單元,紋理單元GL_TEXTURE0默認總是被激活,所以我們在前面的例子裏當我們使用glBindTexture
的時候,無需激活任何紋理單元。
OpenGL至少保證有16個紋理單元供你使用,也就是說你可以激活從GL_TEXTURE0到GL_TEXTRUE15。它們都是按順序定義的,所以我們也可以通過GL_TEXTURE0 + 8的方式獲得GL_TEXTURE8,這在當我們需要循環一些紋理單元的時候會很有用。
我們仍然需要編輯片段着色器來接收另一個採樣器。這應該相對來說非常直接了:
#version 330 core
...
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}
最終輸出顏色現在是兩個紋理的結合。GLSL內建的mix函數需要接受兩個值作爲參數,並對它們根據第三個參數進行線性插值。如果第三個值是0.0
,它會返回第一個輸入;如果是1.0
,會返回第二個輸入值。0.2
會返回80%
的第一個輸入顏色和20%
的第二個輸入顏色,即返回兩個紋理的混合色。
我們現在需要載入並創建另一個紋理;你應該對這些步驟很熟悉了。記得創建另一個紋理對象,載入圖片,使用glTexImage2D生成最終紋理。對於第二個紋理我們使用一張你學習OpenGL時的面部表情圖片。
爲了使用第二個紋理(以及第一個),我們必須改變一點渲染流程,先綁定兩個紋理到對應的紋理單元,然後定義哪個uniform採樣器對應哪個紋理單元:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
我們還要通過使用glUniform1i設置每個採樣器的方式告訴OpenGL每個着色器採樣器屬於哪個紋理單元。我們只需要設置一次即可,所以這個會放在渲染循環的前面:
ourShader.use(); // 別忘記在激活着色器前先設置uniform!
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 手動設置
ourShader.setInt("texture2", 1); // 或者使用着色器類設置
while(...)
{
[...]
}
通過使用glUniform1i設置採樣器,我們保證了每個uniform採樣器對應着正確的紋理單元。你應該能得到下面的結果:
你可能注意到紋理上下顛倒了!這是因爲OpenGL要求y軸0.0
座標是在圖片的底部的,但是圖片的y軸0.0
座標通常在頂部。很幸運,stb_image.h
能夠在圖像加載時幫助我們翻轉y軸,只需要在加載任何圖像前加入以下語句即可:
stbi_set_flip_vertically_on_load(true);
在讓stb_image.h
在加載圖片時翻轉y軸之後你就應該能夠獲得下面的結果了:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <stb_image.h>
#include <learnopengl/shader_s.h>
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// build and compile our shader zprogram
// ------------------------------------
Shader ourShader("4.2.texture.vs", "4.2.texture.fs");
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
float vertices[] = {
// positions // colors // texture coords
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left
};
unsigned int indices[] = {
0, 1, 3, // first triangle
1, 2, 3 // second triangle
};
unsigned int VBO, VAO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// color attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// texture coord attribute
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
// load and create a texture
// -------------------------
unsigned int texture1, texture2;
// texture 1
// ---------
glGenTextures(1, &texture1);
glBindTexture(GL_TEXTURE_2D, texture1);
// set the texture wrapping parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// set texture filtering parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// load image, create texture and generate mipmaps
int width, height, nrChannels;
stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis.
// The FileSystem::getPath(...) is part of the GitHub repository so we can find files on any IDE/platform; replace it with your own image path.
unsigned char *data = stbi_load(FileSystem::getPath("resources/textures/container.jpg").c_str(), &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
// texture 2
// ---------
glGenTextures(1, &texture2);
glBindTexture(GL_TEXTURE_2D, texture2);
// set the texture wrapping parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// set texture filtering parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// load image, create texture and generate mipmaps
data = stbi_load(FileSystem::getPath("resources/textures/awesomeface.png").c_str(), &width, &height, &nrChannels, 0);
if (data)
{
// note that the awesomeface.png has transparency and thus an alpha channel, so make sure to tell OpenGL the data type is of GL_RGBA
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
// tell opengl for each sampler to which texture unit it belongs to (only has to be done once)
// -------------------------------------------------------------------------------------------
ourShader.use(); // don't forget to activate/use the shader before setting uniforms!
// either set it manually like so:
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0);
// or set it via the texture class
ourShader.setInt("texture2", 1);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// bind textures on corresponding texture units
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
// render container
ourShader.use();
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
代碼需要部分修改:1、文件路徑 2、添加新庫 3、更改變量名稱