Advertisement

way to display text

Started by November 30, 2020 03:24 PM
10 comments, last by Calin 3 years, 11 months ago

previously (with my DX9 render) I had each letter as a separate texture, at runtime I had these letters textures applied/changed at on a wall made of (fixed) squares like airport old fashion panels used to to announce arrivals/departures.

Now I want to use one large texture containing all the letters and then play with the texture coordinates on the square to display the right letter. My question is do I need to use a separate vertex buffer for each square (4 vertices)? a lot of text means a huge pile of vertex buffers that I need to generate at run time, it also means I have to translate these in the right position.

My project`s facebook page is “DreamLand Page”

Now I want to use one large texture containing all the letters and then play with the texture coordinates on the square to display the right letter.

I have used this tool to generate such texture + uv atlas. Pretty nice.

My question is do I need to use a separate vertex buffer for each square (4 vertices)? a lot of text means a huge pile of vertex buffers that I need to generate at run time, it also means I have to translate these in the right position.

You can use one large vertex buffer, generate all the quads of text on CPU into that, then upload and use one draw call.
You want to limit uploads and draw calls the most, so maybe you can divide your whole GUI into just a few such draws.
Doing this from scratch every frame should not cause performance problems, but it makes sense to display with a latency of one or more frames, so the data transfer does not cause idle waiting.

Advertisement

Here is a sample font that I found on google some time ago.

Each character is 16x16 pixels, and there are 16 characters by 16 characters == 256 characters.

It's a start.

Some sample code is:

bool init_character_set(void)
{
	BMP font;

	if (false == font.load("font.bmp"))
	{
		cout << "could not load font.bmp" << endl;
		return false;
	}

	size_t char_index = 0;

	vector< vector<GLubyte> > char_data;
	vector<unsigned char> char_template(char_width * char_height);
	char_data.resize(num_chars, char_template);

	for (size_t i = 0; i < num_chars_wide; i++)
	{
		for (size_t j = 0; j < num_chars_high; j++)
		{
			size_t left = i * char_width;
			size_t right = left + char_width - 1;
			size_t top = j * char_height;
			size_t bottom = top + char_height - 1;

			for (size_t k = left, x = 0; k <= right; k++, x++)
			{
				for (size_t l = top, y = 0; l <= bottom; l++, y++)
				{
					size_t img_pos = 4 * (k * image_height + l);
					size_t sub_pos = x * char_height + y;

					char_data[char_index][sub_pos] = font.Pixels[img_pos]; // Assume grayscale, only use r component
				}
			}

			char_index++;
		}
	}

	for (size_t n = 0; n < num_chars; n++)
	{
		if (is_all_zeroes(char_width, char_height, char_data[n]))
		{
			font_character_image img;

			img.width = char_width / 4;
			img.height = char_height;

			img.monochrome_data.resize(img.width * img.height, 0);

			mimgs.push_back(img);
		}
		else
		{
			size_t first_non_zeroes_column = 0;
			size_t last_non_zeroes_column = char_width - 1;

			for (size_t x = 0; x < char_width; x++)
			{
				bool all_zeroes = is_column_all_zeroes(x, char_width, char_height, char_data[n]);

				if (false == all_zeroes)
				{
					first_non_zeroes_column = x;
					break;
				}
			}

			for (size_t x = first_non_zeroes_column + 1; x < char_width; x++)
			{
				bool all_zeroes = is_column_all_zeroes(x, char_width, char_height, char_data[n]);

				if (false == all_zeroes)
				{
					last_non_zeroes_column = x;
				}
			}

			size_t cropped_width = last_non_zeroes_column - first_non_zeroes_column + 1;

			font_character_image img;
			img.width = cropped_width;
			img.height = char_height;
			img.monochrome_data.resize(img.width * img.height, 0);

			for (size_t i = 0; i < num_chars_wide; i++)
			{
				for (size_t j = 0; j < num_chars_high; j++)
				{
					const size_t left = first_non_zeroes_column;
					const size_t right = left + cropped_width - 1;
					const size_t top = 0;
					const size_t bottom = char_height - 1;

					for (size_t k = left, x = 0; k <= right; k++, x++)
					{
						for (size_t l = top, y = 0; l <= bottom; l++, y++)
						{
							const size_t img_pos = l * char_width + k;
							const size_t sub_pos = y * cropped_width + x;

							img.monochrome_data[sub_pos] = char_data[n][img_pos];
						}
					}
				}
			}

			mimgs.push_back(img);
		}
	}

	RGB text_colour;
	text_colour.r = 255;
	text_colour.g = 255;
	text_colour.b = 255;

	for (size_t i = 0; i < mimgs.size(); i++)
		mimgs[i].opengl_init(text_colour);

	return true;
}

Where:

class font_character_image
{
public:
	size_t width;
	size_t height;
	vector<unsigned char> monochrome_data;

	GLuint tex_handle = 0, vao = 0, vbo = 0, ibo = 0;

	void opengl_init(RGB text_colour)
	{
		// Clean up, in case this opengl_init() function is called more than once
		cleanup();

		glGenVertexArrays(1, &vao);
		glGenBuffers(1, &vbo);
		glGenBuffers(1, &ibo);

		vector<unsigned char> rgba_data(4 * width * height, 0);

		for (size_t i = 0; i < width; i++)
		{
			for (size_t j = 0; j < height; j++)
			{
				size_t mono_index = j * width + i;
				size_t rgba_index = 4 * mono_index;

				rgba_data[rgba_index + 0] = text_colour.r;
				rgba_data[rgba_index + 1] = text_colour.g;
				rgba_data[rgba_index + 2] = text_colour.b;
				rgba_data[rgba_index + 3] = monochrome_data[mono_index];
			}
		}

		glGenTextures(1, &tex_handle);
		glBindTexture(GL_TEXTURE_2D, tex_handle);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast<GLsizei>(width), static_cast<GLsizei>(height), 0, GL_RGBA, GL_UNSIGNED_BYTE, &rgba_data[0]);
	}

	void draw(GLuint shader_program, size_t x, size_t y, size_t win_width, size_t win_height)
	{
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		complex<float> v0w(static_cast<float>(x), static_cast<float>(y));
		complex<float> v1w(static_cast<float>(x), static_cast<float>(y + this->height));
		complex<float> v2w(static_cast<float>(x + this->width), static_cast<float>(y + this->height));
		complex<float> v3w(static_cast<float>(x + this->width), static_cast<float>(y));

		complex<float> v0ndc = get_ndc_coords_from_window_coords(win_width, win_height, v0w);
		complex<float> v1ndc = get_ndc_coords_from_window_coords(win_width, win_height, v1w);
		complex<float> v2ndc = get_ndc_coords_from_window_coords(win_width, win_height, v2w);
		complex<float> v3ndc = get_ndc_coords_from_window_coords(win_width, win_height, v3w);

		// data for a fullscreen quad (this time with texture coords)
		const GLfloat vertexData[] = {
		//	       X     Y     Z					  U     V     
			  v0ndc.real(), v0ndc.imag(), 0,      0, 1, // vertex 0
			  v1ndc.real(), v1ndc.imag(), 0,      0, 0, // vertex 1
			  v2ndc.real(), v2ndc.imag(), 0,      1, 0, // vertex 2
			  v3ndc.real(), v3ndc.imag(), 0,      1, 1, // vertex 3
		}; // 4 vertices with 5 components (floats) each



		// https://raw.githubusercontent.com/progschj/OpenGL-Examples/master/03texture.cpp

		glBindVertexArray(vao);
		glBindBuffer(GL_ARRAY_BUFFER, vbo);



		// fill with data
		glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 4 * 5, vertexData, GL_STATIC_DRAW);


		// set up generic attrib pointers
		glEnableVertexAttribArray(0);
		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (char*)0 + 0 * sizeof(GLfloat));

		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (char*)0 + 3 * sizeof(GLfloat));

		// generate and bind the index buffer object

		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);

		static const GLuint indexData[] = {
			3,1,0, // first triangle
			2,1,3, // second triangle
		};

		glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint) * 2 * 3, indexData, GL_STATIC_DRAW);

		glBindVertexArray(0);

		glUseProgram(ortho.get_program());

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, tex_handle);

		glUniform1i(uniforms.ortho.tex, 0);

		glBindVertexArray(vao);

		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
	}

	void cleanup(void)
	{
		if(glIsTexture(tex_handle))
			glDeleteTextures(1, &tex_handle);

		if(glIsVertexArray(vao))
			glDeleteVertexArrays(1, &vao);
		
		if(glIsBuffer(vbo))
			glDeleteBuffers(1, &vbo);
		
		if(glIsBuffer(ibo))
			glDeleteBuffers(1, &ibo);
	}

	~font_character_image(void)
	{
		cleanup();
	}
};

and

vector<font_character_image> mimgs;

const size_t num_chars = 256;
const size_t image_width = 256;
const size_t image_height = 256;
const size_t char_width = 16;
const size_t char_height = 16;
const size_t num_chars_wide = image_width / char_width;
const size_t num_chars_high = image_height / char_height;

Thanks JoeJ, do you think its silly to use like 60 to 100 vertex buffers. Its not going to be a slaughter right? (dealing with two workspaces, shader and cpu, is a bit confusing, I`d rather do everything within the cpp file confines)

