…and removing the need for compatibility scripts!
- Recently, a GameMaker user asked me if I could update the “3D Road” project from the game-maker.ru site to GMS2 and remove the need for compatibility scripts.Since this is a general project that proves a good example for updating any D3D based project to GMS2, I think it is worth me writing about!
“3D Road” is a GameMaker 7 project from 2010 which appears to be created by a user named Vendet., which demos dynamically generating a 3D road model using 2 paths (a layout and a height map) and some math.
The problem is that it’s a 10 year old project targeting an (at this point) ancient version of GameMaker, and updating it straight to GMS2 does not work – it doesn’t crash, but textures are mapped incorrectly, the field of view is messed up and it has to use compatibility scripts to “function”.
So, let’s fix this, going step by step on how to upgrade this project.
Here is a download for the updated project that can be imported to GMS2: roadtest22GMS2GuideVer.yyz
I’ve provided this download for learning purposes as you get to poke around the code, but it would probably help to read the guide, to help understand the process of updating this project.
A supplemental video version of this guide is available on Youtube: Video
1) Download and Prepare the Project
First, we want to download the base project from the game-maker.ru website. Download the GameMaker 7 Project from this page. There is a ZIP folder containing the “roadtest22.gmk” file.
Now, we need to load it into GameMaker Studio 2. There is one problem, however – GameMaker Studio 2 cannot import .GMK files. But there is a solution – GameMaker Studio 1.4 can. You can download this older version of Studio if you have a GMS2 license. It’s available on the Downloads page of your YoYoGames account, in a tab labelled “GameMaker: Studio 1.4”.
First, import the GMK into Studio 1.4 and save the project – you’ll need to makes sure the import file filter includes *.gmk files. If you launch it at this point, you’ll see that it runs, but the textures are messed up. This is because of some texture optimisations (like atlasing) that were added in newer versions that work great in 2D, but require some tweaks to work correctly with 3D rendering in Studio versions of GameMaker. We will worry about these changes in GMS2.
Now, open GMS2 and import the new Studio 1.4 project. Once you’ve saved this imported version, you can delete the Studio 1.4 version if you want, since it won’t be needed anymore.
If you run the game now, you’ll see that it is even worse.
Not only are the textures wrong, even the view projection is completely different – it’s orthographic now!
Now we must update the project and fix all these issues.
Let’s deal with this step by step:
2) Fix the Camera
First, we’ll fix the camera. This will first make it possible to look at the game, and make it easier to tell what our fixes are doing.
Open up “obj_camera”. This handles some important stuff, but we want to go through stuff step by step.
Let’s begin with the “Create” event. Here’s what it looks like to begin with:
? Create: obj_camerad3d_start();
pos = 0;
z = 0;
targetx = 0;
targety = 0;
targetz = 0;
This is perfectly reasonable, but the d3d_start()
function is a compatibility script, so we want to remove it. The equivalent functions are gpu_set_ztestenable(true)
and gpu_set_zwriteenable(true)
. This enables reading and writing to the zbuffer, allowing proper 3D depth sorting. For the sake of consistency, let’s also make sure texture filtering and texture repetition is enabled with gpu_set_texrepeat(true)
and gpu_set_texfilter(true)
. Optionally, we can also add gpu_set_cullmode(cull_counterclockwise)
which will optimise rendering by disabling the drawing of the back-faces of triangles.
Here’s the updated event:
? Create: obj_cameragpu_set_ztestenable(true);
gpu_set_zwriteenable(true);
gpu_set_texrepeat(true);
gpu_set_texfilter(true);
gpu_set_cullmode(cull_counterclockwise);
pos = 0;
z = 0;
targetx = 0;
targety = 0;
targetz = 0;
We’ll be adding more code to this event, so keep it open.
Let’s look at the step event.
? Step: obj_camerax = path_get_x(path0, pos);
y = path_get_y(path0, pos);
z = path_get_y(path1, pos) + mouse_y;
targetx = path_get_x(path0, (pos+0.01) mod 1);
targety = path_get_y(path0, (pos+0.01) mod 1);
targetz = path_get_y(path1, (pos+0.01) mod 1);
pos += 0.001;
if (pos > 1) pos -= 1.0;
The tweaks I am going to do are the following: I’m going to halve the rate of increase of pos
to 0.0005 and increase the games FPS to 60 in the Main options. This is not essential, but makes the game nicer to look at. I am also going to change mouse_y
to device_mouse_y_to_gui(0)
so that view positions don’t interfere with the value. This will be more helpful later.
Since the default resource names are bothering me, I also renamed the paths from path0
and path1
to path_road_xy
and path_road_z
respectively, and updated the code using Find & Replace (so that the road object updates too).
The updated version of the event:
? Step: obj_camerax = path_get_x(path_road_xy, pos);
y = path_get_y(path_road_xy, pos);
z = path_get_y(path_road_z, pos) + device_mouse_y_to_gui(0);
targetx = path_get_x(path_road_xy, (pos+0.01) mod 1);
targety = path_get_y(path_road_xy, (pos+0.01) mod 1);
targetz = path_get_y(path_road_z, (pos+0.01) mod 1);
pos += 0.0005;
if (pos > 1) pos -= 1.0;
Now let’s look at the draw event:
✏️ Draw: obj_camerad3d_set_fog(true, make_color_rgb(155,155,155), 1, 500);
d3d_set_projection(x,y,z+5,targetx,targety,targetz+4, 0,0,1);
This can be broken down to two things: Setting the fog (and by extension, the clipping plane) and setting up the camera matrices and applying them.
First, the fog. We can replace this function just by putting gpu_set_fog()
in the create event with the exact same arguments.
Now let’s cover the camera. In the create event, we’ll want to create a new GameMaker Studio 2 camera, with “camera_create()”. We’ll also want to set up a projection, bind it to a view and make sure views are enabled. Simply put, this is the necessary code:
//Enable the use of views
view_enabled = true;
//Create the camera, bind the camera to a view, and make the view visible.
camera = camera_create();
view_set_camera(0, camera);
view_set_visible(0, camera);
//Set the camera projection matrix.
camera_set_proj_mat(camera,
matrix_build_projection_perspective_fov(-60, -view_get_wport(0)/view_get_hport(0), 1, 32000)
);
The FOV and aspect ratio are negative because GameMaker Studio 2 uses a weird hack to put 3D space in right-handed mode by default, but this is incompatible with older versions and breaks the usage of up-vectors. These negative values flip everything back into left-handed space and make other values behave more consistently. If this doesn’t mean anything to you, don’t worry about it too much.
I chose 60 degrees for the vertical FOV because it’s one I’m comfortable with, but you can adjust it to taste. If you are making a public game, this value should be user-adjustable, for accessibility reasons.
The aspect ratio is the viewport width divided by the viewport height. This is pretty standard in making sure everything looks right, as it keeps things square. It basically determines the horizontal FOV. You can actually vary the ratio somewhat to make some cool effects, but that’s beyond the scope of this guide.
The znear and zfar values are the defaults GameMaker will use. Usually, you want to keep this range as low as possible, as it can cause z-fighting issues if it’s too big. This depends on your worldscale though.
After that, we just need to tell the camera where to look. For this, I added the following line to the end of the step event:
camera_set_view_mat(camera, matrix_build_lookat(x,y,z+5,targetx,targety,targetz+4, 0,0,1));
The arguments used are the exact same ones used in the original d3d_set_projection function.
Now this is done, we can actually just delete the Draw Event from the camera object entirely, since the view will manage the remaining stuff.
Now, the camera should contain the following two events, with the following code:
? Create: obj_cameragpu_set_ztestenable(true);
gpu_set_zwriteenable(true);
gpu_set_texrepeat(true);
gpu_set_texfilter(true);
gpu_set_cullmode(cull_counterclockwise);
gpu_set_fog(true, make_color_rgb(155,155,155), 1, 500);
pos = 0;
z = 0;
targetx = 0;
targety = 0;
targetz = 0;
//Enable the use of views
view_enabled = true;
//Create the camera, bind the camera to a view, and make the view visible.
camera = camera_create();
view_set_camera(0, camera);
view_set_visible(0, camera);
//Set the camera projection matrix.
camera_set_proj_mat(camera,
matrix_build_projection_perspective_fov(-60, -view_get_wport(0)/view_get_hport(0), 1, 32000)
);
? Step: obj_camerax = path_get_x(path_road_xy, pos);
y = path_get_y(path_road_xy, pos);
z = path_get_y(path_road_z, pos) + device_mouse_y_to_gui(0);
targetx = path_get_x(path_road_xy, (pos+0.01) mod 1);
targety = path_get_y(path_road_xy, (pos+0.01) mod 1);
targetz = path_get_y(path_road_z, (pos+0.01) mod 1);
pos += 0.0005;
if (pos > 1) pos -= 1.0;
camera_set_view_mat(camera, matrix_build_lookat(x,y,z+5,targetx,targety,targetz+4, 0,0,1));
If we run the game, we can see that the projection is now correct, even if the textures are completely broken.
At this point, we can close the camera object because we are done with it, and move on to textures
3) Fix the Textures (Easy Method)
Now we’ll fix the textures. I’m calling this the “Easy Method” because there are two major ways to fix this, with this being the easier method at the cost of more texture swaps. I will discuss the second method later, which equally has drawbacks, but tends to result in better performance – especially on mobile platforms.
First, we’ll fix the thing where all the textures appear on each surface. This can be done by marking every sprite with “Use Separate Texture Page”.
Now everything is on a separate texture page. Let’s see how it looks.
That’s a slight improvement. Each thing is using its own texture now, but on some things it’s… small and separated.
The reason is that the textures are not powers of 2 in dimensions, so they get padded up and the UVs map to this full, padded size. We can fix this by re-scaling the textures to power of 2 sizes (note that width and height can be two different powers of two – the images do not need to be square)
We can delete “background1” since it is unused. “background0” is already pow2 so it needs no changes, which is also why the road looked fine immediately.
At this point, I’m going to rename the sprites too, and update references with global Find and Replace, since they aren’t exactly… descriptive.
- background0 becomes spr_road
- background2 becomes spr_grass
- bkg_concrete becomes spr_concrete, purely for consistency.
Now the textures are usable sizes, let’s look again:
Perfect! It looks almost identical to the original – you can hardly tell some textures were re-scaled.
At this point, we could consider the update done, but I was asked to replace the compatibility scripts, so I will!
4) Replacing the Remaining Compatibility Scripts
All the remaining compatibility scripts are used in the obj_road object. It is the most complicated thing in here and needs the most work to be updated. There are hardly any comments, no local variables and uninformative instance variable names.
Let’s start simple by updating texture fetching and remove the need for “background” compatibility scripts.
In the create event, there is this code snippet (after updating resource names, that is):
tex = background_get_texture( spr_road );
tex1 = background_get_texture( spr_grass );
tex2 = background_get_texture( spr_concrete );
To update this, all we need to do is replace background_get_texture
with sprite_get_texture
and add an image argument (which will be 0, since all these sprites have only one image).
I’m going to do an extra step, and move this code to the draw event and mark the variables as locals with var
since they aren’t used outside of drawing. I’m also going to give them recognisable names, and update the references in the draw event.
This means that the code above is deleted from Create and the following code is added to the top of the Draw event:
var tex_road = sprite_get_texture( spr_road, 0 );
var tex_grass = sprite_get_texture( spr_grass, 0 );
var tex_concrete = sprite_get_texture( spr_concrete, 0 );
and all references to tex
, tex1
and tex2
are updated to tex_road
, tex_grass
and tex_concrete
respectively, using Search and Replace.
While we’re in draw, we can delete references to d3d_set_culling
, since we replaced it with gpu_set_cullmode
earlier, in the Create event of obj_camera. This will make the floor disappear, but we’ll fix that soon, by replacing d3d_draw_floor
Since every 3D thing in this demo is an unchanging model, we will replace them with vertex buffers generated in the Create event. First, we’ll need a vertex format. Since there is no lighting, we’ll generate a simple Position-Color-Texture format:
Vertex Format Definitionvertex_format_begin();
vertex_format_add_position_3d();
vertex_format_add_color();
vertex_format_add_texcoord();
VERTEX_FORMAT = vertex_format_end();
Let’s begin with a buffer for the grassy field. This is basically a big square with a repeating texture. Generating it is as easy as generating two triangles. We’ll use some local variables based on the old d3d_draw_floor(-1800,-1800,-1,1800,1800,-1,tex_grass,80,80)
to make it easier to tweak:
Generate a Textured Plane//Define bounds and UVs
var _xmin = -1800, _xmax = 1800, _ymin = -1800, _ymax = 1800, _z = -1;
var _umin = 0, _umax = 80, _vmin = 0, _vmax = 80;
var _color = c_white, _alpha = 1;
FIELD_VBUFF = vertex_create_buffer();
//I use this short local variable to make typing quicker.
var _b = FIELD_VBUFF;
vertex_begin(_b, VERTEX_FORMAT);
//Tri 1
vertex_position_3d(_b, _xmin, _ymin, _z);
vertex_color(_b, _color, _alpha);
vertex_texcoord(_b, _umin, _vmin);
vertex_position_3d(_b, _xmax, _ymin, _z);
vertex_color(_b, _color, _alpha);
vertex_texcoord(_b, _umax, _vmin);
vertex_position_3d(_b, _xmin, _ymax, _z);
vertex_color(_b, _color, _alpha);
vertex_texcoord(_b, _umin, _vmax);
//Tri 2
vertex_position_3d(_b, _xmin, _ymax, _z);
vertex_color(_b, _color, _alpha);
vertex_texcoord(_b, _umin, _vmax);
vertex_position_3d(_b, _xmax, _ymin, _z);
vertex_color(_b, _color, _alpha);
vertex_texcoord(_b, _umax, _vmin);
vertex_position_3d(_b, _xmax, _ymax, _z);
vertex_color(_b, _color, _alpha);
vertex_texcoord(_b, _umax, _vmax);
vertex_end(_b);
//Freeze to improve performance
vertex_freeze(_b);
It looks like a lot of code for making a square, so it might be handy to make a script for this sort of thing. I would suggest checking out “Slightly Less Tedious Vertex Buffer Building“, which I made to make vertex buffer building much more user-friendly through the power of #macro
.
Note we do not set a texture here – we still do that in draw. All we need to do is replace the d3d_draw_floor
line with vertex_submit(FIELD_VBUFF, pr_trianglelist, tex_field)
and our field is back!
Now we have the most complicated part – updating the road generation. This is made up of 2 for
loops – one in Create, which pre-builds some variables, and one in Draw, which uses these variables to generate meshes. Here are those loops currently:
The Loops
Create Loopsubdiv = 200;
texrep = 20;
spokediv = 5;
road_width = 10;
count = 0;
for (i = 0; i < subdiv; i+= 1) {
x1 = path_get_x(path_road_xy, i/subdiv);
y1 = path_get_y(path_road_xy, i/subdiv);
x2 = path_get_x(path_road_xy, (i+1)/subdiv);
y2 = path_get_y(path_road_xy, (i+1)/subdiv);
z1 = path_get_y(path_road_z, i/subdiv);
z2 = path_get_y(path_road_z, (i+1)/subdiv);
nx = y2-y1;
ny = x1-x2;
len = sqrt(nx*nx+ny*ny);
nx /= len;
ny /= len;
slx1[count] = x1 - nx*road_width;
sly1[count] = y1 - ny*road_width;
slx2[count] = x1 + nx*road_width;
sly2[count] = y1 + ny*road_width;
slz1[count] = z1;
slz2[count] = z2;
x1 = path_get_x(path_road_xy, (i+1)/subdiv);
y1 = path_get_y(path_road_xy, (i+1)/subdiv);
x2 = path_get_x(path_road_xy, (i+2)/subdiv mod 1);
y2 = path_get_y(path_road_xy, (i+2)/subdiv mod 1);
nx = y2-y1;
ny = x1-x2;
len = sqrt(nx*nx+ny*ny);
nx /= len;
ny /= len;
slx3[count] = x1 - nx*road_width;
sly3[count] = y1 - ny*road_width;
slx4[count] = x1 + nx*road_width;
sly4[count] = y1 + ny*road_width;
su1[count] = 0; sv1[count] = i/subdiv;
su2[count] = 1; sv2[count] = i/subdiv;
su3[count] = 0; sv3[count] = (i+1)/subdiv;
su4[count] = 1; sv4[count] = (i+1)/subdiv;
sv1[count] = (sv1[count]*texrep) mod 1;
sv2[count] = (sv2[count]*texrep) mod 1;
sv3[count] = (sv3[count]*texrep) mod 1;
sv4[count] = (sv4[count]*texrep) mod 1;
count += 1;
}
Draw Loopcount = 0;
for (i = 0; i < subdiv; i+= 1) {
lx1 = slx1[count];
ly1 = sly1[count];
lx2 = slx2[count];
ly2 = sly2[count];
lx3 = slx3[count];
ly3 = sly3[count];
lx4 = slx4[count];
ly4 = sly4[count];
z1 = slz1[count];
z2 = slz2[count];
u1 = su1[count];
u2 = su2[count];
u3 = su3[count];
u4 = su4[count];
v1 = sv1[count];
v2 = sv2[count];
v3 = sv3[count];
v4 = sv4[count];
d3d_primitive_begin_texture(pr_trianglelist, tex_road);
d3d_vertex_texture(lx1,ly1,z1, u1,v1);
d3d_vertex_texture(lx2,ly2,z1, u2,v2);
d3d_vertex_texture(lx4,ly4,z2, u4,v4);
d3d_vertex_texture(lx1,ly1,z1, u1,v1);
d3d_vertex_texture(lx4,ly4,z2, u4,v4);
d3d_vertex_texture(lx3,ly3,z2, u3,v3);
d3d_primitive_end();
draw_set_color(make_color_rgb(100,100,100));
d3d_primitive_begin_texture(pr_trianglelist, tex_road);
d3d_vertex(lx1,ly1,z1-1);
d3d_vertex(lx4,ly4,z2-1);
d3d_vertex(lx2,ly2,z1-1);
d3d_vertex(lx1,ly1,z1-1);
d3d_vertex(lx3,ly3,z2-1);
d3d_vertex(lx4,ly4,z2-1);
// road side left
d3d_vertex(lx1,ly1,z1-1);
d3d_vertex(lx3,ly3,z2);
d3d_vertex(lx3,ly3,z2-1);
d3d_vertex(lx1,ly1,z1-1);
d3d_vertex(lx1,ly1,z1);
d3d_vertex(lx3,ly3,z2);
// road side right
d3d_vertex(lx2,ly2,z1-1);
d3d_vertex(lx4,ly4,z2-1);
d3d_vertex(lx4,ly4,z2);
d3d_vertex(lx2,ly2,z1-1);
d3d_vertex(lx4,ly4,z2);
d3d_vertex(lx2,ly2,z1);
d3d_primitive_end();
draw_set_color(c_white);
// draw road shadow
col = 0;
d3d_primitive_begin_texture(pr_trianglelist, tex_road);
d3d_vertex_texture_color(lx1,ly1,-0.5, u1,v1, col, 0.5);
d3d_vertex_texture_color(lx2,ly2,-0.5, u2,v2, col, 0.5);
d3d_vertex_texture_color(lx4,ly4,-0.5, u4,v4, col, 0.5);
d3d_vertex_texture_color(lx1,ly1,-0.5, u1,v1, col, 0.5);
d3d_vertex_texture_color(lx4,ly4,-0.5, u4,v4, col, 0.5);
d3d_vertex_texture_color(lx3,ly3,-0.5, u3,v3, col, 0.5);
d3d_primitive_end();
if (count mod spokediv == 0) {
mx = 0.5*(lx1+lx2);
my = 0.5*(ly1+ly2);
draw_set_color(make_color_rgb(200,200,200));
d3d_draw_cylinder(mx-3,my-3,0, mx+3,my+3,z1-1, tex_concrete, 1,1, false, 8);
draw_set_color(c_white);
}
count += 1;
}
The useful thing about using vertex buffers is we can now combine these loops. While combining them, we can also make some variables local and simplify some stuff too. Any array read and write in these loops can be removed. count
can be removed, since its value is the same as i
Here is what the logic looks like when combined:
Combined Loop Logicvar subdiv = 200;
var texrep = 20;
var spokediv = 5;
var road_width = 10;
for (var i = 0; i < subdiv; i+= 1) {
var x1 = path_get_x(path_road_xy, i/subdiv);
var y1 = path_get_y(path_road_xy, i/subdiv);
var x2 = path_get_x(path_road_xy, (i+1)/subdiv);
var y2 = path_get_y(path_road_xy, (i+1)/subdiv);
var z1 = path_get_y(path_road_z, i/subdiv);
var z2 = path_get_y(path_road_z, (i+1)/subdiv);
var nx = y2-y1;
var ny = x1-x2;
var len = sqrt(nx*nx+ny*ny);
nx /= len;
ny /= len;
var lx1 = x1 - nx*road_width;
var ly1 = y1 - ny*road_width;
var lx2 = x1 + nx*road_width;
var ly2 = y1 + ny*road_width;
x1 = path_get_x(path_road_xy, (i+1)/subdiv);
y1 = path_get_y(path_road_xy, (i+1)/subdiv);
x2 = path_get_x(path_road_xy, (i+2)/subdiv mod 1);
y2 = path_get_y(path_road_xy, (i+2)/subdiv mod 1);
nx = y2-y1;
ny = x1-x2;
len = sqrt(nx*nx+ny*ny);
nx /= len;
ny /= len;
var lx3 = x1 - nx*road_width;
var ly3 = y1 - ny*road_width;
var lx4 = x1 + nx*road_width;
var ly4 = y1 + ny*road_width;
var u1 = 0, v1 = i/subdiv;
var u2 = 1, v2 = i/subdiv;
var u3 = 0, v3 = (i+1)/subdiv;
var u4 = 1, v4 = (i+1)/subdiv;
v1 = (v1*texrep) mod 1;
v2 = (v2*texrep) mod 1;
v3 = (v3*texrep) mod 1;
v4 = (v4*texrep) mod 1;
///-snip- Generate Road
///-snip- Generate Shadow
if (i mod spokediv == 0) {
var mx = 0.5*(lx1+lx2);
var my = 0.5*(ly1+ly2);
///-snip- Generate Support
}
}
Now we must add vertex buffer building to replace the primitive building. The fewest vertex buffers I can use here, without changing how the textures work or using shaders, is 2 – one for the road and shadow, and one for the concrete pillars. Both need vertex buffers creating and beginning before the loop:
ROAD_BUFFER = vertex_create_buffer();
vertex_begin(ROAD_BUFFER, VERTEX_FORMAT);
PILLAR_BUFFER = vertex_create_buffer();
vertex_begin(PILLAR_BUFFER, VERTEX_FORMAT);
and ending and freezing after the loop
vertex_end(ROAD_BUFFER);
vertex_freeze(ROAD_BUFFER);
vertex_end(PILLAR_BUFFER);
vertex_freeze(PILLAR_BUFFER);
Let’s start with the concrete pillars, since they are only a little code. We basically just need to generate a cylinder.
Based on the original d3d_draw_cylinder
used, we can use this code:
var mx = 0.5*(lx1+lx2);
var my = 0.5*(ly1+ly2);
var _color = make_color_rgb(200,200,200);
var _alpha = 1;
var _radius = 3;
var _steps = 8;
for(var j = 0; j < _steps; ++j) {
var _x1 = mx + lengthdir_x(_radius, ((j)/_steps) * 360);
var _x2 = mx + lengthdir_x(_radius, ((j+1)/_steps) * 360);
var _y1 = my + lengthdir_y(_radius, ((j)/_steps) * 360);
var _y2 = my + lengthdir_y(_radius, ((j+1)/_steps) * 360);
var u1 = j/_steps;
var u2 = (j+1)/_steps;
vertex_position_3d(PILLAR_BUFFER, _x1, _y1, 0);
vertex_color(PILLAR_BUFFER, _color, _alpha);
vertex_texcoord(PILLAR_BUFFER, u1, 0);
vertex_position_3d(PILLAR_BUFFER, _x1, _y1, z1-1);
vertex_color(PILLAR_BUFFER, _color, _alpha);
vertex_texcoord(PILLAR_BUFFER, u1, 1);
vertex_position_3d(PILLAR_BUFFER, _x2, _y2, 0);
vertex_color(PILLAR_BUFFER, _color, _alpha);
vertex_texcoord(PILLAR_BUFFER, u2, 0);
vertex_position_3d(PILLAR_BUFFER, _x2, _y2, 0);
vertex_color(PILLAR_BUFFER, _color, _alpha);
vertex_texcoord(PILLAR_BUFFER, u2, 0);
vertex_position_3d(PILLAR_BUFFER, _x1, _y1, z1-1);
vertex_color(PILLAR_BUFFER, _color, _alpha);
vertex_texcoord(PILLAR_BUFFER, u1, 1);
vertex_position_3d(PILLAR_BUFFER, _x2, _y2, z1-1);
vertex_color(PILLAR_BUFFER, _color, _alpha);
vertex_texcoord(PILLAR_BUFFER, u2, 1);
}
This goes where the ///-snip- Generate Support
comment was. It basically spins in a circle and generates a wall until a cylinder is built.
Finally, let’s put the road generation into a vertex buffer. This is basically replacing all the d3d_vertex functions with vertex buffer functions instead, and adding default data where needed. Rather than going through every conversion, I’ll just show you what that looks like:
_color = c_white;
//Road Top
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, u1,v1);
vertex_position_3d(ROAD_BUFFER, lx2,ly2,z1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, u2,v2);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, u4,v4);
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, u1,v1);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, u4,v4);
vertex_position_3d(ROAD_BUFFER, lx3,ly3,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, u3,v3);
//Road underside
_color = make_color_rgb(100,100,100);
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx2,ly2,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx3,ly3,z2-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
// road side left
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx3,ly3,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx3,ly3,z2-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx3,ly3,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
// road side right
vertex_position_3d(ROAD_BUFFER, lx2,ly2,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx2,ly2,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx2,ly2,z1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
// draw road shadow
_color = c_black;
vertex_position_3d(ROAD_BUFFER, lx1,ly1,-0.5);
vertex_color(ROAD_BUFFER, _color, 0.5);
vertex_texcoord(ROAD_BUFFER, u1,v1);
vertex_position_3d(ROAD_BUFFER, lx2,ly2,-0.5);
vertex_color(ROAD_BUFFER, _color, 0.5);
vertex_texcoord(ROAD_BUFFER, u2,v2);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,-0.5);
vertex_color(ROAD_BUFFER, _color, 0.5);
vertex_texcoord(ROAD_BUFFER, u4,v4);
vertex_position_3d(ROAD_BUFFER, lx1,ly1,-0.5);
vertex_color(ROAD_BUFFER, _color, 0.5);
vertex_texcoord(ROAD_BUFFER, u1,v1);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,-0.5);
vertex_color(ROAD_BUFFER, _color, 0.5);
vertex_texcoord(ROAD_BUFFER, u4,v4);
vertex_position_3d(ROAD_BUFFER, lx3,ly3,-0.5);
vertex_color(ROAD_BUFFER, _color, 0.5);
vertex_texcoord(ROAD_BUFFER, u3,v3);
This code goes where ///-snip- Generate Road
and ///-snip- Generate Shadow
were
Since we’re using combined buffers, there are some untextured bits that now have a texture – road sides, underside and shadow. The shadow has no issue with this, since it is drawn black and transparent. However, the sides and bottom do have a problem, since now they wouldn’t be a solid color. To cheat this, I set their UVs to [0.5,0.5], where the white road stripe is, hiding this issue completely.
Now we just need to submit these buffers in the draw event:
vertex_submit(ROAD_BUFFER, pr_trianglelist, tex_road);
vertex_submit(PILLAR_BUFFER, pr_trianglelist, tex_concrete);
The order of submission would normally matter here, because it affects how the road shadow will blend with other models, but the pillar and shadow positions do not intersect, so no issues are noticable (unless you decide to submit the road buffer before the grass field buffer)
Here’s the final code for obj_road:
Final Code For obj_road
? Create: obj_roadvertex_format_begin();
vertex_format_add_position_3d();
vertex_format_add_color();
vertex_format_add_texcoord();
VERTEX_FORMAT = vertex_format_end();
//Define bounds and UVs
var _xmin = -1800, _xmax = 1800, _ymin = -1800, _ymax = 1800, _z = -1;
var _umin = 0, _umax = 80, _vmin = 0, _vmax = 80;
var _color = c_white, _alpha = 1;
FIELD_VBUFF = vertex_create_buffer();
//I use this short local variable to make typing quicker.
var _b = FIELD_VBUFF;
vertex_begin(_b, VERTEX_FORMAT);
//Tri 1
vertex_position_3d(_b, _xmin, _ymin, _z);
vertex_color(_b, _color, _alpha);
vertex_texcoord(_b, _umin, _vmin);
vertex_position_3d(_b, _xmax, _ymin, _z);
vertex_color(_b, _color, _alpha);
vertex_texcoord(_b, _umax, _vmin);
vertex_position_3d(_b, _xmin, _ymax, _z);
vertex_color(_b, _color, _alpha);
vertex_texcoord(_b, _umin, _vmax);
//Tri 2
vertex_position_3d(_b, _xmin, _ymax, _z);
vertex_color(_b, _color, _alpha);
vertex_texcoord(_b, _umin, _vmax);
vertex_position_3d(_b, _xmax, _ymin, _z);
vertex_color(_b, _color, _alpha);
vertex_texcoord(_b, _umax, _vmin);
vertex_position_3d(_b, _xmax, _ymax, _z);
vertex_color(_b, _color, _alpha);
vertex_texcoord(_b, _umax, _vmax);
vertex_end(_b);
//Freeze to improve performance
vertex_freeze(_b);
ROAD_BUFFER = vertex_create_buffer();
vertex_begin(ROAD_BUFFER, VERTEX_FORMAT);
PILLAR_BUFFER = vertex_create_buffer();
vertex_begin(PILLAR_BUFFER, VERTEX_FORMAT);
var subdiv = 200;
var texrep = 20;
var spokediv = 5;
var road_width = 10;
for (var i = 0; i < subdiv; i+= 1) {
var x1 = path_get_x(path_road_xy, i/subdiv);
var y1 = path_get_y(path_road_xy, i/subdiv);
var x2 = path_get_x(path_road_xy, (i+1)/subdiv);
var y2 = path_get_y(path_road_xy, (i+1)/subdiv);
var z1 = path_get_y(path_road_z, i/subdiv);
var z2 = path_get_y(path_road_z, (i+1)/subdiv);
var nx = y2-y1;
var ny = x1-x2;
var len = sqrt(nx*nx+ny*ny);
nx /= len;
ny /= len;
var lx1 = x1 - nx*road_width;
var ly1 = y1 - ny*road_width;
var lx2 = x1 + nx*road_width;
var ly2 = y1 + ny*road_width;
x1 = path_get_x(path_road_xy, (i+1)/subdiv);
y1 = path_get_y(path_road_xy, (i+1)/subdiv);
x2 = path_get_x(path_road_xy, (i+2)/subdiv mod 1);
y2 = path_get_y(path_road_xy, (i+2)/subdiv mod 1);
nx = y2-y1;
ny = x1-x2;
len = sqrt(nx*nx+ny*ny);
nx /= len;
ny /= len;
var lx3 = x1 - nx*road_width;
var ly3 = y1 - ny*road_width;
var lx4 = x1 + nx*road_width;
var ly4 = y1 + ny*road_width;
var u1 = 0, v1 = i/subdiv;
var u2 = 1, v2 = i/subdiv;
var u3 = 0, v3 = (i+1)/subdiv;
var u4 = 1, v4 = (i+1)/subdiv;
v1 = (v1*texrep) mod 1;
v2 = (v2*texrep) mod 1;
v3 = (v3*texrep) mod 1;
v4 = (v4*texrep) mod 1;
_color = c_white;
//Road Top
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, u1,v1);
vertex_position_3d(ROAD_BUFFER, lx2,ly2,z1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, u2,v2);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, u4,v4);
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, u1,v1);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, u4,v4);
vertex_position_3d(ROAD_BUFFER, lx3,ly3,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, u3,v3);
//Road underside
_color = make_color_rgb(100,100,100);
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx2,ly2,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx3,ly3,z2-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
// road side left
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx3,ly3,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx3,ly3,z2-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx1,ly1,z1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx3,ly3,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
// road side right
vertex_position_3d(ROAD_BUFFER, lx2,ly2,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx2,ly2,z1-1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,z2);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
vertex_position_3d(ROAD_BUFFER, lx2,ly2,z1);
vertex_color(ROAD_BUFFER, _color, 1);
vertex_texcoord(ROAD_BUFFER, 0.5, 0.5);
// draw road shadow
_color = c_black;
vertex_position_3d(ROAD_BUFFER, lx1,ly1,-0.5);
vertex_color(ROAD_BUFFER, _color, 0.5);
vertex_texcoord(ROAD_BUFFER, u1,v1);
vertex_position_3d(ROAD_BUFFER, lx2,ly2,-0.5);
vertex_color(ROAD_BUFFER, _color, 0.5);
vertex_texcoord(ROAD_BUFFER, u2,v2);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,-0.5);
vertex_color(ROAD_BUFFER, _color, 0.5);
vertex_texcoord(ROAD_BUFFER, u4,v4);
vertex_position_3d(ROAD_BUFFER, lx1,ly1,-0.5);
vertex_color(ROAD_BUFFER, _color, 0.5);
vertex_texcoord(ROAD_BUFFER, u1,v1);
vertex_position_3d(ROAD_BUFFER, lx4,ly4,-0.5);
vertex_color(ROAD_BUFFER, _color, 0.5);
vertex_texcoord(ROAD_BUFFER, u4,v4);
vertex_position_3d(ROAD_BUFFER, lx3,ly3,-0.5);
vertex_color(ROAD_BUFFER, _color, 0.5);
vertex_texcoord(ROAD_BUFFER, u3,v3);
if (i mod spokediv == 0) {
var mx = 0.5*(lx1+lx2);
var my = 0.5*(ly1+ly2);
var _color = make_color_rgb(200,200,200);
var _alpha = 1;
var _radius = 3;
var _steps = 8;
for(var j = 0; j < _steps; ++j) {
var _x1 = mx + lengthdir_x(_radius, ((j)/_steps) * 360);
var _x2 = mx + lengthdir_x(_radius, ((j+1)/_steps) * 360);
var _y1 = my + lengthdir_y(_radius, ((j)/_steps) * 360);
var _y2 = my + lengthdir_y(_radius, ((j+1)/_steps) * 360);
var u1 = j/_steps;
var u2 = (j+1)/_steps;
vertex_position_3d(PILLAR_BUFFER, _x1, _y1, 0);
vertex_color(PILLAR_BUFFER, _color, _alpha);
vertex_texcoord(PILLAR_BUFFER, u1, 0);
vertex_position_3d(PILLAR_BUFFER, _x1, _y1, z1-1);
vertex_color(PILLAR_BUFFER, _color, _alpha);
vertex_texcoord(PILLAR_BUFFER, u1, 1);
vertex_position_3d(PILLAR_BUFFER, _x2, _y2, 0);
vertex_color(PILLAR_BUFFER, _color, _alpha);
vertex_texcoord(PILLAR_BUFFER, u2, 0);
vertex_position_3d(PILLAR_BUFFER, _x2, _y2, 0);
vertex_color(PILLAR_BUFFER, _color, _alpha);
vertex_texcoord(PILLAR_BUFFER, u2, 0);
vertex_position_3d(PILLAR_BUFFER, _x1, _y1, z1-1);
vertex_color(PILLAR_BUFFER, _color, _alpha);
vertex_texcoord(PILLAR_BUFFER, u1, 1);
vertex_position_3d(PILLAR_BUFFER, _x2, _y2, z1-1);
vertex_color(PILLAR_BUFFER, _color, _alpha);
vertex_texcoord(PILLAR_BUFFER, u2, 1);
}
}
}
vertex_end(ROAD_BUFFER);
vertex_freeze(ROAD_BUFFER);
vertex_end(PILLAR_BUFFER);
vertex_freeze(PILLAR_BUFFER);
✏️ Draw: obj_roadvar tex_road = sprite_get_texture( spr_road, 0 );
var tex_grass = sprite_get_texture( spr_grass, 0 );
var tex_concrete = sprite_get_texture( spr_concrete, 0 );
vertex_submit(FIELD_VBUFF, pr_trianglelist, tex_grass);
vertex_submit(ROAD_BUFFER, pr_trianglelist, tex_road);
vertex_submit(PILLAR_BUFFER, pr_trianglelist, tex_concrete);
Now you may delete the compatibility scripts, and the project will work just fine! We have successfully updated a 10 year old 3D project from GameMaker 7 to GameMaker Studio 2.
But wait! There’s more: That second method of updating textures!
5) Fix the Textures (Other Methods)
There are two other major ways to correct textures without making them pow2 sizes and on separate texture pages. In fact, it is preferable to use as few texture pages as possible – especially on mobile – because loading texture pages into graphics memory can be very slow!
The first method is to assign the UVs of a texture when creating the buffer. All you need to do is use texture_get_uvs
and use the appropriate values in vertex_texcoord
. This is nice as it doesn’t require a shader or extra math in a shader, but is more complicated to implement, has issues working with externally created models and limits the model to only one specific texture. It also doesn’t support texture tiling.
Since that method is so limited, I won’t be implementing it in the demo.
Instead, I will use method 2 – correct the UVs in a shader, which is far more flexible and really easy to implement.
Model UVs are passed to the shader as-is, then the UVs are re-mapped to corrected values passed as a uniform
.
To appreciate this, disable “Separate Texture Page” in each sprite. Now if you run the game, all textures will be on everything again.
Now create a shader – I’m calling it sh_remap_uvs
. Leave it as GLSL ES, and go to the fragment shader. We don’t need to modifiy the vertex program at all.
The shader needs a uniform variable that holds the correct texture UVs, and a function to remap the original UVs to these correct values.
The final fragment program looks like this:
? sh_remap_uvs: Fragmentvarying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform vec4 texuvs;
vec2 remap_texcoord_tiled(vec2 texCoord, vec4 mapping) {
return vec2(mix(mapping[0], mapping[2], fract(texCoord.x)),
mix(mapping[1], mapping[3], fract(texCoord.y)));
}
void main()
{
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, remap_texcoord_tiled(v_vTexcoord, texuvs) );
}
This shader takes input vertex UVs, gets the fractional component (thus tiling really big UV’s – like for the grassy floor) and maps it to the actual UVs of the texture on the texture page.
After this, all we need to do is set the shader, pass the correct UVs and submit the vertices, so we need to update the Draw event of obj_road
to this:
✏️ Draw: obj_roadvar tex_road = sprite_get_texture( spr_road, 0 );
var tex_grass = sprite_get_texture( spr_grass, 0 );
var tex_concrete = sprite_get_texture( spr_concrete, 0 );
shader_set(sh_remap_uvs);
var _u_uv = shader_get_uniform(sh_remap_uvs, "texuvs");
var UVs = texture_get_uvs(tex_grass);
shader_set_uniform_f(_u_uv, UVs[0], UVs[1], UVs[2], UVs[3]);
vertex_submit(FIELD_VBUFF, pr_trianglelist, tex_grass);
var UVs = texture_get_uvs(tex_road);
shader_set_uniform_f(_u_uv, UVs[0], UVs[1], UVs[2], UVs[3]);
vertex_submit(ROAD_BUFFER, pr_trianglelist, tex_road);
var UVs = texture_get_uvs(tex_concrete);
shader_set_uniform_f(_u_uv, UVs[0], UVs[1], UVs[2], UVs[3]);
vertex_submit(PILLAR_BUFFER, pr_trianglelist, tex_concrete);
shader_reset();
And that’s it! Now it’s possible to use textures of any resolution and on combined texture pages, including the original resolution textures! This does need some remapping tweaks to cope with textures that get cropped by GameMaker, but it works great otherwise.
Well, I think that covers everything! If you have any questions, leave a comment and I will try to answer!
I download the project and thrilled to see it working… As I am new to the gaming world, I would like to now how to use key button to drive on the road. As It seems now it is automatic following the path.
Also it would like nice we can add a HUD.