Reasonable lag? with instanced drawing 250k rectangles

Hey i managed to make my application do instanced rendering and im rendering 250K rectangles at once with one draw call.

i’v implemented a functionality so that when my mouse position is greater/lesser than the window dimensions the window moves in that direction.

However when i hover over the reactangle cluster containg 250K rectangle in different sizes it lags a bit.

So im just wondering if some lag should be expected when rendering 250K rectangles at once.

My graphics card is Nvidia GeForce GTX 1060 6gb

I’ll append my cpp file and my shader:

VertexShader

#version 330 core  
 
layout (location = 0) in vec3 inPos;
layout (location = 3) in vec4 inCol;
layout (location = 7) in mat4 modelMatrix;

out vec4 colorFromVS;

uniform mat4 view;
uniform mat4 projection;


void main()
{
	vec3 position = inPos;
	gl_Position = projection * view * modelMatrix * vec4(position, 1.0f);
	colorFromVS = inCol;

}

.cpp file

int main()
{

	GLFWwindow *window = {};
	window = setupGLFWandGLAD(SCREEN_WIDTH, SCREEN_HEIGHT, "cj_GL_GLFW");


	Shader shaderObject = {};
	shaderObject.createVS("vertexShader.glsl");
	shaderObject.createFS("fragmentShader.glsl");
	shaderObject.linkShaderProgram();

	//Camera camera = {};
	//camera.init();

	float verts[] = 
	{
		 0.0f,   0.0f, 0.0f,	1.0f, 0.0f, 0.0f, 1.0f,
		 0.0f,   1.0f, 0.0f,	0.0f, 1.0f, 0.0f, 1.0f,
		 1.0f,   1.0f, 0.0f,	0.0f, 0.0f, 1.0f, 1.0f,
		 1.0f,   0.0f, 0.0f,	0.0f, 0.0f, 0.0f, 1.0f
	};

	unsigned int indx[]=
	{
		0, 1, 2,
		2, 0, 3
	};

	srand(time(NULL));

	glm::mat4 modelMatrix[NUMOBJECTS];

	for (int i = 0; i < NUMOBJECTS; i++)
	{
		float rPosX =  (float)(rand() % 700);
		float rPosY =  (float)(rand() % 500);

		float rScaleX = (float)(rand() % 100);
		float rScaleY = (float)(rand() % 100);
		modelMatrix[i] = glm::mat4(1.0f);
		modelMatrix[i] = glm::translate(modelMatrix[i], glm::vec3(rPosX, rPosY, 0.0f));
		modelMatrix[i] = glm::scale(modelMatrix[i], glm::vec3(rScaleX, rScaleY, 0.0f));
	}



	unsigned int instanceVBO, VBO, IBO, VAO;

	
	glGenBuffers(1, &instanceVBO);
	glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(modelMatrix), &modelMatrix, GL_STATIC_DRAW);

	glGenVertexArrays(1, &VAO);
	glBindVertexArray(VAO);

	glGenBuffers(1, &VBO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);

	glGenBuffers(1, &IBO);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indx), indx, GL_STATIC_DRAW);

	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)0); 
	
	glEnableVertexAttribArray(3);
	glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(3 * sizeof(float))); 

	glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
	glEnableVertexAttribArray(7);
	glVertexAttribPointer(7, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)0);
	glVertexAttribDivisor(7, 1);

	glEnableVertexAttribArray(8);
	glVertexAttribPointer(8, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(1 * sizeof(glm::vec4)));
	glVertexAttribDivisor(8, 1);                                                                     
                                                                                                         
	glEnableVertexAttribArray(9);                                                                    
	glVertexAttribPointer(9, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(2 * sizeof(glm::vec4)));
	glVertexAttribDivisor(9, 1);                                                                     
                                                                                                         
	glEnableVertexAttribArray(10);                                                                   
	glVertexAttribPointer(10, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(3 * sizeof(glm::vec4)));
	glVertexAttribDivisor(10, 1);

	unsigned int projectionLocation = glGetUniformLocation(shaderObject.shaderProgram, "view");
	unsigned int viewLocation 	= glGetUniformLocation(shaderObject.shaderProgram, "projection");


	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

	shaderObject.useProgram();

	glm::mat4 projection = glm::mat4(1.0f);
	projection = glm::ortho(0.0f, 800.0f, 600.0f, 0.0f, -1.0f, 1.0f);

	glm::mat4 view = glm::mat4(1.0f);
	glm::vec3 eye = {};
	glm::vec3 center = {};
	glm::vec3 up = {};

	eye 		= glm::vec3(0.0f, 0.0f, 0.0f);
	center 		= glm::vec3(0.0f, 0.0f, -1.0f);
	up 		= glm::vec3(0.0f, 1.0f, 0.0f);

	while(!(glfwWindowShouldClose(window)) && running)
	{
		setViewportToWindowSize(window);
		checkInput(window);

		MouseCoords mouseCoords = getMousePosNDC(window);

		processInput(mouseCoords);

		glClearColor(0.1f, 0.1f, 0.1f, 0.5f);
		glClear(GL_COLOR_BUFFER_BIT);


                if(mouseCoords.x >= (1.0f -0.01f))
                {
                	//camera.move(5.0f, 0.0f);
                	
			eye.x 	 += 0.021f;
			center.x += 0.021f;

			
                }
                else if(mouseCoords.x <= -1.0f)
                {
			eye.x 	 -= 0.021f;
			center.x -= 0.021f;
                	//camera.move(-5.0f, 0.0f);
                }
                if(mouseCoords.y >= 1.0f)
                {
                	//camera.move(0.0f, -5.0f);
			eye.y 	 += 0.021f;
			center.y += 0.021f;

                }
                else if(mouseCoords.y <= -1.0f)
                {
                	//camera.move(0.0f, 5.0f);
			eye.y 	 -= 0.021f;
			center.y -= 0.021f;
                }
                if(keyPressed['W'])
                {
                	//camera.zoomIn(10.0f, 10.0f);
                }
                if(keyPressed['S'])
                {
                	//camera.zoomOut(10.0f, 10.0f);
                }

		view = glm::mat4(1.0f);
		view = glm::lookAt(eye, center, up);

		glUniformMatrix4fv(viewLocation, 1, GL_FALSE, glm::value_ptr(view));
		glUniformMatrix4fv(projectionLocation, 1, GL_FALSE, glm::value_ptr(projection));


		glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0, NUMOBJECTS);

		glfwSwapBuffers(window);
		glfwPollEvents();

	}

	glDeleteProgram(shaderObject.shaderProgram);
	glDeleteBuffers(1, &VBO);
	glDeleteBuffers(1, &IBO);
	glDeleteBuffers(1, &VAO);
	glDeleteBuffers(1, &instanceVBO);

	glfwTerminate();
	return 0;
}

What’s your frame time (in msec)? And is that frame time consistent, or does it really depend on what you’re doing with the mouse?

(i.e. Disable VSync, and report the time (in msec) between successive frames, as measured just after the glfwSwapBuffers() call. So that the cost of printing the frame time to the console doesn’t disrupt your frame timings, consider only reporting it every 60th frame (or every Nth frame, with N >= 2).)

Do not use instanced rendering for rectangles. You will gain nothing from this compared to just writing out the quad data directly and using it as normal vertex data.

Even if im about to add textures and maybe lighting to the rectangles, and have them moving?

i changed glfwSwapInteral(0);

I also print the time in miliseconds each 60th frame, but if i make rectangles that are small, like 20x20 pixels then i can print like 500K+ rectangles with around 16ms/frame.

the problam arouses when i make the rectangles bigger like 120x120 then i get a lag when drawing 250K rects and the “moving screen with mouse” makes it a bit worse.

Ok, that’s a good start. I’d next suggest you set up a sub-frame timer or two to make sure you know where the time is being spent (e.g. make sure the time spent in getMousePosNDC() and processInput() is negligible, even when using the mouse).

A few thoughts for you to consider:

When you render larger rectangles, you’re consuming more fill (i.e. spending more time in the rasterizer running fragment shaders and doing framebuffer reads and writes). 120*120*250K touches 18X more pixels than 20*20*500K, so it’s possible you are fill limited. Up around 120x120 with 250K instances, if you see an approximately a linear change in frame time for a linear increase/decrease in the number of pixels touched, then it’s likely that you are.

