前往
大廳
主題

【程式】OpenGL 3.3 使用貼圖

Shark | 2021-07-18 11:42:16 | 巴幣 1228 | 人氣 1001

前篇:OpenGL 3.3 架設基本繪圖管線
前一篇還沒有使用uniform buffer、texture和sampler,這次要加上這三種物件,並且寫比較複雜的shader。

整個系列可以看這篇目錄,「Direct3D與OpenGL」的部分。
Shark流程式教學一覽



本篇使用的圖檔,可以下載回去用,把檔名改成「char1.png」放在與exe檔相同資料夾。

用這個軟體做的
キャラクターなんとか機  http://khmix.sakura.ne.jp/download.shtml

#define GL_GLEXT_PROTOTYPES
#define GLX_GLXEXT_PROTOTYPES
#include<GL/gl.h> //間接引用GL/glext.h
#include<GL/glx.h> //間接引用X11/Xlib.h

#include<stdio.h>
#include<time.h> //使用clock_gettime()和nanosleep()
#include<fcntl.h> //使用open()
#include<sys/stat.h> //使用fstat()
#include<gdk-pixbuf/gdk-pixbuf.h> //讀取圖檔
#include<x86intrin.h>
//使用_rotr()和__bswapd()

const int WINDOW_W=400,WINDOW_H=400;
//OpenGL global狀態
Display* dsp;
Window window;
GLXContext context;
//OpenGL物件
uint32_t programID;
uint32_t vertexData;
uint32_t vertexArrayObj;
//本篇新增的物件
uint32_t uniformBuffer;
uint32_t texture;
uint32_t sampler;

//要傳給GPU的資料在下面說明

//這些函式在下面說明

static int initGL();
static int initSettings();
static void nextFrame();
static void deinitSettings();
static void deinitGL();
static char* loadWholeFile(const char* fileName, uint32_t* outFileSize);
static int loadShader(uint32_t shaderID, const char* fileName);
static void initVertexData1(uint32_t* outVertexData, uint32_t* outVao);
static uint32_t loadTexture(const char* fileName);

//這個函式留待之後介紹layout時解說
static void initVertexData2(uint32_t* outVertexData, uint32_t* outVao);

int main(){
  dsp = XOpenDisplay( NULL );
  window = XCreateSimpleWindow(dsp,
    DefaultRootWindow(dsp),
    0, 0,WINDOW_W, WINDOW_H, //xywh
    0, 0, 0);

  if(initGL()){
    printf("Can not initialize OpenGL\n");
    return 0;
  }
  if(initSettings()){
    return 0;
  }

  //設標題
  XStoreName(dsp, window, "simplepipeline");
  //設定事件mask
  Atom wmDelete = XInternAtom(dsp, "WM_DELETE_WINDOW", True);
  XSetWMProtocols(dsp, window, &wmDelete, 1);
  XMapWindow( dsp, window );

  XEvent evt;
  int isEnd=0;
  struct timespec prevTime,nextTime;
  while(!isEnd){
    clock_gettime(CLOCK_MONOTONIC, &prevTime);
    //接收事件
    while(XPending(dsp)){
      XNextEvent(dsp, &evt);
      switch(evt.type){
      case ClientMessage:
        if(evt.xclient.data.l[0]== wmDelete){ isEnd=1; }
        break;
      }
    }

    //正式寫遊戲時,遊戲邏輯放在此處

    nextFrame(); //更新畫面

    clock_gettime(CLOCK_MONOTONIC, &nextTime); //單位為奈秒(10^-9秒)
    int64_t elapsedTime = (nextTime.tv_sec-prevTime.tv_sec)*1000000000
      +(nextTime.tv_nsec-prevTime.tv_nsec); //求出經過的奈秒數
    struct timespec sleepTime={0, 16000000-elapsedTime};
    if(sleepTime.tv_nsec>0){ nanosleep(&sleepTime, NULL); }
  }

  XDestroyWindow( dsp, window );
  XFlush(dsp);
  deinitSettings();
  deinitGL();
  XCloseDisplay( dsp );
  return 0;
}
將視窗尺寸設得比較大以配合圖檔,且多了一些header用來讀檔案。

這次要傳給GPU的資料如下
//uniform buffer
const float WINDOW_SIZE[]={WINDOW_W, WINDOW_H};

