【C++/OpenGL】NO.8 ASCII文字をテクスチャを使って描画
前回の記事で、配列からテクスチャを生成し(≧▽≦)マークを描画する、という事をしました。
shizenkarasuzon.hatenablog.com
今回は、これを使って文字列を描画してみます。
準備(配列作成)
文字をテクスチャとして描画するには、前回のプログラムのように、文字列のテクスチャを配列化する必要があります。
(このように直接画像ファイルから読み込む方法もありますが、pngファイルの透明度読み込みに苦戦しておりまだ解決していないので、とりあえず配列を作成する方法でいきます)
今回テクスチャは↓の画像を使います
(サイズは512*512)
この画像データを配列に変換する必要があります
で、以下のようなPythonプログラムを実行します
# -*-coding:utf-8-*- import cv2 import numpy as np def CreateFont(): # ビットマップフォントの画像ファイルを読み込む img = cv2.imread("font.bmp", 0) print(" ---------------------------------- ") print(f"bitmap width = {np.shape(img)[1]}") print(f"bitmap height = {np.shape(img)[0]}") print(" ---------------------------------- ") hex_text = "{" temp_value = 0 counter = 0 # transparent -> 0 # white -> 1 for y in range(np.shape(img)[0]): for x in range(np.shape(img)[1]): counter += 1 temp_value = temp_value << 1 if img[y][x] > 200: temp_value += 1 if counter >= 8: hex_text +=hex(temp_value) + ", " counter = 0 temp_value = 0 if counter == 0: hex_text += "\n" hex_text += "}" print(hex_text) CreateFont()
上のpyファイルとフォントの画像ファイル(「font.bmp」という名前で保存)を同じディレクトリにおいて、pyファイルを実行します。
すると、
---------------------------------- bitmap width = 512 bitmap height = 512 ---------------------------------- {0x7f, 0xfe, 0x0, 0x0, 0x3c, .......大量の配列.........}
の様に、画像ファイルが配列となって出力されるので、これを↓のソースファイルのfont_arrayにコピペします。
サンプルプログラム
以下のサンプルプログラムは「HELLO WORLD!」という文字列を表示するプログラムです。
因みに、このプログラムではstd::vectorを自作したやつを使っています。
#include <locale.h> #include <assert.h> #include <memory> #include <memory.h> #define MY_ASSERT(A) assert(A) #ifndef MAX #define MAX(a, b) ((a) > (b) ? (a) : (b)) #endif #ifndef MIN #define MIN(a, b) ((a) < (b) ? (a) : (b)) #endif #define disp(A) (std::cout << #A << " = " << A << "\n") template<typename T> struct myVector { int Size; int Capacity; T* Data; int DeltaCapacityGrow; // Provide standard typedefs but we don't use them ourselves. typedef T value_type; typedef value_type* iterator; typedef const value_type* const_iterator; inline myVector() { Size = Capacity = 0; DeltaCapacityGrow = 100; Data = NULL; } inline myVector(const myVector<T>& src) { Size = Capacity = 0; Data = NULL; DeltaCapacityGrow = 100; operator=(src); } inline myVector<T>& operator=(const myVector<T>& src) { clear(); resize(src.Size); memcpy(Data, src.Data, (size_t)Size * sizeof(T)); return *this; } inline ~myVector() { if (Data) free(Data); } inline void setDeltaCapacityGrow(int A) { DeltaCapacityGrow = A; } inline bool empty() const { return Size == 0; } inline int size() const { return Size; } inline int size_in_bytes() const { return Size * (int)sizeof(T); } inline int capacity() const { return Capacity; } inline T& operator[](int i) { MY_ASSERT(i < Size); return Data[i]; } inline const T& operator[](int i) const { MY_ASSERT(i < Size); return Data[i]; } inline void clear() { DeltaCapacityGrow = 100; if (Data) { Size = Capacity = 0; free(Data); Data = NULL; } } inline T* begin() { return Data; } inline const T* begin() const { return Data; } inline T* end() { return Data + Size; } inline const T* end() const { return Data + Size; } inline T& front() { MY_ASSERT(Size > 0); return Data[0]; } inline const T& front() const { MY_ASSERT(Size > 0); return Data[0]; } inline T& back() { MY_ASSERT(Size > 0); return Data[Size - 1]; } inline const T& back() const { MY_ASSERT(Size > 0); return Data[Size - 1]; } inline bool _grow_capacity(int num) { T* p_new = (T*)realloc(Data, (num + Capacity) * sizeof(T)); if (p_new == NULL) { std::cout << "メモリ確保失敗"; MY_ASSERT(p_new == NULL); return false; } else { memcpy(p_new, Data, Capacity * sizeof(int)); Data = p_new; Capacity += num; return true; } } inline void push_back(const T& v) { if (Size == Capacity) _grow_capacity(DeltaCapacityGrow); memcpy(&Data[Size], &v, sizeof(v)); Size++; } inline void pop_back() { MY_ASSERT(Size > 0); Size--; } inline void resize(int new_size) { if (new_size > Capacity) _grow_capacity(new_size - Capacity); Size = new_size; } inline void resize(int new_size, const T& v) { if (new_size > Capacity) _grow_capacity(new_size - Capacity); if (new_size > Size) for (int n = Size; n < new_size; n++) memcpy(&Data[n], &v, sizeof(v)); Size = new_size; } inline void shrink(int new_size) { MY_ASSERT(new_size <= Size); Size = new_size; } // Resize a vector to a smaller size, guaranteed not to cause a reallocation inline void display() { std::cout << "data = ["; for (int i = 0; i < Size; i++) { std::cout << Data[i] << ", "; } std::cout << "\n"; } };
※特に理由はないですが、std::vector擬きの構造体の名前をここの記事ではuiVector にしていたものをmyVector に変えています。
OpenGLのメイン処理のプログラムは↓です
#include <GL/glew.h> #include <GLFW/glfw3.h> #include <iostream> #include <fstream> const GLubyte font_array[] = {/*上のPythonプログラムで作成したテクスチャの配列をコピペする*/} GLint makeShader() { const char* vertex_shader = "#version 400\n" "layout(location = 0) in vec2 position;\n" "layout(location = 1) in vec2 vuv;\n" "out vec2 Flag_uv;\n" "void main(void) {\n" "Flag_uv = vuv;\n" "gl_Position =vec4(position, 0.0, 1.0f);\n" "}\n"; const char* fragment_shader = "#version 400\n" "in vec2 Flag_uv;"//頂点シェーダで計算された、テクスチャの残高 "uniform sampler2D Texture;" //追加:テクスチャを入手 "out vec4 outFragmentColor; \n" "void main(void) {\n" //texture2D関数→指定されたUV座標(Flag_uv)のテクスチャの色を返す関数 "outFragmentColor = texture2D(Texture, Flag_uv); \n" "}\n"; GLuint vs, fs; GLuint shader_programme; vs = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vs, 1, &vertex_shader, NULL); glCompileShader(vs); fs = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fs, 1, &fragment_shader, NULL); glCompileShader(fs); shader_programme = glCreateProgram(); glAttachShader(shader_programme, fs); glAttachShader(shader_programme, vs); glLinkProgram(shader_programme); return shader_programme; } GLuint loadFont() { // テクスチャIDの生成 GLuint texID; glGenTextures(1, &texID); // テクスチャをGPUに転送 glPixelStorei(GL_UNPACK_ALIGNMENT, 1); float index[] = { 0.0, 1.0 }; glPixelMapfv(GL_PIXEL_MAP_I_TO_R, 2, index); glPixelMapfv(GL_PIXEL_MAP_I_TO_G, 2, index); glPixelMapfv(GL_PIXEL_MAP_I_TO_B, 2, index); glPixelMapfv(GL_PIXEL_MAP_I_TO_A, 2, index); glBindTexture(GL_TEXTURE_2D, texID); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 512, 512, 0, GL_COLOR_INDEX, GL_BITMAP, font_array); // テクスチャを拡大縮小する時のフィルタリング方法を指定 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 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); /* delete[] textureBuffer;*/ glBindTexture(GL_TEXTURE_2D, 0); return texID; } //myVectorは、https://shizenkarasuzon.hatenablog.com/entry/2020/06/14/003939 の「std::vector自作」節のuiVector 構造体の名前をmyVectorに変えただけのものです。 //この記事の上の方にmyVectorのプログラムを載せています。 myVector<GLfloat> points; //頂点座標を格納する配列 myVector<GLfloat> vertex_uv; // テクスチャのUV座標を格納する配列 int word_length; void makeCharPorigon(float x, float y, float width, float height, const char *words) { points.clear(); vertex_uv.clear(); word_length = strlen(words); float delta_x = width / word_length; float delta_u = 1.0 / 16.0; float delta_v = 1.0 / 16.0; for (int i = 0; i < word_length; i++) { //左上座標から反時計回り points.push_back(x + i * delta_x); points.push_back(y + height); points.push_back(x + i * delta_x); points.push_back(y); points.push_back(x + (i + 1) * delta_x); points.push_back(y); points.push_back(x + (i + 1) * delta_x); points.push_back(y + height); float uv_left = delta_u * float((words[i]-32) % 16); float uv_top = delta_v * float((words[i]-32) / 16); std::cout << int(words[0]-32) << "\n"; // 1/16 = 0.0625 vertex_uv.push_back(uv_left); vertex_uv.push_back(uv_top); vertex_uv.push_back(uv_left); vertex_uv.push_back(uv_top + delta_v); vertex_uv.push_back(uv_left + delta_u); vertex_uv.push_back(uv_top + delta_v); vertex_uv.push_back(uv_left + delta_u); vertex_uv.push_back(uv_top); } } int main() { GLFWwindow *window = NULL; if (!glfwInit()){ fprintf(stderr, "ERROR: could not start GLFW3\n"); return 1; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1); window = glfwCreateWindow(640, 480, "Texture", NULL, NULL); if (!window) { fprintf(stderr, "ERROR: could not open window with GLFW3\n"); glfwTerminate(); return 1; } glfwMakeContextCurrent(window); glewExperimental = GL_TRUE; glewInit(); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); //Alpha要素のあるテクスチャを描画する glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); GLint shader = makeShader(); makeCharPorigon(-1.0, 0.0, 2.0, 0.3, "HELLO WORLD!"); // std::cout << "\n\n--------------------------\n"; // std::cout << "points = \n"; // points.display(); // std::cout << "uv = \n"; // vertex_uv.display(); // std::cout << "\n--------------------------\n"; GLuint vao, vertex_vbo, uv_vbo; glGenVertexArrays(1, &vao); glBindVertexArray(vao); glGenBuffers(1, &vertex_vbo); glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo); glBufferData(GL_ARRAY_BUFFER, points.size_in_bytes(), (const GLvoid*)points.Data, GL_STATIC_DRAW); //追加:テクスチャのUV座標を格納するためのVBOを作成 glGenBuffers(1, &uv_vbo); glBindBuffer(GL_ARRAY_BUFFER, uv_vbo); glBufferData(GL_ARRAY_BUFFER, vertex_uv.size_in_bytes(), (const GLvoid*)vertex_uv.Data, GL_STATIC_DRAW); //追加:テクスチャ読み込み GLuint texID = loadFont(); // 追加:テクスチャ情報を送るuniform属性を設定する int textureLocation = glGetUniformLocation(shader, "texture"); while (!glfwWindowShouldClose(window)) { glClearColor(1.0, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glUseProgram(shader); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*)0); //追加:作成したVBOを有効化し、描画に使用する glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, uv_vbo); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*)0); //追加:テクスチャのデータを送る glUniform1i(textureLocation, 0); glBindTexture(GL_TEXTURE_2D, texID); // ポリゴン(画像)描画 glDrawArrays(GL_QUADS, 0, 4 * word_length); glDisableVertexAttribArray(0); glDisableVertexAttribArray(1); glfwPollEvents(); glfwSwapBuffers(window); } glfwTerminate(); return 0; }
実行結果
HELLO WORLDと表示されます。
解説
だいたいこの記事とやっていることは一緒です。
shizenkarasuzon.hatenablog.com
ただテクスチャのデータが16x16から512x512に大きくなっただけです。
一文字ずつ長方形のポリゴンを作成して、その文字にあたるテクスチャのUV座標をしています。「HELLO WORLD!」と表示するので全部で11個の長方形ポリゴンがあるわけです。
頂点座標とUV座標の計算はmakeCharPorigon関数で行っているのでそこを見てみてください
前回のプログラムと変わったことと言えば、glDrawArrays()関数の第一引数がGL_QUADSになっています。
GL_QUADSは異なる四角形を連続で描画したい時に使います。4頂点を一つの四角形とみなします。