Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>I see this question is a bit old, but I decided to give an answer anyway for those who find this question by searching.<br> The standard way to represent 2D/3D transformations nowadays is by using <strong>homogeneous coordinates</strong>. <em>[x,y,w]</em> for 2D, and <em>[x,y,z,w]</em> for 3D. Since you have three axes in 3D as well as translation, that information fits perfectly in a 4x4 transformation matrix. I will use column-major matrix notation in this explanation. All matrices are 4x4 unless noted otherwise.<br> The stages from 3D points and to a rasterized point, line or polygon looks like this: </p> <ol> <li>Transform your 3D points with the inverse camera matrix, followed with whatever transformations they need. If you have surface normals, transform them as well but with w set to zero, as you don't want to translate normals. The matrix you transform normals with must be <em>isotropic</em>; scaling and shearing makes the normals malformed.</li> <li>Transform the point with a clip space matrix. This matrix scales x and y with the field-of-view and aspect ratio, scales z by the near and far clipping planes, and plugs the 'old' z into w. After the transformation, you should divide x, y and z by w. This is called the <em>perspective divide</em>.</li> <li>Now your vertices are in clip space, and you want to perform clipping so you don't render any pixels outside the viewport bounds. <strong>Sutherland-Hodgeman clipping</strong> is the most widespread clipping algorithm in use.</li> <li>Transform x and y with respect to w and the half-width and half-height. Your x and y coordinates are now in viewport coordinates. w is discarded, but 1/w and z is usually saved because 1/w is required to do perspective-correct interpolation across the polygon surface, and z is stored in the z-buffer and used for depth testing.</li> </ol> <p>This stage is the actual projection, because z isn't used as a component in the position any more.</p> <h2>The algorithms:</h2> <h3>Calculation of field-of-view</h3> <p>This calculates the field-of view. Whether tan takes radians or degrees is irrelevant, but <em>angle</em> must match. Notice that the result reaches infinity as <em>angle</em> nears 180 degrees. This is a singularity, as it is impossible to have a focal point that wide. If you want numerical stability, keep <em>angle</em> less or equal to 179 degrees.</p> <pre><code>fov = 1.0 / tan(angle/2.0) </code></pre> <p>Also notice that 1.0 / tan(45) = 1. Someone else here suggested to just divide by z. The result here is clear. You would get a 90 degree FOV and an aspect ratio of 1:1. Using homogeneous coordinates like this has several other advantages as well; we can for example perform clipping against the near and far planes without treating it as a special case.</p> <h3>Calculation of the clip matrix</h3> <p>This is the layout of the clip matrix. <em>aspectRatio</em> is Width/Height. So the FOV for the x component is scaled based on FOV for y. Far and near are coefficients which are the distances for the near and far clipping planes. </p> <pre><code>[fov * aspectRatio][ 0 ][ 0 ][ 0 ] [ 0 ][ fov ][ 0 ][ 0 ] [ 0 ][ 0 ][(far+near)/(far-near) ][ 1 ] [ 0 ][ 0 ][(2*near*far)/(near-far)][ 0 ] </code></pre> <h3>Screen Projection</h3> <p>After clipping, this is the final transformation to get our screen coordinates.</p> <pre><code>new_x = (x * Width ) / (2.0 * w) + halfWidth; new_y = (y * Height) / (2.0 * w) + halfHeight; </code></pre> <h2>Trivial example implementation in C++</h2> <pre class="lang-cpp prettyprint-override"><code>#include &lt;vector&gt; #include &lt;cmath&gt; #include &lt;stdexcept&gt; #include &lt;algorithm&gt; struct Vector { Vector() : x(0),y(0),z(0),w(1){} Vector(float a, float b, float c) : x(a),y(b),z(c),w(1){} /* Assume proper operator overloads here, with vectors and scalars */ float Length() const { return std::sqrt(x*x + y*y + z*z); } Vector Unit() const { const float epsilon = 1e-6; float mag = Length(); if(mag &lt; epsilon){ std::out_of_range e(""); throw e; } return *this / mag; } }; inline float Dot(const Vector&amp; v1, const Vector&amp; v2) { return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z; } class Matrix { public: Matrix() : data(16) { Identity(); } void Identity() { std::fill(data.begin(), data.end(), float(0)); data[0] = data[5] = data[10] = data[15] = 1.0f; } float&amp; operator[](size_t index) { if(index &gt;= 16){ std::out_of_range e(""); throw e; } return data[index]; } Matrix operator*(const Matrix&amp; m) const { Matrix dst; int col; for(int y=0; y&lt;4; ++y){ col = y*4; for(int x=0; x&lt;4; ++x){ for(int i=0; i&lt;4; ++i){ dst[x+col] += m[i+col]*data[x+i*4]; } } } return dst; } Matrix&amp; operator*=(const Matrix&amp; m) { *this = (*this) * m; return *this; } /* The interesting stuff */ void SetupClipMatrix(float fov, float aspectRatio, float near, float far) { Identity(); float f = 1.0f / std::tan(fov * 0.5f); data[0] = f*aspectRatio; data[5] = f; data[10] = (far+near) / (far-near); data[11] = 1.0f; /* this 'plugs' the old z into w */ data[14] = (2.0f*near*far) / (near-far); data[15] = 0.0f; } std::vector&lt;float&gt; data; }; inline Vector operator*(const Vector&amp; v, const Matrix&amp; m) { Vector dst; dst.x = v.x*m[0] + v.y*m[4] + v.z*m[8 ] + v.w*m[12]; dst.y = v.x*m[1] + v.y*m[5] + v.z*m[9 ] + v.w*m[13]; dst.z = v.x*m[2] + v.y*m[6] + v.z*m[10] + v.w*m[14]; dst.w = v.x*m[3] + v.y*m[7] + v.z*m[11] + v.w*m[15]; return dst; } typedef std::vector&lt;Vector&gt; VecArr; VecArr ProjectAndClip(int width, int height, float near, float far, const VecArr&amp; vertex) { float halfWidth = (float)width * 0.5f; float halfHeight = (float)height * 0.5f; float aspect = (float)width / (float)height; Vector v; Matrix clipMatrix; VecArr dst; clipMatrix.SetupClipMatrix(60.0f * (M_PI / 180.0f), aspect, near, far); /* Here, after the perspective divide, you perform Sutherland-Hodgeman clipping by checking if the x, y and z components are inside the range of [-w, w]. One checks each vector component seperately against each plane. Per-vertex data like colours, normals and texture coordinates need to be linearly interpolated for clipped edges to reflect the change. If the edge (v0,v1) is tested against the positive x plane, and v1 is outside, the interpolant becomes: (v1.x - w) / (v1.x - v0.x) I skip this stage all together to be brief. */ for(VecArr::iterator i=vertex.begin(); i!=vertex.end(); ++i){ v = (*i) * clipMatrix; v /= v.w; /* Don't get confused here. I assume the divide leaves v.w alone.*/ dst.push_back(v); } /* TODO: Clipping here */ for(VecArr::iterator i=dst.begin(); i!=dst.end(); ++i){ i-&gt;x = (i-&gt;x * (float)width) / (2.0f * i-&gt;w) + halfWidth; i-&gt;y = (i-&gt;y * (float)height) / (2.0f * i-&gt;w) + halfHeight; } return dst; } </code></pre> <p>If you still ponder about this, the OpenGL specification is a really nice reference for the maths involved. The DevMaster forums at <a href="http://www.devmaster.net/" rel="noreferrer">http://www.devmaster.net/</a> have a lot of nice articles related to software rasterizers as well.</p>
    singulars
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload