## terrain texture coordinate calculation

Moderator
Posts: 1,140
Joined: 2005.07
Post: #1
I'm creating a class to import a grayscale height map and create a terrain based on that, complete with different layers of textures to map to it and random variations on steep slopes. I have everything done, except I haven't figured out how to calculate the texture coordinates. I would like to have something more sophisticated than simply treating it like a single plane and letting the texture stretch. I figure I'll need to have a normal, right, and up vector for each triangle, and calculate it somehow from there, but I haven't been able to apply it in a way that works. Any help would be appreciated.

Edit: to clarify, I'm using 2D textures with texture combiners to interpolate between the levels, so I don't think using glTexGen will help me here like with 3D textures.
Sage
Posts: 1,199
Joined: 2004.10
Post: #2
I have code that auto generates textures along the lines of what you're asking for. The approach I take is to, for each triangle, determine what the major axis is ( positive X, negative Y, whatever ) and to apply texture coordinates using a planar projection, but clamped to whole number multiples so contiguous triangles will share tex coords.

It *mostly* works... but I'm not really happy with it yet. I use it for generating detail texture coords for normal models too, not just terrain.

I can send you my super simple code if you're interested.
Moderator
Posts: 1,140
Joined: 2005.07
Post: #3
I just thought of a way last night before I went to bed: since I start out with essentially quads that are evenly spaced in x and y, I can take the "bottom" edge and use that length as the change in s for that particular patch, and do the same thing with the y etc., and when I need to break up a triangle I can take the fraction. When I start out, I can do a lookup for the lower left corner and see if it's there for a starting point, too. I'll try it today and see if it works. If it doesn't, I'll ask for that code.
Moderator
Posts: 1,140
Joined: 2005.07
Post: #4
Well, I tried my method, and I came up with a problem: when a polygon gets rotated around, the texture coordinates end up getting mixed up, creating a strip of completely messed up texture coordinates. In that case, I would appreciate any explanation and code you have to offer on your method.

Edit: actually, the triangles can't really be rotated around, but they still get pretty screwed up. Now that I think (more) about it, I think it's from an incredible height difference (with a cliff) causing a major difference in the sample points I use to derive my texture coordinates, which are used before I split the quad up into smaller triangles. From the looks of it, your method is a generalization of my method, so it would certainly go a long way in helping me get this working. I may even be able to improve upon it by looking up the vertices I'm inserting so I can ensure that the vertex coordinates are shared.

Editx2: I have been able to fix a lot of the problems I was having, but it's obvious I can't get rid of all the distortions with my current method. Though I'm still on the market for a workable solution, I've temporarily just set the texture coordinates to a scaled version of the vertex position. I would have to say, with the rock texture I'm using, I think it looks better stretched on the cliffs, since it ends up appearing more striped along with the vertical pattern of the cliff.
Sage
Posts: 1,199
Joined: 2004.10
Post: #5
The technique I'm using produced this screenshots:

Looks good enough here:

And here:

But looks fake here:

I don't have my code at hand right now, but I can post it later today. It's stupid how simple it is, and it works pretty well so long as your texture is of a "terrain" type, which is to say it's more or less low frequency.
Moderator
Posts: 1,140
Joined: 2005.07
Post: #6
Awesome, I'll be awaiting the sample code. I love it when the best solution is simple. For low frequency, so you mean that they are repeated over a large area? Right now, I believe the repeat goes over about 4 patches, (which turns out to be quite a few polygons depending on the slope, since I split them up at discrete height levels to make sure all the vertices line up), would that be enough, or will I need to spread it over an even larger area?
Sage
Posts: 1,199
Joined: 2004.10
Post: #7
Low frequency's probably a poor way to put it. I mean textures that have a low tendency to produce visual patterning when viewed from a distance. I process all my terrain textures in photoshop to eliminate low frequency patterns.

Here's my code -- this is from my old engine, and is immediate mode ( actually, it's rendered into a display list ). I think it's probably clear enough how it works that you can write a proper VBO type implementation.

Code:
```void TriTreeNode::draw( bool lit ) {     /*         You'll never have one but not the other, so we can just         test on leftChild.     */     if ( leftChild )     {         leftChild->draw( lit );         rightChild->draw( lit );     }     else     {         /*             Generate tex coords, first.             Index B is the right-angle-corner. AC is the hypoteneuse.         */                          vec3 nA( terrain->_normals[ aIndex ] ),              nB( terrain->_normals[ bIndex ] ),              nC( terrain->_normals[ cIndex ] ),              vA( terrain->_vertices[ aIndex ] ),              vB( terrain->_vertices[ bIndex ] ),              vC( terrain->_vertices[ cIndex ] ),              vcA( lit ? terrain->_vertexColors[ aIndex ] : terrain->_ambientVertexColors[ aIndex ] ),              vcB( lit ? terrain->_vertexColors[ bIndex ] : terrain->_ambientVertexColors[ bIndex ] ),              vcC( lit ? terrain->_vertexColors[ cIndex ] : terrain->_ambientVertexColors[ cIndex ] );                      vec2 tcAPrimary, tcBPrimary, tcCPrimary, tcADetail, tcBDetail, tcCDetail;                  vec3 triangleNormal = cross( vA - vB, vC - vB ), normalBias( 1.0f, 1.0f, 1.0f );         triangleNormal.normalize();                  float scale = 1.0f;                  vec3 scaledVAPrimary = vA / scale,              scaledVBPrimary = vB / scale,              scaledVCPrimary = vC / scale;         vec3 scaledVADetail = vA / scale,              scaledVBDetail = vB / scale,              scaledVCDetail = vC / scale;                               switch( majorAxis( triangleNormal * normalBias ))         {             case PositiveX:             {                 tcAPrimary = vec2( snap( scaledVAPrimary.y ), snap( scaledVAPrimary.z ));                 tcBPrimary = vec2( snap( scaledVBPrimary.y ), snap( scaledVBPrimary.z ));                 tcCPrimary = vec2( snap( scaledVCPrimary.y ), snap( scaledVCPrimary.z ));                 tcADetail = vec2( snap( scaledVADetail.y ), snap( scaledVADetail.z ));                 tcBDetail = vec2( snap( scaledVBDetail.y ), snap( scaledVBDetail.z ));                 tcCDetail = vec2( snap( scaledVCDetail.y ), snap( scaledVCDetail.z ));                 break;             }             case NegativeX:             {                 tcAPrimary = vec2( snap( scaledVAPrimary.y ), -snap( scaledVAPrimary.z ));                 tcBPrimary = vec2( snap( scaledVBPrimary.y ), -snap( scaledVBPrimary.z ));                 tcCPrimary = vec2( snap( scaledVCPrimary.y ), -snap( scaledVCPrimary.z ));                 tcADetail = vec2( snap( scaledVADetail.y ), -snap( scaledVADetail.z ));                 tcBDetail = vec2( snap( scaledVBDetail.y ), -snap( scaledVBDetail.z ));                 tcCDetail = vec2( snap( scaledVCDetail.y ), -snap( scaledVCDetail.z ));                              break;             }             case PositiveY:             {                 tcAPrimary = vec2( snap( scaledVAPrimary.x ), snap( scaledVAPrimary.z ));                 tcBPrimary = vec2( snap( scaledVBPrimary.x ), snap( scaledVBPrimary.z ));                 tcCPrimary = vec2( snap( scaledVCPrimary.x ), snap( scaledVCPrimary.z ));                 tcADetail = vec2( snap( scaledVADetail.x ), snap( scaledVADetail.z ));                 tcBDetail = vec2( snap( scaledVBDetail.x ), snap( scaledVBDetail.z ));                 tcCDetail = vec2( snap( scaledVCDetail.x ), snap( scaledVCDetail.z ));                              break;             }             case NegativeY:             {                 tcAPrimary = vec2( snap( scaledVAPrimary.x ), -snap( scaledVAPrimary.z ));                 tcBPrimary = vec2( snap( scaledVBPrimary.x ), -snap( scaledVBPrimary.z ));                 tcCPrimary = vec2( snap( scaledVCPrimary.x ), -snap( scaledVCPrimary.z ));                 tcADetail = vec2( snap( scaledVADetail.x ), -snap( scaledVADetail.z ));                 tcBDetail = vec2( snap( scaledVBDetail.x ), -snap( scaledVBDetail.z ));                 tcCDetail = vec2( snap( scaledVCDetail.x ), -snap( scaledVCDetail.z ));                              break;             }             case PositiveZ:             {                 tcAPrimary = vec2( snap( scaledVAPrimary.x ), snap( scaledVAPrimary.y ));                 tcBPrimary = vec2( snap( scaledVBPrimary.x ), snap( scaledVBPrimary.y ));                 tcCPrimary = vec2( snap( scaledVCPrimary.x ), snap( scaledVCPrimary.y ));                 tcADetail = vec2( snap( scaledVADetail.x ), snap( scaledVADetail.y ));                 tcBDetail = vec2( snap( scaledVBDetail.x ), snap( scaledVBDetail.y ));                 tcCDetail = vec2( snap( scaledVCDetail.x ), snap( scaledVCDetail.y ));                                                  break;             }             case NegativeZ:             {                 /*                     Technically, NegativeZ shouldn't show up on a heightfield                     terrain, but I may at some point implement mushrooming.                 */                 tcAPrimary = vec2( snap( scaledVAPrimary.x ), snap( scaledVAPrimary.y ));                 tcBPrimary = vec2( snap( scaledVBPrimary.x ), snap( scaledVBPrimary.y ));                 tcCPrimary = vec2( snap( scaledVCPrimary.x ), snap( scaledVCPrimary.y ));                 tcADetail = vec2( snap( scaledVADetail.x ), snap( scaledVADetail.y ));                 tcBDetail = vec2( snap( scaledVBDetail.x ), snap( scaledVBDetail.y ));                 tcCDetail = vec2( snap( scaledVCDetail.x ), snap( scaledVCDetail.y ));                                  break;             }         }                  /*             Look up z-index for the 3d texture coordinates         */                  #warning Texture coords are ugly, collapse to one, since we're using the texture matrix for scaling                  float tcAPrimaryZ = terrain->altGradValueForPoint( vA, nA ),               tcBPrimaryZ = terrain->altGradValueForPoint( vB, nB ),               tcCPrimaryZ = terrain->altGradValueForPoint( vC, nC );                                          glNormal3fv( nC );         glMultiTexCoord3f( GL_TEXTURE0, tcCPrimary.x, tcCPrimary.y, tcCPrimaryZ );         glMultiTexCoord2fv( GL_TEXTURE1, tcCDetail );         glMultiTexCoord2fv( GL_TEXTURE2, tcCDetail );         glColor3fv( vcC );         glVertex3fv( vC );         glNormal3fv( nB );         glMultiTexCoord3f( GL_TEXTURE0, tcBPrimary.x, tcBPrimary.y, tcBPrimaryZ );         glMultiTexCoord2fv( GL_TEXTURE1, tcBDetail );         glMultiTexCoord2fv( GL_TEXTURE2, tcBDetail );         glColor3fv( vcB );         glVertex3fv( vB );         glNormal3fv( nA );         glMultiTexCoord3f( GL_TEXTURE0, tcAPrimary.x, tcAPrimary.y, tcAPrimaryZ );         glMultiTexCoord2fv( GL_TEXTURE1, tcADetail );         glMultiTexCoord2fv( GL_TEXTURE2, tcADetail );         glColor3fv( vcA );         glVertex3fv( vA );     } }```