//頂點資料
typedef struct{
  float pos[2];
  short texCoord[2];
  uint32_t color;
} VertexData1;
VertexData1 VERTEX_DATA1[4]={
  {100,0,  0,  0,  0xffffffff},
  {100,400,0,  400,0xffffffff},
  {400,0,  300,0,  0xffffffff},
  {400,400,300,400,0xffffffff},
};

typedef struct{
  float pos[8];
  short texCoord[8];
  uint32_t color[4];
} VertexData2;
VertexData2 VERTEX_DATA2={
  {100,0,100,400,400,0,400,400},
  {0,0,0,400,300,0,300,400},
  {0xffffffff,0xffffffff,0xffffffff,0xffffffff},
};
資料跟D3D篇一樣,只是那篇使用C++而本篇使用C,純C用struct定義資料型態必須寫成typedef struct。
說明照搬那邊的:
VERTEX_DATA1和VERTEX_DATA2是相同頂點資料用不同layout儲存,本篇只用1,2等之後寫layout的教學再使用。
這次頂點資料有三項:位置、貼圖坐標、顏色,之前說過D3D和OpenGL的畫面坐標是-1~1、貼圖坐標是0~1,但2D畫面習慣上以畫面左上角為原點、以像素為單位,這裡頂點資料就這樣給,在shader裡換算成D3D和OpenGL標準。
雖然本篇故意在vertex shader裡算,這裡只有4個頂點,在CPU把4個頂點全部算好才傳給GPU其實效能不會差多少,但如果一次畫很多個三角形(如tilemap和粒子系統),在vertex shader裡算比較方便。

坐標是這樣,右圖的ABCD是頂點順序。

多數繪圖軟體和函式庫以左上角為原點,但上傳貼圖的函式glTexImage2D()規定給它的資料以左下角為原點,所以圖在OpenGL內部是倒立的狀態。不過OpenGL讀取貼圖也以左下角為原點,兩次顛倒抵消,能畫出正立的圖片。
但如果用framebuffer object把圖畫在記憶體內的點陣圖,再把它當成貼圖畫在另一張點陣圖,就要對坐標特別處理了。

shader這次採用讀外部檔的方式,由於OpenGL 3.3沒有提供事先編譯的方式,主程式要把shader原始碼載入,在執行時編譯。
準備這兩個檔案,因為GLSL程式起點固定叫做main(),不能把兩個shader放在同一個檔案。
//usetexture.vert.glsl
#version 330
layout(std140) uniform uniform1{
  vec2 windowSize;
};
uniform sampler2D sampler1; //GLSL的sampler物件包含sampler和貼圖

//頂點資料,對應到struct VertexData1
layout(location=0) in vec2 inPos;
layout(location=1) in vec2 inTexCoord;
layout(location=2) in vec4 inColor;

//vertex傳給fragment shader的資料
varying vec2 varTexCoord;
varying vec4 varColor;

void main(){
  vec2 outPos=inPos*vec2(2,-2)/windowSize+vec2(-1,1);
  gl_Position=vec4(outPos, 0, 1);
  ivec2 texSize=textureSize(sampler1, 0);
  varTexCoord=inTexCoord/texSize;
  varColor=inColor;
}
//usetexture.frag.glsl
#version 330
uniform sampler2D sampler1;

varying vec2 varTexCoord;
varying vec4 varColor;

void main(){
  vec4 texColor=texture(sampler1, varTexCoord);
  gl_FragColor=texColor*varColor;
}
shader的輸入與輸出」提到的輸出入在這裡出現了,看本篇時可以跟那篇對照著看。

shader裡面的處理方式跟D3D篇一樣,以下是從D3D篇照抄:
vertex shader坐標轉換的算式怎麼求出,回想一下學校學過的二元一次方程式。
畫面坐標要把左邊的換算成右邊

二元一次方程式的標準式「x'=xa+b, y'=yc+d」,把兩組坐標代進去,得到兩組聯立方程式
X坐標
-1 = 0 + b
1 = windowW×a + b
Y坐標
1 = 0 + d
-1 = windowH×c + d

解出a,b,c,d如下
  x' =( 2/windowW)x - 1
  y' =(-2/windowH)y + 1
因為shader可以一次計算四維向量,可以寫成這樣
  outPos = inPos×(2,-2)/(windowW, windowH) + (-1,1)

貼圖坐標只要除以貼圖寬高即可,內建函式textureSize()可以取得貼圖寬高。
顏色就原封不動傳給rasterizer內插。

