Skip to main content
GameDev.net gamedev.net
🔒 Locked

[Request for help] Texture mapping a subdivided icosahedron

Started by IainC Sep 27, 2002 at 4:55 AM 21 replies 17.5k views
Original Post
IainC
IainC
Hi all, I'm using a subdivided icosahedron as an approximation to a sphere; I'm doing it this way instead of using a quadric because I'm going to want to deform the sphere and this method gives me uniform point distribution. The technique is described in example 2-13 of the Red Book. The problem I'm having is texturing the thing. I want to be able to pass a function the normal of the current point and have it pass back appropriate UV coords.
          
// ******************** DEFINES ********************


#define X_BASE 0.525731112119133606f
#define Z_BASE 0.850650808352039932f

// ******************** STRUCTS ********************


struct Vector3f {
	GLfloat x, y, z;

	void Set(GLfloat m_x, GLfloat m_y, GLfloat m_z) {
		x = m_x; y = m_y; z = m_z;
	}
};

// ******************** CLASS DEFINITION ********************


class Icosahedron {
public:
	Icosahedron();
	~Icosahedron();

	void Init(GLfloat m_radius, int subdivisions);
	void Render();

private:
	void Normalize(float v[3]);
	void DrawTriangle(float *v1, float *v2, float *v3);
	void Subdivide(float *v1, float *v2, float *v3, long depth);
	void GetTextureCoord(ASEVector3f *normal, float *targetU, float *targetV);

	GLuint displayList;
	GLfloat radius;
};

// ******************** CLASS IMPLEMENTATION ********************


GLfloat vdata[12][3] = {
  {-X_BASE,0.0,Z_BASE},{X_BASE,0.0,Z_BASE},{-X_BASE,0.0,-Z_BASE},{X_BASE,0.0,-Z_BASE},
  {0.0,Z_BASE,X_BASE},{0.0,Z_BASE,-X_BASE},{0.0,-Z_BASE,X_BASE},{0.0,-Z_BASE,-X_BASE},
  {Z_BASE,X_BASE,0.0},{-Z_BASE,X_BASE,0.0},{Z_BASE,-X_BASE,0.0},{-Z_BASE,-X_BASE,0.0}};

GLuint tindices[20][3] = {
  {1,4,0},{4,9,0},{4,5,9},{8,5,4},{1,8,4},
  {1,10,8},{10,3,8},{8,3,5},{3,2,5},{3,7,2},
  {3,10,7},{10,6,7},{6,11,7},{6,0,11},{6,1,0},
  {10,1,6},{11,0,9},{2,11,9},{5,2,9},{11,2,7}};

Icosahedron::Icosahedron() {
}

Icosahedron::~Icosahedron() {
}

void Icosahedron::Init(GLfloat m_radius, int subdivisions) {
	radius = m_radius;
	displayList = glGenLists(1);
	glNewList(displayList, GL_COMPILE);
	for (int i = 0; i < 20; i++) {
	   Subdivide(&vdata[tindices[i][0]][0], &vdata[tindices[i][1]][0], &vdata[tindices[i][2]][0], subdivisions);
	}
	glEndList();
}

void Icosahedron::Render() {
	glScalef(radius, radius, radius);
	glCallList(displayList);
}

void Icosahedron::DrawTriangle(float *v1, float *v2, float *v3) {
	Vector3f point;
	GLfloat texU, texV;

	glBegin(GL_TRIANGLES);
		point.Set (v1[0], v1[1], v1[2]);
		glNormal3f (point.x, point.y, point.z);		// (Points are already normalised)

		GetTextureCoord (&point, &texU, &texV);
		glTexCoord2f(texU, texV);
		glVertex3f (point.x, point.y, point.z);

		point.Set (v2[0], v2[1], v2[2]);
		glNormal3f (point.x, point.y, point.z);
		GetTextureCoord (&point, &texU, &texV);
		glTexCoord2f(texU, texV);
		glVertex3f (point.x, point.y, point.z);

		point.Set (v3[0], v3[1], v3[2]);
		glNormal3f (point.x, point.y, point.z);
		GetTextureCoord (&point, &texU, &texV);
		glTexCoord2f(texU, texV);
		glVertex3f (point.x, point.y, point.z);
	glEnd();
}

