OpenGL編程/現代OpenGL教程 03

維基教科書,自由的教學讀本

屬性:傳遞額外的頂點信息[編輯]

我們程度中可能需要比純粹坐標更多的東西,例如:顏色。 來一起給OpenGL傳遞些RGB顏色信息。

我們使用了一個屬性(attribute)來傳遞坐標,所以我們也可以爲顏色增加一個屬性。 修改一下我們的全局變量:

GLuint vbo_triangle, vbo_triangle_colors;
GLint attribute_coord2d, attribute_v_color;

以及init_resources

  GLfloat triangle_colors[] = {
    1.0, 1.0, 0.0,
    0.0, 0.0, 1.0,
    1.0, 0.0, 0.0,
  };
  glGenBuffers(1, &vbo_triangle_colors);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_colors);
  glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_colors), triangle_colors, GL_STATIC_DRAW);

  [...]

  attribute_name = "v_color";
  attribute_v_color = glGetAttribLocation(program, attribute_name);
  if (attribute_v_color == -1) {
    cerr << "Could not bind attribute " << attribute_name << endl;
    return false;
  }

現在,在render過程中,我們可以爲我們的3個頂點各傳遞一個RGB顏色。我選擇了黃色、藍色和紅色,但可以隨意選擇你自己喜愛的顏色:)

  glEnableVertexAttribArray(attribute_v_color);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_colors);
  glVertexAttribPointer(
    attribute_v_color, // attribute
    3,                 // number of elements per vertex, here (r,g,b)
    GL_FLOAT,          // the type of each element
    GL_FALSE,          // take our values as-is
    0,                 // no extra data between each position
    0                  // offset of first element
  );

在我們完成了這些屬性後,告之於OpenGL——在該函數的最後:

  glDisableVertexAttribArray(attribute_v_color);

最後,也要在頂點着色器中聲明它:

attribute vec3 v_color;

在這個時候,如果執行程序,我們會得到:

Could not bind attribute v_color

這是因爲我們還沒有使用v_color。[1]

這裏的問題是:我們想要在區片着色器中上色,而不是在頂點着色器中! 來我們一起看看如何做……

變域:從頂點着色器到區片着色器傳遞信息[編輯]

我們使用一個變域(varying)變量來取代一個屬性。它是這麼回事:

  • 頂點着色器的一個輸出變量
  • 區片着色器的一個輸入變量
  • 它是自動插值的(interpolated)

總之,它是在兩着色器之間的交流通道。爲了理解爲什麼說它是自動插值的,我們來看個例子。

我們需要在兩個着色器中聲明我們的新變域——就叫f_color吧。


在triangle.v.glsl中:

attribute vec2 coord2d;
attribute vec3 v_color;
varying vec3 f_color;
void main(void) {
  gl_Position = vec4(coord2d, 0.0, 1.0);
  f_color = v_color;
}

然後在triangle.f.glsl中:

varying vec3 f_color;
void main(void) {
  gl_FragColor = vec4(f_color.x, f_color.y, f_color.z, 1.0);
}

(注意:如果你在使用GLES2,記得檢查下面關於可移植性的章節。)

來看看結果吧:

Triangle

喔,它實際上居然產生了多於3個顏色!

OpenGL爲每個像素進行了關於頂點像素的自動插值。 這解釋了變域的名稱:它爲每個頂點進行變化,且之後它更是在每個區片有更多變化。

我們不需要在C代碼中聲明變域——這是因爲在C代碼和變域之間沒有接口相通。

交織坐標和顏色[編輯]

爲了更好地理解glVertexAttribPointer函數,我們來把兩個屬性糅合到單個C數組中:

  GLfloat triangle_attributes[] = {
     0.0,  0.8,   1.0, 1.0, 0.0,
    -0.8, -0.8,   0.0, 0.0, 1.0,
     0.8, -0.8,   1.0, 0.0, 0.0,
  };
  glGenBuffers(1, &vbo_triangle);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
  glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_attributes), triangle_attributes, GL_STATIC_DRAW);

