mMcFab
Member
I will leave a (dd/mm/yy) formatted date here, so you know when it was last updated: 16/04/18 - as of IDE: 2.1.4.288, RUNTIME: 2.1.4.203 - Added a tiny tidbit about layer_force_draw_depth() ti setup section
I'll also update the guide if there a better methods that I haven't thought of/found out about yet.
I AM NO LONGER UPDATING THIS PAGE. FURTHER UPDATES OF THIS GUIDE CAN BE FOUND ON MY WEBSITE
Updated Guide Here (Includes a bit of info about transparency and render order): https://maddestudiosgames.com/guide-getting-started-with-3d-in-gms2-project-download/
The website features full syntax highlighting and documentation links too, to make guide-writing much easier.
I have received/seen a few questions regarding getting 3D working and I want to get involved in the forum now that GMS2 is coming, so here's a kickstarting guide.
I have posted this guide in the new GMS2 discussion board, as it would not really be appropriate to post it in another forum yet.
When a dedicated forum for GMS2 programming+guides is created, I will request this guide be moved there.
I recommend that you get @YellowAfterlife's plugin for forum code syntax highlighting. It makes reading code here way easier: https://yellowafterlife.itch.io/syntax-highlighting-for-gamemaker-forum
GM Version: IDE: 2.1.4.288, RUNTIME: 2.1.4.203
Target Platform: ALL
Download: Built Project (A zip, so free users can have a look!)
Links: N/A
Summary:
So, you want to get started in 3D with GMS2? Well, here's a few things you should know first:
Now that that's out of the way, here's a list of what I'll be covering:
I will also cover some basic shader use in future (Both GLSL ES and HLSL), but only when the full release comes out, as we currently have no access to shaders. I will also cover using lighting, but they are more useful when combined with shaders so I may leave lighting until the full release too.
UPDATE ON THIS^^: I don't think I'll actually cover any shader stuff in this specific tutorial - it's a large topic that applies to both 2D and 3D, with loads of guides out already. I may make a more 3D-focused guide, or I may just release some extensions with 3D shaders, with a bunch of commented code to help you get the idea of what's going on. Lighting is an intersing one though. Hmm.
If you go through the guide from start to finish, this is pretty much what you'll see:
Not super impressive, but it covers the basics.
Relevant Documentation Links:
So, lets get started then!
Tutorial:
1) Setting up 3D requirements and a 3D projection with the new camera
2) Using a vertex buffer to build a model and render it
3) Transforming models with the new matrix functionality
4) Other useful d3d_* functions that have been replaced (texture repeat, texture interpolation etc.)
5) Other odds-n-ends I think I should consider
Okay, I think that covers the basics! I will be adding to this, especially when shaders are public. If you have any questions or corrections, let me know!
I'm also considering doing more tutorials like this - perhaps including "Getting to Know the New Camera" - but I'm not sure how I should do it. I'd like to just post them in the forum, but it takes a lot of my time. Do any of you think a Patreon or a Udemy course would be a good idea? Let me know please.
I'll also update the guide if there a better methods that I haven't thought of/found out about yet.
I AM NO LONGER UPDATING THIS PAGE. FURTHER UPDATES OF THIS GUIDE CAN BE FOUND ON MY WEBSITE
Updated Guide Here (Includes a bit of info about transparency and render order): https://maddestudiosgames.com/guide-getting-started-with-3d-in-gms2-project-download/
The website features full syntax highlighting and documentation links too, to make guide-writing much easier.
I have received/seen a few questions regarding getting 3D working and I want to get involved in the forum now that GMS2 is coming, so here's a kickstarting guide.
I have posted this guide in the new GMS2 discussion board, as it would not really be appropriate to post it in another forum yet.
When a dedicated forum for GMS2 programming+guides is created, I will request this guide be moved there.
I recommend that you get @YellowAfterlife's plugin for forum code syntax highlighting. It makes reading code here way easier: https://yellowafterlife.itch.io/syntax-highlighting-for-gamemaker-forum
GM Version: IDE: 2.1.4.288, RUNTIME: 2.1.4.203
Target Platform: ALL
Download: Built Project (A zip, so free users can have a look!)
Links: N/A
Summary:
So, you want to get started in 3D with GMS2? Well, here's a few things you should know first:
- I will be referring to "GameMaker Studio 2" as "GMS2" throughout the guide, because I am too lazy to keep writing it in full.
- This guide is written with beginners to 3D in mind. If I'm explaining something and you think you already know all about it, just skip it. Alternatively, if something I've written is confusing, don't be afraid to ask and I'll try to be more clear!
- I will be referencing matrix/matrices a lot. They are necessary to do almost anything in 3D, but you don't necessarily have to know how they work. A lot of matrices are autogenerated and assigned in the code without ever needing to know how they work. However, knowing how they work can help a lot.
- GMS2, much like GM:S 1.X, is NOT a 3D engine. It is primarily designed for 2D, so 3D games require a LOT of manual work to set up.
- You will need to know how to use GML. I don't know if any D&D functions have been added for 3D (I don't believe so anyway), but this guide will be GML based anyway.
- This guide may be easier if you ever used the "d3d_*" functions, however, they no longer exist. This guide is written with beginners of 3D in mind anyway.
- You can no longer import models using "d3d_model_load" (well, you CAN, but that uses compatibility scripts and is not recommended), so it's a good idea to write your own model importer/exporter. I̶ ̶m̶a̶y̶ ̶w̶r̶i̶t̶e̶ ̶a̶n̶ ̶i̶m̶p̶o̶r̶t̶e̶r̶ ̶i̶n̶ ̶f̶u̶t̶u̶r̶e̶ ̶s̶o̶ ̶p̶e̶o̶p̶l̶e̶ ̶c̶a̶n̶ ̶c̶o̶n̶t̶i̶n̶u̶e̶ ̶u̶s̶i̶n̶g̶ ̶"̶.̶d̶3̶d̶"̶/̶"̶.̶g̶m̶m̶o̶d̶"̶ ̶f̶i̶l̶e̶s̶ ̶i̶n̶ ̶f̶u̶t̶u̶r̶e̶.̶ UPDATE: I've written an exporter for Blender that creates a buffer file that can be loaded with buffer_load - check the extension here: https://marketplace.yoyogames.com/assets/5839/export-3d-blendertogm
- The new tiles do work in a 3D environment, and their z-coordinate is directly taken from layer depth.
- I'll not be covering HUD set up, as that works the same as it has since 1.X (draw GUI), and there are already guides and help available.
- Remember to use the manual! It answers a LOT of the questions you may have, and can give you more information about a function.
- If the manual doesn't help you, you can ask a question here, and I will try to help!
Now that that's out of the way, here's a list of what I'll be covering:
- Setting up 3D requirements and a 3D projection with the new camera
- Using a vertex buffer to build a model and render it
- Transforming models with the new matrix functionality
- Other useful d3d_* functions that have been replaced (texture repeat, texture interpolation, backface culling, etc.)
- Other odds-n-ends I think I should consider (batching, layer depth)
I will also cover some basic shader use in future (Both GLSL ES and HLSL), but only when the full release comes out, as we currently have no access to shaders. I will also cover using lighting, but they are more useful when combined with shaders so I may leave lighting until the full release too.
UPDATE ON THIS^^: I don't think I'll actually cover any shader stuff in this specific tutorial - it's a large topic that applies to both 2D and 3D, with loads of guides out already. I may make a more 3D-focused guide, or I may just release some extensions with 3D shaders, with a bunch of commented code to help you get the idea of what's going on. Lighting is an intersing one though. Hmm.
If you go through the guide from start to finish, this is pretty much what you'll see:
Not super impressive, but it covers the basics.
Relevant Documentation Links:
So, lets get started then!
Tutorial:
1) Setting up 3D requirements and a 3D projection with the new camera
To begin with, the following code will all go in the create event of some kind of control object, unless I specify otherwise.
Okay, before we even get started, we need to enable some things.
In 3D, we have something called the "z-buffer" - this basically enables the sorting of draw order, so things that are further away appear to be behind things that are closer (Note that this behaviour CAN be changed in GMS2, . If we don't enable the z-buffer, then rendering will look very odd, as things far away may draw on top of things that are closer (even within a single model!)
To enable the z-buffer, we just use this code:
You may also find it useful to force layers to draw at a depth of 0 (this will stop layers translating the z-coordinate of what you draw on any layer - though you may find the default behaviour preferable in some cases)
(documentation for layer_force_draw_depth)
Believe it or not, that's all the required set up needed to get rendering in 3D!
Now we can move onto the next step - setting up and using a camera in a 3D environment.
Since the GMS2 update, views have been totally replaced with cameras, but so has the d3d projection functions - they are now the same thing, which is far more convenient when you get used to them.
The first thing we'll want to do is create a camera, assign it to a view, as well as enable views and make yours visible. You can still create views in the room editor, but sometimes it's nice to have all the code in front of you.
Technically speaking, you don't even need a view to set up a 3D projection, however, it makes managing the camera much easier.
Note that, as previously, the game window size will default to the view port/room size of the first room. For this reason, you should have a "set-up room", to get things ready and set the size to be appropriate. Otherwise, you could use the window_* functions combined with view_set_Xport to get the size you want.
Anyway, the code!
First, getting our view to actually appear (you don't need to do this if you have set up a view in the room editor)
Next, we need to create a camera, and make it fully functional. In order to do this, we must create the camera, assign a projection matrix to it and bind it to the camera. We will keep the camera variable, though we will be using "view_camera[0]" to reference it later. We keep the camera variable in case we want to bind the camera to another view later, perhaps in another room, without re-creating it.
NOTE: In GMS2, setting the projection differs from 1.4 - by default, it now does a weird conversion to right-handed coordinate space to stay consistent with 2D, but this makes the up-vector act weird. To make the projection act like 1.4 and before and use left-handed space and make the up-vector act as expected, you must make the fov and ratio values negative, hence the code above.
You may notice that we have assigned a projection matrix, but didn't actually point a camera at anything. This happens later, and separately - this is one of the benefits of the new camera system - we only have to build the projection once, hence saving a lot of time compared to calling d3d_set_projection_ext every draw frame, as half the math is already done.
If you want your camera to have an orthographic projection (instead of perspective), replace the matrix build with this function:
There is one more thing you need to do to get your camera ready! Note that we still haven't assigned a lookat matrix to the camera! This is used to actually point a camera from a location, to a location.
Now, you could update this matrix in step or draw, but cameras come with a new, cool function - "camera_set_update_script()" (there are begin_script and end_script functions too! Check the manual to see more about them!).
Basically, this script is called every draw update per view, assuming the camera is bound to the view. This means you can enable frame skipping, or disable a view, and a camera won't waste time updating what it doesn't need to!
So, to use a camera update script, you must create a script. Ideally, the contents of these scripts should be able to be self contained, and the only external references should be globals or instance variables that you know exist, otherwise the game will probably crash.
For this demo, I just created a camera that would spin around the center of the room, based on time. The script is named "camera_update_script", and contains the code:
Note that we now reference view_camera[0] now. I have done this because it is the only proper way to guarantee that we are targeting the camera that belongs to view0.
As another point of interest, you could set up a test so that the camera only updates the lookat matrix if it has moved or changed orientation, which can save even more processing time.
Okay, the last thing you need to do is assign the update script to the camera. Back in the create event (where your other code should be), just add this:
An that is it! If you set up this camera in a simple, tiled room, it should look something like this:
Aiming and moving your camera around via user input is a little more work and more project-specific (e.g. 3rd person will have different controls to first person, and a flight sim would be different again), but as long as you have a sound understanding of algebra and trigonometry, it should be pretty straightforward.
Okay, before we even get started, we need to enable some things.
In 3D, we have something called the "z-buffer" - this basically enables the sorting of draw order, so things that are further away appear to be behind things that are closer (Note that this behaviour CAN be changed in GMS2, . If we don't enable the z-buffer, then rendering will look very odd, as things far away may draw on top of things that are closer (even within a single model!)
To enable the z-buffer, we just use this code:
Code:
gpu_set_zwriteenable(true);//Enables writing to the z-buffer
gpu_set_ztestenable(true);//Enables the depth testing, so far away things are drawn beind closer things
Code:
layer_force_draw_depth(true, 0);
Believe it or not, that's all the required set up needed to get rendering in 3D!
Now we can move onto the next step - setting up and using a camera in a 3D environment.
Since the GMS2 update, views have been totally replaced with cameras, but so has the d3d projection functions - they are now the same thing, which is far more convenient when you get used to them.
The first thing we'll want to do is create a camera, assign it to a view, as well as enable views and make yours visible. You can still create views in the room editor, but sometimes it's nice to have all the code in front of you.
Technically speaking, you don't even need a view to set up a 3D projection, however, it makes managing the camera much easier.
Note that, as previously, the game window size will default to the view port/room size of the first room. For this reason, you should have a "set-up room", to get things ready and set the size to be appropriate. Otherwise, you could use the window_* functions combined with view_set_Xport to get the size you want.
Anyway, the code!
First, getting our view to actually appear (you don't need to do this if you have set up a view in the room editor)
Code:
//First, we need to enable views and make our view visible (in this case, view 0)
view_enabled = true;//Enable the use of views
view_set_visible(0, true);//Make this view visible
Code:
//First, create the camera. We could use camera_create_view, but that is more useful in a 2D environment
camera = camera_create();
//Then, we need to build a projection matrix. I keep this in instance scope in case I need to reassign it later. (Though you can retrieve matrices from a camera with camera_get functions
//I use matrix_build_projection_perspective_fov, as it gives the most control over how your projections looks.
//Here's how I use the arguments: I give a 60 degree vertical field of view, with a ratio of view_wport/view_hport, with a 32 unit near clipping plane, and a 32000 far clipping plane. Some of these values may need tweaking to your liking.
projMat = matrix_build_projection_perspective_fov(-60, -view_get_wport(0)/view_get_hport(0), 32, 32000);
//Now we assign the projection matrix to the camera
camera_set_proj_mat(camera, projMat);
//Finally, we bind the camera to the view
view_set_camera(0, camera);
You may notice that we have assigned a projection matrix, but didn't actually point a camera at anything. This happens later, and separately - this is one of the benefits of the new camera system - we only have to build the projection once, hence saving a lot of time compared to calling d3d_set_projection_ext every draw frame, as half the math is already done.
If you want your camera to have an orthographic projection (instead of perspective), replace the matrix build with this function:
Code:
matrix_build_projection_ortho(view width, view height, znear, zfar);
Now, you could update this matrix in step or draw, but cameras come with a new, cool function - "camera_set_update_script()" (there are begin_script and end_script functions too! Check the manual to see more about them!).
Basically, this script is called every draw update per view, assuming the camera is bound to the view. This means you can enable frame skipping, or disable a view, and a camera won't waste time updating what it doesn't need to!
So, to use a camera update script, you must create a script. Ideally, the contents of these scripts should be able to be self contained, and the only external references should be globals or instance variables that you know exist, otherwise the game will probably crash.
For this demo, I just created a camera that would spin around the center of the room, based on time. The script is named "camera_update_script", and contains the code:
Code:
//Set up camera location
var zz = -640;
var xx = lengthdir_x(720,-current_time/10) + (room_width*0.5);//Rotation is negative now to match with the old gif and spin clockwise
var yy = lengthdir_y(720,-current_time/10) + (room_height*0.5);
//Build a matrix that looks from the camera location above, to the room center. The up vector points to -z
mLookat = matrix_build_lookat(xx,yy,zz, (room_width*0.5),(room_height*0.5),0, 0,0,-1);
//Assign the matrix to the camera. This updates were the camera is looking from, without having to unnecessarily update the projection.
camera_set_view_mat(view_camera[0], mLookat);
As another point of interest, you could set up a test so that the camera only updates the lookat matrix if it has moved or changed orientation, which can save even more processing time.
Okay, the last thing you need to do is assign the update script to the camera. Back in the create event (where your other code should be), just add this:
Code:
//Assigns the update script named "camera_update_script" to the camera belonging to view0
camera_set_update_script(view_camera[0], camera_update_script);
Aiming and moving your camera around via user input is a little more work and more project-specific (e.g. 3rd person will have different controls to first person, and a flight sim would be different again), but as long as you have a sound understanding of algebra and trigonometry, it should be pretty straightforward.
2) Using a vertex buffer to build a model and render it
Okay, compared to the camera, this is probably harder to understand. However! Don't be put off! I kept putting off learning buffer related things for years, simply because of the word "buffer" - it sounded hard and scary, but they're pretty simply - especially in the context of vertex buffers, where a lot of work is done for you when it comes to reading the buffer.
Again, we'll define most of this stuff in a create event, unless otherwise specified.
So, the vertex buffer is used to build models - or maybe more appropriately - "vertex layouts" - they work in both 2D and 3D, and allow you to draw things with stuff like custom attributes, or with the bare minimum data needed to render, to minimise overhead. When used in conjunction with shaders, they are super powerful.
A vertex buffer is effectively comprised of 2 parts:
- the format, which defines what information the buffer contains
- the buffer, which actually contains the information passed to a shader for rendering
The first thing you'll want to do is build a vertex format. The format contains information about what every single vertex in the buffer will contain. The one we will create will contain a 3D position, a color and a texture coordinate.
It is important to remember the order that you define information in a vertex format, as data added to the buffer must be in the same order. Each defined vertex must also contain all the information, even if you feel a specific vertex does not need part of it.
NOTE: The format described below represents what we lovingly call the "full-fat format" - it has all the elements required to use GameMakers default shader - if you miss any of these elements, drawing will error with an "invalid input layout". If you wish to use fewer components, you need to write a shader. This can have a few benefits such as slightly lower memory usage and lower overhead when the buffer is sent to the shader.
The code:
Now we have a format, we can create and build a vertex buffer. In this case, I'll be building a simple, white plane (the flat type, not the flying one), built with triangle list usage in mind (to see more information on this, check the manual for "draw_primitive_begin". You can build far more complex models, but that would be a little time consuming for this tutorial.
To build the plane, we effectively need 2 triangles. They are defined as follows:
All that's left is to render the vertex buffer - really that is it! I even set up the plane to take texture coordinates properly, so it can have a texture, which is good for floors or ceilings.
So, let's see the monster code that is necessary to render this buffer. This goes in the draw event:
And that's how to build a basic vertex buffer and render it. Hopefully, you'll be able to use this info to build buffers that are a little more complicated than a flat plane, which has little use anyway since the new tiles work in 3D.
One more thing to note is that you'll want to delete your vertex buffers and formats when you're done with them to free up memory.
Vertex buffers are deleted with "vertex_delete_buffer(buffer)", and formats are deleted with "vertex_format_delete(format)". Note that you can only delete a format when all buffers that use it are deleted, otherwise the runner will crash. There is also the function "vertex_freeze(buffer)" - this will disable editing/updating the buffer, but provides a performance increase.
So, now you know how to create a vertex buffer, but it can be a painfully boring process. It's good to understand what they are and how they work (and really useful for creating dynamic meshes), but there's a much easier way to create static meshes - Blender! And I made an export for Blender that creates models that can be loaded with buffer_load or my extension for GameMaker!
The extension includes a simple model_load("") script and vertex format building script - the models can then be rendered with vertex_submit.
(I've also got a Blender export/import working for rigged/animated models, but it needs so much documentation still... But it's coming!)
If you want to move the model around, check out the next part: "Transforming models with the new matrix functionality"
Again, we'll define most of this stuff in a create event, unless otherwise specified.
So, the vertex buffer is used to build models - or maybe more appropriately - "vertex layouts" - they work in both 2D and 3D, and allow you to draw things with stuff like custom attributes, or with the bare minimum data needed to render, to minimise overhead. When used in conjunction with shaders, they are super powerful.
A vertex buffer is effectively comprised of 2 parts:
- the format, which defines what information the buffer contains
- the buffer, which actually contains the information passed to a shader for rendering
The first thing you'll want to do is build a vertex format. The format contains information about what every single vertex in the buffer will contain. The one we will create will contain a 3D position, a color and a texture coordinate.
It is important to remember the order that you define information in a vertex format, as data added to the buffer must be in the same order. Each defined vertex must also contain all the information, even if you feel a specific vertex does not need part of it.
NOTE: The format described below represents what we lovingly call the "full-fat format" - it has all the elements required to use GameMakers default shader - if you miss any of these elements, drawing will error with an "invalid input layout". If you wish to use fewer components, you need to write a shader. This can have a few benefits such as slightly lower memory usage and lower overhead when the buffer is sent to the shader.
The code:
Code:
//Begin defining a format
vertex_format_begin();
vertex_format_add_position_3d();//Add 3D position info
vertex_format_add_color();//Add color info
vertex_format_add_textcoord();//Texture coordinate info
//End building the format, and assign the format to the variable "format"
format = vertex_format_end();
To build the plane, we effectively need 2 triangles. They are defined as follows:
Code:
//Create the vertex buffer. Another function, vetex_create_buffer_ext can be used to create the buffer with its size predefined and fixed.
//With the standard vertex_create_buffer, the buffer will just grow automatically as needed.
vb_plane = vertex_create_buffer();
//Begin building the buffer using the format defined previously
vertex_begin(vb_plane, format);
//Using size to keep it square if we decide to change how bug it is.
var size = 32;
//Add the six vertices needed to draw a simple square plane.
//The first triangle
vertex_position_3d(vb_plane, -size, -size, 0);
vertex_color(vb_plane, c_white, 1);
vertex_texcoord(vb_plane, 0, 0);
vertex_position_3d(vb_plane, size, -size, 0);
vertex_color(vb_plane, c_white, 1);
vertex_texcoord(vb_plane, 1, 0);
vertex_position_3d(vb_plane, -size, size, 0);
vertex_color(vb_plane, c_white, 1);
vertex_texcoord(vb_plane, 0, 1);
//The second triangle. The winding order has been maintained so drawing is consistent if culling is enabled.
vertex_position_3d(vb_plane, -size, size, 0);
vertex_color(vb_plane, c_white, 1);
vertex_texcoord(vb_plane, 0, 1);
vertex_position_3d(vb_plane, size, -size, 0);
vertex_color(vb_plane, c_white, 1);
vertex_texcoord(vb_plane, 1, 0);
vertex_position_3d(vb_plane, size, size, 0);
vertex_color(vb_plane, c_white, 1);
vertex_texcoord(vb_plane, 1, 1);
//Finish building the buffer.
vertex_end(vb_plane);
So, let's see the monster code that is necessary to render this buffer. This goes in the draw event:
Code:
//Arguments are just vertex_buffer, primitive type and texture.
vertex_submit(vb_plane, pr_trianglelist, -1);
One more thing to note is that you'll want to delete your vertex buffers and formats when you're done with them to free up memory.
Vertex buffers are deleted with "vertex_delete_buffer(buffer)", and formats are deleted with "vertex_format_delete(format)". Note that you can only delete a format when all buffers that use it are deleted, otherwise the runner will crash. There is also the function "vertex_freeze(buffer)" - this will disable editing/updating the buffer, but provides a performance increase.
So, now you know how to create a vertex buffer, but it can be a painfully boring process. It's good to understand what they are and how they work (and really useful for creating dynamic meshes), but there's a much easier way to create static meshes - Blender! And I made an export for Blender that creates models that can be loaded with buffer_load or my extension for GameMaker!
The extension includes a simple model_load("") script and vertex format building script - the models can then be rendered with vertex_submit.
(I've also got a Blender export/import working for rigged/animated models, but it needs so much documentation still... But it's coming!)
If you want to move the model around, check out the next part: "Transforming models with the new matrix functionality"
3) Transforming models with the new matrix functionality
Ok, at this point, you have a little permission to shout. If you'd just done the guide on vertex buffers, you could be shouting "Yeah, okay, but how do I move the model with building it over and over again at different positions! This is DUMB!!!"
Luckily for you, there is a solution!
Using matrices, you can transform where is drawn. In a most basic sense, you can transform the translation, rotation and scaling of how something is drawn. If you understand slightly more complex matrix usage, you can even do things like shearing a drawing. Don't let this put you off though! You don't *need* to understand matrices to use matrix_build, but it does help!
So, in order to transform your drawing, you use the function "matrix_build". This takes in rotation, translation and scaling as arguments, and builds a matrix. The order of transforms is YXZ, built to render in the order rotate->scale->translate. Note that this can have unwanted results when scaling each axis non-uniformly, as the scaling happens after rotation, unlike when drawing sprites which scales and then rotates. To work around this, you must build at least 2 matrices and multiply them (or use the new matrix stack). NOTE: I did request that order be configurable/changed, but it isn't planned.
This gif shows how the result may be unexpected (matrix on the left, sprite_ext on the right, same transform input)
Anyway, onto building the matrix. For this example, I will use a matrix to move the drawing to the center of the room, and rotate it by 45 degrees about the Z-axis. We must then assign this matrix to the world matrix. This is best done in the draw event, right before you need it.
You can then render what you want, or submit your vertex buffers. It works on both.
After drawing your transformed output, you should reset the transform matrix to the identity, so drawing returns to normal.
If we use the vertex format from the previous tutorial, we'll have code like this:
And that concludes the basics of transforming in 3D. These methods also work in 2D, as this is an exact replacement of the d3d_transform functions.
The above performs the same way as d3d_transform_set_*
To replace d3d_transform_add_*, you can multiply matrices together, using matrix_multiply. This is used like this:
In future, I will add info about the matrix_stack. From what I can tell, it works roughly the same way as the d3d_transform stack, but I am not yet confident enough to write a guide about it. It seems to be a handy way of sotring matrices globally without the use of vars.
Luckily for you, there is a solution!
Using matrices, you can transform where is drawn. In a most basic sense, you can transform the translation, rotation and scaling of how something is drawn. If you understand slightly more complex matrix usage, you can even do things like shearing a drawing. Don't let this put you off though! You don't *need* to understand matrices to use matrix_build, but it does help!
So, in order to transform your drawing, you use the function "matrix_build". This takes in rotation, translation and scaling as arguments, and builds a matrix. The order of transforms is YXZ, built to render in the order rotate->scale->translate. Note that this can have unwanted results when scaling each axis non-uniformly, as the scaling happens after rotation, unlike when drawing sprites which scales and then rotates. To work around this, you must build at least 2 matrices and multiply them (or use the new matrix stack). NOTE: I did request that order be configurable/changed, but it isn't planned.
This gif shows how the result may be unexpected (matrix on the left, sprite_ext on the right, same transform input)
Code:
var mat = matrix_build(room_width * 0.5, room_height * 0.5, 0, 0, 0, 45, 1, 1, 1);
//The world matrix is what is used to transform drawing within "world" or "object" space.
matrix_set(matrix_world, mat);
After drawing your transformed output, you should reset the transform matrix to the identity, so drawing returns to normal.
Code:
//Resetting transforms can be done like this:
matrix_set(matrix_world, matrix_build_identity());
Code:
var mat = matrix_build(room_width * 0.5, room_height * 0.5, 0, 0, 0, 45, 1, 1, 1);
//The world matrix is what is used to transform drawing within "world" or "object" space.
matrix_set(matrix_world, mat);
//Draw the buffer
vertex_submit(vb_plane, pr_trianglelist, -1);
//Resetting transforms can be done like this:
matrix_set(matrix_world, matrix_build_identity());
The above performs the same way as d3d_transform_set_*
To replace d3d_transform_add_*, you can multiply matrices together, using matrix_multiply. This is used like this:
Code:
newMatrix = matrix_multiply(currentTransformMatrix, addedTransformMatrix);
4) Other useful d3d_* functions that have been replaced (texture repeat, texture interpolation etc.)
Well, this is more like a list, but I'll go through them anyway. This is more useful to those used to d3d_* functions, but I'll have a breif explanation on each on wwhat they do. It's not a definitive list, but it contains the functions I used the most.
One of the most useful functions is culling, which boosts performance by not rendering both sides of a "triangle". This used to be "d3d_set_culling()". The new equivalent is "gpu_set_cullmode()".
The direct equivalent is using "gpu_set_cullmode(cull_counterclockwise);", however we can now reverse the cull order by using "gpu_set_cullmode(cull_clockwise)" instead - this is good for shader makers in particular.
To disable culling, just use "gpu_set_cullmode(cull_noculling)"
Note that depending on how you build your projection matrix, you may need to reverse the cull order
"gpu_set_texfilter" is used for enabling linear interpolation. An _ext version exists for setting specific texture stages.
"gpu_set_texrepeat" is used to enable texture repeating. An _ext version exists for setting specific texture stages.
All the blendmode functions have been replaced with "gpu_get/set_blendmode" functions. Look em' up! They are far more flexible now. You can also enable/disable blending with "gpu_set_blendenable".
Fog and alpha tests are now "gpu_*" functions.
d3d_lights still exist! They are just draw_lights instead.
d3d_set_shading is gone! So you no longer have a choice between automatic flat or gouraud shading - the alternative is to write a shader and make sure your normals are correct for the type of shading you want. Luckily, my Blender export handles both flat and smooth normals on export.
"d3d_set_perspective()" is gone too (the one that flips the y axis in 3D mode), but it can be replicated. Noting the above cameras, we used negative fov and aspect values in order to use left-handed space.
e.g, go from this:
to this:
One of the most useful functions is culling, which boosts performance by not rendering both sides of a "triangle". This used to be "d3d_set_culling()". The new equivalent is "gpu_set_cullmode()".
The direct equivalent is using "gpu_set_cullmode(cull_counterclockwise);", however we can now reverse the cull order by using "gpu_set_cullmode(cull_clockwise)" instead - this is good for shader makers in particular.
To disable culling, just use "gpu_set_cullmode(cull_noculling)"
Note that depending on how you build your projection matrix, you may need to reverse the cull order
"gpu_set_texfilter" is used for enabling linear interpolation. An _ext version exists for setting specific texture stages.
"gpu_set_texrepeat" is used to enable texture repeating. An _ext version exists for setting specific texture stages.
All the blendmode functions have been replaced with "gpu_get/set_blendmode" functions. Look em' up! They are far more flexible now. You can also enable/disable blending with "gpu_set_blendenable".
Fog and alpha tests are now "gpu_*" functions.
d3d_lights still exist! They are just draw_lights instead.
d3d_set_shading is gone! So you no longer have a choice between automatic flat or gouraud shading - the alternative is to write a shader and make sure your normals are correct for the type of shading you want. Luckily, my Blender export handles both flat and smooth normals on export.
"d3d_set_perspective()" is gone too (the one that flips the y axis in 3D mode), but it can be replicated. Noting the above cameras, we used negative fov and aspect values in order to use left-handed space.
e.g, go from this:
Code:
matrix_build_projection_perspective_fov(-60, -view_get_wport(0)/view_get_hport(0), 32, 32000);
Code:
matrix_build_projection_perspective_fov(60, view_get_wport(0)/view_get_hport(0), 32, 32000);
5) Other odds-n-ends I think I should consider
Layer depth - this is a thing that can and will affect any 3D rendering! Layer depth of the instance that is drawing is added on relative to the current world matrix! Keep this in mind if something seems to have its Z offset weirdly.
Vertex batches - the more of these, the slower your game can run as more info has to be sent to the graphics pipeline. This is more of a concern on mobile targets as most modern PCs can handle a lot, but optimisation is still important and good.
When using normal rendering (e.g., draw_sprite), GameMaker automatically handles batches for you, but there are a few things that can break a batch (into more parts - don't worry, your game won't be broken):
This is pretty challenging for a couple of reasons.
Currently, I've been just using separate buffers anyway. As long as you freeze them, they're usually still really fast.
I've rendered 2000 separate texured cubes, 25 terrain tiles each consisting of 32,768 faces, and 3 rigged, animated and textured characters each with about 9 meshes while staying above 90 fps_real on my low-mid range PC. Performance could definitely be better (and it will be when I've got basic batching in), but that's just a rough baseline for concern. In my experience, the limit is much harsher on mobile platforms in general.
Vertex batches - the more of these, the slower your game can run as more info has to be sent to the graphics pipeline. This is more of a concern on mobile targets as most modern PCs can handle a lot, but optimisation is still important and good.
When using normal rendering (e.g., draw_sprite), GameMaker automatically handles batches for you, but there are a few things that can break a batch (into more parts - don't worry, your game won't be broken):
- Texture Swaps - this makes it important to organise texture pages to minimise the amount of swaps (e.g, keep GUI on one texture page and render it in one group)
- Shader changes - whether you are changing the shader target or just updating a uniform, the batch will be broken
- matrix_set() - when on of the main render matrices is set, internal matrices need updating and shader uniforms for gm_Matrices are updated. Hence, the batch is broken.
- There are probably more, but I'm not an expert at what makes GameMaker batches break, so I've just listed the ones I'm confident about - if you have a correction or know of something else that breaks the batch, I'll list it here with a " - thanks, YOURNAME!".
This is pretty challenging for a couple of reasons.
- Sprites are on texture pages. Texture Page swaps break a batch, and there's no way to tell what texture page a sprite is on in GameMaker. That really sucks. The only current workaround I know of is to make your own texture atlas and manage sprite rendering yourself.
- Packing info into one buffer is annoying - vertex_begin clears the vertex buffer first, and there is no "vertex_append" equivalent. In order to do this, you've got to manage all vertex information in a regular buffer (which is fairly easy if you know how vertex buffers are structured) and then convert it to a vbuffer.
- In order to not break a batch and still be able to transform vertices, you've got to change them CPU side with matrix_transform_vertex(). This can get pretty slow with a lot of vertices, particularly in GML with a lot of array access calls to get the new location, then lots of buffer writes. This can be partially solved with an extension that directly accesses the buffer, but that's far from ideal. If you're only transforming the location of vertices or doing basic scaling, it can be faster though, as you don't need matrices.
Currently, I've been just using separate buffers anyway. As long as you freeze them, they're usually still really fast.
I've rendered 2000 separate texured cubes, 25 terrain tiles each consisting of 32,768 faces, and 3 rigged, animated and textured characters each with about 9 meshes while staying above 90 fps_real on my low-mid range PC. Performance could definitely be better (and it will be when I've got basic batching in), but that's just a rough baseline for concern. In my experience, the limit is much harsher on mobile platforms in general.
Okay, I think that covers the basics! I will be adding to this, especially when shaders are public. If you have any questions or corrections, let me know!
I'm also considering doing more tutorials like this - perhaps including "Getting to Know the New Camera" - but I'm not sure how I should do it. I'd like to just post them in the forum, but it takes a lot of my time. Do any of you think a Patreon or a Udemy course would be a good idea? Let me know please.
Last edited: