shader Instancing

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Shadername

Zurück zur Shadersammlung

Beschreibung Autor Version
Benutzung von GL_ARB_draw_instanced Coolcat 1.0

Bilder

Terrain mit per Instancing gerenderten Pflanzen.

Was ist Instancing?

Dieses Tutorial befasst sich mit dem Thema Instancing und der Extension GL_ARB_draw_instanced. Ein häufiger Anwendungsfall ist das rendern vieler gleichartiger Objekte. Hier im Beispiel geht es um das rendern von Vegetation, es gibt aber auch viele weitere Anwendungen. Man könnte die Technik beispielsweise auch für Billboards einsetzen. Die Voraussetzung für Instancing ist, dass sich die zu renderden Objekte die selben Vertex-Daten teilen. Die Primitiv-Anzahl und der Primitiv-Typ müssen identisch sein. Die Extension beschleunigt derartige Anwendungen, in dem die Anzahl der API-Aufrufe und die Menge redundanter Daten reduziert wird.

Voraussetzungen

Instancing erfordert relativ neue Grafikhardware (ca. ab Geforce 8), man muss sich also überlegen, ob man diese Technik wirklich einsetzen möchte. Insbesondere beim rendern von hunderten identischer Objekte mit nur wenigen Polygonen bringt Instancing jedoch einen massiven Performance-Schub.

Für dieses Beispiel werden die folgenden Extensions benötigt:

Funktionsweise

Im Beispiel werden insgesamt 1600 über das Terrain verteilte Büsche gerendert. Der Busch liegt als VBO mit Indices vor. Statt nun den Busch mit einer Schleife und glDrawElements zu zeichnen, wird Instancing verwendet. Statt 1600 Aufrufen von glDrawElements, wird nur ein einziger Aufruf von glDrawElementsInstancedARB benötigt.

Jedes Objekt benötigt eine eigene ModelView-Matrix. Diese Matrix wird im Vertexshader erzeugt. Ein Texturbuffer-Objekt (TBO) liefert mit 4 Floats pro Objekt Daten zur Position und Skalierung. Natürlich kann man hier auch eine Rotation etc. einbauen, alles nur eine Frage der Komplexität.

Statt einem TBO kann man auch eine gewöhnliche Textur verwenden. Ein TBO eignet sich aber besser, da die Grafikkarte keine Texel-Interpolation vornimmt, sondern einfach nur einen Array-Zugriff mit Integer-Koordinaten.

Code

Initialisierung

Hier wird ein Vertexbuffer-Objekt erzeugt, welches Daten zur Position und Skalierung einer jeden Pflanze enthält. Auf dieses VBO wird später im Shader als Texturbuffer-Objekt (TBO) zugegriffen.

Ich verwende hier ein Graustufen-Bild welches angibt wo Pflanzen gesetzt werden dürfen. Dieses Bild hat an den Stellen eine helle Farbe an denen das Terrain flach ist. Steile Stellen sind dunkel.

Dieses Vertexbuffer-Objekt könnte man auch in jedem Frame neu mit Daten füllen. Es wäre also auch möglich einen Sichtbarkeitstest durchzuführen. Natürlich muss man dabei insbesondere bei sehr kleinen Objekten abwägen, ob es nicht schneller ist einfach alles zu rendern.

	plantCount = 1600; // insgesamt werden 1600 Pflanzen erzeugt

	// create buffer object
	glGenTextures(1, &m_tboTransform);
	glGenBuffers(1, &m_vboTransform);
	unsigned int data_size = plantCount*4*sizeof(float);
	glBindBuffer(GL_ARRAY_BUFFER, m_vboTransform);
	glBufferData(GL_ARRAY_BUFFER, data_size, NULL, GL_STATIC_DRAW);
	float* data = (float*)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
	int pos=0;
	unsigned int i = 0;
	while (i<plantCount) {
		// zufällige Position ausdenken
		CVector3f position = rndVec() * 204.0f;

		// Darf hier eine Pflanze hingesetzt werden?
		CVector2f lookup;
		lookup.x = (position.x - TERRAIN_XSHIFT) / TERRAIN_SIZE;
		lookup.y = (position.z - TERRAIN_ZSHIFT) / TERRAIN_SIZE;
		int color = getColor(image, lookup.x, lookup.y); 
		if (color < 100) { continue; }

		// Terrainhöhe bzw. y-Koordinate ermitteln
		position.y = getTerrainHeight(position.x, position.z);

		// Positionsdaten schreiben
		data[pos++] = position.x;
		data[pos++] = position.y;
		data[pos++] = position.z;

		// Skalierung abhängig von Steigung des Terrains und etwas Zufall.
		data[pos++] = 1.3f + ((color - 100) / 100.0f) * (rnd() + 2.0f); 

		++i;
	}
	glUnmapBuffer(GL_ARRAY_BUFFER);

	m_prgInstancing = 0;
	loadShaders(); // <--- sollte ja hoffentlich klar sein...

	glUseProgram(m_prgInstancing);
	glUniform1i(glGetUniformLocation(m_prgInstancing, "texLayer0"), 0);
	glUniform1i(glGetUniformLocation(m_prgInstancing, "tboTransform"), 1);