If so, what can you do? Touch fewer pixels, or make touching pixels less expensive. Examples of the former would be reducing the size of your rectangles (in pixels). Examples of the latter would be to do things like disabling blending, alpha test, optimizing your fragment shaders (if they were expensive), adjusting your primitive rendeirng order to take better advantage of early Z and stencil testing, possibly disabling MSAA (if enabled), etc.

Now if you’re not fill limited, then you’ll need to look for your bottleneck(s) elsewhere. One example (as Alfonse hinted): While geometry instanced rendering is very convenient (and often fast enough), you may find that you can push triangles down the pipeline faster by using other methods, such as pseudo-instancing. That is, instead of just providing OpenGL with just the geometry for one rectangle and then re-transforming the instances in the shaders based on per-instance transforms fetched by the shader, go ahead and pre-transform the individual rectangle instances by those per-instance transforms up-front, generate the list of triangles, and then at draw time just render those pre-placed trianges directly using a normal non-instanced draw call (e.g. glDrawElements()).

Now as to that slowdown you indicated that you see when hovering over (or moving?) things with the mouse. You’re going to need to tell us what’s special about your processing (GL and otherwise) when this occurs to provide any useful input. Could be an inefficient GPU update method, but who knows?

Yes. When looking at a broad spectrum of hardware, the general rule is that instances shouldn’t be too small or too big. A quad is definitely too small.

Just write the data as individual quads rather than instances and render them all in a single draw.

Okay but here’s my other program where i don’t use instanced drawing but instead using glDrawElements of each gameObject.

Here i get 33 miliseconds per frame when rendering 50K rectangles (with textures) and there’s quite a bit of loading time before anything is drawn from beginning of execution.

VertexShader:

#version 330 core  
 
layout (location = 0) in vec3 inPos;
layout (location = 1) in vec4 inCol;
layout (location = 2) in vec2 inTexCoord;

out vec4 colorFromVS;
out vec2 texCoord;

uniform vec2 uniTexCoord;
uniform vec2 uniTexDenominator;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;


void main()
{
	vec3 position = inPos;
	gl_Position = projection * view * model * vec4(position, 1.0);

	colorFromVS = inCol;

	vec2 texElement = inTexCoord;

	texElement.x = (texElement.x/uniTexDenominator.x) + (uniTexCoord.x / uniTexDenominator.x );
	texElement.y = (texElement.y/uniTexDenominator.y) + (uniTexCoord.y / uniTexDenominator.y );


	texCoord = texElement;
}

FragmentShader:

#version 330 core

out vec4 FragColor;

in vec4 colorFromVS;
in vec2 texCoord;

uniform sampler2D ourTexture;

void main()
{
	vec4 texColor = texture(ourTexture, texCoord) * colorFromVS; 
	
	// DISCARDS THE ALPHA PIXELS
	if(texColor.a < 0.1)
	{
		discard;
	}

	FragColor =  texColor;
}

Game.h:

bool running = true;



void processInput(MouseCoords mouseCoords);


void processInput(MouseCoords mouseCoords)
{

	if(keyPressed[ESC])
	{
		running = false;
	}
}


struct GameObject
{
	float x;
	float y;
	float width;
	float height;

	glm::mat4 model;

	Texture texture = {};
	glm::vec2 texPos = {};
	glm::vec2 texDenominator = {};

	int modelUniformLocation;
	int texCoordUniformLocation;
	int texDenominatorUniformLocation;

	void setShader()
	{
	}
	void initialize(float in_x, float in_y, float in_width, float in_height) 
	{
		x 		= in_x;
		y 		= in_y;
		width 		= in_width;
		height		= in_height;
		
		texDenominator.x = 0.0f;
		texDenominator.y = 0.0f;

		texPos.x = 0.0f;
		texPos.y = 0.0f;

		model 		= glm::mat4(1.0f);;
	}
	void setUniforms(int in_modelUniform, int in_texCoordUniform, int in_texDenominatorUniform)
	{
		modelUniformLocation		= in_modelUniform;
		texCoordUniformLocation		= in_texCoordUniform;
         	texDenominatorUniformLocation	= in_texDenominatorUniform;
        } 
	void loadTexture(char *textureFilePath, GLint internalFormat, GLenum format)
	{
		texture.load(textureFilePath, internalFormat, format);
	}

