I thought this was 3D, so why can’t I use the z coordinate ?
Hopefully, you guys have read my previous article and play with the example class, or with your own. You may have noticed that changing the z coordinate didn’t changed anything. Let me explain why.
Your 3D scene is rendered in 2D, in some area called the clipspace. The clipspace is basically your screen, and every point that is behind your screen needs to be projected into the clipspace so it can be drawn.
I said earlier that x and y coordinates where going from -1 to 1. Well, it’s not true. It’s actually the clipspace coordinate that goes from -1 to 1. Imagine that the clipspace would have the same width and height as your screen, or your browser windows, you would have to compute the coordinate for every screen size, and for every size change ! Having a normalized clipspace is what allows us to forget about screen size and resolutions and focus on our scene coordinate.
Now, by default and without any other instruction, your graphic card project your vertices to your clipspace without any projection or any sense of perspective. That is why if you have a point out of clipspace coordinate, like x=2, you can’t see it.
Since we were only moving each vertex coordinate to the ouput point, here is the equation for any projected point
// mov op va1 xP = x yP = y
The perspective divide
To be able to render 3D on a 2D plan (your screen), we need perspective. Perspective is what makes the borders of a road to look like they are converging when they are actually parallels lines. The equation is actually rather simple : The farther a point is, the closest to the middle it appears. Here is the equation :
xP = K1 * x / z yP = K2 * y / z
With K1 and K2 some constants depending on things such as the field of view, or the aspect ratio of your clipspace. This is a perspective divide.
You can notice that if you divide by z, then z can’t be equal to 0. We will talk about this later.
When using the perspective divide, here is the result of the projection of 3D object 1 and 3D object 2 from previous scheme
When we coded our first vertex shader, we just copied each vertex coordinate into the Output Point. You now learned that computing the output point actually defines the position of the projection of a vertex into the clipspace.
To be able to translate, rotate, or scale an object, we won’t be modifying all of his vertices. Why ?
- It would be really complex to compute the new position of every vertices when rotating by 45° on the Y axis, then scaling it up to 2.37 times.
- We would need to upload the coordinate into the vertex buffer again, which would completely lose any interest in using Stage3D. Remember, if the graphic card can render triangles so fast, it’s because everything is ready in the video ram.
Instead of uploading new coordinates to the V-Ram, we will compute the output point using a Matrix. This Matrix will be uploaded as a constant in every frame. Constant are very fast to update in the V-Ram unlike Vertex Buffers or Texture.
Updating the HelloTriangle example
Now, you can either open you HelloTriangle project, or download the following one. I recommend you to take your last project if you already have it since there is only a few line to add, but if you prefer to take my sources, you should be looking for the HelloMatrix class.
First thing we need to do is to create a Matrix3D, and upload it as a Vertex Constant to the Graphic Card. Go to the render function, on line 194. The HelloTriangle should already have a Matrix3D class member declared called m. So just instantiate it, append a translation to it either on x or y, and use the context.setProgramConstantsFromMatrix method to upload it to the GPU. Here is what I have :
// create a matrix3D, apply a translation on it, then set it as a vertex constant m = new Matrix3D(); m.appendTranslation(Math.sin(getTimer()/500)*.5, 0, 0); context.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, m, true); // 0 means that we will retrieve the matrix under vc0
I choose to create a translation according to a Timer so you can see the triangle moving on the x axis.
At this point, if you compile your class, you won’t see any change. This is because we need to instruct our GPU how to use the Matrix, and this will be done in the Vertex Shader.
Updating the Vertex Shader
Obviously, updating the Vertex Shader will happen in the AGAL code. Time for us to learn how to invoke constants.
- vc : Vextex Constant. Called by their first register (ex : vc0). Be carefull, matrix constants take 4 registers, so if you upload a Matrix to vc0, the next constant must be set on vc4.
- fc : Fragment Constant. Same thing as above, but for fragment shaders.
Locate your Vertex Shader AGAL code, it should be around line 161. What we want to do is to compute the position of each vertices using the Matrix3D we stored as a constant instead of just copying x and y coordinate to clipspace.
To perform a 4×4 Matrix operation on a Vertex, you need to use the opcode m44 using this syntax
m44 destination, vertex, matrix
Where destination is the output point, the vertex is store in Vertex Attribute 0, and the Matrix in Vertex Constant 0. Got it ? Here what you should get :
// VERTEX SHADER var code:String = ""; code += "m44 op, va0, vc0n"; / Perform a 4x4 matrix operation on each vertices code += "mov v0, va1n";
That’s it ! Now, on every frame, we will create a new Matrix3D, append a translation to it on x axis between -0.5 and 0.5, upload it to the GPU, then execute the program that will perform a m44 operation on each vertices to reflect the translation we made.
Go on, compile, you should see your triangle moving from left to right.
Back to perspective
Now we know how to use a matrix to transform the final rendering of our triangle. Understanding the Math behind perspective is great but you don’t want to do it every time, don’t you ? Hopefully, Adobe provided a downloadable class, PerspectiveMatrix3D.
This class will let you create a Matrix3D with some intelligible parameters to render perspective.
Now, you can either continue to update your HelloTriangle class, or take the same package as above and look for the “AddingPerspective” class.
The AddingPerspective class is actually drawing a square so that the effect of perspective can be noticed more easily. You know how to draw a triangle, drawing a square is just drawing two triangles. You can have a look in the sources, but we will be back to Quads (squares) on the next article which will deals with indexes. Either way, the following example can be achieved using a triangle or a quad, it doesn’t matter.
The PerspectiveMatrix3D class
Among many thing, the perspectiveMatrix3D allows you to define matrix parameter to render perspective using 4 parameters :
- The FoV or Field of View. The FoV, in radians, represent how wide is your field of view. We will set it to 45°
- The aspect ratio is the ratio of your backbuffer. We will set it to (width / height).
- The zNear is the minimum z coordinate that your eye can see. We will set it to 0.1. A word on that later.
- The zFar is the maximum z coordinate that your eye can see. We will set it to 1000.
Go to the render method and instantiate a new PerspectiveMatrix3D object, then apply it the previous parameters.
var projection:PerspectiveMatrix3D = new PerspectiveMatrix3D(); projection.perspectiveFieldOfViewLH(45*Math.PI/180, 4/3, 0.1, 1000);
About the zNear
You may wonder why we won’t render the z from 0, and start at 0.1. Well. Remember the perspective divide was
xP = K1 * x / z yP = K2 * y / z
As we are dividing by z, and because, I hope you know that, dividing by zero is impossible, we can’t have the zNear parameter equal to 0 because the equation couldn’t be computed for objects with a z coordinate set to 0.
This is actually a kind of problem since our triangle’s vertices z coordinates are set to 0. Hold on, don’t go change the VertexBuffer, we learned how to move an object right ? We can simply append a translation on the z axis to push your object a little forward.
What we need to do now is :
- create the PerpectiveMatrix3D as above
- Do some rotation on the m Matrix so we can actually notice the effect of perspective
- Translate a little bit forward our vertices so that they are behind zNear value
- Multiply the first Matrix with the PerspectiveMatrix to add perspective to the final render.
What I get is this :
var projection:PerspectiveMatrix3D = new PerspectiveMatrix3D(); projection.perspectiveFieldOfViewLH(45*Math.PI/180, 4/3, 0.1, 1000); m = new Matrix3D(); m.appendRotation(getTimer()/30, Vector3D.Y_AXIS); m.appendRotation(getTimer()/10, Vector3D.X_AXIS); m.appendTranslation(0, 0, 2); m.append(projection);
Compile, and here it is ! a rotating triangle with some sense of perspective ! If you took my class, you should see a rotating square instead of a triangle.
As always, a little practice on your own is the best way to learn so here is what you can try
- Set R, G and B value to [0-255] instead of [0-1], upload a [255,255,255,255] Vector Fragment Constant, then divide your color values before moving it to the Output Color. You may use the div AGAL opcode
This article was less into code, and I think I will keep it that way for now, for 2 reasons :
- The less I write the more you code
- And it was actually way too long to write the hello triangle article while describing each single lines of code.
Anyway, I will always be giving the class I use as an example, and those class will be documented. If you think that I should go back to something more verbose, just tell me, feedback is always appreciated.
As always, if you have any questions, feel free to ask !