fragment shader讀取貼圖裡的像素,跟頂點資料裡的顏色相乘。

OpenGL 3.3雖然不能事先編譯,但有一個工具glslangValidator可以在把shader包進主程式前檢查語法。Fedora要裝glslang的套件;Ubuntu要20.04版,Mint要20版以後才有這個套件,名稱是glslang-tools。
Windows的話就自己去官網找下載的地方。
https://www.khronos.org/opengles/sdk/tools/Reference-Compiler/

像這樣打可檢查語法錯誤
glslangValidator usetexture.vert.glsl
glslangValidator usetexture.frag.glsl
它會根據副檔名判斷這是哪一階段的shader、語言是GLSL還是HLSL,把檔案命名為.vert.glsl和.frag.glsl目的在此。

如果要查uniform block裡各變數的位置作為寫C struct的參考,好像必須用「glslangValidator -i 檔名」再從裡面找出offset資訊,不知道有沒有更簡單的方法。
另外如果想在OpenGL比較新的版本用SPIR-V,將程式碼編譯成SPIR-V也是用這個工具。

static int initGL(){
  const int fbConfigAttr[]={
    GLX_X_RENDERABLE,True,
    GLX_DOUBLEBUFFER,True,
    GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR,
    GLX_RED_SIZE, 8, GLX_GREEN_SIZE, 8, GLX_BLUE_SIZE, 8, GLX_ALPHA_SIZE,8,
    None
  };
  int fbCount;
  GLXFBConfig* fbc = glXChooseFBConfig(dsp, DefaultScreen(dsp),fbConfigAttr,&fbCount);
  if(fbc==NULL){ return 1; }

  //產生OpenGL 3.3 context
  const int contextAttribs[] = {
    GLX_CONTEXT_MAJOR_VERSION_ARB,3,
    GLX_CONTEXT_MINOR_VERSION_ARB,3,
    GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB,
    None,
  };
  context = glXCreateContextAttribsARB(dsp, fbc[0], 0,True, contextAttribs);
  XFree(fbc);
  if(context==0){ return 1; }
  glXMakeCurrent(dsp, window, context);

  //設定Vsync
  PFNGLXSWAPINTERVALMESAPROC glXSwapIntervalMESA=
    (PFNGLXSWAPINTERVALMESAPROC)glXGetProcAddress("glXSwapIntervalMESA");
  if(glXSwapIntervalMESA){
    glXSwapIntervalMESA(0);
  }
  PFNGLXSWAPINTERVALSGIPROC glXSwapIntervalSGI=
    (PFNGLXSWAPINTERVALSGIPROC)glXGetProcAddress("glXSwapIntervalSGI");
  if(glXSwapIntervalSGI){
    glXSwapIntervalSGI(0);
  }
  return 0;
}

static void deinitGL(){
  glXDestroyContext(dsp, context);
}
initGL()、deinitGL和之前一樣。

static void nextFrame(){
  glClear(GL_COLOR_BUFFER_BIT);
  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
  glXSwapBuffers(dsp, window);
}
glDrawArrays()第一參數的幾何形狀改成GL_TRIANGLE_STRIP,第三參數的頂點數量改成4。
本篇要講的東西很多,先不介紹各種幾何形狀。雖然OpenGL 3.0以後把四邊形列為deprecated,OpenGL ES也一開始就沒支援四邊形,只有四個頂點的情況下用triangle strip可以畫四邊形。

static void deinitSettings(){
  glDeleteProgram(programID);
  uint32_t buffers[]={vertexData, uniformBuffer};
  glDeleteBuffers(2, buffers);
  glDeleteVertexArrays(1, &vertexArrayObj);

  glDeleteTextures(1, &texture);
  glDeleteSamplers(1, &sampler);
}
要刪除的物件增加3個。
buffer物件有兩個,這裡利用glDeleteBuffers()可以一次刪除多個物件的特性,傳入長度為2的陣列。



接下來的initSettings()是主題,用到的輔助函式會在旁邊解說。

-Vertex & Fragment Shader-

//傳回來的指標要用free()釋放
static char* loadWholeFile(const char* fileName, uint32_t* outFileSize){
  int file=open(fileName, O_RDONLY);
  if(file==-1){
    *outFileSize=0;
    return NULL;
  }
  struct stat buf;
  fstat(file, &buf);
  long fileSize=buf.st_size;
  char* data=(char*)malloc(fileSize);
  read(file, data, fileSize);
  close(file);
  *outFileSize=fileSize;
  return data;
}

//傳回0代表成功、非0代表失敗
static int loadShader(uint32_t shaderID, const char* fileName){
  uint32_t fileSize;
  const char* shaderCode=loadWholeFile(fileName, &fileSize);
  if(shaderCode==NULL){
    return 1;
  }
  glShaderSource(shaderID, 1, &shaderCode, &fileSize);
  free((void*)shaderCode);
  glCompileShader(shaderID);
  //檢查shader錯誤
  int ret;
  glGetShaderiv(shaderID, GL_COMPILE_STATUS, &ret); //ret=0代表有錯誤
  if(!ret){
    int infoLen;
    glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &infoLen); //取得訊息長度
    char info[infoLen]; //配置空間
    glGetShaderInfoLog(shaderID, infoLen, NULL, info); //取得訊息
    printf("%s:\n%s",fileName, info);
  }
  return !ret;
}

//傳回0代表成功,非0代表失敗
static int initSettings(){
  //compile vertex shader
  uint32_t vsID=glCreateShader(GL_VERTEX_SHADER);
  loadShader(vsID, "usetexture.vert.glsl");
  //compile fragment shader
  uint32_t fsID=glCreateShader(GL_FRAGMENT_SHADER);
  loadShader(fsID, "usetexture.frag.glsl");

  //create program object
  programID=glCreateProgram();
  glAttachShader(programID, vsID);
  glAttachShader(programID, fsID);
  glLinkProgram(programID);
  glDetachShader(programID, vsID);
  glDetachShader(programID, fsID);
  glDeleteShader(vsID); //之後不會再用到shader物件,可刪除
  glDeleteShader(fsID);
  //check program error
  int ret;
  glGetProgramiv(programID,GL_LINK_STATUS, &ret);
  if(!ret){
    int infoLen;
    glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &infoLen);
    char info[infoLen];
    glGetProgramInfoLog(programID, infoLen, NULL, info);
    printf("program linking:\n%s", info);
    glDeleteProgram(programID);
    return 1;
  }
  glUseProgram(programID);
loadWholeFile()用到的Linux API函式請參照這篇:檔案操作—Linux篇,作業系統沒有直接提供「傳入檔名→讀取整個檔案」的函式,但這功能有時候會用到,寫一個函式做這件事。

把shader程式碼讀入記憶體之後,按照「OpenGL 3.3 架設基本繪圖管線」的方法建立shader與program物件、檢查error。

-Vertex buffer & Input assembler-

static void initVertexData1(uint32_t* outVertexData, uint32_t* outVao){
  //vertex buffer
  glGenBuffers(1, outVertexData);
  glBindBuffer(GL_ARRAY_BUFFER, *outVertexData);
  glBufferData(GL_ARRAY_BUFFER, sizeof(VERTEX_DATA1),VERTEX_DATA1, GL_STATIC_DRAW);

  //vertex array object
  glGenVertexArrays(1,outVao);
  glBindVertexArray(*outVao);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 2, GL_FLOAT,0,
    sizeof(VertexData1), 0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 2, GL_SHORT,0,
    sizeof(VertexData1), (void*)offsetof(VertexData1, texCoord));
  glEnableVertexAttribArray(2);
  glVertexAttribPointer(2, GL_BGRA, GL_UNSIGNED_BYTE,1,
    sizeof(VertexData1), (void*)offsetof(VertexData1, color));
}

//initSettings()內容
  //vertex data & vertex array object

  initVertexData1(&vertexData, &vertexArrayObj);
建buffer和vertex array物件的方法跟「OpenGL 3.3 架設基本繪圖管線」一樣,本篇頂點有三項資料,要呼叫三次glEnableVertexAttribArray()和glVertexAttribPointer()設定格式。glVertexAttribPointer()的參數是什麼意思留待下一篇介紹。

-Uniform buffer-

  //uniform buffer
  glGenBuffers(1, &uniformBuffer);
  glBindBuffer(GL_UNIFORM_BUFFER, uniformBuffer);
  glBufferData(GL_UNIFORM_BUFFER, sizeof(WINDOW_SIZE), WINDOW_SIZE, GL_STATIC_DRAW);

  //將這個物件與shader裡的buffer物件對應
  uint32_t uniformIndex=glGetUniformBlockIndex(programID, "uniform1");
  glUniformBlockBinding(programID, uniformIndex, 2);
  glBindBufferBase(GL_UNIFORM_BUFFER, 2, uniformBuffer);
本篇新增的東西之一。與vertex buffer一樣是buffer所以建立的方法很像,只是bind的第一參數換成GL_UNIFORM_BUFFER。

-Sampler-

  //sampler
  glGenSamplers(1, &sampler);
  glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //default
  glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glSamplerParameteri(sampler, GL_TEXTURE_WRAP_S, GL_REPEAT); //default
  glSamplerParameteri(sampler, GL_TEXTURE_WRAP_T, GL_REPEAT); //default

  //將這個物件與shader裡的sampler物件對應

  int location=glGetUniformLocation(programID, "sampler1");
  glUniform1i(location, 2);
  glBindSampler(2, sampler);
本篇新增的東西之二。跟大部分OpenGL物件一樣用Gen函式產生識別碼,不過修改sampler屬性不需要先bind,因為操作sampler的函式用第一參數指定要操作哪個sampler。
FILTER和WRAP是什麼請參照這篇貼圖的部分:shader的輸入與輸出,MAG_FILTER和MIN_FILTER分別設定放大和縮小要做什麼處理,WRAP_S和WRAP_T分別是貼圖的X坐標和Y坐標,兩個方向可以用不同鋪排方式。
OpenGL wiki: Sampler Object
有幾行有標示「//default」,sampler物件建好之後就會填入一些預設值,這幾行剛好跟預設的值相同,把這幾行拿掉執行結果不會變。

-Texture-

//讀取圖檔、解碼、產生texture物件
//成功傳回OpenGL物件識別碼,失敗傳回0

static uint32_t loadTexture(const char* fileName){
  //用gdk-pixbuf讀取圖檔,把RGBA值存在一個叫pixels的變數
  GError* error=NULL;
  GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file(fileName,&error);
  if(pixbuf==NULL){
    return 0;
  }
  int w = gdk_pixbuf_get_width(pixbuf);
  int h = gdk_pixbuf_get_height(pixbuf);
  uint32_t* pixels = (uint32_t*)malloc(w*h*4);
  GdkPixbuf* outPixbuf=gdk_pixbuf_new_from_data((uint8_t*)pixels,
    GDK_COLORSPACE_RGB, 1,8,w,h,w*4,NULL,0);
  gdk_pixbuf_copy_area(pixbuf,0,0,w,h,outPixbuf,0,0);
  g_object_unref(pixbuf);
  g_object_unref(outPixbuf);
  //RGBA轉BGRA
  uint32_t* tempP=pixels;
  for(int i=0; i<w*h; i++,tempP++){
    *tempP=_rotr(__bswapd(*tempP), 8);
  }

  //建立texture物件
  uint32_t textureID;
  glGenTextures(1, &textureID);
  glBindTexture(GL_TEXTURE_2D, textureID);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w,h,0, GL_BGRA,GL_UNSIGNED_BYTE, pixels);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAX_LEVEL, 0); //設mipmap層數
  free(pixels);
  return textureID;
}
//initSettings()內容
  //texture

  glActiveTexture(GL_TEXTURE2); //目前要操作2號slot
  texture = loadTexture("char1.png");
本篇新增的東西之三,用到一個輔助函式loadTexture()。用一個函式庫:gdk-pixbuf讀取圖檔和解碼,方法參照這篇:讀取圖檔的方法-Linux篇
再抄一段D3D篇的過來:PNG、JPG、webP這些編碼過的格式不能給GPU使用,因為GPU讀取貼圖需要迅速找到任意位置的像素(即隨機存取,random access),這些格式必須完全解碼才能得知每個像素的值。用在GPU的壓縮格式必須設計成能隨機存取,D3D和OpenGL有支援一些壓縮格式,以後用到再介紹。

gdk-pixbuf解碼出來的byte順序是RGBA,之後把它轉換成BGRA。R,G,B,A四個分量,每個分量8bit剛好形成一個32位元整數,四個byte的順序D3D和OpenGL可以支援RGBA(最低位元組是R)和BGRA(最低位元組是B),筆者選用BGRA的理由是大部分繪圖軟體的十六進位顏色值都是BGR,可以直接把軟體裡的值複製貼上到程式裡。
只用C/C++語法轉換byte順序有點麻煩,這裡用兩個組合語言指令做這工作:bswap和ror,

__bswapd()和_rotr()是所謂的intrinsic function,有些組合語指令沒有直接對應的C/C++語法,C/C++只能用函式的型式提供功能,這些函式編譯後會直接轉換成組合語言指令,不會產生函式呼叫。

之後建立貼圖物件,老樣子的Gen、Bind兩步驟,再用glTexImage2D()設定貼圖格式並上傳資料。參數如下:
1:target。glBindTexture()把貼圖ID設給一個叫GL_TEXTURE_2D的變數,然後glTexImage2D()讀取這個變數得知要操作哪個貼圖物件。
  貼圖種類有很多,除了本篇用的2D貼圖以外,還有1D、3D、cube map、貼圖陣列等等。
2:如果有使用mipmap且建立物件時就要上傳mipmap,要用這個參數指定層數,不使用mipmap就填0。
3:在顯示記憶體裡以何種格式儲存。
4、5:寬高。
6:border。是早期版本的功能,但現在的OpenGL取消了,必須填0。
7、8:你傳進去的資料(第9參數)是什麼格式,7是有幾個顏色分量和byte順序,8是各分量是什麼型態。
9:要上傳的資料,byte數會從寬高和格式求出。

3,7,8全部可以填什麼格式要看官方文件
OpenGL wiki: GLAPI/glTexImage2D
內部格式和第7,8參數如果是不同格式,GPU會做轉換,如果不能轉換就產生error code,取得error code的方法以前提過,是用glGetError()。
內部格式可以填這篇文件裡的Base Internal Formats讓GPU自行決定各分量bit數,也可以填Sized Internal Formats指定bit數;不需要指定byte順序,GPU會看情況自行決定。
下面還有Sized Depth and Stencil Internal Formats和Compressed Internal Formats兩個表,要求GPU配置一塊depth或stencil buffer也是用這個函式,文件裡有列出壓縮格式但上傳壓縮過的貼圖要用另一個函式:glCompressedTexImage2D()。

下一行「glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAX_LEVEL, 0)」絕對不能少,本篇沒有用到mipmap的功能,但貼圖物件被建出來時是設定成要套用mipmap,如果沒準備好mipmap就不能使用這個物件,所以要用這一行告訴OpenGL這個貼圖不用mipmap。
(筆者有一次因為沒打這行卡了好幾個小時)

-Rasterizer, depth, stencil, and blend-

  //rasterizer
  glDisable(GL_CULL_FACE); //default
  glDisable(GL_MULTISAMPLE);
  glViewport(0, 0, WINDOW_W, WINDOW_H);

  //depth and stencil
  glDisable(GL_DEPTH_TEST); //default
  glDisable(GL_STENCIL_TEST); //default

  //blend

  glEnable(GL_BLEND);
  glBlendEquation(GL_FUNC_ADD); //default
  glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

  const float color[]={1,1,1,1};
  glClearColor(color[0],color[1],color[2],color[3]);
  return 0;
}
//initSettings()結束
rasterizer、depth和stencil跟以前一樣,只有blend換了算式,改成正式繪圖軟體使用的。
  (Sc, Sa):pixel shader輸出的顏色和alpha,數值範圍0~1
  (Dc, Da):畫面上的顏色和alpha
    color = ScSa + Dc(1-Sa)
    alpha = screen(Sa, Da) = Sa + Da(1-Sa)
  screen()是圖層模式的濾色模式

glClearColor()把背景設成白色,且把alpha設成1簡化blend的算式。如果畫面的alpha不是1,那其實D3D和OpenGL的blend設定湊不出正確的alpha blend算式,要用premultiply alpha的技巧,讀取圖檔時做一點特別處理。
以前有寫過一篇筆記:premultiply alpha的妙用



如果檔名是usetexture.c,用這個指令build
gcc usetexture.c -o usetexture -s -Os -lX11 -lGL `pkg-config --cflags --libs gdk-pixbuf-2.0`
比上次多了`pkg-config --cflags --libs gdk-pixbuf-2.0`是因為用gdk-pixbuf讀取圖檔。

執行的樣子。




uniform buffer、texture、sampler這三種物件如何將主程式和shader的物件對應,可以想成顯卡少女的工作台有個置物櫃,有很多格子。