void Icosahedron::GetTextureCoord(ASEVector3f *normal, float *targetU, float *targetV) {
	// ****************************************

	// ****************************************

	// IT'S THIS FUNCTION I CAN'T GET TO WORK!!

	// ****************************************

	// ****************************************

}

void Icosahedron::Normalize(float v[3]) {
   GLfloat d = (float)sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
   if (d == 0.0) return;
   v[0] /= d; v[1] /= d; v[2] /= d;
}

void Icosahedron::Subdivide(float *v1, float *v2, float *v3, long depth) {
   GLfloat v12[3], v23[3], v31[3];
   GLint i;

   if (depth == 0) {
      DrawTriangle(v1, v2, v3);
      return;
   }
   for (i = 0; i < 3; i++) {
      v12[i] = v1[i]+v2[i];
      v23[i] = v2[i]+v3[i];
      v31[i] = v3[i]+v1[i];
   }
   Normalize(v12);
   Normalize(v23);
   Normalize(v31);
   Subdivide(v1, v12, v31, depth-1);
   Subdivide(v2, v23, v12, depth-1);
   Subdivide(v3, v31, v23, depth-1);
   Subdivide(v12, v23, v31, depth-1);
}

  
Any thoughts? Thanks in advance for any help anyone can provide! Brgrds, IainC www.coldcity.com code, pics, life Grr I can't make the source tag keep my formatting... [edited by - IainC on September 27, 2002 6:00:20 AM]
[size="2"]www.coldcity.com code, art, life
vincoof
vincoof
Convert your normals from Cartesian coordinates (x, y, z) to Spherical coordinates (rho, theta, phi) and then use theta and phi as your U and V coordinates. Note that during the conversion, rho should be the constant 1 if your normals are already normalized.

Note: you may need some extra work to get seamless texturing in the top/down/left/right edges of the texture.
IainC
IainC
vincoof,

I already tried that, with the following:


  
void Icosahedron::GetTextureCoord(Vector3f *normal, float *targetU, float *targetV) {
*targetV = (float)atan(normal->y/normal->x);
*targetU = (float)(sqrt((normal->x*normal->x) + (normal->y*normal->y)))/normal->z;
}


But no joy.. I was using a texture that should have worked seamlessly:



But I just got horrible effects:



I wonder if maybe my texture options are at fault; I''m using:


  
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, 4, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);


(apologies if posting pictures is too much for people''s bandwidth)

Any further ideas??

Many thanks for your help.
[size="2"]www.coldcity.com code, art, life
IainC
IainC
I know the first thing someone''s going to say is "use GL_CLAMP, your texture is repeating" but the effect I get then is just as disturbing
[size="2"]www.coldcity.com code, art, life
Enigma
Enigma
Ok, can't guarantee this'll work since i'm working by observation (of the pictures you posted) and paper, but try this for your method:

    
void Icosahedron::GetTextureCoord(Vector3f *normal, float *targetU, float *targetV){
float normalisedX = 0;
float normalisedZ = -1;
if (((normal->x * normal->x) + (normal->z * normal->z)) > 0){
normalisedX = (normal->x * normal->x) / ((normal->x * normal->x) + (normal->z * normal->z));
normalisedZ = (normal->z * normal->z) / ((normal->x * normal->x) + (normal->z * normal->z));
}
if (normalisedZ == 0){
*targetU = ((normalisedX * PI) / 2);
}
else {
*targetU = atan(normalisedX / normalisedZ);
if (normalisedZ < 0){
*targetU += PI;
}
if (*targetU < 0){
*targetU += 2 * PI;
}
}
*targetU /= 2 * PI;
*targetV = (-normal->y + 1) / 2;
}


Explanation:

First, moving radially around the planet we want to map texture coords so that at longitude 0º we have texture coord U = 0, and at longitude xº we have texture coord U = x/360. This is independant of latitude, so we ignore the normal's y component and re-normalise the x/z components. We also want a default value for the north and south poles so the these points map to the top centre and bottom centre of the texture.

Next we need to map to the latitude. The texture you provided appears to be distorted such that it could be projected directly onto a globe. That is to say that the equator is expanded and the poles are reduced such that the equators, when projected, would retain their size due to the relative flatness of the equator while the poles would be stretched. This means that y coordinates can be mapped very easily simply by mapping the -1 -> 1 range linearly onto a 0 -> 1 range.

As I said, I haven't tested this but give it a go and see what it does!

Enigma

EDIT: spelling

[edited by - enigma on September 27, 2002 8:54:32 AM]
vincoof
vincoof
IainC: I''m afraid that your equation is not correct, especially because you should have taken care of the fact that texture coordinates should be expressed in the [0,1] range. For instance, atan returns a value in the [-pi/2,+pi/2] range !
Anyway, you invented the wave effect. That''s cool enough !

Enigma: your equations seem correct. Even though I think it still does not correct seams, it should give better results than above.
vincoof
vincoof
BTW, you''re calling glTexParameter and glTexImage2D correctly. That is not where the problem comes from.
IainC
IainC
vincoof: Ah, of course! I see what you mean about the range being wrong now I look at it. Thanks for checking my texture settings.

Enigma: Thanks a lot for providing that code - I''ll be damn impressed if it works, I can''t code anything without at least a brief compile/crash/debug cycle Great explanation too.

Now I''ve looked at the maths in more detail I agree that Enigma''s code looks good - will go try it and post the result.
[size="2"]www.coldcity.com code, art, life
IainC
IainC
Sorry for the big image!

Enigma''s function:


Hmmm - It''s certainly a lot closer.

I''ve put some axes on the pics, R=x, G=y, B=z.

Notice that the polar regions are in the right place - getting some wierd kaleidoscope-esque repetition and distortion.

The texturemap''s being applied at least partially correctly, it''s just not falling over the points quite as it should...

I tried editing my original function so that the [-pi/2, pi/2] range was scaled to [0, 1] but the results were remarkably similar to Enigma''s results - there''s something more wrong than just not lining up seamlessly, due to the wierd tesselations.

Hmmmm...

Any further thoughts guys? Really appreciate the help from you both.

www.coldcity.com
code, pics, life
[size="2"]www.coldcity.com code, art, life
Enigma
Enigma
I think all the problems lie in the renormalisation I did. Firstly, I forgot to square root after squaring and dividing, so the magnitudes would be slightly out. I also didn''t take into account the direction after renormalising - I just used the magnitude. Try this instead:

  
void Icosahedron::GetTextureCoord(Vector3f *normal, float *targetU, float *targetV){
float normalisedX = 0;
float normalisedZ = -1;
if (((normal->x * normal->x) + (normal->z * normal->z)) > 0){
normalisedX = sqrt((normal->x * normal->x) / ((normal->x * normal->x) + (normal->z * normal->z)));
if (normal->x < 0){
normalisedX = -normalisedX;
}
normalisedZ = sqrt((normal->z * normal->z) / ((normal->x * normal->x) + (normal->z * normal->z)));
if (normal->z < 0){
normalisedZ = -normalisedZ;
}
}
if (normalisedZ == 0){
*targetU = ((normalisedX * PI) / 2);
}
else {
*targetU = atan(normalisedX / normalisedZ);
if (normalisedZ < 0){
*targetU += PI;
}
if (*targetU < 0){
*targetU += 2 * PI;
}
}
*targetU /= 2 * PI;
*targetV = (-normal->y + 1) / 2;
}


Enigma
vincoof
vincoof
And maybe you should replace :

  if (normalisedZ < 0){
*targetU += PI;
}
with:
  if (normalisedZ < 0){
*targetU = PI - *targetU;
}
Enigma
Enigma
vincoof:

I don't think so. The equations to that point should leave the values in the range 0 -> PI/2 */* -PI/2 -> PI/2 */* -PI/2 -> 0 where */* indicates that value change at the functions tangents. By then adding PI to the middle section you get a distribution of 0 -> PI/2 */* PI/2 -> 3PI/2 */* -PI/2 -> 0. Then adding 2PI if the value is < 0 maps the distribution to 0 -> PI/2 */* PI/2 -> 3PI/2 */* 3PI/2 -> 2PI.

