Click here to Skip to main content
15,895,011 members
Articles / Multimedia / GDI+

A 3D Plotting Library in C#

Rate me:
Please Sign up or sign in to vote.
4.91/5 (171 votes)
9 Jun 2006CPOL9 min read 456.7K   13.9K   430  
A library which draws 3D images on any GDI+ Graphics object.
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;

namespace CPI.Plot3D
{
    /// <summary>
    /// Represents a location in 3D space.
    /// </summary>
    public struct Point3D : IEquatable<Point3D>
    {
        # region Constants

        /// <summary>
        /// The maximum distance two coordinates can be from each other
        /// for them to be considered approximately equal.
        /// </summary>
        public const float Tolerance = .000001f;

        # endregion

        # region Private Fields

        private readonly float x;
        private readonly float y;
        private readonly float z;

        # endregion

        # region Constructors

        /// <summary>
        /// Instantiates a new Point3D.
        /// </summary>
        /// <param name="x">The point's X coordinate.</param>
        /// <param name="y">The point's Y coordinate.</param>
        /// <param name="z">The point's Z coordinate.</param>
        public Point3D(float x, float y, float z)
        {
            // We don't allow Infinity or NAN as coordinates
            if (float.IsInfinity(x) || float.IsNaN(x))
                throw new ArgumentException("Argument x cannot be infinity or NAN");
            if (float.IsInfinity(y) || float.IsNaN(y))
                throw new ArgumentException("Argument y cannot be infinity or NAN");
            if (float.IsInfinity(z) || float.IsNaN(z))
                throw new ArgumentException("Argument z cannot be infinity or NAN");

            this.x = x;
            this.y = y;
            this.z = z;
        }

        # endregion

        # region Properties

        /// <summary>
        /// The point's X coordinate.
        /// </summary>
        public float X
        {
            get
            {
                return x;
            }
        }

        /// <summary>
        /// The point's Y coordinate.
        /// </summary>
        public float Y
        {
            get
            {
                return y;
            }
        }

        /// <summary>
        /// The point's Z coordinate.
        /// </summary>
        public float Z
        {
            get
            {
                return z;
            }
        }

        # endregion

        # region Methods

        /// <summary>
        /// Gets the location of the point projected onto the XY plane at the Z origin, from a specified
        /// camera's perspective.
        /// </summary>
        /// <param name="cameraPosition">The location to base perspective calculations on.</param>
        /// <returns>A PointF representing the projected point.</returns>
        /// <remarks>
        /// Calculations are internally performed with double precision arithmetic, then rounded
        /// down to floats at the end.
        /// </remarks>
        public PointF GetScreenPosition(Point3D cameraPosition)
        {
            PointF returnValue = new PointF();

            returnValue.X = (float)
                (((((double)this.X - (double)cameraPosition.X) 
                * (-1 * (double)cameraPosition.Z)) 
                / ((double)this.Z - (double)cameraPosition.Z))
                + (double)cameraPosition.X);

            returnValue.Y = (float)
                (((((double)this.Y - (double)cameraPosition.Y) 
                * (-1 * (double)cameraPosition.Z)) 
                / ((double)this.Z - (double)cameraPosition.Z)) 
                + (double)cameraPosition.Y);

            if (float.IsInfinity(returnValue.X) || float.IsNaN(returnValue.X)
                || float.IsInfinity(returnValue.Y) || float.IsNaN(returnValue.Y))
            {
                throw new ArgumentOutOfRangeException("cameraPosition", string.Format("The point cannot be represented with the specified camera position because the X or Y coordinate of the projected point was either Infinity or NaN." + Environment.NewLine + "Object Point: {0}" + Environment.NewLine + "Camera Point {1}", this.ToString(), cameraPosition.ToString()));
            }

            return returnValue;
        }


        /// <summary>
        /// Determines whether this instance is very nearly equal to a specified Point3D structure.
        /// </summary>
        /// <remarks>
        /// Since floating point math is kind of fuzzy, we're taking a "close enough" approach 
        /// to equality with this method.  If the individual coordinates of two points fall within
        /// a small tolerance, we'll consider them to be approximately equal.
        ///  
        /// Remember, though, that the uncertainty here can be cumulative.  For example:
        /// if pointA.Equals(pointB) and pointB.Equals(pointC), then it's an absolute certainty
        /// that pointA.Equals(pointC).
        /// However, if pointD.ApproximatelyEquals(pointE) and pointE.ApproximatelyEquals(pointF),
        /// it is NOT certain whether pointD.ApproximatelyEquals(pointF).
        /// </remarks>
        /// <param name="other">A Point3D structure to compare to this instance.</param>
        /// <returns>True if the X,Y,Z components are approximately equal; false otherwise.</returns>
        public bool ApproximatelyEquals(Point3D other)
        {
            return (
                (Math.Abs(this.X - other.X) < Tolerance)
                && (Math.Abs(this.Y - other.Y) < Tolerance)
                && (Math.Abs(this.Z - other.Z) < Tolerance)
            );
        }