跟D3D11不一樣的是,OpenGL所有shader階段共用一個置物櫃,且把貼圖和sampler放在同一個格子。program物件裡有一張表把變數名稱和格子編號對應,由於OpenGL要把全部階段的shader連結成一個program才能使用,這個對應表是全部階段共用。
(註:有兩個擴充:ARB_shading_language_420pack和ARB_separate_shader_objects會改變本節介紹的規則,因為分別是4.2與4.1版才列為標準配備,本篇不介紹)

本篇的shader裡有這兩段,其中的uniform1和sampler1是變數名稱,兩階段同名的變數會視為同一個物件。
//vertex shader
layout(std140) uniform uniform1{
  vec2 windowSize;
};
uniform sampler2D sampler1;
//fragment shader
uniform sampler2D sampler1;
link之後program會記住裡面有"uniform1"和"sampler1"兩個變數,留待之後對應到格子。

變數名稱與格子的對應,以及指示顯卡少女把物件放進格子,是在主程式裡做。
這一段是設定uniform buffer
//填對應表,將名稱"uniform1"對應到2號
uint32_t uniformIndex=glGetUniformBlockIndex(programID, "uniform1");
glUniformBlockBinding(programID, uniformIndex, 2);
//將物件"uniformBuffer"放入2號格子
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uniformBuffer);
格子的索引是從0開始,本篇故意用2。
不知為何不用一個函式直接填入(programID, "uniform1", 2)就好,還要取得uniformIndex再用另一個函式設定,但這就是OpenGL的規定。

上面說對應表是存在program物件裡,如果有3個program物件,要呼叫3次glGetUniformBlockIndex()和glUniformBlockBinding()分別設定對應,但只要呼叫一次glBindBufferBase()就可以3個program共用這個uniform buffer。

這一段是設定sampler物件
//填對應表,將名稱"sampler1"對應到2號
int location=glGetUniformLocation(programID, "sampler1");
glUniform1i(location, 2);
//將物件"sampler"放入2號格子
glBindSampler(2, sampler);
同樣要用一個暫時變數location。
glGetUniformLocation()用第一參數指定要操作哪個program,但glUniform1i()沒有programID的參數,它用前面的glUseProgram()決定要操作的program。OpenGL有個討厭的地方是有些函式只由參數決定行為,但有些函式會讀取內部的全域變數,規則不一致。

texture物件的話,因為GLSL的sampler物件也包含貼圖,sampler部分的前兩行也同時設定texture的格子對應。
把物件放到2號格子要這樣做
//設全域變數,設定之後要操作2號格子
glActiveTexture(GL_TEXTURE2);
//將物件"textureID"放入2號格子,本篇是在loadTexture()裡呼叫此
glBindTexture(GL_TEXTURE_2D, textureID);
格子編號不是填數字,而是用常數GL_TEXTURE0、GL_TEXTURE1……,這些是連續整數,所以也可以用「GL_TEXTURE0+i」的方式。

可以做到這樣,在vertex和fragment shader用不同的變數名稱,但是對應到相同格子。
但這會讓人寫程式混亂,同一個slot最好還是用相同名稱。
//vertex shader
uniform sampler2D sampler1;
//fragment shader
uniform sampler2D sampler2;
//主程式
int location = glGetUniformLocation(programID, "sampler1");
glUniform1i(location, 2);
location = glGetUniformLocation(programID, "sampler2");
glUniform1i(location, 2);

具體有幾個格子隨硬體、驅動程式和作業系統而異,要用glGetIntegerv()查詢以下的值:
GL_MAX_VERTEX_UNIFORM_BLOCKS
GL_MAX_FRAGMENT_UNIFORM_BLOCKS
GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS
GL_MAX_TEXTURE_IMAGE_UNITS
  …………
在命令列打「glxinfo -B -l」會列出目前環境中OpenGL各功能的上限,其中有這些值。
參考 OpenGL wiki: Resource limitations
OpenGL規格會要求晶片廠商支援一定數量以上,照這篇說明,3.3版至少有12格uniform buffer(編號0~11),16格sampler。

sampler物件是OpenGL 3.3版新增的東西,3.2版以前的做法是每個貼圖物件都內附一個sampler,想設定取樣方式要修改貼圖屬性,如果看OpenGL比較早版本的教學會看到這種用法。
glBindTexture(GL_TEXTURE_2D, textureID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
查這兩個函式的說明會看到很多相同的設定值
glTexParameter
glSamplerParameter

創作回應

相關創作

更多創作