I'm new to 3D programming and am having a terrible time getting my texture to fill my meshes properly. I've got it sizing correctly on the walls but the texture on the roof is running on an angle and is stretched out too far.
I have several methods to create the mesh but they are all eventually sent to AddTriangle method, where the TextureCoordinates are set.
public static void AddTriangle(this MeshGeometry3D mesh, Point3D[] pts)
{
// Create the points.
int index = mesh.Positions.Count;
foreach (Point3D pt in pts)
{
mesh.Positions.Add(pt);
mesh.TriangleIndices.Add(index++);
mesh.TextureCoordinates.Add(new Point(pt.X + pt.Z, 0 - pt.Y));
}
}
Here is how my material is set up.
imageBrush.ImageSource = new BitmapImage(new Uri("pack://application:,,,/Textures/shingles1.jpg"));
imageBrush.TileMode = TileMode.Tile;
imageBrush.ViewportUnits = BrushMappingMode.Absolute;
imageBrush.Viewport = new Rect(0, 0, 25, 25);
SidingColor = new DiffuseMaterial(imageBrush);
SidingColor.Color = RGB(89, 94, 100);
My texture looks like this:
And here is the results I'm getting.
That's as close as I could get after hours of fooling around and googling.
Whew that was a little more difficult than I anticipated.
Here are few resources that helped me find a solution.
How to convert a 3D point on a plane to UV coordinates?
From the link below I realized the above formula above formula was correct but for a right hand coordinate system. I converted it and that was the final step.
http://www.math.tau.ac.il/~dcor/Graphics/cg-slides/geom3d.pdf
Here is the code that works in case someone else has this question.
public static void AddTriangle(this MeshGeometry3D mesh, Point3D[] pts)
{
if (pts.Count() != 3) return;
//use the three point of the triangle to calculate the normal (angle of the surface)
Vector3D normal = CalculateNormal(pts[0], pts[1], pts[2]);
normal.Normalize();
//calculate the uv products
Vector3D u;
if (normal.X == 0 && normal.Z == 0) u = new Vector3D(normal.Y, -normal.X, 0);
else u = new Vector3D(normal.X, -normal.Z, 0);
u.Normalize();
Vector3D n = new Vector3D(normal.Z, normal.X, normal.Y);
Vector3D v = Vector3D.CrossProduct(n, u);
int index = mesh.Positions.Count;
foreach (Point3D pt in pts)
{
//add the points to create the triangle
mesh.Positions.Add(pt);
mesh.TriangleIndices.Add(index++);
//apply the uv texture positions
double u_coor = Vector3D.DotProduct(u, new Vector3D(pt.Z,pt.X,pt.Y));
double v_coor = Vector3D.DotProduct(v, new Vector3D(pt.Z, pt.X, pt.Y));
mesh.TextureCoordinates.Add(new Point(u_coor, v_coor));
}
}
private static Vector3D CalculateNormal(Point3D firstPoint, Point3D secondPoint, Point3D thirdPoint)
{
var u = new Point3D(firstPoint.X - secondPoint.X,
firstPoint.Y - secondPoint.Y,
firstPoint.Z - secondPoint.Z);
var v = new Point3D(secondPoint.X - thirdPoint.X,
secondPoint.Y - thirdPoint.Y,
secondPoint.Z - thirdPoint.Z);
return new Vector3D(u.Y * v.Z - u.Z * v.Y, u.Z * v.X - u.X * v.Z, u.X * v.Y - u.Y * v.X);
}
Related
I have been working on a 2D physics engine using polygons.
And i am having trouble implementing the actual physics part. For a bit of background, i am not experienced at all when it comes to physics and therefor even if a found how to do the entire physics thing online, i would not be able to implement it into my project.
My goal is:
To have polygons fall with gravity.
Have weight drag etc.
Collision between multiple polygons.
What i have already made:
A way of displaying and creating multiple polygons.
Moving and rotating specified object(polygon).
Coeffients for drag, gravity and weight.
Hit boxes and visual boxes. (Visual boxes are what gets displayed and hit boxes are for physics)
A center point for every object. (So far is used for rotation)
A tick for when everything gets calculated. (Gametick/tickrate or whatever you wanna call it)
What i was not able to add / looking for:
Actual gravity.
Collision detection
Velocity for each object.
Collision between object.
Code snippets / how stuff works so far:
Beware that my code is janky and could be made better or more efficient.
Efficiency is not what im looking for!
Function for creating object:
public Object CreateNew(PointF[] hb, PointF[] vb, float rt, Color cl, bool gr, PointF ps)
{
Object obj = new Object
{
pos = ps,
rotation = rt,
offsets = vb,
hitBox = hb,
visBox = vb,
gravity = gr,
clr = cl,
};
#region center
List<Vector2> v2Points = new List<Vector2>();
foreach (PointF p in obj.offsets)
{
v2Points.Add(new Vector2(p.X, p.Y));
}
PointF point = ToPoint(Centroid(v2Points));
obj.center = new PointF(point.X, point.Y);
#endregion
return obj;
}
Function for changing position of object:
public Object ChangePosition(PointF pos, double rot, Object obj)
{
//////////////
int i = 0;
foreach (PointF p in obj.visBox)
{
float minPosX = (float)Math.Sqrt((Math.Pow(obj.center.X - pos.X, 2) + Math.Pow(0 - 0, 2)));
float minPosY = (float)Math.Sqrt((Math.Pow(obj.center.Y - pos.Y, 2) + Math.Pow(0 - 0, 2)));
obj.visBox[i] = new PointF(obj.offsets[i].X + pos.X, obj.offsets[i].Y + pos.Y);
i++;
}
i = 0;
foreach (PointF p in obj.hitBox)
{
float minPosX = (float)Math.Sqrt((Math.Pow(obj.center.X - pos.X, 2) + Math.Pow(0 - 0, 2)));
float minPosY = (float)Math.Sqrt((Math.Pow(obj.center.Y - pos.Y, 2) + Math.Pow(0 - 0, 2)));
obj.hitBox[i] = new PointF(obj.offsets[i].X + pos.X, obj.offsets[i].Y + pos.Y);
i++;
}
obj.pos = pos;
List<Vector2> v2Points = new List<Vector2>();
foreach (PointF p in obj.offsets)
{
v2Points.Add(new Vector2(p.X, p.Y));
}
PointF point = ToPoint(Centroid(v2Points));
obj.center = point;
List<Vector2> v2Points2 = new List<Vector2>();
foreach (PointF p in obj.hitBox)
{
v2Points2.Add(new Vector2(p.X, p.Y));
}
PointF point2 = ToPoint(Centroid(v2Points2));
obj.centerHitBox = point2;
obj.hitBox = RotatePolygon(obj.hitBox, obj.center, rotation * -1);
obj.visBox = RotatePolygon(obj.visBox, obj.center, rotation * -1);
obj.offsets = RotatePolygon(obj.offsets, obj.center, rotation * -1);
obj.hitBox = RotatePolygon(obj.hitBox, obj.center, rot);
obj.visBox = RotatePolygon(obj.visBox, obj.center, rot);
obj.offsets = RotatePolygon(obj.offsets, obj.center, rot);
rotation = rot;
return obj;
}
Pastebin link to object script:
https://pastebin.com/9SnG4vyj
I will provide more information or scripts if anybody needs it!
I am building a test 3D renderer in WinForms using the objects in System.Numerics such as Vector3 and Matrix4x4.
The object drawn is a point cloud, centered around (0,0,0), and rotated about the origin. Each node renders as dots on the screen. Here is what the 3D shape should look like
Fake Perspective
and more specifically when viewed from the front the perspective should be obvious with the blue dots that are further away from the eye to be at a smaller distance from the center
Fake Perspective
The pipeline is roughly as follows:
Rotation transformation
Matrix4x4 RY = Matrix4x4.CreateRotationY(ry);
Perspective transformation (fov=90, aspect=1.0f, near=1f, far=100f)
Matrix4x4 P = Matrix4x4.CreatePerspectiveFieldOfView(fov.Radians(), 1.0f, 1f, 100f);
Camera transformation
Matrix4x4 C = RY * P;
var node = Vector3.Transform(face.Nodes[i], C);
Project to 2D
Vector2 point = new Vector2(node.X, node.Y);
View transformation
Matrix3x2 S = Matrix3x2.CreateScale(height / scale, -height / scale);
Matrix3x2 T = Matrix3x2.CreateTranslation(width / 2f, height / 2f);
Matrix3x2 V = S*T
point = Vector2.Transform(point, V);
Pixel Coordinates & Render
PointF pixel = new PointF(point.X, point.Y);
e.Graphics.FillEllipse(brush,pixel.X - 2, pixel.Y - 2, 4, 4);
So what I am seeing is an orthographic projection.
Program Output
The blue nodes further away are not smaller as expected. Somehow the perspective transformation is being ignored.
So my question is my usage of Matrix4x4.CreatePerspectiveFieldOfView() correct in step #2? And is the projection from 3D to 2D in step #4 correct?
Steps #1, #5 and #6 seem to be working exactly as intended, my issue is with steps #2-#4 somewhere.
Example code to reproduce the issue
Form1.cs
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
public Shape Object { get; set; }
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
this.Object = Shape.DemoShape1();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
float width = ClientSize.Width, height = ClientSize.Height;
float scale = 40f, fov = 90f;
Matrix4x4 RY = Matrix4x4.CreateRotationY(ry);
Matrix4x4 RX = Matrix4x4.CreateRotationX(rx);
Matrix4x4 P = Matrix4x4.CreatePerspectiveFieldOfView(fov.Radians(), 1.0f, 1f, 100f);
Matrix4x4 C = RY * RX * P;
Matrix3x2 S = Matrix3x2.CreateScale(
height / scale, -height / scale);
Matrix3x2 T = Matrix3x2.CreateTranslation(
width / 2f, height / 2f);
Matrix3x2 V = S * T;
using (var pen = new Pen(Color.Black, 0))
{
var arrow = new AdjustableArrowCap(4f, 9.0f);
pen.CustomEndCap = arrow;
using (var brush = new SolidBrush(Color.Black))
{
// Draw coordinate triad (omited)
// Each face has multiple nodes with the same color
foreach (var face in Object.Faces)
{
brush.Color = face.Color;
PointF[] points = new PointF[face.Nodes.Count];
for (int i = 0; i < points.Length; i++)
{
// transform nodes into draw points
var item = Vector4.Transform(face.Nodes[i], C);
var point = Vector2.Transform(item.Project(), V);
points[i] = point.ToPoint();
}
// Draw points as dots
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
for (int i = 0; i < points.Length; i++)
{
e.Graphics.FillEllipse(brush,
points[i].X - 2, points[i].Y - 2,
4, 4);
}
}
}
}
}
}
GraphicsExtensions.cs
public static class GraphicsExtensions
{
public static PointF ToPoint(this Vector2 vector)
=> new PointF(vector.X, vector.Y);
public static Vector2 Project(this Vector3 vector)
=> new Vector2(vector.X, vector.Y);
public static Vector2 Project(this Vector4 vector)
=> new Vector2(vector.X, vector.Y);
public static float Radians(this float degrees) => (float)(Math.PI/180) * degrees;
public static float Degrees(this float radians) => (float)(180/Math.PI) * radians;
}
For a while now I've been using the following function to rotate a series of Points around a pivot point in various programs of mine.
private Point RotatePoint(Point point, Point pivot, double radians)
{
var cosTheta = Math.Cos(radians);
var sinTheta = Math.Sin(radians);
var x = (cosTheta * (point.X - pivot.X) - sinTheta * (point.Y - pivot.Y) + pivot.X);
var y = (sinTheta * (point.X - pivot.X) + cosTheta * (point.Y - pivot.Y) + pivot.Y);
return new Point((int)x, (int)y);
}
This has always worked great, until I tried to rotate a shape repeatedly by small amounts. For example, this is what I get from calling it for 45° on a rectangular polygon made up of 4 points:
foreach (var point in points)
Rotate(point, center, Math.PI / 180f * 45);
But this is what I get by calling rotate 45 times for 1°:
for (var i = 0; i < 45; ++i)
foreach (var point in points)
Rotate(point, center, Math.PI / 180f * 1)
As long as I call it only once it's fine, and it also seems like it gets gradually worse the lower the rotation degree is. Is there some flaw in the function, or am I misunderstanding something fundamental about what this function does?
How could I rotate repeatedly by small amounts? I could save the base points and use them to update the current points whenever the rotation changes, but is that the only way?
Your Point position measure is off because of the integer rounding generated by the RotatePoint() method.
A simple correction in the method returned value, using float coordinates, will produce the correct measure:
To test it, create a Timer and register its Tick event as RotateTimerTick():
Added a rotation spin increment (see the rotationSpin Field) to emphasize the motion effect.
PointF pivotPoint = new PointF(100F, 100F);
PointF rotatingPoint = new PointF(50F, 100F);
double rotationSpin = 0D;
private PointF RotatePoint(PointF point, PointF pivot, double radians)
{
var cosTheta = Math.Cos(radians);
var sinTheta = Math.Sin(radians);
var x = (cosTheta * (point.X - pivot.X) - sinTheta * (point.Y - pivot.Y) + pivot.X);
var y = (sinTheta * (point.X - pivot.X) + cosTheta * (point.Y - pivot.Y) + pivot.Y);
return new PointF((float)x, (float)y);
}
private void RotateTimerTick(object sender, EventArgs e)
{
rotationSpin += .5;
if (rotationSpin > 90) rotationSpin = 0;
rotatingPoint = RotatePoint(rotatingPoint, pivotPoint, (Math.PI / 180f) * rotationSpin);
Panel1.Invalidate(new Rectangle(new Point(50,50), new Size(110, 110)));
}
private void Panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.FillEllipse(Brushes.White, new RectangleF(100, 100, 8, 8));
e.Graphics.FillEllipse(Brushes.Yellow, new RectangleF(rotatingPoint, new SizeF(8, 8)));
}
This is the result using float values:
And this is what happens using integer values:
If you want you can use the Media3D to only deal with matrix and simplify the coding. Something as simple as this will work.
public Point3D Rotate(Point3D point, Point3D rotationCenter, Vector3D rotation, double degree)
{
// create empty matrix
var matrix = new Matrix3D();
// translate matrix to rotation point
matrix.Translate(rotationCenter - new Point3D());
// rotate it the way we need
matrix.Rotate(new Quaternion(rotation, degree));
// apply the matrix to our point
point = matrix.Transform(point);
return point;
}
Then you simply call the method and specify the rotation. Lets say you work with 2D (like in your example) and lets assume we work with XY plane so the rotation is in Z. You can do something like :
var rotationPoint = new Point3D(0, 0, 0);
var currentPoint = new Point3D(10, 0, 0);
// rotate the current point around the rotation point in Z by 45 degree
var newPoint = Rotate(currentPoint, rotation, new Vector3D(0, 0, 1), 45d);
I recently got into using MonoGame, and I love the library.
However, I seem to be having some issues with drawing bezier curves
The result that my code produces looks something like this
Look bad, no?
The lines aren't smooth at all.
Let me show you some of the code:
//This is what I call to get all points between which to draw.
public static List<Point> computeCurvePoints(int steps)
{
List<Point> curvePoints = new List<Point>();
for (float x = 0; x < 1; x += 1 / (float)steps)
{
curvePoints.Add(getBezierPointRecursive(x, pointsQ));
}
return curvePoints;
}
//Calculates a point on the bezier curve based on the timeStep.
private static Point getBezierPointRecursive(float timeStep, Point[] ps)
{
if (ps.Length > 2)
{
List<Point> newPoints = new List<Point>();
for (int x = 0; x < ps.Length-1; x++)
{
newPoints.Add(interpolatedPoint(ps[x], ps[x + 1], timeStep));
}
return getBezierPointRecursive(timeStep, newPoints.ToArray());
}
else
{
return interpolatedPoint(ps[0], ps[1], timeStep);
}
}
//Gets the linearly interpolated point at t between two given points (without manual rounding).
//Bad results!
private static Point interpolatedPoint(Point p1, Point p2, float t)
{
Vector2 roundedVector = (Vector2.Multiply(p2.ToVector2() - p1.ToVector2(), t) + p1.ToVector2());
return new Point((int)roundedVector.X, (int)roundedVector.Y);
}
//Method used to draw a line between two points.
public static void DrawLine(this SpriteBatch spriteBatch, Texture2D pixel, Vector2 begin, Vector2 end, Color color, int width = 1)
{
Rectangle r = new Rectangle((int)begin.X, (int)begin.Y, (int)(end - begin).Length() + width, width);
Vector2 v = Vector2.Normalize(begin - end);
float angle = (float)Math.Acos(Vector2.Dot(v, -Vector2.UnitX));
if (begin.Y > end.Y) angle = MathHelper.TwoPi - angle;
spriteBatch.Draw(pixel, r, null, color, angle, Vector2.Zero, SpriteEffects.None, 0);
}
//DrawLine() is called as following. "pixel" is just a Texture2D with a single black pixel.
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
for(int x = 0; x < curvePoints.Count-1; x++)
{
DrawExtenstion.DrawLine(spriteBatch, pixel, curvePoints[x].ToVector2(), curvePoints[x + 1].ToVector2(), Color.Black, 2);
}
spriteBatch.End();
base.Draw(gameTime);
}
I managed to make the line a bit smoother by adding some manual Math.Round() calls to my interpolatedPoint method
//Gets the linearly interpolated point at t between two given points (with manual rounding).
//Better results (but still not good).
private static Point interpolatedPoint(Point p1, Point p2, float t)
{
Vector2 roundedVector = (Vector2.Multiply(p2.ToVector2() - p1.ToVector2(), t) + p1.ToVector2());
return new Point((int)Math.Round(roundedVector.X), (int)Math.Round(roundedVector.Y));
}
This produces the following result:
I had to remove one picture since Stackoverflow doesn't let me use more than two links
Are there any ways I can get this curve to be absolutely smooth?
Perhaps there is a problem with the DrawLine method?
Thanks in advance.
EDIT:
Okay, I managed to make the curve look a lot better by doing all the calculations with Vector2Ds and only converting it to a Point at the moment that it needs to be drawn
It still isn't perfect though :/
As Mike 'Pomax' Kamermans said,
it seems to have been a problem with the 2D surface not allowing subpixel drawing and thus causing rounding issues
Following craftworkgames' advice, I adapted the algorithm to draw the curve in 3D using a BasicEffect. This also allows for antialiasing, which smoothes out the curve a lot.
The result is the following:
A lot better!
Thank you very much for the helpful advice!
EDIT:
Here is the code I used for doing this.
I would also like to add that this webpage (http://gamedevelopment.tutsplus.com/tutorials/create-a-glowing-flowing-lava-river-using-bezier-curves-and-shaders--gamedev-919) helped me a lot while writing this code.
Also, please note that some of the names I used for defining the methods might not really make sense or can be confusing. This was something I quickly put together on an evening.
//Used for generating the mesh for the curve
//First object is vertex data, second is indices (both as arrays)
public static object[] computeCurve3D(int steps)
{
List<VertexPositionTexture> path = new List<VertexPositionTexture>();
List<int> indices = new List<int>();
List<Vector2> curvePoints = new List<Vector2>();
for (float x = 0; x < 1; x += 1 / (float)steps)
{
curvePoints.Add(getBezierPointRecursive(x, points3D));
}
float curveWidth = 0.003f;
for(int x = 0; x < curvePoints.Count; x++)
{
Vector2 normal;
if(x == 0)
{
//First point, Take normal from first line segment
normal = getNormalizedVector(getLineNormal(curvePoints[x+1] - curvePoints[x]));
}
else if (x + 1 == curvePoints.Count)
{
//Last point, take normal from last line segment
normal = getNormalizedVector(getLineNormal(curvePoints[x] - curvePoints[x-1]));
} else
{
//Middle point, interpolate normals from adjacent line segments
normal = getNormalizedVertexNormal(getLineNormal(curvePoints[x] - curvePoints[x - 1]), getLineNormal(curvePoints[x + 1] - curvePoints[x]));
}
path.Add(new VertexPositionTexture(new Vector3(curvePoints[x] + normal * curveWidth, 0), new Vector2()));
path.Add(new VertexPositionTexture(new Vector3(curvePoints[x] + normal * -curveWidth, 0), new Vector2()));
}
for(int x = 0; x < curvePoints.Count-1; x++)
{
indices.Add(2 * x + 0);
indices.Add(2 * x + 1);
indices.Add(2 * x + 2);
indices.Add(2 * x + 1);
indices.Add(2 * x + 3);
indices.Add(2 * x + 2);
}
return new object[] {
path.ToArray(),
indices.ToArray()
};
}
//Recursive algorithm for getting the bezier curve points
private static Vector2 getBezierPointRecursive(float timeStep, Vector2[] ps)
{
if (ps.Length > 2)
{
List<Vector2> newPoints = new List<Vector2>();
for (int x = 0; x < ps.Length - 1; x++)
{
newPoints.Add(interpolatedPoint(ps[x], ps[x + 1], timeStep));
}
return getBezierPointRecursive(timeStep, newPoints.ToArray());
}
else
{
return interpolatedPoint(ps[0], ps[1], timeStep);
}
}
//Gets the interpolated Vector2 based on t
private static Vector2 interpolatedPoint(Vector2 p1, Vector2 p2, float t)
{
return Vector2.Multiply(p2 - p1, t) + p1;
}
//Gets the normalized normal of a vertex, given two adjacent normals (2D)
private static Vector2 getNormalizedVertexNormal(Vector2 v1, Vector2 v2) //v1 and v2 are normals
{
return getNormalizedVector(v1 + v2);
}
//Normalizes the given Vector2
private static Vector2 getNormalizedVector(Vector2 v)
{
Vector2 temp = new Vector2(v.X, v.Y);
v.Normalize();
return v;
}
//Gets the normal of a given Vector2
private static Vector2 getLineNormal(Vector2 v)
{
Vector2 normal = new Vector2(v.Y, -v.X);
return normal;
}
//Drawing method in main Game class
//curveData is a private object[] that is initialized in the constructor (by calling computeCurve3D)
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
var camPos = new Vector3(0, 0, 0.1f);
var camLookAtVector = Vector3.Forward;
var camUpVector = Vector3.Up;
effect.View = Matrix.CreateLookAt(camPos, camLookAtVector, camUpVector);
float aspectRatio = graphics.PreferredBackBufferWidth / (float)graphics.PreferredBackBufferHeight;
float fieldOfView = MathHelper.PiOver4;
float nearClip = 0.1f;
float farClip = 200f;
//Orthogonal
effect.Projection = Matrix.CreateOrthographic(480 * aspectRatio, 480, nearClip, farClip);
foreach (var pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
effect.World = Matrix.CreateScale(200);
graphics.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList,
(VertexPositionTexture[])curveData[0],
0,
((VertexPositionTexture[])curveData[0]).Length,
(int[])curveData[1],
0,
((int[])curveData[1]).Length/3);
}
base.Draw(gameTime);
}
Also, this image may be able to show what the code does a little bit better
So, I needed something like this working with SpriteBatch, so I poked around at the original code a bit (with the Point -> Vector2 and rounding changes.
If you render every other segment as a different color, and with a large enough width and low enough steps, you can see why it resulted in jagged lines with larger values of width. It turns out the lines go past where they should end!
Lines going past their end:
This is because the DrawLine function adds width to length of the segment. However, without this, you see a bunch of disconnected segments for anything that actually curves.
Lines being disconnected:
There's probably some math you can do to get the appropriate value to add here, based on the angle of the connecting points. I don't know math well enough for that, so I'm just using a fixed value for them all. (10 seems to be the sweet spot for the image I posted, although it isn't perfect due to the low step count.)
(The following is DrawLine adjusted with the width being added, to using a constant instead.)
// Method used to draw a line between two points.
public static void DrawLine(this SpriteBatch spriteBatch, Texture2D pixel, Vector2 begin, Vector2 end, Color color, int width = 1)
{
Rectangle r = new Rectangle((int)begin.X, (int)begin.Y, (int)(end - begin).Length() + 10, width);
Vector2 v = Vector2.Normalize(begin - end);
float angle = (float)Math.Acos(Vector2.Dot(v, -Vector2.UnitX));
if (begin.Y > end.Y) angle = MathHelper.TwoPi - angle;
spriteBatch.Draw(pixel, r, null, color, angle, Vector2.Zero, SpriteEffects.None, 0);
}
I'm attempting to calculate the area of a polygon that lies on a plane (a collection co-planar points forming a non-intersecting closed shape), and I know a method that can calculate the area of an irregular (or any) polygon in two dimensions - but not three. My solution is to rotate the plane so that it's normal is 0 in the z direction (so I can treat it like it's 2D) and then run the 2D area function.
The problem is I have NO idea how to actually determine the rotation axes and amounts to flatten a plane on it's Z-axis. I do my rotation through the easiest method I could find for 3 dimensional rotation: Rotation Matrices. So, given that I'm trying to use rotation matrices to do my rotation, how do I figure out the angles to rotate my plane by to be oriented in the same direction as another vector? I don't actually know much calculus or Euclidean geometry, so whichever solution requires me to teach myself the least of both is the ideal solution. Is there a better way?
Here's my attempt below, which doesn't even come close to getting the plane flat on the Z axis. This is an instance method of my "Surface" class, which is a derivative of my "Plane" class, and has an array of co-planar points (IntersectPoints) forming a closed polygon.
public virtual double GetArea()
{
Vector zUnit = new Vector(0, 0, 1); //vector perprendicualr to z
Vector nUnit = _normal.AsUnitVector();
Surface tempSurface = null;
double result = 0;
if (nUnit != zUnit && zUnit.Dot(nUnit) != 0) //0 = perprendicular to z
{
tempSurface = (Surface)Clone();
double xAxisAngle = Vector.GetAxisAngle(nUnit, zUnit, Physics.Formulae.Axes.X);
double yAxisAngle = Vector.GetAxisAngle(nUnit, zUnit, Physics.Formulae.Axes.Y);
double rotationAngle = Vector.GetAxisAngle(nUnit, zUnit, Physics.Formulae.Axes.Z);
tempSurface.Rotate(xAxisAngle, yAxisAngle, rotationAngle); //rotating plane so that it is flat on the Z axis
}
else
{
tempSurface = this;
}
for (int x = 0; x < tempSurface.IntersectPoints.Count; x++) //doing a cross sum of each point
{
Point curPoint = tempSurface.IntersectPoints[x];
Point nextPoint;
if (x == tempSurface.IntersectPoints.Count - 1)
{
nextPoint = tempSurface.IntersectPoints[0];
}
else
{
nextPoint = tempSurface.IntersectPoints[x + 1];
}
double cross1 = curPoint.X * nextPoint.Y;
double cross2 = curPoint.Y * nextPoint.X;
result += (cross1 - cross2); //add the cross sum of each set of points to the result
}
return Math.Abs(result / 2); //divide cross sum by 2 and take its absolute value to get the area.
}
And here are my core rotation and get axis angle methods:
private Vector Rotate(double degrees, int axis)
{
if (degrees <= 0) return this;
if (axis < 0 || axis > 2) return this;
degrees = degrees * (Math.PI / 180); //convert to radians
double sin = Math.Sin(degrees);
double cos = Math.Cos(degrees);
double[][] matrix = new double[3][];
//normalizing really small numbers to actually be zero
if (Math.Abs(sin) < 0.00000001)
{
sin = 0;
}
if (Math.Abs(cos) < 0.0000001)
{
cos = 0;
}
//getting our rotation matrix
switch (axis)
{
case 0: //x axis
matrix = new double[][]
{
new double[] {1, 0, 0},
new double[] {0, cos, sin * -1},
new double[] {0, sin, cos}
};
break;
case 1: //y axis
matrix = new double[][]
{
new double[] {cos, 0, sin},
new double[] {0, 1, 0},
new double[] {sin * -1, 0, cos}
};
break;
case 2: //z axis
matrix = new double[][]
{
new double[] {cos, sin * -1, 0},
new double[] {sin, cos, 0},
new double[] {0, 0, 1}
};
break;
default:
return this;
}
return Physics.Formulae.Matrix.MatrixByVector(this, matrix);
}
public static double GetAxisAngle(Point a, Point b, Axes axis, bool inDegrees = true)
{ //pretty sure this doesnt actually work
double distance = GetDistance(a, b);
double difference;
switch (axis)
{
case Axes.X:
difference = b.X - a.X;
break;
case Axes.Y:
difference = b.Y - a.Y;
break;
case Axes.Z :
difference = b.Z - a.Z;
break;
default:
difference = 0;
break;
}
double result = Math.Acos(difference / distance);
if (inDegrees == true)
{
return result * 57.2957; //57.2957 degrees = 1 radian
}
else
{
return result;
}
}
A robust way to do this is to do a sum of the cross-products of the vertices of each edge. If your vertices are co-planar, this will produce a normal to the plane, whose length is 2 times the area of the closed polygon.
Note that this method is very similar to the 2D method linked in your question, which actually calculates a 2D equivalent of the 3D cross-product, summed for all edges, then divides by 2.
Vector normal = points[count-1].cross(points[0]);
for(int i=1; i<count; ++i) {
normal += points[i-1].cross(points[i]);
}
double area = normal.length() * 0.5;
Advantages of this method:
If your vertices are only approximately planar, it still gives the right answer
It doesn't depend on the angle of the plane.
In fact you don't need to deal with the angle at all.
If you want to know the plane orientation, you've got the normal already.
One possible difficulty: if your polygon is very small, and a long way away from the origin, you can get floating point precision problems. If that case is likely to arise, you should first translate all of your vertices so that one is at the origin, like so:
Vector normal(0,0,0);
Vector origin = points[count-1];
for(int i=1; i<count-1; ++i) {
normal += (points[i-1]-origin).cross(points[i]-origin);
}
double area = normal.length() * 0.5;
You need not to rotate the plane (or all points). Just calculate an area of polygon projection to Z-plane (if it is not perpendicular to polygon plane), for example, with you GetArea function, and divide result by cosinus of Poly-plane - Z-plane angle - it is equal to scalar product of zUnit and nUnit (I suggest that nUnit is normal vector to polygon plane)
TrueArea = GetArea() / zUnit.Dot(nUnit)