Rendern

Der folgende Code rendert alle Objekte mit einem einzigen Aufruf von glDrawElementsInstancedARB. Diverse Einstellungen bezüglich Z-Buffer und Blending habe ich hier ausgelassen, da sie nichts mit dem eigentlichen Instancing zu tun haben.

	glUseProgram(m_prgInstancing);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_pMesh->getIBO());
	glBindBuffer(GL_ARRAY_BUFFER, m_pMesh->getVBO());
	glInterleavedArrays(GL_T2F_N3F_V3F, 0, 0);

	glEnable(GL_TEXTURE_2D);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, m_pMesh->getTex());

	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_BUFFER_EXT, m_tboTransform);
	glTexBufferEXT(GL_TEXTURE_BUFFER_EXT,GL_RGBA32F_ARB, m_vboTransform);

	glDrawElementsInstancedARB(GL_TRIANGLES, m_pMesh->getFaceCount()*3, GL_UNSIGNED_INT, 0, plantCount);

Vertexshader

Hier versteckt sich der Trick. Der Shader liest mit Hilfe von gl_InstanceID Informationen zur Position und Skalierung des aktuell gerenderten Objektes aus dem Textur-Buffer-Objekt tboTransform und wendet diese auf gl_Vertex an.

#extension GL_EXT_gpu_shader4 : require
#extension GL_ARB_draw_instanced : require

uniform samplerBuffer tboTransform;

varying vec3 normal;
varying vec3 position;

void main()
{
	vec4 transform = texelFetchBuffer(tboTransform, gl_InstanceID);
	vec4 vertex = vec4((gl_Vertex.xyz * transform.w + transform.xyz),gl_Vertex.w);

	normal = normalize(gl_NormalMatrix * gl_Normal);
	position = vec3(gl_ModelViewMatrix * vertex);
	gl_Position = gl_ModelViewProjectionMatrix * vertex;

	gl_TexCoord[0] = gl_MultiTexCoord0;
}

Fragmentshader

Der Fragmentshader ist relativ langweilig. Es handelt sich um einen einfachen Per-Pixel-Lighting Shader.

uniform sampler2D texLayer0;

varying vec3 normal;
varying vec3 position;

void main()
{
	gl_FragColor = gl_FrontLightProduct[0].ambient;

	float diffuse = max(dot(gl_LightSource[0].position.xyz, normal), 0.0);
 	gl_FragColor += vec4(diffuse,diffuse,diffuse,1);

	vec4 texcolor = texture2D(texLayer0, gl_TexCoord[0].xy);
	gl_FragColor *=  texcolor;

	if (diffuse > 0.0) {
		vec3 r = normalize(
			(2.0 * dot(normal, gl_LightSource[0].position.xyz) * normal)
			- gl_LightSource[0].position.xyz 
		);
		float f = dot(r, normalize(-position));
		float specular = pow(max(f, 0.0), gl_FrontMaterial.shininess);
		gl_FragColor += gl_FrontLightProduct[0].specular * specular;
	}

	gl_FragColor.a = texcolor.a;
}