Accuracy problem when applying self built rotation matrices
Hi all,
im using Managed DirectX (with c#) and ive been working on rotating a shape in 3d world co-ordinates around its own origin x degrees clockwise (thats a 2D rotation). now this seemed to be working fine (ill show the code below) until i applied a simple, predictable angle to check accuracy: 90 degrees clockwise.
the shape (in this case a simple triangle) was rotated approx 89 degrees clockwise and is noticably misaligned. the same happens with 270 degrees (its about 271 degrees). 180 degrees and 0 degrees work fine, and this gives me a clue as to where the problem lies...
i am using the following solution to the problem.
1. translate shape so that the shapes centre (origin) is equal to world space origin.
2. rotate x degree's. this rotates around world space's origin, thereby also rotating around the shapes origin.
3. translate shape back to its original position but with the new rotation.
parts 1 and 3 work fine.
part 2 does not, and i think this is the problem. here are the matrices i am building: (note, i am transposing the finished matrices because directX uses single rows instaed of single collumns when multiplying matrices)
//Translate to Origin Matrix
Matrix TMat = new Matrix();
TMat.M11 = 1;
TMat.M12 = 0;
TMat.M14 = -(float)CentreX;
TMat.M21 = 0;
TMat.M22 = 1;
TMat.M24 = -(float)CentreY;
TMat.M31 = 0;
TMat.M32 = 0;
TMat.M34 = -(float)CentreZ;
TMat.M33 = 1;
TMat.M44 = 1;
TMat.Transpose(TMat);
//Rotation Matrix
Matrix Rmat = new Matrix();
Rmat.M11 = (float)Math.Cos((RotX * (Math.PI / 180)));
Rmat.M12 = -(float)Math.Sin((RotX * (Math.PI / 180)));
Rmat.M21 = (float)Math.Sin((RotX * (Math.PI / 180)));
Rmat.M22 = (float)Math.Cos(RotX * (Math.PI / 180));
//Make last 2 rows indentity rows.
Rmat.M33 = 1;
Rmat.M44 = 1;
Rmat.Transpose(Rmat);
//Translate from origin to position
Matrix TMat2 = new Matrix();
TMat2.M11 = 1;
TMat2.M12 = 0;
TMat2.M14 = (float)CentreX;
TMat2.M21 = 0;
TMat2.M22 = 1;
TMat2.M24 = (float)CentreY;
TMat2.M31 = 0;
TMat2.M32 = 0;
TMat2.M34 = (float)CentreZ;
TMat2.M33 = 1;
TMat2.M44 = 1;
TMat2.Transpose(TMat2);
dev.Transform.World = TMat * Rmat * TMat2;
if we look at the middle matrix, which is applying the rotation, you can see part of it is in the form of :
Cos theta, -(Sin theta)
Sin theta, Cos theta this being the matrix for this particular rotation
i.e.:
Rmat.M11 = (float)Math.Cos((RotX * (Math.PI / 180)));
Rmat.M12 = -(float)Math.Sin((RotX * (Math.PI / 180)));
Rmat.M21 = (float)Math.Sin((RotX * (Math.PI / 180)));
Rmat.M22 = (float)Math.Cos(RotX * (Math.PI / 180));
i think this is the problem.
more specifically, i think that M11 and M22 are the problems, because when using angles of 0 degrees and 180 degrees, the result of the math is + or - 1, and the "important" matrix indexes are M12 and M21 which are stupidly long numbers with loads decimals! However, using 90 degrees or 270 degrees shifts the "importance" to the rows where Cos is applied.
So does any1 have any idea why the rottion i misaligned? is it to do with the Math.Cos function, to do with loss of accuracy in the cast from double to float or...what?! or is there a better way to do all this?
cheers for your help guys,
james
EDIT: Heres something strange...if i apply an angle of exactly 88.25, i get a 90 degree rotaton.....
and actually, if i apply an angle of 90, and move the shap closer and closer to world space origin i get a better fit, unil i actually get a true angle. go further than the origin, and i get it misaligned, but "bent" the other way i u see wot i mean... i guess this cud mean the problem is with the translaton as well....
oh, just incidently, when im actually applying a dynamic angle to the rotation, i will be uing the folowing code:
private float GetAngle(float X, float Y)
{
return ((float)Math.Atan(Y / X)) * (float)(360 / 2.5);// - (float) Math.Atan(Y / X)));
}
apart from when the angle is 90 or 270, theres seems to be no problem with this.
I'm not sure if this is the case in your code but have you considered dividing by 180.0 instead of 180? If you divide by integers the result will be an integer. Perhaps this is not it because PI is probably a floating-point, but you can try.
Greetz,
Illco
Greetz,
Illco
Hey
First I don't see the reason for creating transposed matrix manually, and then transposing it back to true directx form. You can create it right the first time.
DirectX matrix
M11 M12 M13 M14
M21 M22 M23 M24
M31 M32 M33 M34
M41 M42 M43 M44
Rotational part
M11 M12 M13
M21 M22 M23
M31 M32 M33
for example rotation about Z axis
Translational part
M41 M42 M43 (X Y Z)
If you use it like this there is no need to call Transpose function.
There is nothing wrong with your calcs.
If you don't translate your shape to origin (X=0,Y=0,Z=0 or M41=0 M42=0 M43=0) you are not going to get accurate rotation. So, close to the world origin will give you "error". Exactly at the world origin will give you nicely aligned shape.
Example:
Red axes - global
Blue axes - Local
Original shape position:
Translate shape "close" to the world/global origin:
Rotate about World axis (perpendicular to the screen):
Translate back to original position:
Result is missaligned shape.
But, all this really depends on what you are trying to do.
Using your approach (even though I am not sure what values you have for CenterX,CentreY and CentreZ):
Instead off
TMat.M14 = -(float)CentreX;
TMat.M24 = -(float)CentreY;
TMat.M34 = -(float)CentreZ;
use
TMat.M14 = 0;
TMat.M24 = 0;
TMat.M34 = 0;
V
[Edited by - Vuli on August 26, 2005 10:35:23 AM]
First I don't see the reason for creating transposed matrix manually, and then transposing it back to true directx form. You can create it right the first time.
DirectX matrix
M11 M12 M13 M14
M21 M22 M23 M24
M31 M32 M33 M34
M41 M42 M43 M44
Rotational part
M11 M12 M13
M21 M22 M23
M31 M32 M33
for example rotation about Z axis
M11= Cos(angle) M12=Sin(angle) M13=0M21=-Sin(angle) M22=Cos(angle) M23=0M31= 0 M32=0 M33=1
Translational part
M41 M42 M43 (X Y Z)
If you use it like this there is no need to call Transpose function.
There is nothing wrong with your calcs.
Quote:
and actually, if i apply an angle of 90, and move the shap closer and closer to world space origin i get a better fit, unil i actually get a true angle. go further than the origin, and i get it misaligned, but "bent" the other way i u see wot i mean... i guess this cud mean the problem is with the translaton as well....
If you don't translate your shape to origin (X=0,Y=0,Z=0 or M41=0 M42=0 M43=0) you are not going to get accurate rotation. So, close to the world origin will give you "error". Exactly at the world origin will give you nicely aligned shape.
Example:
Red axes - global
Blue axes - Local
Original shape position:
Translate shape "close" to the world/global origin:
Rotate about World axis (perpendicular to the screen):
Translate back to original position:
Result is missaligned shape.
But, all this really depends on what you are trying to do.
Using your approach (even though I am not sure what values you have for CenterX,CentreY and CentreZ):
Instead off
TMat.M14 = -(float)CentreX;
TMat.M24 = -(float)CentreY;
TMat.M34 = -(float)CentreZ;
use
TMat.M14 = 0;
TMat.M24 = 0;
TMat.M34 = 0;
V
[Edited by - Vuli on August 26, 2005 10:35:23 AM]
Illco , ur rite ... pi is a double to start with so using 180.0 makes no difference...
but thanks for the suggestion!
Vuli,
i tried the following line of code:
dev.Transform.World = TMat * Rmat;
instead of:
dev.Transform.World = TMat * Rmat * TMat2;
and the result is a perfectly rotted shape shring its centre with the worlds origin.
this makes me think that the poblem is with the third matrix..translating back to the old position...
//Translate from origin to position
Matrix TMat2 = new Matrix();
TMat2.M11 = 1;
TMat2.M12 = 0;
TMat2.M14 = (float)CentreX;
TMat2.M21 = 0;
TMat2.M22 = 1;
TMat2.M24 = (float)CentreY;
TMat2.M31 = 0;
TMat2.M32 = 0;
TMat2.M34 = (float)CentreZ;
TMat2.M33 = 1;
TMat2.M44 = 1;
TMat2.Transpose(TMat2);
all i have done with this is copied the first matrix and got rid of the -signs in front of the x y z values.....
can u see the problem?
cheers,
james
btw, thatnks to both of yous for such quick replies!
but thanks for the suggestion!
Vuli,
i tried the following line of code:
dev.Transform.World = TMat * Rmat;
instead of:
dev.Transform.World = TMat * Rmat * TMat2;
and the result is a perfectly rotted shape shring its centre with the worlds origin.
this makes me think that the poblem is with the third matrix..translating back to the old position...
//Translate from origin to position
Matrix TMat2 = new Matrix();
TMat2.M11 = 1;
TMat2.M12 = 0;
TMat2.M14 = (float)CentreX;
TMat2.M21 = 0;
TMat2.M22 = 1;
TMat2.M24 = (float)CentreY;
TMat2.M31 = 0;
TMat2.M32 = 0;
TMat2.M34 = (float)CentreZ;
TMat2.M33 = 1;
TMat2.M44 = 1;
TMat2.Transpose(TMat2);
all i have done with this is copied the first matrix and got rid of the -signs in front of the x y z values.....
can u see the problem?
cheers,
james
btw, thatnks to both of yous for such quick replies!
Hey
There is no problem with how you setup your matrices.
The problem is probably matrix multiplication order.
If these are the steps you want to perform:
1. Translate to origin (Matrix1)
2. Rotate (Matrix2)
3. Translate to original position (Matrix3)
If you look at math only
MatrixFinal = Matrix3 * Matrix2 * Matrix1
Order is reversed.
I am not quite sure how C# handles matrix multiplication
You can try:
dev.Transform.World = TMat2 * (TMat * Rmat);
Check this out: https://blogs.msdn.com/mikepelton/archive/2005/08/06/448513.aspx
V
There is no problem with how you setup your matrices.
The problem is probably matrix multiplication order.
If these are the steps you want to perform:
1. Translate to origin (Matrix1)
2. Rotate (Matrix2)
3. Translate to original position (Matrix3)
If you look at math only
MatrixFinal = Matrix3 * Matrix2 * Matrix1
Order is reversed.
I am not quite sure how C# handles matrix multiplication
You can try:
dev.Transform.World = TMat2 * (TMat * Rmat);
Check this out: https://blogs.msdn.com/mikepelton/archive/2005/08/06/448513.aspx
Quote:
MSDN:
The order in which the matrix multiplication is performed is crucial. The preceding formula reflects the left-to-right rule of matrix concatenation. That is, the visible effects of the matrices that you use to create a composite matrix occur in left-to-right order. A typical world matrix is shown in the following example. Imagine that you are creating the world matrix for a stereotypical flying saucer. You would probably want to spin the flying saucer around its center - the y-axis of model space - and translate it to some other location in your scene. To accomplish this effect, you first create a rotation matrix, and then multiply it by a translation matrix, as shown in the following formula.
W=Ry*Tw
In this formula, Ry is a matrix for rotation about the y-axis, and Tw is a translation to some position in world coordinates.
V
yea i read that matrix multiplication has to be done in the correct order, and i no my order is correct..i have also tried flippng the two transofmration matrices around and messing bout with multiplication order and i either get nothing on screen - an error- or i get the same misalignment...
its really starting to cheese me off now, cso theres notin wrong with the code !!!! the only thing i can even think of is that some accuracy is lost in the casting from doubles to floats...but this shudnt happen....
any ideas?
EDIT...one "solution" is to either set the z to a deeper value or to use a smaller triangle..this removes the misalignment to the naked eye, but im sure its still there
its really starting to cheese me off now, cso theres notin wrong with the code !!!! the only thing i can even think of is that some accuracy is lost in the casting from doubles to floats...but this shudnt happen....
any ideas?
EDIT...one "solution" is to either set the z to a deeper value or to use a smaller triangle..this removes the misalignment to the naked eye, but im sure its still there
Hello again
Lets assume for now that your matrix stuff is correct.
Casting from doubles to floats will introduce roundoff errror, but nothing you can see. Unless....
Look at this
private float GetAngle(float X, float Y)
{
return ((float)Math.Atan(Y / X)) * (float)(360 / 2.5);// - (float) Math.Atan(Y / X)));
}
In C# for:
someVar=360/2.5;
C# returns 144 (which is fine)
for:
someVar=360/7;
C# returns 51 (incorrect since 360/7 is realy 51.4285)
It does not matter if you use
someVar=(float)(360/7)
C# still returns 51.0000 (since cast happens after calculation)
If you use:
someVar= 360f / 7f
C# returns 51.4285 (correct value)
The same would work for
Math.Atan(Y / X))
if Y=360 and X=7
Math.Atan would use 51 instead of 51.4285
This could be one of the problems.
Back to matrices.
I still don't know what CentreX CentreY CentreZ values you are using.
If either one of them is different from zero
first matrix would be incorrect (in a sense that it would not translate your shape to world origin).
...or there has be something I am missing in the way you are creating and using your matrices.
How do you create your shape?
What is the original shape orientation/position?
Are you trying to use local origin as your shape center
or you want an offset center?
...etc
All of these will affect the end result you are expecting.
V
Lets assume for now that your matrix stuff is correct.
Casting from doubles to floats will introduce roundoff errror, but nothing you can see. Unless....
Look at this
private float GetAngle(float X, float Y)
{
return ((float)Math.Atan(Y / X)) * (float)(360 / 2.5);// - (float) Math.Atan(Y / X)));
}
In C# for:
someVar=360/2.5;
C# returns 144 (which is fine)
for:
someVar=360/7;
C# returns 51 (incorrect since 360/7 is realy 51.4285)
It does not matter if you use
someVar=(float)(360/7)
C# still returns 51.0000 (since cast happens after calculation)
If you use:
someVar= 360f / 7f
C# returns 51.4285 (correct value)
The same would work for
Math.Atan(Y / X))
if Y=360 and X=7
Math.Atan would use 51 instead of 51.4285
This could be one of the problems.
Back to matrices.
I still don't know what CentreX CentreY CentreZ values you are using.
If either one of them is different from zero
first matrix would be incorrect (in a sense that it would not translate your shape to world origin).
Quote:
//Translate to Origin Matrix
Matrix TMat = new Matrix();
TMat.M11 = 1;
TMat.M12 = 0;
TMat.M14 = -(float)CentreX;
TMat.M21 = 0;
TMat.M22 = 1;
TMat.M24 = -(float)CentreY;
TMat.M31 = 0;
TMat.M32 = 0;
TMat.M34 = -(float)CentreZ;
TMat.M33 = 1;
TMat.M44 = 1;
TMat.Transpose(TMat);
...or there has be something I am missing in the way you are creating and using your matrices.
How do you create your shape?
What is the original shape orientation/position?
Are you trying to use local origin as your shape center
or you want an offset center?
...etc
All of these will affect the end result you are expecting.
V
Creating the Shape:
buff[1] = new VertexBuffer(typeof(CustomVertex.PositionNormalColored), 3, dev, 0,
CustomVertex.PositionNormalColored.Format, Pool.Default);
CustomVertex.PositionNormalColored[] verts = new CustomVertex.PositionNormalColored[3];
verts[0] = new CustomVertex.PositionNormalColored(-LightX, -LightY, LightZ-3,0,0,1 ,Color.Yellow.ToArgb());
verts[1] = new CustomVertex.PositionNormalColored(-LightX - 0.1f, -LightY + 0.2f, LightZ-3,0,0,1, Color.Yellow.ToArgb());
verts[2] = new CustomVertex.PositionNormalColored(-LightX + 0.1f, -LightY+ 0.2f, LightZ-3,0,0,1, Color.Yellow.ToArgb());
GraphicsStream str = buff[1].Lock(0, 0, 0);
str.Write(verts);
buff[1].Unlock();
Getting centre of shape and applying matrix
GetCentre(verts[0].X, verts[0].Y, verts[1].Y,verts[0].Z,out cX,out cY, out cZ);
Init.RotateShape(dev, /*GetAngle(-LightX,-LightY)*/ 90.0f, cX,cY, cZ); //YOU can see here, i am passing 90 in as a test rathe than using the get angle function!
private void GetCentre(float x1, float y1, float y2, float z, out float newX, out float newY, out float newZ)
{
newX = x1;
newY = (float)(y1 + (y2 - y1) / 2);
newZ = z;
}
private float GetAngle(float X, float Y)
{
return ((float)Math.Atan(Y / X)) * (float)(360 / 2.5f);// - (float)Math.Atan(Y / X)));
}
You have said twice now that you think i shud pass in 0 instead of the shapes centre values so i thought i'd explain myself...
Theory behind translation:
1,0,0,Tx
0,1,0,Ty
0,0,1,Tz
0,0,0,1
so i am passing in the negative value of the shape's central point in an effort to translate the shape's centre to the origin. I no this works, because if i use:
dev.Transform.World = TMat;
i get the shape perfectly at the origin.
using
dev.Transform.World = TMat * Rmat;
gives me a perfect rotation still at the origin
using
dev.Transform.World = TMat * Rmat; * TMat2;
should Translate the rotated shape back to where it used to be. it does this, but for some reason, the accuracy of rotation is lost....
i know there is no problem with the second matrix either because ive ran tests that show it to work fine.
thanks for ur ongoing help!
james
buff[1] = new VertexBuffer(typeof(CustomVertex.PositionNormalColored), 3, dev, 0,
CustomVertex.PositionNormalColored.Format, Pool.Default);
CustomVertex.PositionNormalColored[] verts = new CustomVertex.PositionNormalColored[3];
verts[0] = new CustomVertex.PositionNormalColored(-LightX, -LightY, LightZ-3,0,0,1 ,Color.Yellow.ToArgb());
verts[1] = new CustomVertex.PositionNormalColored(-LightX - 0.1f, -LightY + 0.2f, LightZ-3,0,0,1, Color.Yellow.ToArgb());
verts[2] = new CustomVertex.PositionNormalColored(-LightX + 0.1f, -LightY+ 0.2f, LightZ-3,0,0,1, Color.Yellow.ToArgb());
GraphicsStream str = buff[1].Lock(0, 0, 0);
str.Write(verts);
buff[1].Unlock();
Getting centre of shape and applying matrix
GetCentre(verts[0].X, verts[0].Y, verts[1].Y,verts[0].Z,out cX,out cY, out cZ);
Init.RotateShape(dev, /*GetAngle(-LightX,-LightY)*/ 90.0f, cX,cY, cZ); //YOU can see here, i am passing 90 in as a test rathe than using the get angle function!
private void GetCentre(float x1, float y1, float y2, float z, out float newX, out float newY, out float newZ)
{
newX = x1;
newY = (float)(y1 + (y2 - y1) / 2);
newZ = z;
}
private float GetAngle(float X, float Y)
{
return ((float)Math.Atan(Y / X)) * (float)(360 / 2.5f);// - (float)Math.Atan(Y / X)));
}
You have said twice now that you think i shud pass in 0 instead of the shapes centre values so i thought i'd explain myself...
Theory behind translation:
1,0,0,Tx
0,1,0,Ty
0,0,1,Tz
0,0,0,1
so i am passing in the negative value of the shape's central point in an effort to translate the shape's centre to the origin. I no this works, because if i use:
dev.Transform.World = TMat;
i get the shape perfectly at the origin.
using
dev.Transform.World = TMat * Rmat;
gives me a perfect rotation still at the origin
using
dev.Transform.World = TMat * Rmat; * TMat2;
should Translate the rotated shape back to where it used to be. it does this, but for some reason, the accuracy of rotation is lost....
i know there is no problem with the second matrix either because ive ran tests that show it to work fine.
thanks for ur ongoing help!
james
Hello
I did a little test with matrices.
I can not find anything wrong.
My assumption is that X axis rotation is the ONLY rotation.
If this is the case there is absolutely nothing (no translational matrix) that can
modify your rotational component.
I'll be using mathematical matrix notation (I never liked DirectX one :) ):
i.e.
If I assume that you just create the shape than its orientation is described by Identity Matrix. This matrix describes the shape without any rotation or translation.
This is your first translation which specifies new center of rotation (as opposed to standard (0,0,0) )
This is matrix describing 90 degree rotation about X axis
Once rotation is complete I want to go back to original position
(which is (0,0,0) based on first matrix "identityMatrix")
All I have to do is use positive values to bring the translation back original position. This counteracts (-2.5,-2.5,-2.5) from translationMatrix1.
Now, what I need to get for my final matrix is 90 degree rotational component.
As far as rotations are conserned multiplication order is irrelevant (as long as only one rotation matrix exists):
In C#:
finalMatrix rotational component is still 90 degrees (which is fine since that is the only rotation)
finalMatrix translational component (position) is
Now, even if I use different order of multiplication:
In C#:
finalMatrix rotational component is still 90 degrees (which is fine since that is the only rotation)
finalMatrix translational component (position) is
All of these matrices were printed out from C# program.
The only way you can mess up the rotation is if you introduce another matrix with a non zero rotational component.
Print out your matrices as you create them, you may be unintentionally introducing rotation in one of your matrices.
V
[Edited by - Vuli on August 29, 2005 2:31:03 PM]
I did a little test with matrices.
I can not find anything wrong.
My assumption is that X axis rotation is the ONLY rotation.
If this is the case there is absolutely nothing (no translational matrix) that can
modify your rotational component.
I'll be using mathematical matrix notation (I never liked DirectX one :) ):
1 0 0 Tx0 1 0 Ty0 0 1 Tz0 0 0 1
i.e.
If I assume that you just create the shape than its orientation is described by Identity Matrix. This matrix describes the shape without any rotation or translation.
identityMatrix : Original matrix(Identity)1.0000 0.0000 0.0000 0.00000.0000 1.0000 0.0000 0.00000.0000 0.0000 1.0000 0.00000.0000 0.0000 0.0000 1.0000
This is your first translation which specifies new center of rotation (as opposed to standard (0,0,0) )
translationMatrix1 : Translation matrix (offset center of rotation)1.0000 0.0000 0.0000 -2.50000.0000 1.0000 0.0000 -2.50000.0000 0.0000 1.0000 -2.50000.0000 0.0000 0.0000 1.0000
This is matrix describing 90 degree rotation about X axis
rotationMatrix : Rotation matrix (X axis rotation)1.0000 0.0000 0.0000 0.00000.0000 0.0000 -1.0000 0.00000.0000 1.0000 0.0000 0.00000.0000 0.0000 0.0000 1.0000
Once rotation is complete I want to go back to original position
(which is (0,0,0) based on first matrix "identityMatrix")
All I have to do is use positive values to bring the translation back original position. This counteracts (-2.5,-2.5,-2.5) from translationMatrix1.
translationMatrix2 : Translation matrix (back to original position)1.0000 0.0000 0.0000 2.50000.0000 1.0000 0.0000 2.50000.0000 0.0000 1.0000 2.50000.0000 0.0000 0.0000 1.0000
Now, what I need to get for my final matrix is 90 degree rotational component.
As far as rotations are conserned multiplication order is irrelevant (as long as only one rotation matrix exists):
In C#:
finalMatrix = translationMatrix2 * translationMatrix1 * rotationMatrix;
finalMatrix : Final matrix1.0000 0.0000 0.0000 0.00000.0000 0.0000 -1.0000 0.00000.0000 1.0000 0.0000 0.00000.0000 0.0000 0.0000 1.0000
finalMatrix rotational component is still 90 degrees (which is fine since that is the only rotation)
1.0000 0.0000 0.00000.0000 0.0000 -1.00000.0000 1.0000 0.0000
finalMatrix translational component (position) is
0.00000.00000.0000
Now, even if I use different order of multiplication:
In C#:
finalMatrix = translationMatrix1 * rotationMatrix * translationMatrix2;
finalMatrix : Final matrix1.0000 0.0000 0.0000 0.00000.0000 0.0000 -1.0000 5.00000.0000 1.0000 0.0000 0.00000.0000 0.0000 0.0000 1.0000
finalMatrix rotational component is still 90 degrees (which is fine since that is the only rotation)
1.0000 0.0000 0.00000.0000 0.0000 -1.00000.0000 1.0000 0.0000
finalMatrix translational component (position) is
0.00005.00000.0000
All of these matrices were printed out from C# program.
The only way you can mess up the rotation is if you introduce another matrix with a non zero rotational component.
Print out your matrices as you create them, you may be unintentionally introducing rotation in one of your matrices.
V
[Edited by - Vuli on August 29, 2005 2:31:03 PM]
This topic is closed to new replies.
Advertisement
Popular Topics
Advertisement