Wiki

Clone wiki

PolyVox / GLSL and Ogre code for triplanar texturing and multiple textures


Last tested version of PolyVox: Unknown


Some triangles created by the SurfaceExctractor will have vertices that have 2 or 3 different materials. These triangles need to be rendered extra and cannot share any vertices with neighboring triangles. so before rendering a surfacemesh with the method described in Rendering a Volume in Ogre these triangles need to be found and fixed.

there will be one of the following struct for every vertex (also to those that have 3 of the same materials).

struct MaterialAlpha {
	float material[3];
	float alpha[3];
};

for a normal triangle (normal triangle = all 3 vertices have the same material) the MaterialAlpha struct for every vertex is the same and is initalized for a material "mat" this way:

MaterialAlpha ma;
ma.material = {mat, mat, mat};
ma.alpha = {1.0/3.0, 1.0/3.0, 1.0/3.0};

this will cause the shader to interpolate between the same material ... ending up doing nothing but simply drawing the triangle (I believe ma.material[0] = mat; and ma.alpha = {1.0, 0.0, 0.0}; will do the same, but have not tried this so far.

for a triangle that has mat1 at vertex 1 and mat2 at vertices 2 and 3 the MaterialAlpha struct needs to be the same for vertices 2 and 3 but different for vertex1

Vertex1:

MaterialAlpha ma;
ma.material = {mat1, mat2, mat2};
ma.alpha = {1.0, 0.0, 0.0};

Vertices 2 and 3:

MaterialAlpha ma;
ma.material = {mat1, mat2, mat2};
ma.alpha = {0.0, 0.5, 0.5};

Note: it is utterly important that MaterialAlpha::material is exactly the same in all 3 vertices of a triangle. otherwise horrible things happen.

so... to some rendering code:

// extract region "reg" from volume "m_Volume" into SurfaceMesh "mesh"
PolyVox::SurfaceExtractor<PolyVox::MaterialDensityPair44> extr(&m_Volume, reg, &mesh);
extr.execute();
if(mesh.isEmpty()) {
	// you should exit here
}
// easier access
std::vector<PolyVox::PositionMaterialNormal>& vertices = mesh.m_vecVertices;
std::vector<uint32_t>& indices = mesh.m_vecTriangleIndices;

Up to here it doesn't matter how you obtain your mesh, but I showed it anyway.

Now we need to create the MaterialAlpha array containing a MaterialAlpha struct per vertex. we will also be creating 3 new vertices per problematic triangle (triangle with 2 or 3 materials) the index buffer will not change size. there will be exactly as many triangles as there were before, just some of the indexes will be adjusted to point to new vertices.

study this carefully if you want to understand what I'm doing

assert(indices.size() % 3 == 0); // I never had problems with this, I just wanted to really really be on the safe side

// find all triangles that contain more than 1 texture and create new vertices for them.
std::vector<MaterialAlpha> vertex_extra_data(vertices.size());
for(uint32_t i = 0; i < indices.size(); i+=3) {
	uint32_t& i0 = indices.at(i);
	uint32_t& i1 = indices.at(i+1);
	uint32_t& i2 = indices.at(i+2);
	const PolyVox::PositionMaterialNormal vertex0 = vertices.at(i0);
	const PolyVox::PositionMaterialNormal vertex1 = vertices.at(i1);
	const PolyVox::PositionMaterialNormal vertex2 = vertices.at(i2);

	MaterialAlpha ma;
	ma.material = {vertex0.getMaterial(), vertex1.getMaterial(), vertex2.getMaterial()};
	if(vertex0.getMaterial() != vertex1.getMaterial()) {
		if(vertex0.getMaterial() != vertex2.getMaterial()) {
			// all 3 vertices have different materials
			ma.alpha = {1.0, 0.0, 0.0};
			vertex_extra_data.push_back(ma);
			i0 = vertices.size();
			vertices.push_back(vertex0);

			ma.alpha = {0.0, 1.0, 0.0};
			vertex_extra_data.push_back(ma);
			i1 = vertices.size();
			vertices.push_back(vertex1);

			ma.alpha = {0.0, 0.0, 1.0};
			vertex_extra_data.push_back(ma);
			i2 = vertices.size();
			vertices.push_back(vertex2);
		} else {
			// vertex 0 and 2 have the same material
			ma.alpha = {0.5, 0.0, 0.5};
			vertex_extra_data.push_back(ma);
			i0 = vertices.size();
			vertices.push_back(vertex0);

			ma.alpha = {0.0, 1.0, 0.0};
			vertex_extra_data.push_back(ma);
			i1 = vertices.size();
			vertices.push_back(vertex1);

			ma.alpha = {0.5, 0.0, 0.5};
			vertex_extra_data.push_back(ma);
			i2 = vertices.size();
			vertices.push_back(vertex2);
		}
	} else {
		if(vertex0.getMaterial() != vertex2.getMaterial()) {
			// vertex 0 and 1 have same material
			ma.alpha = {0.5, 0.5, 0.0};
			vertex_extra_data.push_back(ma);
			i0 = vertices.size();
			vertices.push_back(vertex0);

			ma.alpha = {0.5, 0.5, 0.0};
			vertex_extra_data.push_back(ma);
			i1 = vertices.size();
			vertices.push_back(vertex1);

			ma.alpha = {0.0, 0.0, 1.0};
			vertex_extra_data.push_back(ma);
			i2 = vertices.size();
			vertices.push_back(vertex2);
		} else {
			// all are equal
			ma.alpha = {1.0/3.0, 1.0/3.0, 1.0/3.0};
			vertex_extra_data[i0] = ma;
			vertex_extra_data[i1] = ma;
			vertex_extra_data[i2] = ma;
		}

	}
}

begin filling the ogre manual object buffer with our vertices. we are "abusing" the texture Coordinate buffer to pass all 3 materials and texture coordinates to the vertex shader and fragment/pixel shader

// get the ogremap ready
m_ogreMap->begin("ApeTexture", Ogre::RenderOperation::OT_TRIANGLE_LIST);


// prepare the buffers
m_ogreMap->estimateVertexCount(vertices.size());
m_ogreMap->estimateIndexCount(indices.size());
for(uint32_t i = 0; i < vertices.size(); i++) {
	const PolyVox::PositionMaterialNormal& vertex = vertices.at(i);
	const MaterialAlpha& ma = vertex_extra_data.at(i);
	PolyVox::Vector3DFloat vertex_pos = vertex.getPosition() + chunk_pos;
	m_ogreMap->position(vertex_pos.getX(), vertex_pos.getY(), vertex_pos.getZ());
	m_ogreMap->normal(vertex.getNormal().getX(), vertex.getNormal().getY(), vertex.getNormal().getZ());
	float x = vertex_pos.getX();
	float y = vertex_pos.getY();
	float z = vertex_pos.getZ();
	const float faktor = 1;
	for(int j = 0; j < 3; j++) {
		m_ogreMap->textureCoord(x/faktor, y/faktor, z/faktor, ma.alpha[j]);
	}
	m_ogreMap->textureCoord(ma.material[0], ma.material[1], ma.material[2]);
}

and dump the index buffer to the ogre manual object. This doesn't change.

for(uint32_t i = 0; i < indices.size(); i++) {
	const uint32_t& index = indices.at(i);
	m_ogreMap->index(index);
}

now, as you might have seen in the c++ code, I'm using a texture called "ApeTexture". This is an ogre texture using my glsl conversion of ape's triplanar hlsl shader.

I'm using some free high resolution textures I found on the internet. You can use any textures here, of different size, type, whatever. every texture_unit may have custom parameters. they are completely independent of each other. In the future I might look into texture arrays, but currently this works just perfectly so there's no reason to.

vertex_program TriplanarVS glsl
{
	source triplanar_vertex_shader.txt
}
fragment_program TriplanarPS glsl
{
	source triplanar_pixel_shader.txt
}

material ApeTexture
{
	technique
	{
		pass
		{
			vertex_program_ref TriplanarVS
			{
			}

			fragment_program_ref TriplanarPS
			{
				param_named lightDirection float4 1 -0.7 1 0
				param_named tex0 int 0
				param_named tex1 int 1
				param_named tex2 int 2
				param_named tex3 int 3
				param_named tex4 int 4
			}
			texture_unit 0
			{
				texture Concrete_15_UV_H_CM_1.jpg 2d
			}
			texture_unit 1
			{
				texture Ground_09_UV_H_CM_1.jpg 2d
			}
			texture_unit 2
			{
				texture Lead_03_UV_H_CM_1.jpg 2d
			}
			texture_unit 3
			{
				texture Rock_02_UV_H_CM_1.jpg 2d
			}
			texture_unit 4
			{
				texture Snow_01_UV_H_CM_1.jpg 2d
			}
		}
	}
}

first you need to create the file "triplanar_vertex_shader.txt":

void main() {
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
    gl_TexCoord[0] = vec4(normalize( gl_Normal), 0.0);

    gl_TexCoord[1] = vec4(gl_MultiTexCoord0);
    gl_TexCoord[2] = vec4(gl_MultiTexCoord1);
    gl_TexCoord[3] = vec4(gl_MultiTexCoord2);

    gl_TexCoord[4] = vec4(gl_MultiTexCoord3);
}

this vertex shader basically does nothing. it just forwards the data to the pixel shader (properly adjusted to your current camera position).

and the "triplanar_pixel_shader.txt":

uniform vec4 lightDirection;
uniform sampler2D tex0;
uniform sampler2D tex1;
uniform sampler2D tex2;
uniform sampler2D tex3;
uniform sampler2D tex4;
uniform sampler2D tex5;
uniform sampler2D tex6;
uniform sampler2D tex7;
uniform sampler2D tex8;
uniform sampler2D tex9;
uniform sampler2D tex10;
uniform sampler2D tex11;
uniform sampler2D tex12;
uniform sampler2D tex13;
uniform sampler2D tex14;
uniform sampler2D tex15;

struct threecolor
{
	vec4 col0;
	vec4 col1;
	vec4 col2;
};

threecolor getColor(vec4 texCoord, float texId)
{
	vec2 coord1 = texCoord.yz;
    vec2 coord2 = texCoord.zx;
    vec2 coord3 = texCoord.xy;
	threecolor ret;
	if(texId < 0.5) {
		ret.col0 = texture2D( tex0, coord1) * texCoord.w;
		ret.col1 = texture2D( tex0, coord2) * texCoord.w;
		ret.col2 = texture2D( tex0, coord3) * texCoord.w;
	} else if(texId < 1.5) {
		ret.col0 = texture2D( tex1, coord1) * texCoord.w;
		ret.col1 = texture2D( tex1, coord2) * texCoord.w;
		ret.col2 = texture2D( tex1, coord3) * texCoord.w;
	} else if(texId < 2.5) {
		ret.col0 = texture2D( tex2, coord1) * texCoord.w;
		ret.col1 = texture2D( tex2, coord2) * texCoord.w;
		ret.col2 = texture2D( tex2, coord3) * texCoord.w;
	} else if(texId < 3.5) {
		ret.col0 = texture2D( tex3, coord1) * texCoord.w;
		ret.col1 = texture2D( tex3, coord2) * texCoord.w;
		ret.col2 = texture2D( tex3, coord3) * texCoord.w;
	} else if(texId < 4.5) {
		ret.col0 = texture2D( tex4, coord1) * texCoord.w;
		ret.col1 = texture2D( tex4, coord2) * texCoord.w;
		ret.col2 = texture2D( tex4, coord3) * texCoord.w;
	} else {
		// white
		ret.col0.x = 1.0;
		ret.col0.y = 1.0;
		ret.col0.z = 1.0;
		ret.col0.w = 1.0;
		ret.col1 = ret.col0;
		ret.col2 = ret.col0;
	}
	return ret;
}

void main() {
    vec3 absNormal;
    vec3 blend_weights;
    vec2 coord1;
    vec2 coord2;
    vec2 coord3;
	vec3 texIds = gl_TexCoord[4].xyz;
	vec4 blended_color;
    vec4 light;
    float ldn;
    float ambient = 0.200000;

	threecolor col0 = getColor( gl_TexCoord[1], texIds.x );
	threecolor col1 = getColor( gl_TexCoord[2], texIds.y );
	threecolor col2 = getColor( gl_TexCoord[3], texIds.z );
    threecolor col;

	col.col0 = col0.col0 + col1.col0 + col2.col0;
	col.col1 = col0.col1 + col1.col1 + col2.col1;
	col.col2 = col0.col2 + col1.col2 + col2.col2;

    absNormal = abs( gl_TexCoord[0].xyz );
    blend_weights = absNormal;
    blend_weights = (blend_weights - 0.267900);
    blend_weights = max( blend_weights, vec3( 0.000000));
    blend_weights /= vec3( ((blend_weights.x  + blend_weights.y ) + blend_weights.z ));
    blended_color = (col.col0  * blend_weights.x ) + (col.col1  * blend_weights.y ) + (col.col2  * blend_weights.z );
    light = ( -normalize( lightDirection ) );
    ldn = max( 0.000000, dot( light, gl_TexCoord[0]));
    gl_FragData[0] = vec4( blended_color * (ambient + ldn) );
}

there is one sampler2D for every texture. I'm not sure where the limit is. reading the docs of glsl and opengl I believe that for most new graphics cards you should be fine as long as you've got less than or equal to 16 textures.

currently this supports only 3 textures, but you can expand the getColor function to fit your needs. any unknown textures will be drawn white, so if you see white stuff you probably need to adjust the pixelshader

please ask any questions about this in the forum. it was quite messy to get this shader to work the way it does, and it might be unreadable (I know what shaders are since about... yesterday).

this vertex shader basically does nothing. it just forwards the data to the pixel shader (properly adjusted to your current camera position).

Updated