1. 程式人生 > >OpenGL學習 02 第一個三角形

OpenGL學習 02 第一個三角形

三角形示例程式碼,補全了LoadShader等部分(使用vs2015 控制檯程式)。

#include "stdafx.h"
#include <iostream>
#include <vector>
using namespace std;

#include <GL/glew.h>
#include <GL/glut.h>
#include <string>       
#include <vector>
#include <iostream>
#include <fstream>

#pragma lib("glu32.lib")
#pragma lib("glew32.lib")

enum VAO_IDs{Triangles, NumVAOs};
enum Buffer_IDs{ArrayBuffer, Numbuffers};
enum Attrib_IDs{vPosition = 0};

GLuint VAOs[NumVAOs];
GLuint Buffers[Numbuffers];

const GLuint NumVertices = 6;

struct ShaderInfo
{
	GLenum shaderType;
	const char* filePath;
	ShaderInfo(GLenum type, const char* path)
		:shaderType(type), filePath(path) {}
};

/*
* 讀取著色器程式原始碼
*/
bool loadShaderSource(const char* filePath, std::string& source)
{
	source.clear();
	std::ifstream in_stream(filePath);
	if (!in_stream)
	{
		return false;
	}
	source.assign(std::istreambuf_iterator<char>(in_stream),
		std::istreambuf_iterator<char>()); // 檔案流迭代器構造字串
	return true;
}

/*
* 從檔案載入頂點和片元著色器
* 傳遞引數為 [(著色器檔案型別,著色器檔案路徑)+]
*/
GLuint LoadShaders(ShaderInfo shaderFileVec[], size_t shaderCount)
{
	GLuint programId;
	std::vector<GLuint> shaderObjectIdVec;
	std::string vertexSource, fragSource;
	std::vector<std::string> sourceVec;
	
	// 讀取檔案原始碼
	for (size_t i = 0; i < shaderCount; ++i)
	{
		std::string shaderSource;
		if (!loadShaderSource(shaderFileVec[i].filePath, shaderSource))
		{
			std::cout << "Error::Shader could not load file:" << shaderFileVec[i].filePath << std::endl;
			return 0;
		}
		sourceVec.push_back(shaderSource);
	}
	bool bSuccess = true;
	// 編譯shader object
	for (size_t i = 0; i < shaderCount; ++i)
	{
		GLuint shaderId = glCreateShader(shaderFileVec[i].shaderType);
		const char *c_str = sourceVec[i].c_str();
		glShaderSource(shaderId, 1, &c_str, NULL);
		glCompileShader(shaderId);
		GLint compileStatus = 0;
		glGetShaderiv(shaderId, GL_COMPILE_STATUS, &compileStatus); // 檢查編譯狀態
		if (compileStatus == GL_FALSE) // 獲取錯誤報告
		{
			GLint maxLength = 0;
			glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &maxLength);
			std::vector<GLchar> errLog(maxLength);
			glGetShaderInfoLog(shaderId, maxLength, &maxLength, &errLog[0]);
			std::cout << "Error::Shader file [" << shaderFileVec[i].filePath << " ] compiled failed,"
				<< &errLog[0] << std::endl;
			bSuccess = false;
		}
		shaderObjectIdVec.push_back(shaderId);
	}
	// 連結shader program
	if (bSuccess)
	{
		programId = glCreateProgram();
		for (size_t i = 0; i < shaderCount; ++i)
		{
			glAttachShader(programId, shaderObjectIdVec[i]);
		}
		glLinkProgram(programId);
		GLint linkStatus;
		glGetProgramiv(programId, GL_LINK_STATUS, &linkStatus);
		if (linkStatus == GL_FALSE)
		{
			GLint maxLength = 0;
			glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &maxLength);
			std::vector<GLchar> errLog(maxLength);
			glGetProgramInfoLog(programId, maxLength, &maxLength, &errLog[0]);
			std::cout << "Error::shader link failed," << &errLog[0] << std::endl;
		}
	}
	// 連結完成後detach 並釋放shader object
	for (size_t i = 0; i < shaderCount; ++i)
	{
		if (programId != 0)
		{
			glDetachShader(programId, shaderObjectIdVec[i]);
		}
		glDeleteShader(shaderObjectIdVec[i]);
	}
	return programId;
}