	void setTexDenominator(float inX, float inY)
	{
		texDenominator.x = inX;
		texDenominator.y = inY;
	}

	void setTexpos(float inX, float inY)
	{
		texPos.x = inX;
		texPos.y = inY;
	}

	void move(float in_x, float in_y)
	{
		x += in_x;
		y += in_y;
	}
	void scale(float in_x, float in_y)
	{
		width = in_x;
		height = in_y;
	}

	void draw()
	{
		
		texture.bind();

		model = glm::mat4(1.0f);
		model = translate(model, glm::vec3(x, y, 0.0f));
		model = glm::scale(model, glm::vec3(width, height, 1.0f));

		glUniformMatrix4fv(modelUniformLocation, 1, GL_FALSE, glm::value_ptr(model));
		glUniform2f(texDenominatorUniformLocation, texDenominator.x, texDenominator.y);
		glUniform2f(texCoordUniformLocation, texPos.x, texPos.y);

		

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

	void deleteObject()
	{
		texture.destroy();
	}
};

Camera.h:

struct Camera
{

	float scaleX;
	float scaleY;

	glm::vec3 eye;
	glm::vec3 center;
	glm::vec3 up; 

	glm::mat4 projection;
	glm::mat4 view;

	int viewUniformLocation;
	int projectionUniformLocation;

	void init()
	{
		scaleX = 800.0f;
		scaleY = 600.0f;

		eye 		= glm::vec3(0.0f, 0.0f, 0.0f);
		center 		= glm::vec3(0.0f, 0.0f, -1.0f);
		up 		= glm::vec3(0.0f, 1.0f, 0.0f);

		view 		= glm::mat4(1.0f);
		view = glm::lookAt(eye, center, up);

		projection 	= glm::ortho(0.0f, scaleX, scaleY, 0.0f, -1.0f, 1.0f);

	}

	void setUniforms(int in_View, int in_Projection)
	{
		viewUniformLocation 	  = in_View;
		projectionUniformLocation = in_Projection;
	}


	void move(float inX, float inY)
	{
		eye.x 	 += inX;
		center.x += inX;

		eye.y 	 += inY;
		center.y += inY;

	}

	void zoomIn(float zoomX, float zoomY)
	{
		scaleX -= zoomX;
		scaleY -= zoomY;
	}

	void zoomOut(float zoomX, float zoomY)
	{
		scaleX += zoomX;
		scaleY += zoomY;
	}