Your proposed modification would leave the distribution as 0 -> PI/2 */* 3PI/2 -> PI/2 */* 3PI/2 -> 0, reversing the texture on one side of the planet and adding extra seams either side.

At least I *think* that's what would happen!

Enigma

EDIT: spelling

[edited by - enigma on September 27, 2002 12:13:16 PM]
vincoof
vincoof
Aw sorry I made a mistake. Thanks for poiiting it out. It was too hurried.
I meant :

  
if (normalisedX < 0){
*targetU = PI - *targetU;
}
IainC
IainC
Almost!

Now I have (with Enigma''s function):



Vincoof - I couldn''t quite see where you meant to put that line - doing this gives me more bizarreness:


  
float normalisedX = 0;
float normalisedZ = -1;
if (((normal->x * normal->x) + (normal->z * normal->z)) > 0){
normalisedX = sqrt((normal->x * normal->x) / ((normal->x * normal->x) + (normal->z * normal->z)));
if (normal->x < 0){
normalisedX = -normalisedX;
}
normalisedZ = sqrt((normal->z * normal->z) / ((normal->x * normal->x) + (normal->z * normal->z)));
if (normal->z < 0){
normalisedZ = -normalisedZ;
}
}
if (normalisedZ == 0){
*targetU = ((normalisedX * ASE_PI) / 2);
} else if (normalisedX < 0){
*targetU = ASE_PI - *targetU;
} else {
*targetU = atan(normalisedX / normalisedZ);
if (normalisedZ < 0){
*targetU += ASE_PI;
//*targetU = ASE_PI - *targetU;

}
if (*targetU < 0){
*targetU += 2 * ASE_PI;
}
}
*targetU /= 2 * ASE_PI;
*targetV = (-normal->y + 1) / 2;


So the only problem that remains is correcting those two seams; would it make it easier if I went for square textures instead? As I realised that that''s going to be what I need to do eventually anyway.... (DOH)

If I followed your explanation correcting I''m suspecting that all I need do to change to square textures is change the last line from
  *targetV = (-normal->y + 1) / 2;  
to
  *targetV = -normal->y / 2;  


I tried to fiddle Enigma''s function so that the seams were corrected, but got nowhere

Thanks again for your continued help guys.

Brgds
I.
[size="2"]www.coldcity.com code, art, life
Enigma
Enigma
I''m not sure what you mean by ''square textures''. In what way is your current texture not square? (I realize the image is a rectangle, but assumed that you would have stretched it to a square texture).

The first seam (zigzagging line) exists because the texture coordinates around the planet are distributed as follows: The first X coordinate is zero or a small positive fraction. The X coordinates then increase linearly around the planet until we reach the point we started. Unfortunately the point we started has a texture coordinate which is about zero so we get a steep drop in coordinates here. What we wanted was to wrap around from one edge of the image to the other, but OpenGL doesn''t know this and instead draws nearly the entire texture ''backwards''. The solution is to spot the seam and give the initial vertex different texture coords according to which polygon it is part of. For the first polygon you give it the normal X coordinate. For the final polygon you give it the X coordinate + 1.

Without going through your source codes more thoroughly than I''ve had time to thus far, I can''t say how to implement this in you class.

As for the second seam - It shouldn''t be there! something has gone wrong! I''ll have a look at it and hopefully I''ll have an answer sometime tomorrow.

Enigma
vincoof
vincoof
My modification should be used with Enigma''s previous method.
Anyhow I''m glad that the current method is correct (well, almost).

Now that the seams are here, it''s probably the most difficult part. To be honest, I don''t know a "simple" method. The only one I would dare using is detecting the polygons that are responsible of the seams and apply specific texture coordinates on them.
Waverider
Waverider
That looks so dang cool

Post a pic of the final product when it lines up the way you like it!