Here's the majorAxis function:
Code:
```enum axis { NegativeX, PositiveX, NegativeY, PositiveY, NegativeZ, PositiveZ }; /**     @return the major axis of a vector. E.g., if the vector     is pointing mostly in the direction of positive y, this will     return PositiveY. And so on. */ inline axis majorAxis( const vec2 &normal ) {     vec2 an( fabsf( normal.x ), fabsf( normal.y ) );          if ( an.x > an.y )     {         return normal.x > 0.0f ? PositiveX : NegativeX;     }          return normal.y > 0.0f ? PositiveY : NegativeY; }```

And my function "snap" actually just returns the input value. I'd originally implemented it to do rounding, but that caused problems.

You'll notice how simple it is -- it's really obvious. But it "generally" works. I'm planning on revisiting it to make it always work, I'll let you know.
Moderator
Posts: 1,140
Joined: 2005.07
Post: #8
Cool, I'll give it a try. Since I posted the thread, I will be obligated to post screenshots when I'm finished. I only have one other (relatively simple) problem left to fix, and then I'll be done.
Moderator
Posts: 1,140
Joined: 2005.07
Post: #9
Well, I finished. You were right: it is ridiculously simple, much more so than I would have thought. Of course, getting the texture coordinates themselves was the easy part: before I assumed there was one texture coordinate per vertex, so I had to come up with a rather clever way to combine them and still have all my assumptions work. (by having each vertex having a list of extra texture coordinates and indices; if that point was already there when I insert them (using a set), then I'd add the new texture coordinate) Before that, my "relatively simple" problem took me more time than I thought to finish: I was trying to interpolate between 4 textures (2 pairs of flat and sloped in transition) using alpha blending instead of regular texture combiners, since I couldn't do an interpolation and preserve lighting. I ended up having to tweak not only the alphas, but the blending formula as well. (to have the first one be GL_SRC_ALPHA and GL_ZERO, then for the following ones replace GL_ZERO with GL_ONE)

Anyways, here are the promised screens.

These shots show the differences with the stretched and major axis method:

I haven't yet decided whether I prefer lack of stretching or lack of seams. It will all depend on how steep my final levels are and what is most visible. (I made a pre-processor #define and conditionals to make it easy to switch back and forth)
Sage
Posts: 1,199
Joined: 2004.10
Post: #10
Looks good!

For what it's worth, the reason I looked into the major axis method ( good name actually ) is that I had implemented "mushrooming" and the stretching when using a planar projection went too far, since I with mushrooming you can have actual vertical edges ( and overhangs ).

EDIT: One more thing, I found that using a 3d texture, with an alt-grad map works great. Well, actually, the 3D texture works poorly, since mipmapping collapses it to 2D eventually. But I'm planning on re-writing my alt-grad map implementation to interpolate between, say, 4 textures in GLSL. I'm actually really looking forward to it. Once I have GPU extruded shadows working, I'm going to port my terrain from my old codebase to my new engine...
Moderator
Posts: 1,140
Joined: 2005.07
Post: #11
Since I finished getting my method done with 2D textures, I find that I like it more than what I would have available if I used 3D textures. For one thing, mipmapping is much nicer, and I can also have any number of layers, not only a power of 2. It also makes the problem of interpolating between the sloped and non-sloped sections a lot easier, and it's easy to do in the fixed-function pipeline. Now that I have solved the problem of interpolating between the 4 textures, I have no problems with it. I'd highly recommend it. If you need any sample code for it, I can provide it, too.
Sage
Posts: 1,199
Joined: 2004.10
Post: #12
I'm waking up an old thread here, but akb825, I would like to see your interpolation code.

That being said, I want to show off and post a screenshot of my texturing technique working really nicely. I'm stoked, because I just implemented specular mapping and it's pretty sweet.

This is automatically textured using the technique I described, and while it's not by any means 100% perfect, if you pick your textures well, it's reliable enough. I've done some research on how to do reliable non-distorted tiled texturing on arbitrary topologies and, well, it turns out to be a really complex topic
Moderator
Posts: 1,140
Joined: 2005.07
Post: #13
Sorry it took me so long to get to this, but I didn't have access to my code when I first read it and forgot to reply when I did. I start by drawing all the solid textures (with no transitions) as normal. To do the blending between 2 groups of textures, I encode the LERP info as the alpha in the color array. For each texture level, I then have 4 classes of interpolations: flat and slope, current level flat and next level flat, current level slope and next level slope, and current level and next level flat and sloped. For the first 3, I bind both textures, then use the LERPed value in the alpha to allow texture combiners to do the work for me. This code sets up the texture combiners:
Code:
```glActiveTextureARB(GL_TEXTURE0_ARB); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_ARB); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB_ARB, GL_INTERPOLATE_ARB);              glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB_ARB, GL_TEXTURE0_ARB); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB_ARB, GL_SRC_COLOR);              glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB_ARB, GL_TEXTURE1_ARB); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB_ARB, GL_SRC_COLOR);              glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE2_RGB_ARB, GL_PRIMARY_COLOR_ARB); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_RGB_ARB, GL_SRC_ALPHA);              glActiveTextureARB(GL_TEXTURE1_ARB); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_ARB); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB_ARB, GL_MODULATE);              glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB_ARB, GL_PREVIOUS_ARB); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB_ARB, GL_SRC_COLOR);              glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB_ARB, GL_PRIMARY_COLOR_ARB); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB_ARB, GL_SRC_COLOR);```
In short, it tells texture 0 to interpolate between the textures set to texture unit 0 and 1 based on the alpha of the source color. Texture unit 1 is set to modulate the previous result with the base color to allow for lighting. For this set of passes, blending is turned off so the alpha doesn't also make the ground transparent.

The last case, since it involves 4 textures at once, requires more passes to complete. I also need to modify the blend function to allow it to do the LERPing for me. I essentially simply have a 4-way LERP kept in the alpha channel as usual. I then have 1 pass for each texture, each time I only bind 1 of the textures. For the first base pass, I set the blend func to GL_SRC_ALPHA, GL_ZERO. (that way I essentially start with a black background) After that, I set glDepthMask to false and set the blend func to GL_SRC_ALPHA, GL_ONE.