glVertexAttribPointer的第5個元素是步幅(stride),它用來告訴OpenGL每組屬性有多長——在我們的例子中是5個浮點型:

  glEnableVertexAttribArray(attribute_coord2d);
  glEnableVertexAttribArray(attribute_v_color);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
  glVertexAttribPointer(
    attribute_coord2d,   // attribute
    2,                   // number of elements per vertex, here (x,y)
    GL_FLOAT,            // the type of each element
    GL_FALSE,            // take our values as-is
    5 * sizeof(GLfloat), // next coord2d appears every 5 floats
    0                    // offset of the first element
  );
  glVertexAttribPointer(
    attribute_v_color,      // attribute
    3,                      // number of elements per vertex, here (r,g,b)
    GL_FLOAT,               // the type of each element
    GL_FALSE,               // take our values as-is
    5 * sizeof(GLfloat),    // next color appears every 5 floats
    (GLvoid*) (2 * sizeof(GLfloat))  // offset of first element
  );

它工作得和之前完全一樣!

應該注意到,對於顏色部分,我們從數組的第3個元素(2 * sizeof(GLfloat))開始。這裏是首個顏色所在的地方——即第一個元素的偏移量(offset)

爲什麼是(GLvoid*)呢?我們可以看到,在早期OpenGL版本中,可以直接傳遞指向C數組的指針(而不是一個緩衝對象)。該方法現已棄用,但glVertexAttribPointer的原型仍保持原狀而未改變,所以我們做得好像是傳遞了一個指針,但實際上傳遞了一個偏移量。

一個用以炫耀的替代方案:

struct attributes {
    GLfloat coord2d[2];
    GLfloat v_color[3];
};
struct attributes triangle_attributes[] = {
    {{ 0.0,  0.8}, {1.0, 1.0, 0.0}},
    {{-0.8, -0.8}, {0.0, 0.0, 1.0}},
    {{ 0.8, -0.8}, {1.0, 0.0, 0.0}},
};
...
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_attributes), triangle_attributes, GL_STATIC_DRAW);

...