[edited by - Waverider on September 27, 2002 7:32:50 PM]
It's not what you're taught, it's what you learn.
IainC
IainC
Enigma: Please ignore my "square texture" tangent, I''m totally confusing the issue and inventing problems where there are none. The reason for the zigzag seam is obvious (now you''ve pointed it out!) - I''ll work on fixing that.

Waverider: I will I think it looks pretty cool in its messed-up incarnations

There is a point to all this, incidentally - eventually this thing won''t even be texturemapped, but to do what I want to I need a way of reliably getting UVs onto a two-dimensional grid, so perfecting it with something as visual as a texture seemed the way to go.

From that you''ve probably figured out I''m working on procedural planets and the 2D grid is actually a noise function that returns the same values when given the same inputs (so no storage of the noise required).

The Icosahedron class below is a simplification of what I''m actually using, which already includes LOD, but the tex coord generation still applies.

Thanks again all,
I.
[size="2"]www.coldcity.com code, art, life
Enigma
Enigma
Well, here''s a (pretty horrible) hack that''ll fix the seams:

  
void Icosahedron::DrawTriangle(float *v1, float *v2, float *v3) {
Vector3f point;
GLfloat texU, texV;
GLfloat texU1, texU2, texU3;
GLfloat texAdd1 = 0;
GLfloat texAdd2 = 0;
GLfloat texAdd3 = 0;

// determine if triangle spans

point.Set (v1[0], v1[1], v1[2]);
GetTextureCoord (&point, &texU1, &texV);
point.Set (v2[0], v2[1], v2[2]);
GetTextureCoord (&point, &texU2, &texV);
point.Set (v3[0], v3[1], v3[2]);
GetTextureCoord (&point, &texU3, &texV);

if (texU2 - texU1 > 0.2 || texU3 - texU1 > 0.2){
texAdd1 = 1;
}
if (texU1 - texU2 > 0.2 || texU3 - texU2 > 0.2){
texAdd2 = 1;
}
if (texU1 - texU3 > 0.2 || texU2 - texU3 > 0.2){
texAdd3 = 1;
}


glBegin(GL_TRIANGLES);
point.Set (v1[0], v1[1], v1[2]);
glNormal3f (point.x, point.y, point.z); // (Points are already normalised)

GetTextureCoord (&point, &texU, &texV);
glTexCoord2f(texU + texAdd1, texV);
glVertex3f (point.x, point.y, point.z);
point.Set (v2[0], v2[1], v2[2]);
glNormal3f (point.x, point.y, point.z);
GetTextureCoord (&point, &texU, &texV);
glTexCoord2f(texU + texAdd2, texV);
glVertex3f (point.x, point.y, point.z);
point.Set (v3[0], v3[1], v3[2]);
glNormal3f (point.x, point.y, point.z);
GetTextureCoord (&point, &texU, &texV);
glTexCoord2f(texU + texAdd3, texV);
glVertex3f (point.x, point.y, point.z);
glEnd();
}


It could be optimised, but since it''s such a hack there''s no point. The value 0.2 is just a value < 1 that is guarenteed to be greater that the difference between any two correct texture coordinates.

It does work - I''ve just spent the last hour hacking various bits of old code together to get a simple OpenGL app to display it. It fixes both problems - I still don''t know what caused the second one but nevermind!

Enigma
Enigma
Enigma
Just spotted what was causing the second seam. In the GetTextureCoord method, this:

  
if (normalisedZ == 0){
*targetU = ((normalisedX * PI) / 2);
}
else {
*targetU = atan(normalisedX / normalisedZ);
if (normalisedZ < 0){
*targetU += PI;
}
if (*targetU < 0){
*targetU += 2 * PI;
}
}

should have been:

  
if (normalisedZ == 0){
*targetU = ((normalisedX * PI) / 2);
}
else {
*targetU = atan(normalisedX / normalisedZ);
if (normalisedZ < 0){
*targetU += PI;
}
}
if (*targetU < 0){
*targetU += 2 * PI;
}

The first if will set targetU to +/- PI/2. The -PI/2 should then have been transformed to 3PI/2 by adding 2PI, but I put the code to add 2PI inside the else clause so for this one case it didn''t get executed.

Enigma

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.