void init(void)
{
	glGenVertexArrays(NumVAOs, VAOs);
	glBindVertexArray(VAOs[Triangles]);
	GLfloat vertices[NumVertices][2] = {
		{-0.90, -0.90},//Triangle 1
		{ 0.85, -0.90},
		{-0.90,  0.85},
		{ 0.90, -0.85}, //Triangle
		{ 0.90,  0.90},
		{-0.85,  0.90}
	};
	glGenBuffers(Numbuffers, Buffers);
	glBindBuffer(GL_ARRAY_BUFFER, Buffers[ArrayBuffer]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	ShaderInfo shaders[] = {
		{GL_VERTEX_SHADER, "triangle.vert"},
		{GL_FRAGMENT_SHADER, "triangle.frag"},
		{GL_NONE, NULL}
	};
	GLuint program = LoadShaders(shaders, 2);
	glUseProgram(program);
	glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, 0);
	glEnableVertexAttribArray(vPosition);
}

void display()
{
	glClear(GL_COLOR_BUFFER_BIT);
	glBindVertexArray(VAOs[Triangles]);
	glDrawArrays(GL_TRIANGLES, 0, NumVertices);
	glFlush();
}
int main(int argc, char** argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA);
	glutInitWindowSize(512, 512);
	//glutInitContextVersion(4, 3);  //這兩個函式找不到定義,註釋掉沒有問題
	//glutInitContextProfile(GLUT_CORE_PROFILE);
	glutCreateWindow(argv[0]);

	if (glewInit()) {
		cerr << "Unable to initialize GLEW ... exiting" << endl; exit(EXIT_FAILURE);
	}
	init();

	glutDisplayFunc(display);

	glutMainLoop();
    return 0;
}
(注意設定glut glew的路徑和lib 以及dll)執行結果如下:

完整程式碼下載:點選開啟連結

OpenGL語法:

OpenGL中的函式都會以字元“gl”作為字首。例如glBindVertexArray()

有的以“glut”開頭,來自於第三方庫“OpenGL Utility Toolkit”(GLUT)。 

函式glewInit()來自於OpenGL Extension Wrangler。

OpenGL庫中定義的常量也是GL_COLOR_BUFFER_BIT的形式,以GL_作為字首。

由於OpenGL是一個“C”語言形式的庫,因此他不能使用函式過載來處理不同各型別的資料,此時它使用函式名稱的細微變化來管理實現同一類功能的函式集。例如glUniform*()它有多種變化形式,例如glUniform2f()和glUniform3fv()。2代表引數值,f代表GLfloat, V代表 vector。常見的型別如下表:

程式碼講解:main()函式:

glutInit()負責初始化GLUT庫,他必須是應用程式呼叫的第一個GLUT函式,它會負責設定其他GLUT例程所必需的資料結構。

glutInitDisplayMode()設定了程式所用的視窗的型別。

glutInitWindowSize()設定了所需的視窗的大小。

glutInitContextVersion()和glutInitContextProfile()設定了我們所需要的OpenGL環境(context)——這是OpenGL內部用於記錄狀態設定和操作的資料結構。

如果當前系統環境可以滿足glutInitDisplayMode()的顯示模式要求,使用glutCreateWindow()會建立一個視窗(此時會呼叫計算機視窗系統的介面),只有在GLUT建立了一個視窗之後(其中也包含建立OpenGL環境的過程),我們才可以使用OpenGL相關的函式。

glewInit()是GLEW(OpenGL Extension Wrangler)。GLEW可以簡化獲取函式地址的過程,並且包含了可以跨平臺使用的其他一些OpenGL程式設計方法。如果沒有GLEW,我們可能還需要執行相當多的工作才能夠執行程式。

glutDisplayFunc()他設定了顯示回撥(display callback),即GLUT在每次更新視窗內容的時候會自動呼叫的例程。這裡傳入display()這個函式的地址。GLUT可以使用一系列回撥函式來處理諸如使用者輸入,重設視窗尺寸等不同操作。