My project`s facebook page is “DreamLand Page”

Calin said:
Thanks JoeJ, do you think its silly to use like 60 to 100 vertex buffers. Its not going to be a slaughter right? (dealing with two workspaces, shader and cpu, is a bit confusing)

No, and i ignore my advises about performance constantly myself : ) It's just good to know, so if issues pile up, we have an idea about potential reasons. (Also my comment about latency is more confusing than helpful i guess.)

But… What's surely not ideal would be to send one matrix and draw call for each letter. If that's what you do and it becomes slow, it might help a lot to use instancing, but IDK.
Building the text meshes on CPU should be no more work for you? It remains the same - calculating position per letter is necessary in any case?

If i were you, i would integrate ImGui and use this to render some text output. That's not good enough for final GUI but for game design it is, and it's super helpful for debug output and interaction with the running program to test all kinds of stuff.

@joej No need to use matrices I think. You can feed the onscreen position of the letter upon creation if you need to move text on screen you throw the vertex buffers to the bin (release) and run the letter creation algorithm again. I don`t need smooth text movement so the creation algorithm is only run every now and then.

@taby Is the ordering of the characters within the image/texture the same with ordering found in c++ char?

My project`s facebook page is “DreamLand Page”

Advertisement

[edited]

Thanks 8Observer8

My project`s facebook page is “DreamLand Page”

changed my mind I no longer wish to make this post, It would have been nice to have the ability to delete a post shortly after making the post.

My project`s facebook page is “DreamLand Page”

Calin said:
ability to delete a post

Click on the 3 dots on the right and select <Hide>

🙂🙂🙂🙂🙂<←The tone posse, ready for action.

This topic is closed to new replies.

Advertisement