        /// <summary>
        /// Returns a string representation of the point in [X,Y,Z] format, with each coordinate 
        /// rounded to the specified number of digits.
        /// </summary>
        /// <param name="decimalPlaces">The number of decimal digits to round the output to.</param>
        /// <returns>A string representing the point's XYZ coordinates.</returns>
        public string ToString(int decimalPlaces)
        {
            return string.Format("[{0}, {1}, {2}]", ((float)Math.Round(X, decimalPlaces)).ToString(), ((float)Math.Round(Y, decimalPlaces)).ToString(), ((float)Math.Round(Z, decimalPlaces)).ToString());
        }

        # endregion

        # region Overridden Methods

        /// <summary>
        /// Returns a string representation of the point in [X,Y,Z] format.
        /// </summary>
        /// <returns>A string representing the point's XYZ coordinates.</returns>
        public override string ToString()
        {
            return string.Format("[{0}, {1}, {2}]", X.ToString(), Y.ToString(), Z.ToString());
        }

        /// <summary>
        /// Returns a value indicating whether this instance is equal to a specified object. 
        /// </summary>
        /// <param name="obj">An object to compare with this instance. </param>
        /// <returns>True if the object equals this instance; false otherwise.</returns>
        public override bool Equals(object obj)
        {
            if (obj is Point3D)
                return this.Equals((Point3D)obj);
            else
                return false;
        }

        /// <summary>
        /// Returns the hash code for this instance. 
        /// </summary>
        /// <remarks>
        /// The hash code is based on the hash codes of the X, Y, and Z coordinates of the point,
        /// but we can't just XOR them all together, otherwise [3,4,5] would return the same hash 
        /// code as [5,3,4], and we wouldn't want that.  So to get a more even distribution, we 
        /// rotate hashY's bits by 8, and hashZ's bits by 16, then we XOR them all together.
        /// (It's also worth pointing out that we're casting the individual hash codes to uints before
        /// operating on them because we want our shift operations to use unsigned semantics.)
        /// </remarks>
        /// <returns>A 32-bit signed integer hash code.</returns>
        public override int GetHashCode()
        {
            uint hashX = (uint)x.GetHashCode();
            uint hashY = (uint)y.GetHashCode();
            uint hashZ = (uint)z.GetHashCode();

            return (int)(hashX ^ ((hashY << 10) + (hashY >> 22)) ^ ((hashZ << 20) + (hashZ >> 12)));
        }

        # endregion

        # region Overloaded Operators

        /// <summary>
        /// Determines whether the specified Point3D instances are equal.
        /// </summary>
        /// <param name="a">The first Point3D instance to compare.</param>
        /// <param name="b">The second Point3D instance to compare.</param>
        /// <returns>True if the Point3D instances are equal; false otherwise.</returns>
        public static bool operator ==(Point3D a, Point3D b)
        {
            return a.Equals(b);
        }

        /// <summary>
        /// Determines whether the specified Point3D instances are unequal.
        /// </summary>
        /// <param name="a">The first Point3D instance to compare.</param>
        /// <param name="b">The second Point3D instance to compare.</param>
        /// <returns>True if the Point3D instances are unequal; false otherwise.</returns>
        public static bool operator !=(Point3D a, Point3D b)
        {
            return !a.Equals(b);
        }

        # endregion

        #region IEquatable<Point3D> Members

        /// <summary>
        /// Returns a value indicating whether this instance is equal to a specified Point3D structure. 
        /// </summary>
        /// <param name="other">A Point3D structure to compare to this instance.</param>
        /// <returns>True if the X,Y,Z components are the same; false otherwise.</returns>
        public bool Equals(Point3D other)
        {
            return (this.x == other.x && this.y == other.y && this.z == other.z);
        }

        #endregion
    }

}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Pete has just recently become a corporate sell-out, working for a wholly-owned subsidiary of "The Man". He counter-balances his soul-crushing professional life by practicing circus acrobatics and watching Phineas and Ferb reruns. Ducky Momo is his friend.

Comments and Discussions