glVertexAttribPointer(
  ...,
  sizeof(struct attributes),  // stride
  (GLvoid*) offsetof(struct attributes, v_color)  // offset
  ...

注意到我們使用offsetof來指定首個顏色的偏移量。

律態:傳遞全局信息[編輯]

屬性(attribute)變量相對的是律態(uniform)變量:對於所有的點(verteces),它們都是一樣的。 注意到我們會在C代碼中有規律地改變它們——但每當一組點(vertices)顯示在屏幕上時,律態總是保持不變。

舉個例子說說:我們打算從C代碼中定義三角形的全局透明度。如同屬性一樣,我們需要聲明(declare)它。

C代碼中的一個全局變量:

GLint uniform_fade;

然後我們在C代碼中聲明(declare)它(依然在程序鏈接過之後):

  const char* uniform_name;
  uniform_name = "fade";
  uniform_fade = glGetUniformLocation(program, uniform_name);
  if (uniform_fade == -1) {
    cerr << "Could not bind uniform_fade " << uniform_name << endl;
    return false;
  }

注意:我們甚至可以將目標設爲某一特定的數組元素——只須在着色器代碼中使用uniform_name(例如"my_array[1]")!

額外地,對於律態,我們也需要顯式地設置它的不可變值。我們在render中請求讓三角形只保留很少的不透明:

glUniform1f(uniform_fade, 0.1);

現在可以在區片着色器中使用該變量:

varying vec3 f_color;
uniform float fade;
void main(void) {
  gl_FragColor = vec4(f_color.x, f_color.y, f_color.z, fade);
}

注意:如果你沒有在代碼中使用該律態,glGetUniformLocation將無法看到它,而且會直接失敗。

OpenGL ES 2可移植性[編輯]

在前面一節,我們提到了GLES2需要精度導引。這些導引告訴OpenGL說我們的數據需要多少精度。精度可以是:

  • lowp
  • mediump
  • highp

舉個例子:lowp通常被用於顏色,並且爲頂點(vertices)使用highp是建議做法。

我們可以爲每個變量指定其精度:

varying lowp vec3 f_color;
uniform lowp float fade;

或者,我們也可以聲明一個默認精度:

precision lowp float;
varying vec3 f_color;
uniform float fade;

遺憾的是,這些精度導引在傳統的OpenGL 2.1中無法奏效,所以我們只有在自己使用GLES2的時候包含它們。

GLSE使用一個預處理器——和C的預處理器很像。我們可以使用諸如#define#ifdef這樣的指令。

只有區片着色器需要我們爲浮點數據聲明一個明確的精度。對於頂點着色器,該精度默認爲highp。對於區片着色器,highp有可能並不可用——這可以使用GL_FRAGMENT_PRECISION_HIGH宏(macro)[2]來檢測。

我們可以改進我們的着色器加載器,以便讓它爲GLES定義一個默認的精度,而在OpenGL 2.1上忽略精度標識(這樣我們仍可以在需要時爲某個變量設置精度):

	GLuint res = glCreateShader(type);

	// GLSL version
	const char* version;
	int profile;
	SDL_GL_GetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, &profile);
	if (profile == SDL_GL_CONTEXT_PROFILE_ES)
		version = "#version 100\n";  // OpenGL ES 2.0
	else
		version = "#version 120\n";  // OpenGL 2.1

	// GLES2 precision specifiers
	const char* precision;
	precision =
		"#ifdef GL_ES                        \n"
		"#  ifdef GL_FRAGMENT_PRECISION_HIGH \n"
		"     precision highp float;         \n"
		"#  else                             \n"
		"     precision mediump float;       \n"
		"#  endif                            \n"
		"#else                               \n"
		// Ignore unsupported precision specifiers
		"#  define lowp                      \n"
		"#  define mediump                   \n"
		"#  define highp                     \n"
		"#endif                              \n";

	const GLchar* sources[] = {
		version,
		precision,
		source
	};
	glShaderSource(res, 3, sources, NULL);

請在腦中牢記:GLES編譯器在顯示錯誤信息時會將上面所有這些行記入總行數。而且很不幸,設置#line 0不會重置編譯器的對行數的計數。

刷新顯示[編輯]

想來如果透明度可以來回變化,那麼會十分美妙。 爲了達到這個目標,

  • 我們可以檢查從用戶開啓應用程序以來所過的秒數;SDL_GetTicks()/1000可以給出它
  • 在其上使用數學的sin函數(sin函數在-1和+1之間以每2.PI=~6.28單位時間來回變化)
  • 在渲染場景之前,準備一個logic函數以更新它的狀態。

mainLoop中,我們在render前調用logic函數:

		logic();
		render(window);

現在添加一個新的logic函數

void logic() {
	// alpha 0->1->0 every 5 seconds
	float cur_fade = sinf(SDL_GetTicks() / 1000.0 * (2*3.14) / 5) / 2 + 0.5;
	glUseProgram(program);
	glUniform1f(uniform_fade, cur_fade);
}

自然地,移除render中對glUniform1f的調用。

編譯並運行...

The animated triangle, partially transparent

這就得到了我們的首個動畫!

在OpenGL實現中有一件事很常見,那就是每當更新物理屏幕的緩衝前等待屏幕的垂直刷新——這被稱爲垂直同步。在這種情況下,三角形會每秒鐘被渲染大約60次(60 FPS)。如果你停用了垂直同步,你的程序會不斷更新三角形,導致較高的CPU使用量。在創建具有透視變化的應用程序時,我們會再碰到垂直同步。

註解[編輯]

  1. 在一個例子中涉及屬性(attribute)和變域(varying)這兩個不同的概念着實有些令人迷惑。我們會努力找到兩個分開的例子以更好地解釋它們
  2. Cf. OpenGL ES Shading Language 1.0.17 Specification.Khronos.org(2009年5月12日).於2011年9月10日查閱., section 4.5.3 Default Precision Qualifiers