glutMainLoop()是一個無限執行的迴圈,它會負責一直處理視窗和作業系統的使用者輸入等操作。gltuMainLoop()會判斷視窗是否需要重新繪製,然後就會呼叫glutDisplayFunc()中註冊的函式。特別需要注意的是,glutMainLoop()是一個無限迴圈,因此不會執行在它之後的所有命令。


init()函式:

初始化頂點陣列物件(Vertex-Array Object  VAO)

使用glGenVertexArrays()分配頂點陣列物件。

void glGenVertexArrays(GLsizei n, GLuint* arrays); 返回n個未使用的物件到陣列arrays中,用作定點陣列物件。

OpenGL中使用glGen*來分配不同型別的OpenGL物件的名稱。這名稱類似C語言中的一個指標變數,我們必須分配記憶體並且用名稱引用它之後,名稱才有意義。在OpenGL中,這個分配的機制叫做繫結物件(bind an object 也就是把這個物件名稱繫結到分配的物件記憶體),它是通過一些列glBind*的函式集合去實現的。在例子中,我們是通過glBindVertexArray()函式建立並且綁定了一個頂點陣列物件。

void glBindVertexArray(GLuint array); 如果array非0且是glGenVertexArrays()所返回的,那麼它將建立一個新的頂點陣列物件並且與其名稱關聯起來。如果array已經建立,則啟用這個頂點陣列物件。如果array為0,那麼OpenGL將不再使用程式所分配的任何頂點陣列物件,並且講渲染狀態重設為頂點陣列的預設值。如果array不是glGenVertexArrays()返回的數值,或者已經被glDeleteVertexArrays()函式釋放了,那麼將會產生一個GL_INVALID_OPERATION錯誤。

當我們第一次繫結物件時,OpenGL內部會分配這個物件所需的記憶體並且將它作為當前物件,即所有後繼的操作都會作用於這個被繫結的物件。在第一次呼叫glBind*()函式之後,新建立的物件都會初始化為其預設狀態,而我們通常需要一些額外的初始化工作來確保這個物件可用。繫結物件的過程有點類似設定鐵路的岔道開關,一旦設定了開關,從這條線路通過的所有列車都會駛向對應的軌道。如果我們將開關設定到另一個狀態,那麼所有之後經過的列車都會駛向另一條軌道。總體上來說:兩種情況下我們需要繫結一個物件。(1)建立物件並初始化它對應的資料時;(2)每次準備使用這個物件時,而它並不是當前繫結的物件時。

當我們完成對頂點資料物件的操作之後,可以呼叫glDeleteVertexArrays()將它進行釋放。

void glDeleteVertexArrays(GLsizei n, GLuint *arrays);刪除n個在arrays中定義的頂點陣列物件,這樣所有的名稱可以再次用作頂點陣列。如果繫結的頂點陣列已經被刪除,那麼當前繫結的頂點陣列物件被重設為0(類似執行了glBindBuffer()函式,並且輸入引數為0),而預設的頂點陣列會變成當前物件。在arrays當中未使用的名稱都會被釋放,但是當前頂點陣列的狀態不會發生任何變化。

最後為了保證程式的完整性,我們可以呼叫gllsVertexArray()檢查某個名稱是否已經被保留為一個頂點陣列物件了。

GLboolean gllsVertexArray(GLuint array);如果array是一個用glGenVertexArrays()建立並且沒有被刪除的VAO名稱,那麼返回GL_TRUE,如果array為0或者不是任何VAO的名稱,那麼返回GL_FALSE。

對於OpenGL中其他的型別,都有glDelete*和glls*的函式。


分配頂點快取物件(Vertex Buffer Objects)

    VAO負責儲存一系列的頂點的資料,這些資料儲存到快取物件當中,並且由當前繫結的VAO管理。我們只有一種頂點陣列物件型別,但是卻有很多種型別的物件,並且其中一部分物件並不負責處理頂點資料。(快取物件就是OpenGL服務端分配和管理的一塊記憶體區域,並且幾乎所有傳入OpenGL的資料都是儲存在快取物件當中的)

    VBO的初始化過程與VAO類似,不過需要有向快取中新增資料的一個過程。

    void glGenBuffers(GLsizei n, GLuint* buffers);返回n個當前未使用的快取物件名稱,並儲存到buffers陣列中。返回到buffers中的名稱不一定是連續的整型資料。這裡返回的名稱只用於分配其他快取物件,它們在繫結之後只會記錄一個可用的狀態。0是一個保留的快取物件名稱。之後可以用glBindBuffer()來繫結它們了,由於OpenGL中有很多種不同型別的快取物件,因此在繫結時,需要指定對應的型別。如GL_ARRAY_BUFFER型別。

    void glBindBuffer(GLenum target, GLuint buffer);指定當前啟用的快取物件,target必須設定為一下型別中的一個:(GL_*_BUFFER 總共有8種: ARRAY ELEMENT_ARRAY PIXEL_PACK PIXEL_UNPACK COPY_READ COPY_WRITE TRANSFORM_FEEDBACK UNIFORM )。 glBindBuffer()完成了三項工作:(1)如果是第一次繫結buffer,buffer不等於0,那麼將建立一個與該名稱相對應的新快取物件;(2)如果buffer已經建立,那麼它將成為當前被啟用的快取物件。(3)如果buffer=0,那麼OpenGL將不再對當前target應用任何快取物件。

    所有的快取物件都可以用glDeleteBuffers()直接釋放掉。 

    void glDeleteBuffers(GLsizei n, const GLuint * buffers); 刪除n個儲存在buffers陣列中的快取物件。被釋放的快取物件可以重用(使用glGenBuffers())。我們可以用gllsBuffer()來判斷一個整數值是否是一個快取物件名稱。

將資料載入快取物件

    glBufferData():它主要有兩個任務:分配頂點資料所需的儲存空間,然後將資料從應用程式的陣列中拷貝到OpenGL服務端的記憶體中。

    void glBufferData(GLenum target, GLsizeptr size, const GLvoid* data, GLenum ussage);在OpenGL服務端記憶體中分配size個儲存單元(通常為byte),用於儲存資料或者索引。如果當前繫結的物件已經存在了關聯的資料,那麼會首先刪除這些資料。

    target的取值對應的含義分別是:頂點屬性資料 ARRAY     索引資料ELEMENT_ARRAY     從OpenGL中獲取的畫素資料PIXEL_PACK  OpenGL的畫素資料PIXEL_UNPACK 快取之間的複製資料COPY_READ和COPY_WRITE  通過transform feedback著色器獲得的結果TRANSFORM_FEEDBACK 一致變數UNIFORM  紋理快取中儲存的紋理資料TEXTURE)。

    size表示儲存資料的總數量。這個數值等於data中儲存的元素的總數乘以單位元素儲存空間的結果。

    data要麼是一個客戶端記憶體的指標,以便初始化快取物件,要麼是NULL。如果傳入的指標合法,那麼將會有size大小的資料從客戶端拷貝到服務端。如果傳入NULL,那麼將保留size大小的未初始化資料,以備後用。

    usage用於設定分配資料之後的讀寫和寫入方式。可用的方式包括: GL_STREAM_DRAW STREAM_READ STREAM_COPY STATIC_DRAW STATIC_READ STATIC_COPY DYNAMIC_DRAW DYNAMIC_READ DYNAMIC_COPY( stream static dynamic 各三種 draw read copy  )

    如果所需的size大小超過了伺服器能夠分配的額度,那麼glBufferData()將產生一個GL_OUT_OF_MEMORY錯誤。如果usage設定的不是可用的模式值,那麼將產生GL_INVALID_VALUE。

    在本文的例子中,因為頂點資料就儲存在一個vertices陣列當中。如果需要靜態地從程式中載入頂點資料,那麼而我們可能需要從模型檔案中讀取這些資料,或者通過某些演算法來生成。由於我們的資料是頂點屬性資料,因此設定這個快取為GL_ARRAY_BUFFER型別,用sizeof(vertices)來計算大小,最後因為我們只是用它來繪製幾何體,不會再執行時對它做出修改,所以設定usage為GL_STATIC_DRAW。

    仔細觀察vertices陣列中的值,可以發現x,y方向都被限定在[-1,1]的範圍內。實際上OpenGL只能夠繪製座標空間內的幾何體圖元。而具有該範圍限制的座標系也成為規格化裝置座標系統(Normalized Device Coordinate NDC)

    初始化頂點與片元著色器

    對於每一個OpenGL程式,當它所使用的OpenGL版本高於或等於3.1時,都需要指定至少兩個著色器:頂點著色器和片元著色器。對於OpenGL程式設計師而言,著色器就是使用OpenGL著色語言(OpenGL Shading Language,GLSL)編寫的一個小型函式。GLSL是構成所有OpenGL著色器的語言,它與C++語言非常類似,儘管GLSL中的所有特性並不能用於OpenGL的每個著色階段。

    triangles.vert內容如下:

#version 430 core  
layout(location = 0) in vec4 vPosition;  
void  
main()  
 {  
     gl_Position = vPosition;  
}  

    事實上,這就是我們之前所說的傳遞著色器(pass-through shader)的例子。它只負責將資料拷貝到輸出資料中。

    “#version 430 core ”指定了我們所用的OpenGL著色語言的版本4.3。 core表示使用OpenGL核心模式(core profile),這與之前GLUT的函式glInitContextProfile()設定的內容應當一致。每個著色器的第一行都應該設定“#version”,否則系統會假設使用“110”版本。但是這與OpenGL核心模式並不相容。

    下一步,我們申明瞭一個著色器變數 vPosition。著色器變數是著色器與外部世界的聯絡所在。也就是說,著色器並不知道自己的資料從哪裡來,它只是在每次執行時,直接獲取資料對應的輸入變數。而我們必須自己完成著色管線的裝配(在外部程式中設定變數對應的屬性值),然後才可以將應用程式的資料與不同OpenGL著色階段相互關聯

    vPosition儲存的是頂點的位置資訊。 vec4是vPosition的型別。它代表GLSL的四維浮點數向量。在程式中我們只使用了兩個座標值來代表一個點,OpenGL會用預設數值自動填充這些缺失的座標值。vec4的預設值為(0,0,0,1),因此當僅僅指定了x和y的座標的時候,其他兩個座標值(z和w)將被自動指定為0和1。 in指定了資料進入著色器的流向,(在其他地方可以看到out)。layout(location=0) 叫做佈局限定符(layout qualifier),目的是為變數提供元資料(meta data)。我們可以用限定符來設定很多不同的屬性,其中有些是與不同的著色階段相關的。這裡vPosition的位置屬性location為0,這個設定與init()函式的最後兩行會共同起作用。(enum Attrib_IDs{vPosition = 0}; 設定了位置為0,glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, 0);是指定0對應的資料指標,glEnableVertexAttribArray(vPosition);開啟屬性。)

    最後,在著色器main()函式中實現它的主體部分。OpenGL的所有著色器,無論是處理哪個著色階段,都會有一個main()函式。對於這個著色器而言,他所實現的就是將輸入的頂點位置複製到頂點著色器的指定輸出位置gl_position中。

    triangles.frag:

#version 430 core  
out vec4 fColor;  
void  
main()  
{  
fColor = vec4(0.0, 0.0, 1.0, 1.0);  
}  

    這個程式碼與上述的很類似。重點內容如下:宣告的變數名為fcolor。使用了out限定符!在這裡,著色器會把fcolor對應的數值輸出,而這也就是片元所對應的顏色值。每個片元都會設定一個四維的向量。OpenGL中的顏色是通過RGBA顏色空間來表示的。每個分量的範圍都是[0,1]。alpha的值被設定為1.0代表顏色是完全不透明的。

    為了輸入頂點著色器的資料,也就是OpenGL將要處理的所有頂點資料,需要在著色器中申明一個in變數,然後使用glVertexAttribPointer()將它關聯到一個頂點屬性陣列。

    void glVertexAttribPointer(GLunit index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* pointer);

    設定index(著色器中的屬性位置,即location的值)位置對應的資料值。pointer表示快取物件中(即當前bind的VBO物件),從起始位置開始計算的陣列資料的偏移值(假設起始位置地址為0),使用基本的系統單位(byte)。size表示每個頂點需要更新的分量數目,可以是(1、2、3、4)或者GL_BGRA(本例中每個頂點只使用了xy表示,即2個數據)。type指定了陣列中每個元素的資料型別(GL_* : BYTE、UNSIGNED_BYTE、SHORT、UNSIGNED_SHORT、INT、UNSIGNED_INT、FIXED、HALF_FLOAT、FLOAT 、DOUBLE)。normalized設定頂點資料在快取前是否需要進行歸一化(或者使用glVertexAttribFourN*()函式)。stride是陣列中每兩個元素之間的大小偏移值(byte)。如果stride為0,那麼資料應該緊密地封裝在一起(即兩個相鄰資料之間沒有其他資料)。
    #define BUFFER_OFFSET(offset) ((void*)offset) 在以往版本的OpenGL當中並不需要用到這個巨集,不過現在我們希望使用它來設定資料在快取物件中的偏移量,而不是像glVertexAttribPointer()原型那樣直接設定一個指向記憶體塊的指標。

    最後我們通過glEnableVertexAttribArray()來啟用頂點屬性陣列。 

    void glEnableVertexAttribArray(GLuint index)   void glDisableVertexAttribArray(GLuint index)設定是否啟用與index索引相關聯的頂點陣列。index必須介於0到GL_MAX_VERTEX_ATTRIBS-1之間。

    渲染

    display()程式碼:

void display()
{
	glClear(GL_COLOR_BUFFER_BIT);
	glBindVertexArray(VAOs[Triangles]);
	glDrawArrays(GL_TRIANGLES, 0, NumVertices);
	glFlush();
}

    首先我們要清除幀快取的資料,在進行渲染。清除的工作有glClear()完成。

    void glClear(GLbitfield mask); mask是一個可以通過邏輯“或”操作來指定多個數值的引數。 顏色快取:GL_COLOR_BUFFER_BIT 深度快取:GL_DEPTH_BUFFER_BIT 模板快取:GL_STENCIL_BUFFER_BIT。glClear()預設使用黑色清除數值。如果要改變清除顏色的數值,可以使用glClearColor();

    void glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha);設定當前使用的清除顏色值,用於RGBA模式下對顏色快取的清除工作。這裡的red green blue alpha都會被截斷到[0,1]的範圍內。預設的清除顏色是(0,0,0,0),在RGBA模式下它表示黑色。清除顏色本身也是OPenGL狀態機制的一個例子,它的數值會一直保留在當前OpenGL環境當中。OpenGL有一個龐大的狀態量列表,當建立一個新的OpenGL環境時,所有的狀態量都會被初始化為預設數值。

    繪製

    首先呼叫glBindVertexArray()來選擇作為頂點資料使用的頂點資料,其次呼叫glDrawArrays()來實現頂點資料向OpenGL管線的傳輸

    void glDrawArrays(GLenum mode, GLiint first, GLsizei count);使用當前繫結的頂點陣列元素來建立一系列的幾何圖元,起始位置為first,而結束位置為first+count-1。mode設定了構建圖元的型別,它可以是:GL_POINTS、GL_LINES GL_LINE_STRIP gL_LINE_LOOP GL_TRIANGLES GL_TRIANGLE_STRIP GL_TRIANGLE_FAN 和GL_PATCHES(不會輸出任何結果,用於細分著色器)中的任意一種。

    最後呼叫glFlush(),即強制所有進行中的OpenGL命令立即完成並傳輸到OpenGL服務端處理。

    void glFlush(void); 強制之前的OpenGL命令立即執行,這樣就可以保證它們在一定時間內全部完成。glFlush()只是強制所有執行中的命令送入OpenGL服務端而已,並且它會立即返回——它並不會等待所有的命令完成,而等待卻是我們所需要的。為此我們需要使用glFinish()命令,它會一直等待所有當前的OpenGL操作完成後,再返回。(你最好只是在開發階段使用glFinish(),雖然它對於判斷OpenGL命令執行效率很有幫助,但是對於程式的整體效能卻有著相當的拖累。)

    啟用和禁用OpenGL的操作

   void glEnable(GLenum capability); void glDisable(GLenum capability); 開啟或者關閉一個模式。例如:深度測試 GL_DEPTH_TEST 控制融合 GL_BLEND  transform feedback過程中的高階渲染控制 GL_RASTERIZER_DISCARD。根據自己的需要來判斷是否開啟某個特性,可以使用gllsEnabled()來返回是否啟用指定模式的資訊。