	void update()
	{
		projection 	= glm::ortho(0.0f, scaleX, scaleY, 0.0f, -1.0f, 1.0f);

		view 		= glm::mat4(1.0f);
		view = glm::lookAt(eye, center, up);

		glUniformMatrix4fv(viewUniformLocation, 1, GL_FALSE, glm::value_ptr(view));
		glUniformMatrix4fv(projectionUniformLocation, 1, GL_FALSE, glm::value_ptr(projection));

	}
};

main.cpp:

#define NUMOBJECTS 50000 
int main()
{

	GLFWwindow *window = {};
	window = setupGLFWandGLAD(SCREEN_WIDTH, SCREEN_HEIGHT, "cj_GL_GLFW");




	RenderState renderer;
	renderer.createRectangle();

	Shader shaderObject = {};
	shaderObject.createVS("vertexShader.glsl");
	shaderObject.createFS("fragmentShader.glsl");
	shaderObject.linkShaderProgram();

	unsigned int vertexUniColorLocation 	= 	glGetUniformLocation(shaderObject.shaderProgram, "uniformCol");
	unsigned int vertexUniPosLocation 	= 	glGetUniformLocation(shaderObject.shaderProgram, "uniPos");
	unsigned int texCoordUniform 		= 	glGetUniformLocation(shaderObject.shaderProgram, "uniTexCoord");
	unsigned int texDenominatorUniform 	= 	glGetUniformLocation(shaderObject.shaderProgram, "uniTexDenominator");
	unsigned int transformLoc 		= 	glGetUniformLocation(shaderObject.shaderProgram, "transform");
	unsigned int modelLoc 			= 	glGetUniformLocation(shaderObject.shaderProgram, "model");
	unsigned int viewLoc 			= 	glGetUniformLocation(shaderObject.shaderProgram, "view");
	unsigned int projectionLoc 		= 	glGetUniformLocation(shaderObject.shaderProgram, "projection");

	renderer.setPolygonMode(GL_FILL);
	renderer.enableBlending();
	renderer.enableDepthTest();

	static float X = 0.0f;
	static float Y = 0.0f;
	static float sX = 0.0f;
	static float sY = 0.0f;
	static float sZ = 0.0f;

	Camera camera = {};
	camera.init();
	camera.setUniforms(viewLoc, projectionLoc);


	GameObject viking;
	viking.initialize(400.0f, 300.0f, 200.0f, 200.0f);
	viking.setUniforms(modelLoc, texCoordUniform, texDenominatorUniform); 
	viking.loadTexture("..\\assets\\viking-test.png", GL_RGBA8, GL_RGBA);
	viking.setTexDenominator(2.0f, 2.0f);

	GameObject tank;
	tank.initialize(200.0f, 100.0f, 16.0f, 16.0f);
	tank.setUniforms(modelLoc, texCoordUniform, texDenominatorUniform); 
	tank.loadTexture("..\\assets\\Unit_Deviator.bmp", GL_RGBA8, GL_RGB);
	tank.setTexDenominator(8.0f, 1.0f);

	GameObject tank2;
	tank2.initialize(200.0f, 500.0f, 100.0f, 100.0f);
	tank2.setUniforms(modelLoc, texCoordUniform, texDenominatorUniform); 
	tank2.loadTexture("..\\assets\\Unit_Devastator.bmp", GL_RGBA8, GL_RGB);
	tank2.setTexDenominator(8.0f, 1.0f);

	GameObject box;
	box.initialize(400.0f, 300.0f, 100.0f, 100.0f);
	box.setUniforms(modelLoc, texCoordUniform, texDenominatorUniform); 
	box.loadTexture("..\\assets\\box.jpg", GL_RGBA8, GL_RGB);

	GameObject flameThrower;
	flameThrower.initialize(0.0f, 0.0f, 64.0f, 32.0f);
	flameThrower.setUniforms(modelLoc, texCoordUniform, texDenominatorUniform); 
	flameThrower.loadTexture("..\\assets\\flameThrower.png", GL_RGBA8, GL_RGBA);
	flameThrower.setTexDenominator(2.0f, 1.0f);

	srand(time(NULL));
	GameObject flamers[NUMOBJECTS] = {};
	for(int i = 0; i < NUMOBJECTS; i++)
	{
		float randWidth = rand()  % (int)800.0f + 1;
		float randHeight = rand() % (int)600.0f + 1;
		//printf("randWidth: %f\trandHeight: %f\n", randWidth, randHeight);
	      	flamers[i].initialize(randWidth, randHeight, 64.0f, 32.0f);
		flamers[i].setUniforms(modelLoc, texCoordUniform, texDenominatorUniform); 
		flamers[i].loadTexture("..\\assets\\flameThrower.png", GL_RGBA8, GL_RGBA);
		flamers[i].setTexDenominator(2.0f, 1.0f);
	}


	double startTime 		= 0.0;
	double endTime 			= 0.0;
	double frameTimeInSeconds 	= 0.0;
	double frameTimeInMili 		= 0.0;

	glfwSetTime(0.0f);

	glUseProgram(shaderObject.shaderProgram);

	while(!(glfwWindowShouldClose(window)) && running)
	{
		startTime = glfwGetTime();

		setViewportToWindowSize(window);
		checkInput(window);

		MouseCoords mouseCoords = getMousePosNDC(window);

		processInput(mouseCoords);

		glClearColor(0.1f, 0.1f, 0.1f, 0.5f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


		if(keyPressed[UP_ARROW])
		{
			tank.move(0.0f, -1.0f);	
			tank.setTexpos(1.0f, 0.0f);
			viking.setTexpos(1.0f, 0.0f);
		}
		if(keyPressed[DOWN_ARROW])
		{
			tank.move(0.0f,  1.0f);	
			tank.setTexpos(6.0f, 0.0f);
			viking.setTexpos(0.0f, 1.0f);
		}
		if(keyPressed[RIGHT_ARROW])
		{
			tank.move(1.0f,  0.0f);	
			tank.setTexpos(0.0f, 0.0f);
			flameThrower.move(1.0f,  0.0f);	
			flameThrower.setTexpos(1.0f, 0.0f);
		}
			
		if(keyPressed[LEFT_ARROW])
		{
			tank.move(-1.0f, 0.0f);	
			tank.setTexpos(4.0f, 0.0f);
			flameThrower.move(-1.0f,  0.0f);	
			flameThrower.setTexpos(0.0f, 0.0f);
		}
		if(keyPressed[UP_ARROW] && keyPressed[LEFT_ARROW])
		{
			tank.setTexpos(3.0f, 0.0f);
		}
		if(keyPressed[UP_ARROW] && keyPressed[RIGHT_ARROW])
		{
			tank.setTexpos(1.0f, 0.0f);
		}
		if(keyPressed[DOWN_ARROW] && keyPressed[LEFT_ARROW])
		{
			tank.setTexpos(5.0f, 0.0f);
		}
		if(keyPressed[DOWN_ARROW] && keyPressed[RIGHT_ARROW])
		{
			tank.setTexpos(7.0f, 0.0f);
		}

		if(mouseCoords.x >= (1.0f -0.01f))
		{
			camera.move(5.0f, 0.0f);
		}
		else if(mouseCoords.x <= -1.0f)
		{
			camera.move(-5.0f, 0.0f);
		}
		if(mouseCoords.y >= 1.0f)
		{
			camera.move(0.0f, -5.0f);
		}
		else if(mouseCoords.y <= -1.0f)
		{
			camera.move(0.0f, 5.0f);
		}
		if(keyPressed['W'])
		{
			camera.zoomIn(10.0f, 10.0f);
			
		}
		if(keyPressed['S'])
		{
			camera.zoomOut(10.0f, 10.0f);
		}
		if(keyPressed['D'])
		{
		}
		if(keyPressed['A'])
		{
		}

		tank.draw();
		tank2.draw();
		flameThrower.draw();
		viking.draw();
		for(int i = 0; i < NUMOBJECTS; i++)
		{
			flamers[i].draw();
		}


		camera.update();



		glfwSwapBuffers(window);

		endTime = glfwGetTime();
		frameTimeInSeconds = endTime - startTime;
		frameTimeInMili = frameTimeInSeconds * 1000.0f;
		printf("ms/frame: %0.3f\n", frameTimeInMili); 

		glfwPollEvents();



	}

	glDeleteProgram(shaderObject.shaderProgram);

	viking.deleteObject();
	renderer.deleteBuffer();
	tank.deleteObject();
	tank2.deleteObject();
	flameThrower.deleteObject();

	for(int i = 0; i < NUMOBJECTS; i++)
	{
		flamers[i].deleteObject();
	}


	glfwTerminate();
	return 0;
}

here’s a gif of running the program

Nobody suggested doing that. The suggestion was to draw all of the rectangles with a single draw call.

Okay but I don’t quite understand how you would go about rendering more objects with one call to glDrawElements() if u’re not going to just have one quad that you change the positions on via uniform?

Do you mean that you should create a vertexBuffer for every object?

or render more indices with the glDrawElements()?

Or do you have some tutorial/articel where one can read about this kind of rendering? :slight_smile:

I don’t understand your question. If you can draw a single quad and a bunch of instances, then that means you must have some way of getting per-instance data to the GPU, right? So every frame, you’re uploading new per-instance data, some values per-quad. Right?

So instead of writing per-instance data, you just write triangles to a buffer object. The triangles you would get if you applied the per-instance data to the quads. Then you render all of those triangles all at once.

What gl functions would you use to write the triangles to the buffer object, glBufferData() ??

You use whatever techniques you were using to write the per-instance data. It’s simply a matter of what data you’re writing and how you’re using the data, not the mechanism of writing it.